8
8
import java .util .Arrays ;
9
9
import java .util .Deque ;
10
10
import java .util .List ;
11
- import java .util .UUID ;
12
11
import java .util .concurrent .ConcurrentLinkedDeque ;
13
12
import java .util .concurrent .Executors ;
14
13
import java .util .concurrent .ScheduledExecutorService ;
21
20
import org .slf4j .Logger ;
22
21
import org .slf4j .LoggerFactory ;
23
22
23
+ import com .fasterxml .jackson .databind .JsonNode ;
24
24
import com .fasterxml .jackson .databind .ObjectMapper ;
25
25
import com .fasterxml .jackson .databind .node .ObjectNode ;
26
26
27
27
import io .apitally .common .dto .ExceptionDto ;
28
28
import io .apitally .common .dto .Header ;
29
29
import io .apitally .common .dto .Request ;
30
+ import io .apitally .common .dto .RequestLogItem ;
30
31
import io .apitally .common .dto .Response ;
31
32
32
33
public class RequestLogger {
@@ -80,7 +81,7 @@ public class RequestLogger {
80
81
private final RequestLoggingConfig config ;
81
82
private final ObjectMapper objectMapper ;
82
83
private final ReentrantLock lock ;
83
- private final Deque <String > pendingWrites ;
84
+ private final Deque <RequestLogItem > pendingWrites ;
84
85
private final Deque <TempGzipFile > files ;
85
86
private TempGzipFile currentFile ;
86
87
private boolean enabled ;
@@ -144,80 +145,27 @@ public void logRequest(Request request, Response response, Exception exception)
144
145
145
146
try {
146
147
String userAgent = findHeader (request .getHeaders (), "user-agent" );
147
- if (shouldExcludePath (request .getPath ()) || shouldExcludeUserAgent (userAgent )
148
- || (config .getCallbacks () != null && config .getCallbacks ().shouldExclude (request , response ))) {
148
+ if (shouldExcludePath (request .getPath ()) || shouldExcludeUserAgent (userAgent )) {
149
149
return ;
150
150
}
151
-
152
- // Process query params and URL
153
- if (request .getUrl () != null ) {
154
- try {
155
- URL url = new URL (request .getUrl ());
156
- String query = url .getQuery ();
157
- if (!config .isQueryParamsIncluded ()) {
158
- query = null ;
159
- } else if (query != null ) {
160
- query = maskQueryParams (query );
161
- }
162
- request .setUrl (new java .net .URL (url .getProtocol (), url .getHost (), url .getPort (),
163
- url .getPath () + (query != null ? "?" + query : "" )).toString ());
164
- } catch (MalformedURLException e ) {
165
- return ;
166
- }
151
+ if (config .getCallbacks () != null && config .getCallbacks ().shouldExclude (request , response )) {
152
+ return ;
167
153
}
168
154
169
- // Process request body
170
155
if (!config .isRequestBodyIncluded () || !hasSupportedContentType (request .getHeaders ())) {
171
156
request .setBody (null );
172
- } else if (request .getBody () != null ) {
173
- if (request .getBody ().length > MAX_BODY_SIZE ) {
174
- request .setBody (BODY_TOO_LARGE );
175
- } else if (config .getCallbacks () != null ) {
176
- byte [] maskedBody = config .getCallbacks ().maskRequestBody (request );
177
- request .setBody (maskedBody != null ? maskedBody : BODY_MASKED );
178
- if (request .getBody ().length > MAX_BODY_SIZE ) {
179
- request .setBody (BODY_TOO_LARGE );
180
- }
181
- }
182
157
}
183
-
184
- // Process response body
185
158
if (!config .isResponseBodyIncluded () || !hasSupportedContentType (response .getHeaders ())) {
186
159
response .setBody (null );
187
- } else if (response .getBody () != null ) {
188
- if (response .getBody ().length > MAX_BODY_SIZE ) {
189
- response .setBody (BODY_TOO_LARGE );
190
- } else if (config .getCallbacks () != null ) {
191
- byte [] maskedBody = config .getCallbacks ().maskResponseBody (request , response );
192
- response .setBody (maskedBody != null ? maskedBody : BODY_MASKED );
193
- if (response .getBody ().length > MAX_BODY_SIZE ) {
194
- response .setBody (BODY_TOO_LARGE );
195
- }
196
- }
197
160
}
198
161
199
- // Process headers
200
- request .setHeaders (
201
- config .isRequestHeadersIncluded ()
202
- ? maskHeaders (request .getHeaders ()).toArray (new Header [0 ])
203
- : new Header [0 ]);
204
- response .setHeaders (
205
- config .isResponseHeadersIncluded ()
206
- ? maskHeaders (response .getHeaders ()).toArray (new Header [0 ])
207
- : new Header [0 ]);
208
-
209
- // Create log item
210
- ObjectNode item = objectMapper .createObjectNode ();
211
- item .put ("uuid" , UUID .randomUUID ().toString ());
212
- item .set ("request" , skipEmptyValues (objectMapper .valueToTree (request )));
213
- item .set ("response" , skipEmptyValues (objectMapper .valueToTree (response )));
162
+ ExceptionDto exceptionDto = null ;
214
163
if (exception != null && config .isExceptionIncluded ()) {
215
- ExceptionDto exceptionDto = new ExceptionDto (exception );
216
- item .set ("exception" , objectMapper .valueToTree (exceptionDto ));
164
+ exceptionDto = new ExceptionDto (exception );
217
165
}
218
166
219
- String serializedItem = objectMapper . writeValueAsString ( item );
220
- pendingWrites .add (serializedItem );
167
+ RequestLogItem item = new RequestLogItem ( request , response , exceptionDto );
168
+ pendingWrites .add (item );
221
169
222
170
if (pendingWrites .size () > MAX_PENDING_WRITES ) {
223
171
pendingWrites .poll ();
@@ -227,6 +175,74 @@ public void logRequest(Request request, Response response, Exception exception)
227
175
}
228
176
}
229
177
178
+ private void applyMasking (RequestLogItem item ) {
179
+ Request request = item .getRequest ();
180
+ Response response = item .getResponse ();
181
+
182
+ if (request .getBody () != null ) {
183
+ // Apply user-provided masking callback for request body
184
+ if (config .getCallbacks () != null ) {
185
+ byte [] maskedBody = config .getCallbacks ().maskRequestBody (request );
186
+ request .setBody (maskedBody != null ? maskedBody : BODY_MASKED );
187
+ }
188
+
189
+ if (request .getBody ().length > MAX_BODY_SIZE ) {
190
+ request .setBody (BODY_TOO_LARGE );
191
+ }
192
+
193
+ // Mask request body fields (if JSON)
194
+ if (!Arrays .equals (request .getBody (), BODY_TOO_LARGE ) && !Arrays .equals (request .getBody (), BODY_MASKED )
195
+ && hasJsonContentType (request .getHeaders ())) {
196
+ request .setBody (maskJsonBody (request .getBody ()));
197
+ }
198
+ }
199
+
200
+ if (response .getBody () != null ) {
201
+ // Apply user-provided masking callback for response body
202
+ if (config .getCallbacks () != null ) {
203
+ byte [] maskedBody = config .getCallbacks ().maskResponseBody (request , response );
204
+ response .setBody (maskedBody != null ? maskedBody : BODY_MASKED );
205
+ }
206
+
207
+ if (response .getBody ().length > MAX_BODY_SIZE ) {
208
+ response .setBody (BODY_TOO_LARGE );
209
+ }
210
+
211
+ // Mask response body fields (if JSON)
212
+ if (!Arrays .equals (response .getBody (), BODY_TOO_LARGE ) && !Arrays .equals (response .getBody (), BODY_MASKED )
213
+ && hasJsonContentType (response .getHeaders ())) {
214
+ response .setBody (maskJsonBody (response .getBody ()));
215
+ }
216
+ }
217
+
218
+ // Process headers
219
+ request .setHeaders (
220
+ config .isRequestHeadersIncluded ()
221
+ ? maskHeaders (request .getHeaders ()).toArray (new Header [0 ])
222
+ : new Header [0 ]);
223
+ response .setHeaders (
224
+ config .isResponseHeadersIncluded ()
225
+ ? maskHeaders (response .getHeaders ()).toArray (new Header [0 ])
226
+ : new Header [0 ]);
227
+
228
+ // Process query params and URL
229
+ if (request .getUrl () != null ) {
230
+ try {
231
+ URL url = new URL (request .getUrl ());
232
+ String query = url .getQuery ();
233
+ if (!config .isQueryParamsIncluded ()) {
234
+ query = null ;
235
+ } else if (query != null ) {
236
+ query = maskQueryParams (query );
237
+ }
238
+ request .setUrl (new java .net .URL (url .getProtocol (), url .getHost (), url .getPort (),
239
+ url .getPath () + (query != null ? "?" + query : "" )).toString ());
240
+ } catch (MalformedURLException e ) {
241
+ // Keep original URL if malformed
242
+ }
243
+ }
244
+ }
245
+
230
246
public void writeToFile () throws IOException {
231
247
if (!enabled || pendingWrites .isEmpty ()) {
232
248
return ;
@@ -236,9 +252,20 @@ public void writeToFile() throws IOException {
236
252
if (currentFile == null ) {
237
253
currentFile = new TempGzipFile ();
238
254
}
239
- String item ;
255
+ RequestLogItem item ;
240
256
while ((item = pendingWrites .poll ()) != null ) {
241
- currentFile .writeLine (item .getBytes (StandardCharsets .UTF_8 ));
257
+ applyMasking (item );
258
+
259
+ ObjectNode itemNode = objectMapper .createObjectNode ();
260
+ itemNode .put ("uuid" , item .getUuid ());
261
+ itemNode .set ("request" , skipEmptyValues (objectMapper .valueToTree (item .getRequest ())));
262
+ itemNode .set ("response" , skipEmptyValues (objectMapper .valueToTree (item .getResponse ())));
263
+ if (item .getException () != null ) {
264
+ itemNode .set ("exception" , objectMapper .valueToTree (item .getException ()));
265
+ }
266
+
267
+ String serializedItem = objectMapper .writeValueAsString (itemNode );
268
+ currentFile .writeLine (serializedItem .getBytes (StandardCharsets .UTF_8 ));
242
269
}
243
270
} finally {
244
271
lock .unlock ();
@@ -389,6 +416,32 @@ private List<Header> maskHeaders(Header[] headers) {
389
416
.collect (Collectors .toList ());
390
417
}
391
418
419
+ private byte [] maskJsonBody (byte [] body ) {
420
+ try {
421
+ String json = new String (body , StandardCharsets .UTF_8 );
422
+ JsonNode node = objectMapper .readTree (json );
423
+ maskJsonNode (node );
424
+ return objectMapper .writeValueAsString (node ).getBytes (StandardCharsets .UTF_8 );
425
+ } catch (Exception e ) {
426
+ return body ;
427
+ }
428
+ }
429
+
430
+ private void maskJsonNode (JsonNode node ) {
431
+ if (node .isObject ()) {
432
+ ObjectNode objectNode = (ObjectNode ) node ;
433
+ objectNode .properties ().forEach (entry -> {
434
+ if (entry .getValue ().isTextual () && shouldMaskBodyField (entry .getKey ())) {
435
+ objectNode .put (entry .getKey (), MASKED );
436
+ } else {
437
+ maskJsonNode (entry .getValue ());
438
+ }
439
+ });
440
+ } else if (node .isArray ()) {
441
+ node .forEach (this ::maskJsonNode );
442
+ }
443
+ }
444
+
392
445
private boolean hasSupportedContentType (Header [] headers ) {
393
446
String contentType = findHeader (headers , "content-type" );
394
447
return contentType != null && ALLOWED_CONTENT_TYPES .stream ()
@@ -410,7 +463,7 @@ private String findHeader(Header[] headers, String name) {
410
463
411
464
private ObjectNode skipEmptyValues (ObjectNode node ) {
412
465
ObjectNode result = objectMapper .createObjectNode ();
413
- node .fields ().forEachRemaining (entry -> {
466
+ node .properties ().forEach (entry -> {
414
467
if (entry .getValue ().isNull ()) {
415
468
return ;
416
469
}
0 commit comments