11from __future__ import annotations
22
3+ import copy
34import json
45import re
56from abc import ABC , abstractmethod
89
910import typing_extensions
1011from opentelemetry .attributes import BoundedAttributes
12+ from opentelemetry .sdk ._logs import LogRecord
1113from opentelemetry .sdk .trace import Event
1214from opentelemetry .semconv .trace import SpanAttributes
1315from opentelemetry .trace import Link
@@ -129,11 +131,15 @@ class BaseScrubber(ABC):
129131 SpanAttributes .URL_FULL ,
130132 SpanAttributes .URL_PATH ,
131133 SpanAttributes .URL_QUERY ,
134+ 'event.name' ,
132135 }
133136
134137 @abstractmethod
135138 def scrub_span (self , span : ReadableSpanDict ): ... # pragma: no cover
136139
140+ @abstractmethod
141+ def scrub_log (self , log : LogRecord ) -> LogRecord : ... # pragma: no cover
142+
137143 @abstractmethod
138144 def scrub_value (self , path : JsonPath , value : Any ) -> tuple [Any , list [ScrubbedNote ]]: ... # pragma: no cover
139145
@@ -142,6 +148,9 @@ class NoopScrubber(BaseScrubber):
142148 def scrub_span (self , span : ReadableSpanDict ):
143149 pass
144150
151+ def scrub_log (self , log : LogRecord ) -> LogRecord :
152+ return log
153+
145154 def scrub_value (self , path : JsonPath , value : Any ) -> tuple [Any , list [ScrubbedNote ]]: # pragma: no cover
146155 return value , []
147156
@@ -158,6 +167,10 @@ def __init__(self, patterns: Sequence[str] | None, callback: ScrubCallback | Non
158167 self ._pattern = re .compile ('|' .join (patterns ), re .IGNORECASE | re .DOTALL )
159168 self ._callback = callback
160169
170+ def scrub_log (self , log : LogRecord ) -> LogRecord :
171+ span_scrubber = SpanScrubber (self )
172+ return span_scrubber .scrub_log (log )
173+
161174 def scrub_span (self , span : ReadableSpanDict ):
162175 scope = span ['instrumentation_scope' ]
163176 if scope and scope .name in ['logfire.openai' , 'logfire.anthropic' ]:
@@ -194,15 +207,19 @@ def __init__(self, parent: Scrubber):
194207 self ._pattern = parent ._pattern # type: ignore
195208 self ._callback = parent ._callback # type: ignore
196209 self .scrubbed : list [ScrubbedNote ] = []
210+ self .did_scrub = False
197211
198212 def scrub_span (self , span : ReadableSpanDict ):
199213 # We need to use BoundedAttributes because:
200214 # 1. For events and links, we get an error otherwise:
201215 # https://github.com/open-telemetry/opentelemetry-python/issues/3761
202216 # 2. The callback might return a value that isn't of the type required by OTEL,
203217 # in which case BoundAttributes will discard it to prevent an error.
204- # TODO silently throwing away the result is bad, and BoundedAttributes might be bad for performance.
205- span ['attributes' ] = BoundedAttributes (attributes = self .scrub (('attributes' ,), span ['attributes' ]))
218+ # TODO silently throwing away the result is bad, and BoundedAttributes is bad for performance.
219+ new_attributes = self .scrub (('attributes' ,), span ['attributes' ])
220+ if self .did_scrub :
221+ span ['attributes' ] = BoundedAttributes (attributes = new_attributes )
222+
206223 span ['events' ] = [
207224 Event (
208225 # We don't scrub the event name because in theory it should be a low-cardinality general description,
@@ -221,6 +238,22 @@ def scrub_span(self, span: ReadableSpanDict):
221238 for i , link in enumerate (span ['links' ])
222239 ]
223240
241+ def scrub_log (self , log : LogRecord ) -> LogRecord :
242+ new_attributes : dict [str , Any ] | None = self .scrub (('attributes' ,), log .attributes )
243+ new_body = self .scrub (('log_body' ,), log .body )
244+
245+ if not self .did_scrub :
246+ return log
247+
248+ if self .scrubbed :
249+ new_attributes = new_attributes or {}
250+ new_attributes [ATTRIBUTES_SCRUBBED_KEY ] = json .dumps (self .scrubbed )
251+
252+ result = copy .copy (log )
253+ result .attributes = BoundedAttributes (attributes = new_attributes )
254+ result .body = new_body
255+ return result
256+
224257 def scrub_event_attributes (self , event : Event , index : int ):
225258 attributes = event .attributes or {}
226259 path = ('otel_events' , index , 'attributes' )
@@ -265,7 +298,9 @@ def scrub(self, path: JsonPath, value: Any) -> Any:
265298
266299 def _redact (self , match : ScrubMatch ) -> Any :
267300 if self ._callback and (result := self ._callback (match )) is not None :
301+ self .did_scrub = self .did_scrub or result is not match .value
268302 return result
303+ self .did_scrub = True
269304 matched_substring = match .pattern_match .group (0 )
270305 self .scrubbed .append (ScrubbedNote (path = match .path , matched_substring = matched_substring ))
271306 return f'[Scrubbed due to { matched_substring !r} ]'
0 commit comments