55from datetime import UTC , datetime
66from pathlib import Path
77
8+ from nooa .tracing ._secret_scrubber import REDACTED , _is_sensitive_key , scrub_value
9+
810
911def enable_http_request_logging (
1012 output_dir : str | Path = "." ,
@@ -89,12 +91,24 @@ def enable_http_request_logging(
8991
9092 def _redact_headers (headers : dict ) -> dict :
9193 """Redact sensitive headers like Authorization."""
92- redacted = dict ( headers )
93- for key in list ( redacted . keys () ):
94- if key . lower () in [ "authorization" , "api- key" , "x-api-key" ] :
94+ redacted = {}
95+ for key , value in headers . items ( ):
96+ if _is_sensitive_key ( key ) :
9597 redacted [key ] = "***REDACTED***"
98+ else :
99+ redacted [key ], _ = scrub_value (value )
96100 return redacted
97101
102+ def _redact_body (body ):
103+ """Recursively scrub secrets before a parsed HTTP body is logged."""
104+ scrubbed , _ = scrub_value (body )
105+ if isinstance (scrubbed , dict ) and scrubbed .get ("grant_type" ) == "authorization_code" :
106+ # "code" is too generic for global key matching, but is a credential
107+ # in an OAuth authorization-code exchange.
108+ if "code" in scrubbed :
109+ scrubbed ["code" ] = REDACTED
110+ return scrubbed
111+
98112 def _write_jsonl_entry (entry : dict ):
99113 """Append a JSON entry to the JSONL error file."""
100114 if jsonl_file is None :
@@ -113,7 +127,7 @@ def _log_request(request, counter):
113127 filename = output_path / f"request_{ counter } _{ model_name } .json"
114128
115129 with open (filename , "w" ) as f :
116- json .dump (body_dict , f , indent = 2 )
130+ json .dump (_redact_body ( body_dict ) , f , indent = 2 )
117131
118132 if verbose :
119133 print (f"\n 💾 Saved HTTP request to: { filename } " )
@@ -144,7 +158,7 @@ def _log_response_sync(response, request, counter, model_name):
144158 try :
145159 response_dict = {
146160 "status_code" : response .status_code ,
147- "headers" : dict (response .headers ),
161+ "headers" : _redact_headers ( dict (response .headers ) ),
148162 }
149163
150164 try :
@@ -169,6 +183,8 @@ def _log_response_sync(response, request, counter, model_name):
169183 except Exception as read_error :
170184 response_dict ["body" ] = f"[Error reading response: { read_error } ]"
171185
186+ response_dict ["body" ] = _redact_body (response_dict ["body" ])
187+
172188 _save_response_file (response_dict , counter , model_name )
173189 except Exception as e :
174190 if verbose :
@@ -180,7 +196,7 @@ async def _log_response_async(response, request, counter, model_name):
180196 try :
181197 response_dict = {
182198 "status_code" : response .status_code ,
183- "headers" : dict (response .headers ),
199+ "headers" : _redact_headers ( dict (response .headers ) ),
184200 }
185201
186202 try :
@@ -206,6 +222,8 @@ async def _log_response_async(response, request, counter, model_name):
206222 except Exception as read_error :
207223 response_dict ["body" ] = f"[Error reading response: { read_error } ]"
208224
225+ response_dict ["body" ] = _redact_body (response_dict ["body" ])
226+
209227 _save_response_file (response_dict , counter , model_name )
210228 except Exception as e :
211229 if verbose :
@@ -236,7 +254,7 @@ def logging_send(self, request):
236254 "url" : str (request .url ),
237255 "method" : request .method ,
238256 "headers" : _redact_headers (dict (request .headers )),
239- "body" : body_dict ,
257+ "body" : _redact_body ( body_dict ) ,
240258 }
241259 except Exception as e :
242260 if verbose :
@@ -265,8 +283,8 @@ def logging_send(self, request):
265283
266284 response_data = {
267285 "status_code" : response .status_code ,
268- "headers" : dict (response .headers ),
269- "body" : body_content ,
286+ "headers" : _redact_headers ( dict (response .headers ) ),
287+ "body" : _redact_body ( body_content ) ,
270288 }
271289
272290 # Write JSONL entry
@@ -324,7 +342,7 @@ async def async_logging_send(self, request):
324342 "url" : str (request .url ),
325343 "method" : request .method ,
326344 "headers" : _redact_headers (dict (request .headers )),
327- "body" : body_dict ,
345+ "body" : _redact_body ( body_dict ) ,
328346 }
329347 except Exception as e :
330348 if verbose :
@@ -353,8 +371,8 @@ async def async_logging_send(self, request):
353371
354372 response_data = {
355373 "status_code" : response .status_code ,
356- "headers" : dict (response .headers ),
357- "body" : body_content ,
374+ "headers" : _redact_headers ( dict (response .headers ) ),
375+ "body" : _redact_body ( body_content ) ,
358376 }
359377
360378 # Write JSONL entry
0 commit comments