@@ -41,6 +41,70 @@ Then run:
4141BRAINTRUST_API_KEY=< YOUR_API_KEY> braintrust eval tutorial_eval.py
4242```
4343
44+ ## Customizing instrumentation exports
45+
46+ Use synchronous span customizers to redact captured values or add metadata without
47+ changing provider responses:
48+
49+ ``` python
50+ import braintrust
51+
52+
53+ class Redact (braintrust .SpanCustomizer ):
54+ def on_span_export (self , data : braintrust.SpanExportData) -> braintrust.SpanExportData:
55+ for field in (" input" , " output" ):
56+ if field in data:
57+ data[field] = " [redacted]"
58+ data.pop(" error" , None )
59+ return data
60+
61+
62+ braintrust.auto_instrument(span_customizers = [Redact()])
63+ braintrust.init_logger(project = " my-project" )
64+ ```
65+
66+ For explicit wrappers or integration setup, call
67+ ` braintrust.set_span_customizers([Redact()]) ` before instrumented calls. Both APIs
68+ replace the process-wide ordered list with a snapshot of the supplied sequence;
69+ the customizer objects themselves are not copied. ` set_span_customizers(None) ` or
70+ an empty sequence disables customization. Omitting ` span_customizers ` (or passing
71+ ` None ` ) to ` auto_instrument() ` leaves existing configuration unchanged. There is no
72+ environment-variable registration.
73+
74+ ` SpanCustomizer ` is an extensible base class with a default no-op
75+ ` on_span_export(data) ` method. Hooks run synchronously in registration order, each
76+ receiving its predecessor's result. Return a plain ` dict ` containing the data to
77+ export: either the input record or a replacement. Replacement is ** not** a merge;
78+ retain the content fields you want to export. Returning ` None ` cannot drop a span.
79+
80+ Hooks apply only to ** native instrumentation-created span records** , not manually
81+ created spans (even children of instrumented spans), dataset rows, feedback, or
82+ spans exported through a separate OpenTelemetry exporter. They run after lazy
83+ values resolve and before attachment processing, merging, masking, and transport
84+ serialization. Existing attachment objects retain their identity; hooks can
85+ remove them before upload without copying their contents.
86+
87+ A span can produce several incremental records, including records flushed before
88+ it ends. Input, output, metrics, and errors need not be present together. Removing
89+ a field does not retract an earlier export, so redact every record containing
90+ sensitive data. Each record uses the configuration at its first export resolution.
91+ Retries reuse the transformed record without calling hooks again.
92+
93+ Identity, parentage, routing, and merge protocol fields are restored after every
94+ hook, including nested protocol arrays. Protected fields are ` id ` , ` span_id ` ,
95+ ` root_span_id ` , ` span_parents ` , ` org_id ` , ` project_id ` , ` experiment_id ` , ` dataset_id ` ,
96+ ` prompt_session_id ` , ` log_id ` , ` function_data ` , ` _is_merge ` , ` _merge_paths ` ,
97+ ` _parent_id ` , ` _object_delete ` , ` _array_delete ` , and ` _xact_id ` . Hooks cannot add an
98+ absent protected field or change the destination.
99+
100+ ** Failures are fail-open.** Exceptions, ` None ` , awaitables, and other invalid return
101+ types are ignored; later hooks and export continue with the current record.
102+ In-place content mutations made before failure are retained, not rolled back.
103+ An asynchronous hook is not awaited. A failing redaction hook can therefore leak
104+ unredacted data: never throw to block export. Returned content must remain valid
105+ for SDK serialization. Keep hooks fast, avoid blocking I/O, and make them safe for
106+ background export threads rather than assuming the caller's thread or context.
107+
44108## Optional Extras
45109
46110Install extras as needed for specific workflows:
0 commit comments