Skip to content

feat: add span customizers (SDK-316) - #217

Open
Andrew Kent (realark) wants to merge 1 commit into
mainfrom
ark/SDK-316-span-customizers
Open

Andrew Kent (realark) wants to merge 1 commit into
mainfrom
ark/SDK-316-span-customizers

Conversation

@realark

@realark Andrew Kent (realark) commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

AI Generated Description

Summary

Implements SDK-316 using the span customizer specification and the Java exporter reference.

  • Add Braintrust::SpanCustomizer#on_span_export and ordered span_customizers: registration through init/config/exporter APIs.
  • Customize detached completed OpenTelemetry SpanData after origin enrichment, before destination grouping and OTLP serialization.
  • Allow mutations/replacements/rerouting while preserving trace, span, and parent IDs and isolating original application/exporter data.
  • Fail the entire batch before any transmission on hook, identity, return-type, or serialization errors; no fallback to unredacted spans.
  • Keep the no-customizer path unchanged and document configuration, concurrency, retry, and failure semantics.

Verification

  • Customizer/exporter/origin/config/state/tracing regression selection: 76 tests, 183 assertions, zero failures/errors/skips.
  • Standalone real local HTTP/OTLP smoke: redaction with preserved span identity and unchanged application data.
  • Changed-file Standard lint and repository precommit passed.

Tradeoffs

Configured customization deep-copies SDK SpanData and buffers encoded destination groups before sending, providing fail-closed batch behavior. Transport retries reuse encoded bytes; an explicit export resubmission reruns hooks. Arbitrary non-marshallable state added to SpanData subclasses fails closed.

@realark
Andrew Kent (realark) force-pushed the ark/SDK-316-span-customizers branch 2 times, most recently from de41942 to 10d8e13 Compare September 24, 2026 14:36
@realark
Andrew Kent (realark) marked this pull request as ready for review September 24, 2026 14:36
@realark
Andrew Kent (realark) requested a review from a team as a code owner September 24, 2026 14:36

@Qard Stephen Belanger (Qard) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally LGTM.

I'm wondering if we should align the plugins system that just landed in bt trace with the span customization work. They're very similar, but plugins also have a context object with a couple things:

https://github.com/braintrustdata/braintrust-coding-agent-plugins/pull/28/changes#diff-a28148101bd6acba8868e4304b7663c4f9faff9c4a8e5d34d9f7d343989324e1R34-R39

That has env, if the operation is an insert or a merge, and the session ID. It also has source, which is the name of the coding agent, but probably unnecessary for languages to report their own language when you're already in that language in the hook.

We could basically just add the context parameter to the span customization spec with just those first three properties. What do you think?

Comment thread lib/braintrust/trace/span_exporter.rb Outdated
Comment thread lib/braintrust/trace/span_exporter.rb Outdated
super(endpoint: endpoint, headers: {"Authorization" => "Bearer #{api_key}"})
end

def export(span_data, timeout: nil)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit messy: its a long function with somewhat duplicated operations, which makes it harder to reason about and the behaviors (origin, customization) harder to isolate from one other. The goal is to make Exporter know as little about span customization as possible, just how to call it for what it needs.

I'd prefer, if possible, to follow the example of SpanOrigin where we isolate its behaviors in a separate component then compose them back in. Because a customizer is state, it'd make more sense for this to be a class instead of a module, maybe SpanCustomizers. In this class you'd isolate the private methods for customize and expose only the public ones that need to be exported.

A clean export function is a good sign where it does simple transforms back to back (e.g. SpanOrigin then SpanCustomizer then super) without lots of if/else clauses and repeated, nearly identical calls (e.g. group_by and super()). We used prepend before to even make SpanOrigin automatic behavior (no code changes to Exporter) which was nice; would be cool here, but might be a tall order.

Given we seem to like to mutate on export, it may make more sense to reformulate our exporter as some kind of middleware stack pattern: might make it easier to reliably compose these behaviors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. AI summary:

Addressed the feedback.

• Added SpanCustomizers to own registration, hook ordering, tracing suppression, writable attributes, and identity validation.
• Simplified the exporter to compose origin → customization → grouping → encoding → transport, with one grouping pass.
• Added SpanOrigin.enrich_batch, shared by explicit composition and the existing prepend behavior.
• Kept whole-batch serialization validation before transmission and the existing no-customizer transport path.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants