Skip to content

feat(llmobs): add agent attribution to Java SDK - #12238

Draft
yahya-mouman wants to merge 3 commits into
masterfrom
yahya/llmobs-agent-attribution
Draft

feat(llmobs): add agent attribution to Java SDK#12238
yahya-mouman wants to merge 3 commits into
masterfrom
yahya/llmobs-agent-attribution

Conversation

@yahya-mouman

Copy link
Copy Markdown

Summary

Agent attribution tracks the nearest agent-kind ancestor span for every LLMObs span and emits it as meta.agent_attribution = {"pagent_name": str|null, "pagent_span_id": str}. This feature is already merged in Python (#18788), JavaScript (#9175), and Go. This PR brings the Java SDK to parity.

Wire contract

  • Propagation headers: _dd.p.llmobs_pagent_span_id, _dd.p.llmobs_pagent_name (in x-datadog-tags)
  • Serialized field: meta.agent_attribution = {"pagent_name": str|null, "pagent_span_id": str}

Three-case resolution (O(1) at span start)

  1. This span is kind=agent → self-attribution (own span ID + name)
  2. In-process LLMObs parent → inherit from LLMObsContext thread-local context
  3. Distributed → read from root APM span's PropagationTags via LLMObsPropagationAccess

Changes

File Change
internal-api/.../LLMObsPropagationAccess.java New bridge interface so agent-llmobs can read/write pagent ptags on DDSpanContext without a direct dd-trace-core dependency
internal-api/.../LLMObsContext.java Two new ContextKey<String> fields + 4-arg attach() overload for pagent propagation
dd-trace-core/.../PropagationTags.java Four new abstract methods for pagent getters/setters
dd-trace-core/.../ptags/PTagsFactory.java Two volatile TagValue fields + implementations following the orgPropagationMarker pattern
dd-trace-core/.../ptags/PTagsCodec.java Two new TagKey constants + size accounting + header emission
dd-trace-core/.../ptags/DatadogPTagsCodec.java Extraction of _dd.p.llmobs_pagent_span_id and _dd.p.llmobs_pagent_name from inbound headers
dd-trace-core/.../DDSpanContext.java Implements LLMObsPropagationAccess by delegating to getPropagationTags()
dd-trace-core/.../LLMObsSpanMapper.java Serializes two internal pagent tags as a single agent_attribution map in meta
agent-llmobs/.../DDLLMObsSpan.java Resolution logic in constructor; wire-safe agent name validation (char-scan, no allocation)
agent-llmobs/.../DDLLMObsSpanAgentAttributionTest.java New JUnit 5 tests: self-attribution, unsafe name, in-process inheritance, transitive, no-ancestor, distributed
dd-trace-core/.../LLMObsSpanMapperTest.java Three new tests: both fields emitted, explicit null name, block absent when no ID

Test plan

  • ./gradlew :dd-java-agent:agent-llmobs:test — new DDLLMObsSpanAgentAttributionTest covers all resolution cases
  • ./gradlew :dd-trace-core:test — new mapper tests verify agent_attribution wire format including explicit-null name path
  • ./gradlew spotlessApply — format check (Java not available locally; CI will verify)
  • Manual: create an agent → llm → tool chain and verify meta.agent_attribution appears on tool and llm spans pointing to the agent

🤖 Generated with Claude Code

yahya-mouman and others added 3 commits August 18, 2026 22:06
Extends agent attribution (pagent_name/pagent_span_id) to dd-trace-java,
matching the existing implementation in dd-trace-py, dd-trace-js, and dd-trace-go.

Every LLMObs span now carries meta.agent_attribution = {pagent_name, pagent_span_id}
identifying its nearest agent-kind ancestor, resolved O(1) at span start.

Changes:
- LLMObsPropagationAccess: new bridge interface in internal-api allowing agent-llmobs
  to read/write _dd.p.llmobs_pagent_* propagation tags on the APM span context
  without a direct dd-trace-core dependency
- LLMObsContext: adds PAGENT_SPAN_ID_KEY/PAGENT_NAME_KEY context keys and extended
  attach() overload so agent attribution propagates in-process to descendants
- PropagationTags + PTags: adds getParentAgentSpanId/Name and updateParentAgentSpanId/Name
  with volatile TagValue fields and header cache invalidation
- PTagsCodec: defines PARENT_AGENT_SPAN_ID_TAG/PARENT_AGENT_NAME_TAG constants,
  emits both in headerValue() and fillTagMap()
- DatadogPTagsCodec: extracts _dd.p.llmobs_pagent_* from incoming x-datadog-tags header
- DDSpanContext: implements LLMObsPropagationAccess by delegating to getPropagationTags()
- DDLLMObsSpan: resolves attribution at span start (agent spans write themselves;
  non-agent spans inherit from context; distributed case reads from root span PTags);
  wire-safe validation for agent names (printable ASCII, no commas/semicolons, ≤256 bytes)
- LLMObsSpanMapper: serializes agent_attribution as a structured sub-map in meta,
  emitting pagent_name as explicit null when name was dropped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests cover the three resolution cases in DDLLMObsSpan (self-as-agent,
in-process context inheritance, distributed propagation) and the
serializer's agent_attribution block emission in LLMObsSpanMapper,
including the explicit-null name path when only pagent_span_id is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…guard

Fix metaSize to subtract 1 whenever pagent_name is in tagsToRemapToMeta
(not only when both pagent fields are present) to prevent an off-by-one
if name is set without span_id, making the formula symmetric with the
actual skip logic in the serializer loop.

Replace getBytes(UTF_8) in agentNameWireSafe with a char-by-char scan,
eliminating the per-agent-span byte array allocation. Because the loop
rejects c > 0x7E, every passing char is single-byte UTF-8 so length()
is an exact byte-count proxy for the 256-byte limit.

Drop the redundant resolvedPagentSpanId != null guard on the agent-kind
propagation block - always non-null at that point.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yahya-mouman yahya-mouman added type: feature Enhancements and improvements comp: core Tracer core labels Aug 19, 2026
@dd-octo-sts

dd-octo-sts Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.86 s 14.57 s [+1.1%; +2.9%] (significantly worse)
startup:insecure-bank:tracing:Agent 13.64 s 13.68 s [-1.1%; +0.6%] (no difference)
startup:petclinic:appsec:Agent 16.41 s 16.25 s [-5.0%; +7.0%] (unstable)
startup:petclinic:iast:Agent 16.91 s 16.93 s [-0.9%; +0.6%] (no difference)
startup:petclinic:profiling:Agent 16.46 s 16.49 s [-6.5%; +6.1%] (unstable)
startup:petclinic:sca:Agent 16.67 s 16.11 s [-0.8%; +7.7%] (no difference)
startup:petclinic:tracing:Agent 16.21 s 16.03 s [+0.1%; +2.2%] (maybe worse)

Commit: c63a6152 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

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

Labels

comp: core Tracer core type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant