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 1bef57374f0..3a0c57a4dd8 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 @@ -79,8 +79,6 @@ public interface Factory { public abstract CharSequence getLastParentId(); - public abstract void updateLastParentId(CharSequence lastParentId); - /** * Gets the original W3C * tracestate header value. @@ -104,6 +102,15 @@ public interface Factory { */ public abstract String headerValue(HeaderType headerType); + /** + * Like {@link #headerValue(HeaderType)} but uses {@code lastParentIdOverride} for the W3C {@code + * p:} (last-parent-id) instead of the stored {@link #getLastParentId() last-parent-id}. Used at + * inject so the injecting span's id is supplied as a parameter rather than mutated into these + * (possibly trace-level, shared) tags — keeping transient per-injection identity out of shared + * state. A {@code null} override falls back to {@link #headerValue(HeaderType)}. + */ + public abstract String headerValue(HeaderType headerType, CharSequence lastParentIdOverride); + /** * Fills a provided tagMap with valid propagated _dd.p.* tags and possibly a new sampling decision * tags _dd.p.dm (root span only) based on the current state, or sets only an error tag if the diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/W3CHttpCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/W3CHttpCodec.java index f8372f0d76c..1d1d55cecc4 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/W3CHttpCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/W3CHttpCodec.java @@ -80,8 +80,11 @@ private void injectTraceParent(DDSpanContext context, C carrier, CarrierSett private void injectTraceState(DDSpanContext context, C carrier, CarrierSetter setter) { PropagationTags propagationTags = context.getPropagationTags(); - propagationTags.updateLastParentId(DDSpanId.toHexStringPadded(context.getSpanId())); - String tracestate = propagationTags.headerValue(W3C); + // Supply the injecting span's id for the W3C `p:` as a parameter rather than mutating it into + // the (possibly trace-level, shared) tags — keeps transient per-injection identity out of + // shared state, so concurrent sibling injects can't race on it. + String tracestate = + propagationTags.headerValue(W3C, DDSpanId.toHexStringPadded(context.getSpanId())); if (tracestate != null && !tracestate.isEmpty()) { setter.set(carrier, TRACE_STATE_KEY, tracestate); } 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 5dc2eaafc7d..e2c0658a1d2 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 @@ -25,6 +25,10 @@ abstract class PTagsCodec { protected static final TagKey UPSTREAM_SERVICES_DEPRECATED_TAG = TagKey.from("upstream_services"); static String headerValue(PTagsCodec codec, PTags ptags) { + return headerValue(codec, ptags, null); + } + + static String headerValue(PTagsCodec codec, PTags ptags, CharSequence lastParentIdOverride) { int estimate = codec.estimateHeaderSize(ptags); if (estimate == 0) { return ""; @@ -32,7 +36,7 @@ static String headerValue(PTagsCodec codec, PTags ptags) { // No encoding validation here because we don't allow arbitrary tag change StringBuilder sb = new StringBuilder(estimate); - int size = codec.appendPrefix(sb, ptags); + int size = codec.appendPrefix(sb, ptags, lastParentIdOverride); if (!ptags.isPropagationTagsDisabled()) { if (ptags.getDecisionMakerTagValue() != null) { size = codec.appendTag(sb, DECISION_MAKER_TAG, ptags.getDecisionMakerTagValue(), size); @@ -174,6 +178,14 @@ static int calcXDatadogTagsSize(int size, TagKey tagKey, TagValue tagValue) { protected abstract int appendPrefix(StringBuilder sb, PTags ptags); + /** + * Encode the prefix, using {@code lastParentIdOverride} for the W3C {@code p:} when non-null + * (inject-time). Codecs without a last-parent-id (e.g. Datadog) ignore the override. + */ + protected int appendPrefix(StringBuilder sb, PTags ptags, CharSequence lastParentIdOverride) { + return appendPrefix(sb, ptags); + } + protected abstract int appendTag(StringBuilder sb, TagElement key, TagElement value, int size); protected abstract int appendSuffix(StringBuilder sb, PTags ptags, int size); 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 4d3135a5fe1..0b5184d448a 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 @@ -420,14 +420,6 @@ public CharSequence getLastParentId() { return lastParentId; } - @Override - public void updateLastParentId(CharSequence lastParentId) { - if (!Objects.equals(this.lastParentId, lastParentId)) { - clearCachedHeader(W3C); - this.lastParentId = TagValue.from(lastParentId); - } - } - @Override @SuppressWarnings("StringEquality") @SuppressFBWarnings("ES_COMPARING_STRINGS_WITH_EQ") @@ -448,6 +440,18 @@ public String headerValue(HeaderType headerType) { return header; } + @Override + public String headerValue(HeaderType headerType, CharSequence lastParentIdOverride) { + if (lastParentIdOverride == null) { + return headerValue(headerType); + } + // Inject-time path: encode fresh with the override; do NOT cache — the W3C `p:` is + // per-injecting-span and these tags may be shared across sibling spans. + String header = + PTagsCodec.headerValue(factory.getDecoderEncoder(headerType), this, lastParentIdOverride); + return (header == null || header.isEmpty()) ? null : header; + } + @Override public void fillTagMap(Map tagMap) { PTagsCodec.fillTagMap(this, tagMap); 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 b0a9959a131..0430bbc3a2c 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 @@ -226,6 +226,11 @@ protected int estimateHeaderSize(PTags pTags) { @Override protected int appendPrefix(StringBuilder sb, PTags ptags) { + return appendPrefix(sb, ptags, null); + } + + @Override + protected int appendPrefix(StringBuilder sb, PTags ptags, CharSequence lastParentIdOverride) { sb.append(DATADOG_MEMBER_KEY); // Append sampling priority (s) if (ptags.getSamplingPriority() != PrioritySampling.UNSET) { @@ -246,7 +251,8 @@ protected int appendPrefix(StringBuilder sb, PTags ptags) { } } // append last ParentId (p) - CharSequence lastParent = ptags.getLastParentId(); + CharSequence lastParent = + lastParentIdOverride != null ? lastParentIdOverride : ptags.getLastParentId(); if (lastParent != null) { if (sb.length() > EMPTY_SIZE) { sb.append(';'); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/PropagationTagsLastParentIdTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/PropagationTagsLastParentIdTest.java new file mode 100644 index 00000000000..643a39126f1 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/PropagationTagsLastParentIdTest.java @@ -0,0 +1,61 @@ +package datadog.trace.core.propagation; + +import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * The inject-time W3C last-parent-id ({@code p:}) is supplied as a parameter to {@link + * PropagationTags#headerValue(PropagationTags.HeaderType, CharSequence)} rather than mutated into + * the (possibly trace-level, shared) tags. This keeps transient per-injection identity out of + * shared state: a sibling span's inject can't pollute another span's header, and the stored inbound + * last-parent-id is never overwritten. + */ +class PropagationTagsLastParentIdTest { + + private static final String SPAN_A = "00000000000000aa"; + private static final String SPAN_B = "00000000000000bb"; + + private static PropagationTags w3c(String header) { + return PropagationTags.factory().fromHeaderValue(W3C, header); + } + + @Test + void overrideSuppliesW3cLastParentId() { + PropagationTags tags = w3c("dd=s:1;o:rum"); + assertTrue(tags.headerValue(W3C, SPAN_A).contains("p:" + SPAN_A)); + } + + @Test + void overrideDoesNotMutateSharedTags_noCrossTalk() { + // One tags instance, two sibling spans injecting through it (the shared-root scenario). + PropagationTags shared = w3c("dd=s:1;o:rum"); // no inbound p: + + String headerA = shared.headerValue(W3C, SPAN_A); + String headerB = shared.headerValue(W3C, SPAN_B); + String headerAagain = shared.headerValue(W3C, SPAN_A); + + assertTrue(headerA.contains("p:" + SPAN_A)); + assertTrue(headerB.contains("p:" + SPAN_B)); + // Injecting B did not change what A injects — no shared mutation. + assertEquals(headerA, headerAagain, "a sibling inject must not change another span's header"); + // The override is never written into the shared tags (no-override header has no p:). + assertFalse(shared.headerValue(W3C).contains("p:"), "override must not mutate the stored tags"); + } + + @Test + void inboundLastParentIdPreservedAndUnmutatedByOverride() { + PropagationTags tags = w3c("dd=s:1;p:" + SPAN_A); // arrived carrying a last-parent-id + + // No-override path (e.g. span-link traceState) keeps the inbound p:. + assertTrue(tags.headerValue(W3C).contains("p:" + SPAN_A)); + // An inject override replaces it for that produced header... + assertTrue(tags.headerValue(W3C, SPAN_B).contains("p:" + SPAN_B)); + // ...without mutating the stored inbound value. + assertTrue( + tags.headerValue(W3C).contains("p:" + SPAN_A), "inbound p: must survive override use"); + } +}