Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ef534bc
feat(llmobs): add agent attribution to Java SDK
yahya-mouman Aug 18, 2026
95f4def
Add JUnit 5 tests for agent attribution in Java SDK
yahya-mouman Aug 18, 2026
ced256a
Harden agent attribution: fix metaSize, remove byte alloc, drop dead …
yahya-mouman Aug 19, 2026
f64bcd3
Fix missing assertNull import in LLMObsSpanMapperTest
yahya-mouman Aug 25, 2026
7fb3a21
Fix agent attribution: drop erroneous trace-ID gate on LLMObsContext …
yahya-mouman Aug 25, 2026
0ddc42e
Address Codex P2: re-add trace-ID gate for pagent context inheritance
yahya-mouman Aug 26, 2026
077e8b8
Fix agent attribution: decode pagent from W3C tracestate; propagate t…
yahya-mouman Aug 27, 2026
f72f2d9
test(llmobs): add nested agent restore and stale-context gate tests
yahya-mouman Aug 27, 2026
6b386c3
fix(llmobs): address Codex P2 review comments on agent attribution
yahya-mouman Aug 28, 2026
b4b915a
fix(llmobs): propagate agent attribution on standalone outgoing requests
yahya-mouman Aug 28, 2026
acecb40
revert(llmobs): remove distributed agent attribution propagation via …
yahya-mouman Aug 28, 2026
f96d561
style(llmobs): apply spotless formatting
yahya-mouman Aug 31, 2026
c53028f
fix(llmobs): close agent spans in Groovy tests to prevent scope leakage
yahya-mouman Aug 31, 2026
b390240
test(llmobs): add integration test for agent attribution via public API
yahya-mouman Aug 31, 2026
639f492
refactor(llmobs): address sabrenner review comments
yahya-mouman Aug 31, 2026
7828327
refactor(llmobs): spell out pagent as parentAgent in all variable names
yahya-mouman Aug 31, 2026
df0963c
refactor(llmobs): move standaloneApmScope to a dedicated follow-up PR
yahya-mouman Aug 31, 2026
ce0c701
style(llmobs): apply spotless formatting to agent attribution files
yahya-mouman Aug 31, 2026
eb272f7
test(llmobs): add APM root scope to AgentAttributionIntegrationTest
yahya-mouman Aug 31, 2026
35e8b84
P1/P2: fix Javadoc, centralize pagent constants, document null-key co…
yahya-mouman Aug 31, 2026
6339a75
Revert OpenAI auto-instrumentation test from AgentAttributionIntegrat…
yahya-mouman Aug 31, 2026
9d4a832
Remove AgentAttributionIntegrationTest — unit tests cover all cases
yahya-mouman Aug 31, 2026
55ea5d1
Fix stale pagent key leakage and missing trace gate in OpenAI decorator
yahya-mouman Aug 31, 2026
5af838a
Use manifest name for pagent attribution; remove agentNameWireSafe re…
yahya-mouman Aug 31, 2026
bbb23b6
spotless: collapse PAGENT_NAME_TAG_INTERNAL onto one line
yahya-mouman Aug 31, 2026
0c01f6c
Fix flaky test: drop unsafe scope re-attachment from annotateAgentMan…
yahya-mouman Aug 31, 2026
3dff323
spotless: expand LLMObsContext.attach call to one-arg-per-line
yahya-mouman Aug 31, 2026
a993935
fix(llmobs): reintroduce standaloneApmScope for standalone agent spans
yahya-mouman Aug 31, 2026
67a981a
docs(llmobs): fix contradictory trace-gate comment after standaloneAp…
yahya-mouman Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import datadog.trace.api.llmobs.LLMObsSpan;
import datadog.trace.api.llmobs.LLMObsTags;
import datadog.trace.api.telemetry.LLMObsMetricCollector;
import datadog.trace.bootstrap.instrumentation.api.AgentScope;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
Expand Down Expand Up @@ -50,6 +51,9 @@ public class DDLLMObsSpan implements LLMObsSpan {
private static final String CONTEXT_VARIABLE_KEYS = "_dd_context_variable_keys";
private static final String QUERY_VARIABLE_KEYS = "_dd_query_variable_keys";
private static final String PARENT_ID_TAG_INTERNAL = "parent_id";
private static final String PAGENT_SPAN_ID_TAG_INTERNAL =
LLMOBS_TAG_PREFIX + LLMObsTags.PAGENT_SPAN_ID;
private static final String PAGENT_NAME_TAG_INTERNAL = LLMOBS_TAG_PREFIX + LLMObsTags.PAGENT_NAME;

private static final String SERVICE = LLMOBS_TAG_PREFIX + "service";
private static final String VERSION = LLMOBS_TAG_PREFIX + "version";
Expand All @@ -63,9 +67,12 @@ public class DDLLMObsSpan implements LLMObsSpan {
private final AgentSpan span;
private final String spanKind;
private final String mlApp;
private final ContextScope scope;
private final boolean hasSessionId;
private final boolean hasAgentVersion;
private final ContextScope scope;
// Non-null only for agent-kind spans started without an ambient APM root. Activating the
// agent's APM span keeps children in the same APM trace so the trace-ID gate passes and
// they inherit agent attribution correctly.
private final AgentScope standaloneApmScope;

private boolean finished = false;

Expand Down Expand Up @@ -156,14 +163,58 @@ public DDLLMObsSpan(
if (this.hasSessionId) {
span.setTag(LLMOBS_TAG_PREFIX + LLMObsTags.SESSION_ID, sessionId);
}
this.hasAgentVersion = resolvedAgentVersion != null && !resolvedAgentVersion.isEmpty();
if (this.hasAgentVersion) {
if (resolvedAgentVersion != null && !resolvedAgentVersion.isEmpty()) {
span.setTag(LLMOBS_TAG_PREFIX + LLMObsTags.AGENT_VERSION, resolvedAgentVersion);
}
span.setTag(LLMOBS_TAG_PREFIX + PARENT_ID_TAG_INTERNAL, parentSpanID);
// Propagate the effective sessionId and agent_version to descendant LLMObs spans via the
// context.
scope = LLMObsContext.attach(span.spanContext(), sessionId, resolvedAgentVersion);

// Resolve agent attribution (O(1)): identify the nearest agent-kind ancestor.
String resolvedParentAgentSpanId = null;
String resolvedParentAgentName = null;

if (Tags.LLMOBS_AGENT_SPAN_KIND.equals(kind)) {
// This span is itself an agent — it becomes the nearest ancestor for its descendants.
// Use the span name as the initial pagent name; annotateAgentManifest() will update it
// to the manifest name if one is provided later.
resolvedParentAgentSpanId = String.valueOf(span.getSpanId());
resolvedParentAgentName = spanName;
} else {
// Inherit from in-process LLMObs parent only when the context belongs to the same trace.
// Matches the gate applied to parent_id and session_id above: a stale LLMObsContext
// leaked across an async boundary would otherwise attribute a span to an agent from a
// different trace. For standalone agent spans (no ambient APM root), standaloneApmScope
// ensures descendants are started under the agent's APM span so this gate passes.
if (null != parent && parent.getTraceId() == span.getTraceId()) {
Comment thread
yahya-mouman marked this conversation as resolved.
resolvedParentAgentSpanId = LLMObsContext.currentParentAgentSpanId();
resolvedParentAgentName = LLMObsContext.currentParentAgentName();
}
}

// Store pagent values as internal tags so the serializer can emit agent_attribution.
if (resolvedParentAgentSpanId != null) {
span.setTag(PAGENT_SPAN_ID_TAG_INTERNAL, resolvedParentAgentSpanId);
if (resolvedParentAgentName != null) {
span.setTag(PAGENT_NAME_TAG_INTERNAL, resolvedParentAgentName);
}
}

// Propagate the effective sessionId and agent attribution to descendant LLMObs spans.
scope =
LLMObsContext.attach(
span.spanContext(),
sessionId,
resolvedAgentVersion,
resolvedParentAgentSpanId,
resolvedParentAgentName);

// For standalone agent spans (no ambient APM root), activate the underlying APM span so
// that child LLMObs spans share the same trace ID and pass the trace-ID gate. Without
// this, children start a fresh APM trace, the gate rejects the agent context, and
// agent attribution is silently dropped.
standaloneApmScope =
Tags.LLMOBS_AGENT_SPAN_KIND.equals(kind) && span.getLocalRootSpan() == span
? AgentTracer.activateSpan(span)
: null;
}

@Override
Expand Down Expand Up @@ -340,6 +391,10 @@ public void annotateAgentManifest(LLMObs.AgentManifest manifest) {
mergeManifest(base, manifest);
base.put("framework", MANUAL_FRAMEWORK);
span.setTag(AGENT_MANIFEST, base);

// Sync pagent name to the manifest name so the serializer emits the manifest name in
// agent_attribution. The manifest name takes priority over the span name set at construction.
span.setTag(PAGENT_NAME_TAG_INTERNAL, (String) base.get("name"));
}

private void mergeManifest(Map<String, Object> base, LLMObs.AgentManifest manifest) {
Expand Down Expand Up @@ -601,6 +656,9 @@ public void finish() {
return;
}
span.finish();
if (standaloneApmScope != null) {
standaloneApmScope.close();
}
scope.close();
finished = true;
boolean isRootSpan = span.getLocalRootSpan() == span;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ class DDLLMObsSpanTest extends DDSpecification{
"v1" == tagVersion.toString()

DDTraceApiInfo.VERSION == innerSpan.getTag(LLMOBS_TAG_PREFIX + "ddtrace.version")

cleanup:
test.finish()
}

def "test llm span string input formatted to messages"() {
Expand Down Expand Up @@ -511,6 +514,9 @@ class DDLLMObsSpanTest extends DDSpecification{
innerSpan.getTag(INPUT_PROMPT) == null
innerSpan.getTag(PROMPT_TRACKING_INSTRUMENTATION_METHOD) == null

cleanup:
test.finish()

where:
spanKind << [
Tags.LLMOBS_AGENT_SPAN_KIND,
Expand Down
Loading