@@ -999,9 +999,12 @@ def utf8_byte_length(value: str) -> int:
999999 return len (value .encode ("utf-8" ))
10001000
10011001
1002+ _LazyLogRecord = LazyValue [dict [str , Any ]] | LazyValue [dict [str , Any ] | None ]
1003+
1004+
10021005class _BackgroundLogger (ABC ):
10031006 @abstractmethod
1004- def log (self , * args : LazyValue [ dict [ str , Any ]] ) -> None :
1007+ def log (self , * args : _LazyLogRecord ) -> None :
10051008 pass
10061009
10071010 @abstractmethod
@@ -1019,7 +1022,7 @@ def __init__(self):
10191022 def enforce_queue_size_limit (self , enforce : bool ) -> None :
10201023 pass
10211024
1022- def log (self , * args : LazyValue [ dict [ str , Any ]] ) -> None :
1025+ def log (self , * args : _LazyLogRecord ) -> None :
10231026 with self .lock :
10241027 self .logs .extend (args )
10251028
@@ -1033,8 +1036,8 @@ def flush(self, batch_size: int | None = None):
10331036 if not self .logs :
10341037 return
10351038
1036- # Unwrap lazy values and extract attachments
1037- logs = [l . get () for l in self .logs ]
1039+ # Filter dropped records before touching attachments.
1040+ logs = [record for item in self .logs if ( record := item . get ()) is not None ]
10381041
10391042 # Extract attachments from all logs
10401043 attachments : list [BaseAttachment ] = []
@@ -1046,7 +1049,7 @@ def flush(self, batch_size: int | None = None):
10461049
10471050 def pop (self ):
10481051 with self .lock :
1049- logs = [l . get () for l in self .logs ] # unwrap the LazyValues
1052+ logs = [record for item in self .logs if ( record := item . get ()) is not None ]
10501053 self .logs = []
10511054
10521055 if not logs :
@@ -1057,7 +1060,11 @@ def pop(self):
10571060 batch = merge_row_batch (logs )
10581061
10591062 if self ._export_customizers :
1060- batch = [_customize_span_export (item , self ._export_customizers ) for item in batch ]
1063+ batch = [
1064+ record
1065+ for item in batch
1066+ if (record := _customize_span_export (item , self ._export_customizers )) is not None
1067+ ]
10611068
10621069 return batch
10631070
@@ -1098,7 +1105,7 @@ def __init__(self, api_conn: LazyValue[HTTPConnection]):
10981105 self .started = False
10991106
11001107 self .logger = logging .getLogger ("braintrust" )
1101- self .queue : "LogQueue[LazyValue[dict[str, Any]] ]" = LogQueue (maxsize = self .queue_maxsize )
1108+ self .queue : "LogQueue[_LazyLogRecord ]" = LogQueue (maxsize = self .queue_maxsize )
11021109
11031110 # Counter for tracking overflow uploads (useful for testing)
11041111 self ._overflow_upload_count = 0
@@ -1113,7 +1120,7 @@ def enforce_queue_size_limit(self, enforce: bool) -> None:
11131120 """
11141121 self .queue .enforce_queue_size_limit (enforce )
11151122
1116- def log (self , * args : LazyValue [ dict [ str , Any ]] ) -> None :
1123+ def log (self , * args : _LazyLogRecord ) -> None :
11171124 self ._start ()
11181125 dropped_items = []
11191126 for event in args :
@@ -1254,16 +1261,20 @@ def flush(self, batch_size: int | None = None):
12541261 )
12551262
12561263 def _unwrap_lazy_values (
1257- self , wrapped_items : Sequence [LazyValue [ dict [ str , Any ]] ]
1264+ self , wrapped_items : Sequence [_LazyLogRecord ]
12581265 ) -> tuple [list [dict [str , Any ]], list ["BaseAttachment" ]]:
12591266 for i in range (self .num_tries ):
12601267 try :
1261- unwrapped_items = [item . get () for item in wrapped_items ]
1268+ unwrapped_items = [record for item in wrapped_items if ( record := item . get ()) is not None ]
12621269 merged_items = merge_row_batch (unwrapped_items )
12631270
1264- # Logger-local hooks run after instrumentation hooks and merging.
1271+ # Logger-local hooks run after span hooks and merging.
12651272 if self ._export_customizers :
1266- merged_items = [_customize_span_export (item , self ._export_customizers ) for item in merged_items ]
1273+ merged_items = [
1274+ record
1275+ for item in merged_items
1276+ if (record := _customize_span_export (item , self ._export_customizers )) is not None
1277+ ]
12671278
12681279 attachments : list ["BaseAttachment" ] = []
12691280 for item in merged_items :
@@ -2489,8 +2500,8 @@ def set_masking_function(masking_function: Callable[[Any], Any] | None) -> None:
24892500 """
24902501 Set a global masking function that will be applied to all logged data before sending to Braintrust.
24912502 The masking function will be applied after records are merged but before they are sent to the backend.
2492- Internally, masking is a logger-local export customizer that runs after instrumentation
2493- customizers and also covers manually logged records.
2503+ Internally, masking is a logger-local export customizer that runs after span
2504+ customizers and also covers non-span records such as datasets and feedback .
24942505
24952506 :param masking_function: A function that takes a JSON-serializable object and returns a masked version.
24962507 Set to None to disable masking.
@@ -3983,6 +3994,7 @@ def _update_span_impl(
39833994 )
39843995
39853996 update_event = bt_safe_deep_copy (update_event )
3997+ customizers = _get_span_customizers ()
39863998
39873999 def parent_ids ():
39884000 exporter = _get_exporter ()
@@ -3991,15 +4003,16 @@ def parent_ids():
39914003 object_id = parent_object_id .get (),
39924004 ).object_id_fields ()
39934005
3994- def compute_record ():
3995- return dict (
4006+ def compute_record () -> dict [ str , Any ] | None :
4007+ record = dict (
39964008 id = id ,
39974009 ** update_event ,
39984010 ** parent_ids (),
39994011 ** {
40004012 IS_MERGE_FIELD : True ,
40014013 },
40024014 )
4015+ return _customize_span_export (record , customizers ) if customizers else record
40034016
40044017 _state .global_bg_logger ().log (LazyValue (compute_record , use_mutex = False ))
40054018
@@ -4815,6 +4828,7 @@ def __init__(
48154828 created = datetime .datetime .now (datetime .timezone .utc ).isoformat (),
48164829 )
48174830 internal = internal or {}
4831+ # Integration consumers use this provenance independently of export hooks.
48184832 self ._instrumentation = internal .get ("instrumentation" ) or "braintrust-python-logger"
48194833 internal_data ["context" ] = merge_span_origin_context (
48204834 caller_location or {},
@@ -4903,7 +4917,7 @@ def log_internal(self, event: dict[str, Any] | None = None, internal_data: dict[
49034917
49044918 # Snapshot at log time so the span cache and the export agree on whether
49054919 # (and how) this record is customized.
4906- customizers = _get_span_customizers () if self . _instrumentation != "braintrust-python-logger" else ()
4920+ customizers = _get_span_customizers ()
49074921 pending_cache_key = (
49084922 object ()
49094923 if customizers
@@ -4933,11 +4947,11 @@ def write_span_cache(record: dict[str, Any]) -> None:
49334947 self .state .span_cache .queue_write (self .root_span_id , self .span_id , cached_span )
49344948
49354949 # Customized records are cached after export customization instead, so
4936- # local scorers never see content that a customizer redacted.
4950+ # local scorers never see content that a customizer redacted or dropped .
49374951 if not customizers :
49384952 write_span_cache (serializable_partial_record )
49394953
4940- def compute_record () -> dict [str , Any ]:
4954+ def compute_record () -> dict [str , Any ] | None :
49414955 exporter = _get_exporter ()
49424956 record = dict (
49434957 ** serializable_partial_record ,
@@ -4948,13 +4962,15 @@ def compute_record() -> dict[str, Any]:
49484962 ).object_id_fields (),
49494963 )
49504964 # Resolve and customize inside the cached LazyValue: every incremental
4951- # instrumentation record is transformed once, before the background
4965+ # span record is transformed once, before the background
49524966 # logger merges, masks, extracts attachments, or retries delivery.
49534967 if customizers :
4954- record = _customize_span_export (record , customizers )
4955- write_span_cache (record )
4968+ customized_record = _customize_span_export (record , customizers )
4969+ if customized_record is not None :
4970+ write_span_cache (customized_record )
49564971 if pending_cache_key is not None :
49574972 self .state .span_cache ._forget_pending_record (self .root_span_id , pending_cache_key )
4973+ return customized_record
49584974 return record
49594975
49604976 # Cache readers and the publisher share resolution, including the cache
0 commit comments