From 5e6003063d4f93d28ec21518bdb083cea1612f56 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 16:35:58 -0400 Subject: [PATCH 1/3] Skip Datadog tags a trace context merge never reads Merging a trace context only reads the tracestate's dd member for the sampling priority, the last parent id and whether its tags are valid. Its other tags were still built as tag values, LLM Observability ids uncached, and collected into a list that the merge then discarded. A merge now skips any tag whose value isn't validated before building it; the trace id, decision maker and trace source are still built and validated as before. validateTagValue now checks only the keys hasValidatedValues names, so the two can't disagree. Co-Authored-By: Claude Opus 5.5 --- .../core/propagation/ptags/PTagsCodec.java | 14 ++++++++++++- .../core/propagation/ptags/W3CPTagsCodec.java | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java index 29867d08d83..c1e6c908a8e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java @@ -361,8 +361,20 @@ protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingStat protected abstract boolean isEmpty(StringBuilder sb, int size); + /** + * Whether values of {@code tagKey} are validated. {@link #validateTagValue} checks only these, so + * a key missing here goes unvalidated everywhere rather than only where this is consulted. + */ + static boolean hasValidatedValues(TagKey tagKey) { + return tagKey.equals(DECISION_MAKER_TAG) + || tagKey.equals(TRACE_ID_TAG) + || tagKey.equals(TRACE_SOURCE_TAG); + } + protected static boolean validateTagValue(TagKey tagKey, TagValue tagValue) { - if (tagKey.equals(DECISION_MAKER_TAG) && !validateDecisionMakerTag(tagValue)) { + if (!hasValidatedValues(tagKey)) { + return true; + } else if (tagKey.equals(DECISION_MAKER_TAG) && !validateDecisionMakerTag(tagValue)) { return false; } else if (tagKey.equals(TRACE_ID_TAG) && !validateTraceId(tagValue)) { return false; 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 9f292518b89..96c341a3fdf 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 @@ -199,6 +199,11 @@ private static R parse(String value, Result result) { } else { TagKey tagKey = TagKey.from(Encoding.W3C, value, tagPos, tagKeyEndsAt); if (tagKey != null) { + if (!result.keepsDatadogTags() && !hasValidatedValues(tagKey)) { + // Its value is never read, and it can't be invalid, so it changes nothing + tagPos = nextTagPos; + continue; + } TagValue tagValue = tagValue(Encoding.W3C, tagKey, value, tagValuePos, tagValueEndsAt); if (!tagKey.equals(UPSTREAM_SERVICES_DEPRECATED_TAG)) { if (!validateTagValue(tagKey, tagValue)) { @@ -1027,6 +1032,12 @@ private static int memberContributionSize( /** What a parse of a tracestate header turns into, at each of its outcomes. */ private interface Result { + /** + * Whether the {@code dd} member's tags are kept. If not, they're only checked for validity, + * which decides between {@link #parsed} and {@link #withoutDatadogTags}. + */ + boolean keepsDatadogTags(); + /** The header is malformed, so nothing from it is kept. */ R rejected(); @@ -1068,6 +1079,11 @@ private static final class PTagsResult implements Result { this.tagsFactory = tagsFactory; } + @Override + public boolean keepsDatadogTags() { + return true; + } + @Override public PropagationTags rejected() { return tagsFactory.empty(); @@ -1137,6 +1153,11 @@ public PropagationTags parsed( private static final Result MERGE_RESULT = new Result() { + @Override + public boolean keepsDatadogTags() { + return false; + } + @Override public W3CTracestate rejected() { return W3CTracestate.NONE; From 8fbcf28f9826a7e83faee608c37495a7f6513a60 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 16:41:42 -0400 Subject: [PATCH 2/3] Don't rescan a merged tracestate that has no otel member Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/propagation/ptags/PTagsFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9add16a41e0..997fada2b3b 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 @@ -955,7 +955,8 @@ public void updateW3CTracestate(String tracestate) { @Override public void updateW3CTracestateFrom(W3CTracestate source, int overridingPriority) { if (source.rawOtelTraceState == null) { - setW3CTracestate(source.tracestate, W3CPTagsCodec.extractOtelTraceState(source.tracestate)); + // the parse already looked for an otel member, so there is none + setW3CTracestate(source.tracestate, null); return; } // Keep the otel member unparsed; its consistency checks wait for the first read From e519965be2a8f74d902ceb83b48bce223a42ed7a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 16:41:45 -0400 Subject: [PATCH 3/3] Fix where a comment says request context data goes Co-Authored-By: Claude Opus 5.5 --- dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 4bc10d31054..8dbc38dc67c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -1949,7 +1949,7 @@ protected static final AgentSpan startSpan( if (parentSpanContext != null && parentSpanContext.isRemote()) { // Preserve AppSec/IAST request context before dropping the remote parent: when // extract=IGNORE or RESTART the TagContext parent is nulled before buildSpanContext - // can copy requestContextDataAppSec/Iast into DDSpanContext. + // can copy requestContextDataAppSec/Iast onto the trace collector. if (parentSpanContext instanceof TagContext) { TagContext tc = (TagContext) parentSpanContext; if (builderRequestContextDataAppSec == null) {