@@ -4,6 +4,7 @@ namespace Apitally;
44using System . Net . Mime ;
55using System . Text ;
66using System . Text . Json ;
7+ using System . Text . Json . Nodes ;
78using System . Text . RegularExpressions ;
89using Apitally . Models ;
910using Microsoft . Extensions . Hosting ;
@@ -55,6 +56,17 @@ class RequestLogger(IOptions<ApitallyOptions> options, ILogger<RequestLogger> lo
5556 "token" ,
5657 "cookie" ,
5758 ] ;
59+ private static readonly string [ ] MaskBodyFieldPatterns =
60+ [
61+ "password" ,
62+ "pwd" ,
63+ "token" ,
64+ "secret" ,
65+ "auth" ,
66+ "card[-_ ]?number" ,
67+ "ccv" ,
68+ "ssn" ,
69+ ] ;
5870
5971 public static readonly string [ ] AllowedContentTypes =
6072 [
@@ -63,7 +75,7 @@ class RequestLogger(IOptions<ApitallyOptions> options, ILogger<RequestLogger> lo
6375 ] ;
6476
6577 private readonly object _lock = new ( ) ;
66- private readonly ConcurrentQueue < string > _pendingWrites = new ( ) ;
78+ private readonly ConcurrentQueue < RequestLogItem > _pendingWrites = new ( ) ;
6779 private readonly ConcurrentQueue < TempGzipFile > _files = new ( ) ;
6880 private readonly List < Regex > _compiledPathExcludePatterns = CompilePatterns (
6981 ExcludePathPatterns ,
@@ -81,6 +93,10 @@ class RequestLogger(IOptions<ApitallyOptions> options, ILogger<RequestLogger> lo
8193 MaskHeaderPatterns ,
8294 options . Value . RequestLogging . HeaderMaskPatterns
8395 ) ;
96+ private readonly List < Regex > _compiledBodyFieldMaskPatterns = CompilePatterns (
97+ MaskBodyFieldPatterns ,
98+ options . Value . RequestLogging . BodyFieldMaskPatterns
99+ ) ;
84100 private readonly JsonSerializerOptions _serializerOptions = new ( )
85101 {
86102 DefaultIgnoreCondition =
@@ -129,81 +145,21 @@ public void LogRequest(Request request, Response response, Exception? exception
129145 return ;
130146 }
131147
132- // Process query params and URL
133- if ( ! string . IsNullOrEmpty ( request . Url ) )
134- {
135- var uri = new Uri ( request . Url ) ;
136- var query = uri . Query . TrimStart ( '?' ) ;
137- if ( ! requestLoggingOptions . IncludeQueryParams )
138- {
139- query = string . Empty ;
140- }
141- else if ( ! string . IsNullOrEmpty ( query ) )
142- {
143- query = MaskQueryParams ( query ) ;
144- }
145- var uriBuilder = new UriBuilder ( uri ) { Query = query } ;
146- request . Url = uriBuilder . Uri . ToString ( ) ;
147- }
148-
149- // Process request body
150148 if (
151149 ! requestLoggingOptions . IncludeRequestBody
152150 || ! HasSupportedContentType ( request . Headers )
153151 )
154152 {
155153 request . Body = null ;
156154 }
157- else if ( request . Body != null )
158- {
159- if ( request . Body . Length > MaxBodySize )
160- {
161- request . Body = BodyTooLarge ;
162- }
163- else
164- {
165- request . Body = requestLoggingOptions . MaskRequestBody ( request ) ?? BodyMasked ;
166- if ( request . Body . Length > MaxBodySize )
167- {
168- request . Body = BodyTooLarge ;
169- }
170- }
171- }
172-
173- // Process response body
174155 if (
175156 ! requestLoggingOptions . IncludeResponseBody
176157 || ! HasSupportedContentType ( response . Headers )
177158 )
178159 {
179160 response . Body = null ;
180161 }
181- else if ( response . Body != null )
182- {
183- if ( response . Body . Length > MaxBodySize )
184- {
185- response . Body = BodyTooLarge ;
186- }
187- else
188- {
189- response . Body =
190- requestLoggingOptions . MaskResponseBody ( request , response ) ?? BodyMasked ;
191- if ( response . Body . Length > MaxBodySize )
192- {
193- response . Body = BodyTooLarge ;
194- }
195- }
196- }
197-
198- // Process headers
199- request . Headers = requestLoggingOptions . IncludeRequestHeaders
200- ? MaskHeaders ( request . Headers )
201- : [ ] ;
202- response . Headers = requestLoggingOptions . IncludeResponseHeaders
203- ? MaskHeaders ( response . Headers )
204- : [ ] ;
205162
206- // Create exception info
207163 var exceptionInfo =
208164 exception != null && requestLoggingOptions . IncludeException
209165 ? new ExceptionInfo
@@ -216,15 +172,14 @@ public void LogRequest(Request request, Response response, Exception? exception
216172 }
217173 : null ;
218174
219- // Create log item
175+ // Create log item and enqueue
220176 var item = new RequestLogItem
221177 {
222178 Request = request ,
223179 Response = response ,
224180 Exception = exceptionInfo ,
225181 } ;
226- var serializedItem = JsonSerializer . Serialize ( item , _serializerOptions ) ;
227- _pendingWrites . Enqueue ( serializedItem ) ;
182+ _pendingWrites . Enqueue ( item ) ;
228183
229184 if ( _pendingWrites . Count > MaxPendingWrites )
230185 {
@@ -249,7 +204,9 @@ private void WriteToFile()
249204 _currentFile ??= new TempGzipFile ( ) ;
250205 while ( _pendingWrites . TryDequeue ( out var item ) )
251206 {
252- _currentFile . WriteLine ( Encoding . UTF8 . GetBytes ( item ) ) ;
207+ ApplyMasking ( item ) ;
208+ var serializedItem = JsonSerializer . Serialize ( item , _serializerOptions ) ;
209+ _currentFile . WriteLine ( Encoding . UTF8 . GetBytes ( serializedItem ) ) ;
253210 }
254211 }
255212 }
@@ -315,6 +272,80 @@ public void Clear()
315272 }
316273 }
317274
275+ private void ApplyMasking ( RequestLogItem item )
276+ {
277+ var requestLoggingOptions = options . Value . RequestLogging ;
278+ var request = item . Request ;
279+ var response = item . Response ;
280+
281+ if ( request . Body != null )
282+ {
283+ // Apply user-provided masking callback for request body
284+ request . Body = requestLoggingOptions . MaskRequestBody ( request ) ?? BodyMasked ;
285+
286+ if ( request . Body . Length > MaxBodySize )
287+ {
288+ request . Body = BodyTooLarge ;
289+ }
290+
291+ // Mask request body fields (if JSON)
292+ if (
293+ ! request . Body . SequenceEqual ( BodyTooLarge )
294+ && ! request . Body . SequenceEqual ( BodyMasked )
295+ && RequestLogger . HasJsonContentType ( request . Headers )
296+ )
297+ {
298+ request . Body = MaskJsonBody ( request . Body ) ;
299+ }
300+ }
301+
302+ if ( response . Body != null )
303+ {
304+ // Apply user-provided masking callback for response body
305+ response . Body = requestLoggingOptions . MaskResponseBody ( request , response ) ?? BodyMasked ;
306+
307+ if ( response . Body . Length > MaxBodySize )
308+ {
309+ response . Body = BodyTooLarge ;
310+ }
311+
312+ // Mask response body fields (if JSON)
313+ if (
314+ ! response . Body . SequenceEqual ( BodyTooLarge )
315+ && ! response . Body . SequenceEqual ( BodyMasked )
316+ && RequestLogger . HasJsonContentType ( response . Headers )
317+ )
318+ {
319+ response . Body = MaskJsonBody ( response . Body ) ;
320+ }
321+ }
322+
323+ // Mask headers
324+ request . Headers = requestLoggingOptions . IncludeRequestHeaders
325+ ? MaskHeaders ( request . Headers )
326+ : [ ] ;
327+ response . Headers = requestLoggingOptions . IncludeResponseHeaders
328+ ? MaskHeaders ( response . Headers )
329+ : [ ] ;
330+
331+ // Mask query params
332+ if ( ! string . IsNullOrEmpty ( request . Url ) )
333+ {
334+ var uri = new Uri ( request . Url ) ;
335+ var query = uri . Query . TrimStart ( '?' ) ;
336+ if ( ! requestLoggingOptions . IncludeQueryParams )
337+ {
338+ query = string . Empty ;
339+ }
340+ else if ( ! string . IsNullOrEmpty ( query ) )
341+ {
342+ query = MaskQueryParams ( query ) ;
343+ }
344+ var uriBuilder = new UriBuilder ( uri ) { Query = query } ;
345+ request . Url = uriBuilder . Uri . ToString ( ) ;
346+ }
347+ }
348+
318349 private bool ShouldExcludePath ( string ? path )
319350 {
320351 return ! string . IsNullOrEmpty ( path )
@@ -337,6 +368,69 @@ private bool ShouldMaskHeader(string name)
337368 return _compiledHeaderMaskPatterns . Any ( p => p . IsMatch ( name ) ) ;
338369 }
339370
371+ private bool ShouldMaskBodyField ( string name )
372+ {
373+ return _compiledBodyFieldMaskPatterns . Any ( p => p . IsMatch ( name ) ) ;
374+ }
375+
376+ private byte [ ] MaskJsonBody ( byte [ ] body )
377+ {
378+ try
379+ {
380+ var json = Encoding . UTF8 . GetString ( body ) ;
381+ var node = JsonNode . Parse ( json ) ;
382+ if ( node != null )
383+ {
384+ MaskJsonNode ( node ) ;
385+ return Encoding . UTF8 . GetBytes ( node . ToJsonString ( _serializerOptions ) ) ;
386+ }
387+ return body ;
388+ }
389+ catch
390+ {
391+ return body ;
392+ }
393+ }
394+
395+ private void MaskJsonNode ( JsonNode node )
396+ {
397+ switch ( node )
398+ {
399+ case JsonObject jsonObject :
400+ var propertiesToMask = new List < string > ( ) ;
401+ foreach ( var property in jsonObject )
402+ {
403+ if (
404+ property . Value is JsonValue jsonValue
405+ && jsonValue . TryGetValue < string > ( out _ )
406+ && ShouldMaskBodyField ( property . Key )
407+ )
408+ {
409+ propertiesToMask . Add ( property . Key ) ;
410+ }
411+ else if ( property . Value != null )
412+ {
413+ MaskJsonNode ( property . Value ) ;
414+ }
415+ }
416+ foreach ( var propertyKey in propertiesToMask )
417+ {
418+ jsonObject [ propertyKey ] = JsonValue . Create ( Masked ) ;
419+ }
420+ break ;
421+
422+ case JsonArray jsonArray :
423+ foreach ( var item in jsonArray )
424+ {
425+ if ( item != null )
426+ {
427+ MaskJsonNode ( item ) ;
428+ }
429+ }
430+ break ;
431+ }
432+ }
433+
340434 private string MaskQueryParams ( string query )
341435 {
342436 if ( string . IsNullOrEmpty ( query ) )
@@ -375,6 +469,13 @@ private static bool HasSupportedContentType(Header[] headers)
375469 ) ;
376470 }
377471
472+ private static bool HasJsonContentType ( Header [ ] headers )
473+ {
474+ var contentType = GetHeaderValue ( headers , "content-type" ) ;
475+ return contentType != null
476+ && Regex . IsMatch ( contentType , @"\bjson\b" , RegexOptions . IgnoreCase ) ;
477+ }
478+
378479 private static string ? GetHeaderValue ( Header [ ] headers , string name )
379480 {
380481 return headers
0 commit comments