Propagate LLM Observability context (session, ml_app, agent attribution) across process boundaries - #12402
Draft
ncybul wants to merge 5 commits into
Draft
Propagate LLM Observability context (session, ml_app, agent attribution) across process boundaries#12402ncybul wants to merge 5 commits into
ncybul wants to merge 5 commits into
Conversation
Adds LLMObs.injectDistributedHeaders/activateDistributedHeaders so applications can manually propagate LLMObs context (ml_app, session_id, agent attribution) across boundaries automatic instrumentation doesn't cover, e.g. an SQS worker reading its own message attributes. Standard APM trace context rides the normal Propagators.defaultPropagator(); LLMObs tags piggyback on the existing x-datadog-tags carrier entry to stay wire-compatible with dd-trace-py/js/go. Claude session: `15543c2c-2abe-408e-b16e-05ddbe972287` Resume: `claude --resume 15543c2c-2abe-408e-b16e-05ddbe972287`
Contributor
|
🎯 Code Coverage (details) 🔗 Commit SHA: fb3651c | Docs | View more details | Give us feedback! |
Contributor
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
…s fields Replaces hand-rolled x-datadog-tags string manipulation in DDLLMObsPropagator with dedicated PropagationTags fields (mirroring dm/tid/ts/opm) for ml_app, session_id, and agent id/name, wired through both the Datadog and W3C codecs via an AgentSpanContext seam so agent-llmobs stays free of a dd-trace-core dependency.
ExtractedContext didn't override the AgentSpanContext.getLLMObs*() getters, so they fell back to the null-returning interface defaults instead of reading its own PropagationTags field — session_id/ml_app/ agent-attribution set on the wire were silently lost when read back via activateDistributedHeaders().
A pre-existing DDLLMObsSpanTest.groovy case never finished its span, leaking an open LLMObsContext scope into whichever test ran next in the same JVM. Add the missing finish(). Also make activateWithoutTraceContextIsNoOp compare against the ambient LLMObsContext going in rather than asserting a global null baseline, since it shouldn't assume it's the only test that ever touches it.
Previously the _dd.p.llmobs_* propagation tags were only ever written onto a span context inside DDLLMObsPropagator.injectDistributedHeaders, so they reached the wire only where an application propagated them by hand. An LLMObs span followed by a plain auto-instrumented HTTP call carried no LLMObs context at all, unlike dd-trace-py. Register an LLMObsContextPropagator as a propagation concern instead. It contributes no headers of its own: it stages ml_app, session_id and agent attribution onto the span context ahead of the tracing propagator, which then serializes them like any other propagation tag. This mirrors dd-trace-py, where LLMObs subscribes to the generic http.span_inject hook rather than owning a wire format, and covers every boundary automatic instrumentation already reaches. Values are resolved from the ambient LLMObsContext at injection time, so the innermost active span wins and leaving a scope stops contributing tags without any save/restore bookkeeping. ml_app moves into LLMObsContext for this, having previously lived only as a span tag. On the receive side, DDLLMObsSpan now inherits session_id and agent attribution from the propagated context when no in-process LLMObs parent applies. DDLLMObsPropagator remains as the manual entry point for carriers no instrumentation covers, such as SQS message attributes.
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an explicit, manual API for propagating LLM Observability context across process boundaries that automatic instrumentation doesn't cover — e.g. an SQS worker reading its own message attributes. Mirrors dd-trace-py's
inject_distributed_headers/activate_distributed_headersmodel (Python/Node/Go already support this; Java did not).Design
TracingIteratorbatch-context-reuse bug in APM's automatic SQS instrumentation rather than fixing it — that's a separate, not-yet-filed issue.Propagators.defaultPropagator(). LLMObs-specific values (ml_app,session_id, agent id/name) are carried as dedicatedPropagationTagsfields —_dd.p.llmobs_ml_app,_dd.p.llmobs_sid,_dd.p.llmobs_pagent_span_id,_dd.p.llmobs_pagent_name— mirroring howdm/tid/ts/opmalready ride insidex-datadog-tags/tracestatet.*. These key names were checked against dd-trace-py's_constants.pyand match exactly, so a mixed-language pipeline can still join a trace across this hop.key=valuepairs directly onto thex-datadog-tagscarrier string fromagent-llmobs, self-contained to that module. This PR replaces that with properPropagationTagsfields indd-trace-core(bothDatadogPTagsCodecandW3CPTagsCodec), wired through a newAgentSpanContextseam (getLLMObsMlApp/updateLLMObsMlApp/etc., default no-ops) soagent-llmobsstill doesn't need a directdd-trace-coredependency. This is the same seam pattern already used fordm/tid/ts/opm.ml_app,session_id, and agent id/name unbounded (no fixed length cap) and relies on the codec's own overflow handling. Java originally added a self-invented 64-char truncation cap on these fields; that's been removed so all 4 fields pass through unbounded, matching Python. One gap is intentionally not replicated: Python proactively computes whether the full tag budget fits within 485 of the 512-bytex-datadog-tagslimit and gracefully truncates/dropsparent_agent_name(then the id) to avoid the whole header being dropped on overflow. Java relies on the existing codec-level behavior instead — the Datadog codec drops the wholex-datadog-tagsheader on overflow (all-or-nothing), while the W3C codec already gracefully drops individual overlong tags without invalidating the rest. Implementing full parity would require threading a "does this fit" check fromPTagsFactoryup throughAgentSpanContexttoDDLLMObsPropagator, which is out of scope for this PR — flagging as a known, deliberate gap for a fast-follow.LLMObs.injectDistributedHeaders/activateDistributedHeaders(dd-trace-api), delegating to a newLLMObs.LLMObsPropagatorinterface, following the same static-field delegation pattern asSPAN_FACTORY/EVAL_PROCESSOR/FEEDBACK_PROCESSOR. Real implementation isDDLLMObsPropagator(agent-llmobs), wired in viaLLMObsInternal.setPropagator(...); defaults to a no-op when the agent isn't active.Bug fixed along the way
ExtractedContext(the context produced byPropagators.defaultPropagator().extract(...)) didn't override the newAgentSpanContext.getLLMObs*()getters, so they fell back to the interface's null-returning defaults instead of readingExtractedContext's ownPropagationTagsfield. This silently droppedsession_id/ml_app/agent-attribution on the consumer side ofactivateDistributedHeaderseven though the values were correctly on the wire. Fixed by adding the four delegating overrides inExtractedContext.Known scope limitations
ml_apprides on the wire for cross-SDK compatibility but is not yet auto-applied to spans started afteractivateDistributedHeaders— callers must still passml_appexplicitly when starting a span.agent_versionis not propagated cross-process (no SDK sends it today).parent_agent_namebudget-check/graceful-truncation parity with dd-trace-py (see Design above) is deferred.Test plan
DDLLMObsPropagatorTest(JUnit 5) — round-trips a plainMap<String, String>carrier (the shape an SQS message-attribute map would take): trace-id join across inject/activate, session_id/agent-attribution propagation, no-op on missing trace context, null-arg validation, ml_app always present even without session/attribution. All 5 tests pass, including the session_id/agent-attribution propagation case that caught theExtractedContextbug above.:dd-java-agent:agent-llmobs:testpasses (no regressions).:dd-trace-core:test(propagation package) passes (no regressions).spotlessApply/spotlessCheckpass.DatadogPTagsCodecandW3CPTagsCodec— tracked as a follow-up on this branch.Claude session:
15543c2c-2abe-408e-b16e-05ddbe972287Resume:
claude --resume 15543c2c-2abe-408e-b16e-05ddbe972287