From d11bd629cdf9fc1b90e25ec085e23962f3c2d132 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Fri, 4 Sep 2026 13:30:21 +0200 Subject: [PATCH] feat(otlp): export cached OpenTelemetry trace state --- .../core/otlp/trace/OtlpTraceCollector.java | 14 +++ .../trace/core/otlp/trace/OtlpTraceJson.java | 17 +-- .../otlp/trace/OtlpTraceJsonCollector.java | 11 +- .../trace/core/otlp/trace/OtlpTraceProto.java | 14 +-- .../otlp/trace/OtlpTraceProtoCollector.java | 12 ++- .../core/propagation/PropagationTags.java | 7 ++ .../core/propagation/ptags/PTagsFactory.java | 8 +- .../core/propagation/ptags/W3CPTagsCodec.java | 4 +- .../trace/OtlpTraceJsonCollectorTest.java | 61 +++++++++-- .../core/otlp/trace/OtlpTraceProtoTest.java | 102 ++++++++++++++++++ 10 files changed, 220 insertions(+), 30 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceCollector.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceCollector.java index 127e27af467..faa9697960b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceCollector.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceCollector.java @@ -3,12 +3,15 @@ import static datadog.trace.core.DDSpanContext.SPAN_SAMPLING_MECHANISM_TAG; import datadog.trace.core.CoreSpan; +import datadog.trace.core.DDSpan; import datadog.trace.core.otlp.common.OtlpPayload; import java.util.List; /** Collects traces ready for export. */ public abstract class OtlpTraceCollector { + private static final String OTEL_TRACE_STATE_PREFIX = "ot="; + /** Adds spans from the given trace to the collector. */ public abstract void addTrace(List> spans); @@ -22,4 +25,15 @@ protected final boolean shouldExport(CoreSpan span) { return span.samplingPriority() > 0 // trace-level sampling priority || span.getTag(SPAN_SAMPLING_MECHANISM_TAG) != null; // span-level sampling priority } + + protected final String getOtlpOtelTraceState(List> spans) { + for (CoreSpan span : spans) { + if (span instanceof DDSpan) { + String otelTraceState = + ((DDSpan) span).spanContext().getPropagationTags().getOtelTraceState(); + return otelTraceState == null ? null : OTEL_TRACE_STATE_PREFIX + otelTraceState; + } + } + return null; + } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java index d9c5e9c3d90..7927baf4c09 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java @@ -32,7 +32,6 @@ import datadog.trace.core.Metadata; import datadog.trace.core.MetadataConsumer; import datadog.trace.core.PendingTrace; -import datadog.trace.core.propagation.PropagationTags; import java.util.List; import java.util.Map; @@ -48,17 +47,23 @@ private OtlpTraceJson() {} /** Writes one complete {@code Span} JSON object. */ public static void writeSpan( - JsonWriter writer, DDSpan span, MetaWriter metaWriter, List links) { - PropagationTags propagationTags = span.spanContext().getPropagationTags(); + JsonWriter writer, + DDSpan span, + MetaWriter metaWriter, + List links, + String otelTraceState) { writer.beginObject(); writer.name("traceId").value(hexTraceId(span.getTraceId())); writer.name("spanId").value(hexSpanId(span.getSpanId())); - String tracestate = propagationTags.getW3CTracestate(); - if (tracestate != null) { - writer.name("traceState").value(tracestate); + String traceState = span.spanContext().getPropagationTags().getW3CTracestate(); + if (traceState == null) { + traceState = otelTraceState; + } + if (traceState != null) { + writer.name("traceState").value(traceState); } if (span.getParentId() != 0) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollector.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollector.java index cbb39ed615b..bfda8da93e5 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollector.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollector.java @@ -46,6 +46,7 @@ public final class OtlpTraceJsonCollector extends OtlpTraceCollector { private OtelInstrumentationScope currentScope; private DDSpan currentSpan; private List currentSpanLinks = Collections.emptyList(); + private String currentOtelTraceState; /** Adds the given trace spans to the collector. */ @Override @@ -56,8 +57,9 @@ public void addTrace(List> spans) { } try { + String otelTraceState = getOtlpOtelTraceState(spans); for (CoreSpan span : spans) { - visitSpan(span); + visitSpan(span, otelTraceState); } } catch (Throwable e) { // reset the buffer for subsequent traces @@ -114,6 +116,7 @@ private void stop() { currentScope = null; currentSpan = null; currentSpanLinks = Collections.emptyList(); + currentOtelTraceState = null; } private void visitScopedSpans(OtelInstrumentationScope scope) { @@ -128,7 +131,7 @@ private void visitScopedSpans(OtelInstrumentationScope scope) { writer.name("spans").beginArray(); } - private void visitSpan(CoreSpan span) { + private void visitSpan(CoreSpan span, String otelTraceState) { if (!shouldExport(span)) { return; } @@ -141,6 +144,7 @@ private void visitSpan(CoreSpan span) { } currentSpan = (DDSpan) span; currentSpanLinks = currentSpan.getLinks(); + currentOtelTraceState = otelTraceState; } // called once we've processed all scopes and span messages @@ -185,12 +189,13 @@ private void completeSpan() { metaWriter.includeProcessTags(); firstSpanInScope = false; } - writeSpan(writer, currentSpan, metaWriter, currentSpanLinks); + writeSpan(writer, currentSpan, metaWriter, currentSpanLinks, currentOtelTraceState); anySpanWritten = true; // reset temporary elements for next span currentSpan = null; currentSpanLinks = Collections.emptyList(); + currentOtelTraceState = null; if (writer.size() > MAX_CAPACITY_BYTES) { throw new IllegalStateException( diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java index f97d05c388d..95a20aa1dc3 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java @@ -46,7 +46,6 @@ import datadog.trace.core.MetadataConsumer; import datadog.trace.core.PendingTrace; import datadog.trace.core.otlp.common.OtlpProtoBuffer; -import datadog.trace.core.propagation.PropagationTags; /** Provides optimized writers for OpenTelemetry's "trace.proto" wire protocol. */ public final class OtlpTraceProto { @@ -81,8 +80,8 @@ public static int recordSpanMessage( DDSpan span, MetaWriter metaWriter, int nestedSpanLinkBytes, - OtlpProtoBuffer protobuf) { - PropagationTags propagationTags = span.spanContext().getPropagationTags(); + OtlpProtoBuffer protobuf, + String otelTraceState) { writeTag(buf, 1, LEN_WIRE_TYPE); writeTraceId(buf, span.getTraceId()); @@ -90,10 +89,13 @@ public static int recordSpanMessage( writeTag(buf, 2, LEN_WIRE_TYPE); writeSpanId(buf, span.getSpanId()); - String tracestate = propagationTags.getW3CTracestate(); - if (tracestate != null) { + String traceState = span.spanContext().getPropagationTags().getW3CTracestate(); + if (traceState == null) { + traceState = otelTraceState; + } + if (traceState != null) { writeTag(buf, 3, LEN_WIRE_TYPE); - writeString(buf, tracestate); + writeString(buf, traceState); } if (span.getParentId() != 0) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProtoCollector.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProtoCollector.java index ba51d9f9f3d..5c4aa186291 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProtoCollector.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProtoCollector.java @@ -47,6 +47,7 @@ public final class OtlpTraceProtoCollector extends OtlpTraceCollector { private OtelInstrumentationScope currentScope; private DDSpan currentSpan; + private String currentOtelTraceState; /** Adds the given trace spans to the collector. */ @Override @@ -57,9 +58,10 @@ public void addTrace(List> spans) { } try { + String otelTraceState = getOtlpOtelTraceState(spans); // OtlpProtoBuffer collects spans in reverse for (int i = spans.size() - 1; i >= 0; i--) { - visitSpan(spans.get(i)); + visitSpan(spans.get(i), otelTraceState); } } catch (Throwable e) { // reset the buffer for subsequent traces @@ -109,6 +111,7 @@ private void stop() { currentScope = null; currentSpan = null; + currentOtelTraceState = null; } private void visitScopedSpans(OtelInstrumentationScope scope) { @@ -118,7 +121,7 @@ private void visitScopedSpans(OtelInstrumentationScope scope) { currentScope = scope; } - private void visitSpan(CoreSpan span) { + private void visitSpan(CoreSpan span, String otelTraceState) { if (!shouldExport(span)) { return; } @@ -131,6 +134,7 @@ private void visitSpan(CoreSpan span) { completeSpan(); } currentSpan = (DDSpan) span; + currentOtelTraceState = otelTraceState; currentSpan.getLinks().forEach(this::visitSpanLink); } @@ -178,10 +182,12 @@ private void completeScope() { // called once we've processed all span-links in a specific span private void completeSpan() { - scopedBytes += recordSpanMessage(buf, currentSpan, metaWriter, spanBytes, protobuf); + scopedBytes += + recordSpanMessage(buf, currentSpan, metaWriter, spanBytes, protobuf, currentOtelTraceState); // reset temporary elements for next span currentSpan = null; + currentOtelTraceState = null; spanBytes = 0; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java index cf84eb9e70f..044fec6727b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java @@ -90,6 +90,13 @@ public abstract void updateOtelTraceState( */ public abstract String getW3CTracestate(); + /** + * Gets the OpenTelemetry {@code ot} tracestate member value, without the member key. + * + * @return The OpenTelemetry tracestate value, or {@code null} if none is present. + */ + public abstract String getOtelTraceState(); + /** * Stores the original W3C * tracestate header value. diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index f6a79d64e3f..b3d8501e0d1 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java @@ -575,10 +575,16 @@ private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) this.otelTraceState = otelTraceState; } - OtelTraceState getOtelTraceState() { + OtelTraceState getOtelTraceStateForW3C() { return otelTraceState; } + @Override + public String getOtelTraceState() { + OtelTraceState state = otelTraceState; + return state == null ? null : state.getValue(); + } + void setOtelTraceState(OtelTraceState otelTraceState) { if (this.otelTraceState != otelTraceState) { this.otelTraceState = otelTraceState; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index 34074be90d1..0dafc693e5c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -270,7 +270,7 @@ protected int estimateHeaderSize(PTags pTags) { size += pTags.tracestate.length(); includesOriginalTracestate = true; } - OtelTraceState otelTraceState = pTags.getOtelTraceState(); + OtelTraceState otelTraceState = pTags.getOtelTraceStateForW3C(); if (otelTraceState != null) { size -= includesOriginalTracestate ? otelTraceState.getOriginalMemberContributionSize() : 0; size += OTEL_MEMBER_KEY.length() + otelTraceState.length() + 1; @@ -749,7 +749,7 @@ private static int cleanUpAndAppendUnknown(StringBuilder sb, W3CPTags w3CPTags, private static boolean appendOtelAndVendorMembers( StringBuilder sb, PTags ptags, boolean hasDatadogMember) { String original = ptags.tracestate; - OtelTraceState otelTraceState = ptags.getOtelTraceState(); + OtelTraceState otelTraceState = ptags.getOtelTraceStateForW3C(); int remainingMembers = MAX_MEMBER_COUNT - (hasDatadogMember ? 1 : 0); int otherMemberPosition = 0; boolean otelTraceStateAppended = false; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index 17ec0c0d17f..ea8d9aad72c 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -130,27 +130,70 @@ void spanTraceStateOmittedWhenNotPropagated() throws IOException { } @Test - void spanTraceStateIncludedWhenPropagated() throws IOException { + void spansExportFullTraceStateWithOtelMember() throws IOException { PropagationTags propagationTags = PropagationTags.factory().empty(); - propagationTags.updateW3CTracestate("vendor=state"); + propagationTags.updateW3CTracestate("vendor=state,ot=rv:0abcdef1234567;th:80000000000000"); + propagationTags.updateTraceSamplingPriority( + PrioritySampling.USER_KEEP, SamplingMechanism.EXTERNAL_OVERRIDE); ExtractedContext parent = new ExtractedContext( DDTraceId.ONE, 0L, - PrioritySampling.UNSET, + PrioritySampling.USER_KEEP, null, propagationTags, TracePropagationStyle.DATADOG); - AgentSpan agentSpan = TRACER.startSpan("test", "op.tracestate", parent); - agentSpan.setResourceName("op.tracestate"); - agentSpan.finish(); + AgentSpan root = TRACER.startSpan("test", "op.tracestate.root", parent); + root.setResourceName("op.tracestate.root"); + AgentSpan child = TRACER.startSpan("test", "op.tracestate.child", root.spanContext()); + child.setResourceName("op.tracestate.child"); + child.finish(); + root.finish(); OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); - collector.addTrace(asList((CoreSpan) agentSpan)); - Map parsedSpan = onlySpan(collector.collectTraces()); + collector.addTrace(asList((CoreSpan) root, (CoreSpan) child)); + List> parsedSpans = allSpans(collector.collectTraces()); + + assertEquals(2, parsedSpans.size()); + for (Map parsedSpan : parsedSpans) { + assertEquals( + "vendor=state,ot=rv:0abcdef1234567;th:80000000000000", parsedSpan.get("traceState")); + } + } + + @Test + void spansExportOtelTraceStateWithoutW3CTraceState() throws IOException { + PropagationTags propagationTags = PropagationTags.factory().empty(); + propagationTags.updateTraceSamplingPriority( + PrioritySampling.USER_KEEP, SamplingMechanism.EXTERNAL_OVERRIDE); + propagationTags.updateOtelTraceState( + DDTraceId.ONE.toLong(), 0.5, true, PrioritySampling.USER_KEEP); + String traceState = "ot=" + propagationTags.getOtelTraceState(); + ExtractedContext parent = + new ExtractedContext( + DDTraceId.ONE, + 0L, + PrioritySampling.USER_KEEP, + null, + propagationTags, + TracePropagationStyle.DATADOG); - assertEquals("vendor=state", parsedSpan.get("traceState")); + AgentSpan root = TRACER.startSpan("test", "op.otel.root", parent); + root.setResourceName("op.otel.root"); + AgentSpan child = TRACER.startSpan("test", "op.otel.child", root.spanContext()); + child.setResourceName("op.otel.child"); + child.finish(); + root.finish(); + + OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); + collector.addTrace(asList((CoreSpan) root, (CoreSpan) child)); + List> parsedSpans = allSpans(collector.collectTraces()); + + assertEquals(2, parsedSpans.size()); + for (Map parsedSpan : parsedSpans) { + assertEquals(traceState, parsedSpan.get("traceState")); + } } @Test diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java index aa9d7c7022b..8f028104390 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java @@ -676,6 +676,69 @@ void testSpanOrderInTracePreserved() throws IOException { "spans must appear in trace order in the payload"); } + @Test + void spansExportFullTraceStateWithOtelMember() throws IOException { + PropagationTags propagationTags = PropagationTags.factory().empty(); + propagationTags.updateW3CTracestate("vendor=state,ot=rv:0abcdef1234567;th:80000000000000"); + propagationTags.updateTraceSamplingPriority( + PrioritySampling.USER_KEEP, SamplingMechanism.EXTERNAL_OVERRIDE); + ExtractedContext parent = + new ExtractedContext( + DDTraceId.ONE, + 0L, + PrioritySampling.USER_KEEP, + null, + propagationTags, + TracePropagationStyle.DATADOG); + + AgentSpan root = TRACER.startSpan("test", "op.tracestate.root", parent); + root.setResourceName("op.tracestate.root"); + AgentSpan child = TRACER.startSpan("test", "op.tracestate.child", root.spanContext()); + child.setResourceName("op.tracestate.child"); + child.finish(); + root.finish(); + + OtlpTraceProtoCollector collector = new OtlpTraceProtoCollector(); + collector.addTrace(asList((DDSpan) root, (DDSpan) child)); + + assertEquals( + asList( + "vendor=state,ot=rv:0abcdef1234567;th:80000000000000", + "vendor=state,ot=rv:0abcdef1234567;th:80000000000000"), + parseSpanTraceStatesFromPayload(collector.collectTraces())); + } + + @Test + void spansExportOtelTraceStateWithoutW3CTraceState() throws IOException { + PropagationTags propagationTags = PropagationTags.factory().empty(); + propagationTags.updateTraceSamplingPriority( + PrioritySampling.USER_KEEP, SamplingMechanism.EXTERNAL_OVERRIDE); + propagationTags.updateOtelTraceState( + DDTraceId.ONE.toLong(), 0.5, true, PrioritySampling.USER_KEEP); + String traceState = "ot=" + propagationTags.getOtelTraceState(); + ExtractedContext parent = + new ExtractedContext( + DDTraceId.ONE, + 0L, + PrioritySampling.USER_KEEP, + null, + propagationTags, + TracePropagationStyle.DATADOG); + + AgentSpan root = TRACER.startSpan("test", "op.otel.root", parent); + root.setResourceName("op.otel.root"); + AgentSpan child = TRACER.startSpan("test", "op.otel.child", root.spanContext()); + child.setResourceName("op.otel.child"); + child.finish(); + root.finish(); + + OtlpTraceProtoCollector collector = new OtlpTraceProtoCollector(); + collector.addTrace(asList((DDSpan) root, (DDSpan) child)); + + assertEquals( + asList(traceState, traceState), parseSpanTraceStatesFromPayload(collector.collectTraces())); + } + private static List parseSpanNamesFromPayload(OtlpPayload payload) throws IOException { CodedInputStream tracesData = CodedInputStream.newInstance(payload.getContent()); tracesData.readTag(); // field 1: TracesData.resource_spans @@ -715,6 +778,45 @@ private static List parseSpanNamesFromPayload(OtlpPayload payload) throw return names; } + private static List parseSpanTraceStatesFromPayload(OtlpPayload payload) + throws IOException { + CodedInputStream tracesData = CodedInputStream.newInstance(payload.getContent()); + tracesData.readTag(); + CodedInputStream resourceSpans = tracesData.readBytes().newCodedInput(); + + CodedInputStream scopeSpans = null; + while (!resourceSpans.isAtEnd()) { + int tag = resourceSpans.readTag(); + if (WireFormat.getTagFieldNumber(tag) == 2) { + scopeSpans = resourceSpans.readBytes().newCodedInput(); + } else { + resourceSpans.skipField(tag); + } + } + assertNotNull(scopeSpans, "ScopeSpans must be present in ResourceSpans"); + + List traceStates = new ArrayList<>(); + while (!scopeSpans.isAtEnd()) { + int tag = scopeSpans.readTag(); + if (WireFormat.getTagFieldNumber(tag) != 2) { + scopeSpans.skipField(tag); + continue; + } + CodedInputStream spanData = scopeSpans.readBytes().newCodedInput(); + String traceState = null; + while (!spanData.isAtEnd()) { + int spanTag = spanData.readTag(); + if (WireFormat.getTagFieldNumber(spanTag) == 3) { + traceState = spanData.readString(); + } else { + spanData.skipField(spanTag); + } + } + traceStates.add(traceState); + } + return traceStates; + } + // ── span construction ───────────────────────────────────────────────────── /** Builds {@link DDSpan} instances from the given specs, collecting them in order. */