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 9c691aa367e..0f5d63c32cd 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 @@ -2170,7 +2170,9 @@ protected static final DDSpanContext buildSpanContext( requestContextDataAppSec = null; requestContextDataIast = null; ciVisibilityContextData = null; - propagationTags = tracer.propagationTagsFactory.empty(); + // Trace-level propagation state lives on the local root; a child's own instance is only + // created on demand (see DDSpanContext#ownPropagationTags) + propagationTags = null; } else { long endToEndStartTime; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index d0f71b4ab01..f14a3d9c2ef 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -25,6 +25,7 @@ import datadog.trace.api.datastreams.PathwayContext; import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.internal.TraceSegment; +import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.api.llmobs.LLMObsPropagationValues; import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.sampling.SamplingMechanism; @@ -48,6 +49,7 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Function; import javax.annotation.Nonnull; import org.slf4j.Logger; @@ -169,7 +171,17 @@ public class DDSpanContext implements AgentSpanContext, ProfilerContext { private final boolean disableSamplingMechanismValidation; - private final PropagationTags propagationTags; + /** + * This span's own propagation tags. Local child spans leave this null until something writes to + * it, since trace-level state is read from the local root. Access via {@link + * #ownPropagationTags()}. + */ + private volatile PropagationTags propagationTags; + + private static final AtomicReferenceFieldUpdater + PROPAGATION_TAGS = + AtomicReferenceFieldUpdater.newUpdater( + DDSpanContext.class, PropagationTags.class, "propagationTags"); private volatile PathwayContext pathwayContext; @@ -425,11 +437,10 @@ public DDSpanContext( this.threadName = THREAD_NAMES.computeIfAbsent(current.getName(), Functions.UTF8_ENCODE); this.disableSamplingMechanismValidation = disableSamplingMechanismValidation; - this.propagationTags = - propagationTags != null - ? propagationTags - : traceCollector.getTracer().getPropagationTagsFactory().empty(); - this.propagationTags.updateTraceIdHighOrderBits(this.traceId.toHighOrderLong()); + if (propagationTags != null) { + propagationTags.updateTraceIdHighOrderBits(this.traceId.toHighOrderLong()); + this.propagationTags = propagationTags; + } this.injectBaggageAsTags = injectBaggageAsTags; this.injectLinksAsTags = injectLinksAsTags; if (origin != null) { @@ -438,7 +449,31 @@ public DDSpanContext( if (samplingPriority != PrioritySampling.UNSET) { setSamplingPriority(samplingPriority, SamplingMechanism.UNKNOWN); } - setTag(PARENT_ID, this.propagationTags.getLastParentId()); + if (propagationTags != null) { + setTag(PARENT_ID, propagationTags.getLastParentId()); + } + } + + /** Returns this span's own propagation tags, creating an empty instance on first use. */ + private PropagationTags ownPropagationTags() { + PropagationTags tags = propagationTags; + if (tags != null) { + return tags; + } + PropagationTags created = newEmptyPropagationTags(); + return PROPAGATION_TAGS.compareAndSet(this, null, created) ? created : propagationTags; + } + + @VisibleForTesting + boolean hasOwnPropagationTags() { + return propagationTags != null; + } + + /** An empty instance equivalent to what a span without its own propagation tags would hold. */ + private PropagationTags newEmptyPropagationTags() { + PropagationTags tags = traceCollector.getTracer().getPropagationTagsFactory().empty(); + tags.updateTraceIdHighOrderBits(traceId.toHighOrderLong()); + return tags; } @Override @@ -678,15 +713,15 @@ public void forceKeep(byte samplingMechanism) { } private void forceKeepThisSpan(byte samplingMechanism) { - propagationTags.forceKeep(samplingMechanism); + ownPropagationTags().forceKeep(samplingMechanism); } public void addPropagatedTraceSource(final int value) { - propagationTags.addTraceSource(value); + ownPropagationTags().addTraceSource(value); } public void updateDebugPropagation(String value) { - propagationTags.updateDebugPropagation(value); + ownPropagationTags().updateDebugPropagation(value); } /** @@ -717,8 +752,9 @@ private boolean setThisSpanSamplingPriority(final int newPriority, final int new if (!validateSamplingPriority(newPriority, newMechanism)) { return false; } + PropagationTags tags = ownPropagationTags(); boolean updated = - propagationTags.tryUpdateTraceSamplingPriority( + tags.tryUpdateTraceSamplingPriority( newPriority, newMechanism, SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism)); @@ -726,7 +762,7 @@ private boolean setThisSpanSamplingPriority(final int newPriority, final int new if (log.isDebugEnabled()) { log.debug( "samplingPriority locked at priority: {}. Refusing to set to priority: {} mechanism: {}", - propagationTags.getSamplingPriority(), + tags.getSamplingPriority(), newPriority, newMechanism); } @@ -745,13 +781,15 @@ public boolean setSamplingPriority( if (!spanContext.validateSamplingPriority(newPriority, newMechanism)) { return false; } - return spanContext.propagationTags.tryUpdateProbabilitySamplingDecision( - newPriority, - newMechanism, - sampleRate, - rateLimiterRejected, - traceIdLowOrderBits, - SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism)); + return spanContext + .ownPropagationTags() + .tryUpdateProbabilitySamplingDecision( + newPriority, + newMechanism, + sampleRate, + rateLimiterRejected, + traceIdLowOrderBits, + SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism)); } private boolean validateSamplingPriority(final int newPriority, final int newMechanism) { @@ -782,7 +820,7 @@ private boolean validateSamplingPriority(final int newPriority, final int newMec @Override public int getSamplingPriority() { - return getRootSpanContextOrThis().propagationTags.getSamplingPriority(); + return getRootSpanContextOrThis().ownPropagationTags().getSamplingPriority(); } public void setSpanSamplingPriority(double rate, int limit) { @@ -814,7 +852,7 @@ public boolean lockSamplingPriority() { return rootSpan.spanContext().lockSamplingPriority(); } - return propagationTags.getSamplingPriority() != PrioritySampling.UNSET; + return ownPropagationTags().getSamplingPriority() != PrioritySampling.UNSET; } public CharSequence getOrigin() { @@ -954,7 +992,7 @@ public String getSpanKindString() { public void setOrigin(final CharSequence origin) { DDSpanContext context = getRootSpanContextOrThis(); context.origin = origin; - context.propagationTags.updateTraceOrigin(origin); + context.ownPropagationTags().updateTraceOrigin(origin); } public void setMetric(final CharSequence key, final Number value) { @@ -1506,7 +1544,9 @@ void processTagsAndBaggage( if ((options & ROOT_PROPAGATION_TAGS) != 0) { serializedPropagationTags = getPropagationTags(); } else if ((options & OWN_PROPAGATION_TAGS) != 0) { - serializedPropagationTags = this.propagationTags; + PropagationTags own = propagationTags; + // Don't retain an instance just to serialize it + serializedPropagationTags = own != null ? own : newEmptyPropagationTags(); } else { serializedPropagationTags = null; } @@ -1731,7 +1771,7 @@ public TraceSegment getTraceSegment() { } public PropagationTags getPropagationTags() { - return getRootSpanContextOrThis().propagationTags; + return getRootSpanContextOrThis().ownPropagationTags(); } @Override diff --git a/dd-trace-core/src/test/java/datadog/trace/core/ChildSpanPropagationTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/ChildSpanPropagationTagsTest.java new file mode 100644 index 00000000000..6d2b3339ffa --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/ChildSpanPropagationTagsTest.java @@ -0,0 +1,149 @@ +package datadog.trace.core; + +import static datadog.trace.api.TracePropagationStyle.DATADOG; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; +import static datadog.trace.core.CoreSpan.OWN_PROPAGATION_TAGS; +import static datadog.trace.core.CoreSpan.ROOT_PROPAGATION_TAGS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.DDTraceId; +import datadog.trace.api.ProductTraceSource; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.propagation.ExtractedContext; +import datadog.trace.core.propagation.PropagationTags; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Local child spans don't hold their own {@link PropagationTags} unless something writes to them, + * yet serialize exactly what an eagerly created empty instance would. + */ +class ChildSpanPropagationTagsTest extends DDCoreJavaSpecification { + + private static final String TRACE_ID_128 = "640cfd8d00000000abcdef0123456789"; + + private CoreTracer tracer; + + @BeforeEach + void setup() { + tracer = tracerBuilder().writer(new ListWriter()).build(); + } + + @AfterEach + void cleanup() { + tracer.close(); + } + + @Test + void childOfLocalRootHoldsNoPropagationTags() { + DDSpan root = (DDSpan) tracer.buildSpan("test", "root").start(); + DDSpan child = childOf(root); + + child.setSamplingPriority(USER_KEEP, MANUAL); + child.spanContext().setOrigin("synthetics"); + child.spanContext().getSamplingPriority(); + child.spanContext().getPropagationTags(); + child.processTagsAndBaggage(MetadataConsumer.NO_OP, ROOT_PROPAGATION_TAGS); + + assertFalse(child.spanContext().hasOwnPropagationTags()); + assertEquals(USER_KEEP, root.spanContext().getSamplingPriority()); + assertSame(root.spanContext().getPropagationTags(), child.spanContext().getPropagationTags()); + } + + @Test + void childSerializesLikeEmptyPropagationTags() { + DDSpan root = (DDSpan) tracer.buildSpan("test", "root").start(); + DDSpan child = childOf(root); + + assertEquals(expectedOwnTags(child), ownTags(child)); + assertFalse(child.spanContext().hasOwnPropagationTags()); + } + + @Test + void childOf128BitTraceSerializesTraceIdHighOrderBits() { + DDSpan root = extractedRoot(DDTraceId.fromHex(TRACE_ID_128), "_dd.p.usr=123"); + DDSpan child = childOf(root); + + Map own = ownTags(child); + + assertEquals(expectedOwnTags(child), own); + assertEquals(Collections.singletonMap("_dd.p.tid", "640cfd8d00000000"), own); + assertFalse(child.spanContext().hasOwnPropagationTags()); + } + + @Test + void rootSerializationUnaffectedByChildren() { + DDSpan root = extractedRoot(DDTraceId.fromHex(TRACE_ID_128), "_dd.p.usr=123"); + childOf(root).setSamplingPriority(USER_KEEP, MANUAL); + + Map rootTags = tags(root, ROOT_PROPAGATION_TAGS); + + assertEquals(root.spanContext().getPropagationTags().createTagMap(), rootTags); + assertEquals("123", rootTags.get("_dd.p.usr")); + assertEquals("640cfd8d00000000", rootTags.get("_dd.p.tid")); + } + + @Test + void traceSourceOnChildStaysOnChild() { + DDSpan root = (DDSpan) tracer.buildSpan("test", "root").start(); + DDSpan child = childOf(root); + + child.setTag("_dd.p.ts", ProductTraceSource.ASM); + + assertTrue(child.spanContext().hasOwnPropagationTags()); + assertEquals("02", ownTags(child).get("_dd.p.ts")); + assertFalse(root.spanContext().getPropagationTags().createTagMap().containsKey("_dd.p.ts")); + } + + @Test + void debugPropagationOnChildStaysOnChild() { + DDSpan root = (DDSpan) tracer.buildSpan("test", "root").start(); + DDSpan child = childOf(root); + + child.setTag("_dd.p.debug", "probe"); + + assertTrue(child.spanContext().hasOwnPropagationTags()); + assertEquals("probe", ownTags(child).get("_dd.p.debug")); + assertFalse(root.spanContext().getPropagationTags().createTagMap().containsKey("_dd.p.debug")); + } + + private DDSpan extractedRoot(DDTraceId traceId, String header) { + PropagationTags propagationTags = + tracer + .getPropagationTagsFactory() + .fromHeaderValue(PropagationTags.HeaderType.DATADOG, header); + ExtractedContext extracted = + new ExtractedContext(traceId, 456, SAMPLER_KEEP, null, propagationTags, DATADOG); + return (DDSpan) tracer.buildSpan("test", "root").asChildOf(extracted).start(); + } + + private DDSpan childOf(DDSpan parent) { + return (DDSpan) tracer.buildSpan("test", "child").asChildOf(parent.spanContext()).start(); + } + + /** What a child span's eagerly created empty propagation tags serialized to. */ + private Map expectedOwnTags(DDSpan span) { + PropagationTags empty = tracer.getPropagationTagsFactory().empty(); + empty.updateTraceIdHighOrderBits(span.getTraceId().toHighOrderLong()); + return empty.createTagMap(); + } + + private static Map ownTags(DDSpan span) { + return tags(span, OWN_PROPAGATION_TAGS); + } + + private static Map tags(DDSpan span, int options) { + Map captured = new HashMap<>(); + span.processTagsAndBaggage(metadata -> captured.putAll(metadata.getBaggage()), options); + return captured; + } +}