From e213175b49f3fa8e7d0dcebe4d01dfbf78d6d687 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Thu, 27 Aug 2026 10:19:59 +0200 Subject: [PATCH 1/7] feat(core): support OTel tracestate sampling --- .../sampling/RateByServiceTraceSampler.java | 25 ++- .../sampling/RuleBasedTraceSampler.java | 12 +- .../java/datadog/trace/core/CoreSpan.java | 7 + .../main/java/datadog/trace/core/DDSpan.java | 24 +- .../datadog/trace/core/DDSpanContext.java | 8 +- .../core/propagation/PropagationTags.java | 3 + .../propagation/ptags/OtelTraceState.java | 212 +++++++++++++++++- .../core/propagation/ptags/PTagsFactory.java | 28 +++ .../trace/common/metrics/SimpleSpan.java | 10 + .../trace/common/writer/TraceGenerator.java | 10 + .../trace/core/CoreSpanBuilderTest.java | 69 +++++- .../trace/core/OtelSamplingDecisionTest.java | 162 +++++++++++++ .../OtelTraceStatePropagationTest.java | 172 ++++++++++++++ .../propagation/ptags/OtelTraceStateTest.java | 94 ++++++++ .../traceAgentTest/java/TraceGenerator.java | 10 + 15 files changed, 825 insertions(+), 21 deletions(-) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java index 0940469c61c..7424dd91493 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java @@ -58,16 +58,19 @@ public > void setSamplingPriority(final T span) { final RateSamplersByEnvAndService rates = serviceRates; RateSampler sampler = rates.getSampler(env, serviceName); + boolean sampled = sampler.sample(span); + int samplingPriority = sampled ? PrioritySampling.SAMPLER_KEEP : PrioritySampling.SAMPLER_DROP; - if (sampler.sample(span)) { + if (rates.hasAgentRates()) { span.setSamplingPriority( - PrioritySampling.SAMPLER_KEEP, + samplingPriority, SAMPLING_AGENT_RATE, sampler.getSampleRate(), - SamplingMechanism.AGENT_RATE); + SamplingMechanism.AGENT_RATE, + sampled); } else { span.setSamplingPriority( - PrioritySampling.SAMPLER_DROP, + samplingPriority, SAMPLING_AGENT_RATE, sampler.getSampleRate(), SamplingMechanism.AGENT_RATE); @@ -161,7 +164,7 @@ public void onResponse( if (canIncrease && anyCapped) { lastCappedNanos = now; } - serviceRates = new RateSamplersByEnvAndService(updatedEnvServiceRates, fallbackSampler); + serviceRates = new RateSamplersByEnvAndService(updatedEnvServiceRates, fallbackSampler, true); } private static RateSampler createRateSampler(final double sampleRate) { @@ -183,15 +186,23 @@ private static final class RateSamplersByEnvAndService { private final Map> envServiceRates; private final RateSampler fallbackSampler; + private final boolean hasAgentRates; RateSamplersByEnvAndService() { - this(Collections.emptyMap(), DEFAULT_SAMPLER); + this(Collections.emptyMap(), DEFAULT_SAMPLER, false); } RateSamplersByEnvAndService( - Map> envServiceRates, RateSampler fallbackSampler) { + Map> envServiceRates, + RateSampler fallbackSampler, + boolean hasAgentRates) { this.envServiceRates = envServiceRates; this.fallbackSampler = fallbackSampler; + this.hasAgentRates = hasAgentRates; + } + + boolean hasAgentRates() { + return hasAgentRates; } RateSampler getFallbackSampler() { diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java index 1746a1a3c54..18381d7d472 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java @@ -146,19 +146,22 @@ public > void setSamplingPriority(final T span) { if (matchedRule == null) { fallbackSampler.setSamplingPriority(span); } else { - if (matchedRule.sample(span)) { + boolean sampled = matchedRule.sample(span); + if (sampled) { if (rateLimiter.tryAcquire()) { span.setSamplingPriority( PrioritySampling.USER_KEEP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), - matchedRule.getMechanism()); + matchedRule.getMechanism(), + true); } else { span.setSamplingPriority( PrioritySampling.USER_DROP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), - matchedRule.getMechanism()); + matchedRule.getMechanism(), + true); } span.setMetric(SAMPLING_LIMIT_RATE, rateLimit); } else { @@ -166,7 +169,8 @@ public > void setSamplingPriority(final T span) { PrioritySampling.USER_DROP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), - matchedRule.getMechanism()); + matchedRule.getMechanism(), + false); } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java index b2ab55c8e25..0f3de33b068 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java @@ -124,6 +124,13 @@ default void processTagsAndBaggageWithStructuredLinks( T setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism); + T setSamplingPriority( + int samplingPriority, + CharSequence rate, + double sampleRate, + int samplingMechanism, + boolean sampled); + T setSpanSamplingPriority(double rate, int limit); T setMetric(CharSequence name, int value); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index 5517952358e..64b840729ce 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -647,6 +647,27 @@ public final DDSpan setSamplingPriority(final int newPriority, int samplingMecha @Override public DDSpan setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { + setSamplingPriorityWithRate(samplingPriority, rate, sampleRate, samplingMechanism); + return this; + } + + @Override + public DDSpan setSamplingPriority( + int samplingPriority, + CharSequence rate, + double sampleRate, + int samplingMechanism, + boolean sampled) { + if (setSamplingPriorityWithRate(samplingPriority, rate, sampleRate, samplingMechanism)) { + context + .getPropagationTags() + .updateOtelTraceState(getTraceId().toLong(), sampleRate, sampled, samplingPriority); + } + return this; + } + + private boolean setSamplingPriorityWithRate( + int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { if (context.setSamplingPriority(samplingPriority, samplingMechanism)) { setMetric(rate, sampleRate); if (samplingMechanism == SamplingMechanism.AGENT_RATE @@ -655,8 +676,9 @@ public DDSpan setSamplingPriority( || samplingMechanism == SamplingMechanism.REMOTE_ADAPTIVE_RULE) { context.getPropagationTags().updateKnuthSamplingRate(sampleRate); } + return true; } - return this; + return false; } @Override 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 adf4cd66156..88f257dc66c 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 @@ -445,7 +445,13 @@ public DDSpanContext( setOrigin(origin); } if (samplingPriority != PrioritySampling.UNSET) { - setSamplingPriority(samplingPriority, SamplingMechanism.UNKNOWN); + if (this.propagationTags.getSamplingPriority() == samplingPriority) { + // Extractors already applied this priority to the propagation tags. Initialize the local + // field without reapplying the decision as an unknown local override. + SAMPLING_PRIORITY_UPDATER.set(this, samplingPriority); + } else { + setSamplingPriority(samplingPriority, SamplingMechanism.UNKNOWN); + } } setTag(PARENT_ID, this.propagationTags.getLastParentId()); } 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 3a0c57a4dd8..80b7d697dbd 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 @@ -67,6 +67,9 @@ public interface Factory { public abstract void forceKeep(int samplingMechanism); + public abstract void updateOtelTraceState( + long traceIdLowOrderBits, double sampleRate, boolean sampled, int samplingPriority); + public abstract int getSamplingPriority(); public abstract void updateTraceOrigin(CharSequence origin); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java index 3b8e837b5b9..e6a67398ebc 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java @@ -1,19 +1,139 @@ package datadog.trace.core.propagation.ptags; +import java.util.ArrayList; +import java.util.List; + final class OtelTraceState { + static final int MAX_VALUE_LENGTH = 256; + + private static final int HEX_DIGITS = 14; + private static final long KNUTH_FACTOR = 1111111111111111111L; + private static final long MAX_THRESHOLD = (1L << 56) - 1; + private static final double THRESHOLD_RANGE = 1L << 56; + private final String value; + private final String randomValue; + private final String threshold; + private final String[] unknownFields; + private final int randomValueCount; private final int inheritedPosition; + private final boolean locallyGeneratedRandomValue; - private OtelTraceState(String value, int inheritedPosition) { + private OtelTraceState( + String value, + String randomValue, + String threshold, + String[] unknownFields, + int randomValueCount, + int inheritedPosition, + boolean locallyGeneratedRandomValue) { this.value = value; + this.randomValue = randomValue; + this.threshold = threshold; + this.unknownFields = unknownFields; + this.randomValueCount = randomValueCount; this.inheritedPosition = inheritedPosition; + this.locallyGeneratedRandomValue = locallyGeneratedRandomValue; } static OtelTraceState parse(String raw, int inheritedPosition) { if (raw == null || raw.isEmpty()) { return null; } - return new OtelTraceState(raw, inheritedPosition); + + List fields = new ArrayList<>(); + List unknownFields = new ArrayList<>(); + String randomValue = null; + String threshold = null; + int randomValueCount = 0; + boolean changed = false; + int start = 0; + while (start < raw.length()) { + int end = raw.indexOf(';', start); + if (end < 0) { + end = raw.length(); + } + String field = raw.substring(start, end); + int separator = field.indexOf(':'); + String key = separator > 0 ? field.substring(0, separator) : field; + String fieldValue = separator > 0 ? field.substring(separator + 1) : ""; + if ("rv".equals(key)) { + if (fieldValue.length() == HEX_DIGITS && isLowercaseHex(fieldValue)) { + fields.add(field); + if (randomValue == null) { + randomValue = fieldValue; + } + randomValueCount++; + } else { + changed = true; + } + } else if ("th".equals(key)) { + if (!fieldValue.isEmpty() + && fieldValue.length() <= HEX_DIGITS + && isLowercaseHex(fieldValue)) { + fields.add(field); + if (threshold == null) { + threshold = fieldValue; + } + } else { + changed = true; + } + } else if (!field.isEmpty()) { + fields.add(field); + unknownFields.add(field); + } else { + changed = true; + } + start = end + 1; + } + + String value = join(fields); + if (value == null) { + return null; + } + if (!value.equals(raw)) { + changed = true; + } + return new OtelTraceState( + value, + randomValue, + threshold, + unknownFields.toArray(new String[0]), + randomValueCount, + changed ? 0 : inheritedPosition, + false); + } + + static OtelTraceState updateProbability( + OtelTraceState current, + long traceIdLowOrderBits, + double sampleRate, + boolean sampled, + int samplingPriority) { + if (sampleRate <= 0 || sampled && samplingPriority <= 0) { + return current == null ? null : current.removeLocalProbability(); + } + + long threshold = Math.round((1 - sampleRate) * THRESHOLD_RANGE); + threshold = Math.max(0, Math.min(threshold, MAX_THRESHOLD)); + long randomValue = (~(traceIdLowOrderBits * KNUTH_FACTOR)) >>> 8; + if (sampled && randomValue < threshold) { + randomValue = threshold; + } else if (!sampled && randomValue >= threshold) { + randomValue = threshold == 0 ? 0 : threshold - 1; + } + + String[] unknownFields = current == null ? new String[0] : current.unknownFields; + return create( + formatRandomValue(randomValue), formatThreshold(threshold), unknownFields, 0, true); + } + + OtelTraceState removeForNonProbabilityDecision() { + if (!locallyGeneratedRandomValue && threshold == null && randomValueCount <= 1) { + return this; + } + String retainedRandomValue = locallyGeneratedRandomValue ? null : randomValue; + return create(retainedRandomValue, null, unknownFields, 0, false); } String getValue() { @@ -27,4 +147,92 @@ int length() { int getInheritedPosition() { return inheritedPosition; } + + private OtelTraceState removeLocalProbability() { + return locallyGeneratedRandomValue ? create(null, null, unknownFields, 0, false) : this; + } + + private static OtelTraceState create( + String randomValue, + String threshold, + String[] unknownFields, + int inheritedPosition, + boolean locallyGeneratedRandomValue) { + StringBuilder value = new StringBuilder(); + append(value, randomValue == null ? null : "rv:" + randomValue); + append(value, threshold == null ? null : "th:" + threshold); + for (String field : unknownFields) { + append(value, field); + } + if (value.length() == 0) { + return null; + } + return new OtelTraceState( + value.toString(), + randomValue, + threshold, + unknownFields, + randomValue == null ? 0 : 1, + inheritedPosition, + locallyGeneratedRandomValue); + } + + private static void append(StringBuilder value, String field) { + if (field == null || field.isEmpty()) { + return; + } + int separatorSize = value.length() == 0 ? 0 : 1; + if (value.length() + separatorSize + field.length() > MAX_VALUE_LENGTH) { + return; + } + if (separatorSize != 0) { + value.append(';'); + } + value.append(field); + } + + private static String join(List fields) { + if (fields.isEmpty()) { + return null; + } + StringBuilder value = new StringBuilder(); + for (String field : fields) { + if (value.length() != 0) { + value.append(';'); + } + value.append(field); + } + return value.toString(); + } + + private static boolean isLowercaseHex(String value) { + for (int i = 0; i < value.length(); i++) { + char character = value.charAt(i); + if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) { + return false; + } + } + return true; + } + + private static String formatRandomValue(long randomValue) { + String hex = Long.toHexString(randomValue); + if (hex.length() == HEX_DIGITS) { + return hex; + } + StringBuilder padded = new StringBuilder(HEX_DIGITS); + for (int i = hex.length(); i < HEX_DIGITS; i++) { + padded.append('0'); + } + return padded.append(hex).toString(); + } + + private static String formatThreshold(long threshold) { + String hex = formatRandomValue(threshold); + int end = hex.length(); + while (end > 1 && hex.charAt(end - 1) == '0') { + end--; + } + return hex.substring(0, end); + } } 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 1661398c819..c01e8fd2aee 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 @@ -233,6 +233,9 @@ public void forceKeep(int samplingMechanism) { } private void doUpdateTraceSamplingPriority(int samplingPriority, int samplingMechanism) { + boolean removeOtelProbability = + samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE + && !isProbabilitySamplingMechanism(samplingMechanism); if (this.samplingPriority != samplingPriority) { // This should invalidate any cached w3c header clearCachedHeader(W3C); @@ -264,6 +267,17 @@ private void doUpdateTraceSamplingPriority(int samplingPriority, int samplingMec } decisionMakerTagValue = null; } + if (removeOtelProbability && otelTraceState != null) { + setOtelTraceState(otelTraceState.removeForNonProbabilityDecision()); + } + } + + @Override + public void updateOtelTraceState( + long traceIdLowOrderBits, double sampleRate, boolean sampled, int samplingPriority) { + setOtelTraceState( + OtelTraceState.updateProbability( + otelTraceState, traceIdLowOrderBits, sampleRate, sampled, samplingPriority)); } @Override @@ -573,5 +587,19 @@ public void updateAndLockDecisionMaker(PropagationTags source) { } } } + + private static boolean isProbabilitySamplingMechanism(int samplingMechanism) { + switch (samplingMechanism) { + case SamplingMechanism.AGENT_RATE: + case SamplingMechanism.REMOTE_AUTO_RATE: + case SamplingMechanism.LOCAL_USER_RULE: + case SamplingMechanism.REMOTE_USER_RATE: + case SamplingMechanism.REMOTE_USER_RULE: + case SamplingMechanism.REMOTE_ADAPTIVE_RULE: + return true; + default: + return false; + } + } } } diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java index 41a2a5a0d14..2c27468f492 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java @@ -337,6 +337,16 @@ public SimpleSpan setSamplingPriority( return this; } + @Override + public SimpleSpan setSamplingPriority( + int samplingPriority, + CharSequence rate, + double sampleRate, + int samplingMechanism, + boolean sampled) { + return this; + } + @Override public SimpleSpan setSpanSamplingPriority(double rate, int limit) { return this; diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java index c8fad4ab0ee..da02fac9e2b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java @@ -413,6 +413,16 @@ public PojoSpan setSamplingPriority( return this; } + @Override + public PojoSpan setSamplingPriority( + int samplingPriority, + CharSequence rate, + double sampleRate, + int samplingMechanism, + boolean sampled) { + return this; + } + @Override public PojoSpan setSpanSamplingPriority(double rate, int limit) { return this; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java index 9d9367551a7..096d83f40d4 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java @@ -12,10 +12,15 @@ import static datadog.trace.api.DDTags.THREAD_ID; import static datadog.trace.api.DDTags.THREAD_NAME; import static datadog.trace.api.TracePropagationStyle.DATADOG; +import static datadog.trace.api.TracePropagationStyle.TRACECONTEXT; +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.LOCAL_USER_RULE; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpan; +import static datadog.trace.common.sampling.RuleBasedTraceSampler.SAMPLING_RULE_RATE; import static datadog.trace.test.junit.utils.config.WithConfigExtension.injectSysConfig; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -362,6 +367,33 @@ void extractedContextShouldPopulateNewSpanDetails(ExtractedContext extractedCont span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.DATADOG)); } + @Test + void extractedContextShouldPreserveOtelTraceState() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue( + PropagationTags.HeaderType.W3C, "dd=s:0,ot=rv:ef284ace7a91e1;th:e6666666666668"); + ExtractedContext extractedContext = + new ExtractedContext( + DDTraceId.ONE, + 2, + PrioritySampling.SAMPLER_DROP, + null, + 0, + Collections.emptyMap(), + Collections.emptyMap(), + null, + propagationTags, + null, + TRACECONTEXT); + + DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(extractedContext).start(); + + assertEquals( + "dd=s:0,ot=rv:ef284ace7a91e1;th:e6666666666668", + span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C)); + } + @Test @WithConfig(key = "trace.propagation.behavior.extract", value = "restart") void buildContextFromExtractedContextWithRestartBehavior() { @@ -375,9 +407,7 @@ void buildContextFromExtractedContextWithRestartBehavior() { Collections.emptyMap(), Collections.emptyMap(), null, - PropagationTags.factory() - .fromHeaderValue( - PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"), + propagationTagsWithOtelState(), null, DATADOG); DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(extractedContext).start(); @@ -394,6 +424,16 @@ void buildContextFromExtractedContextWithRestartBehavior() { assertEquals( extractedContext.getPropagationTags().headerValue(PropagationTags.HeaderType.W3C), link.traceState()); + String initialTraceState = + span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); + assertTrue(initialTraceState == null || !initialTraceState.contains("ot=")); + + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, 0.5, LOCAL_USER_RULE, true); + + String freshTraceState = + span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); + assertFalse(freshTraceState.contains("ef284ace7a91e1")); + assertTrue(freshTraceState.contains(";th:8")); } @Test @@ -409,9 +449,7 @@ void buildContextFromExtractedContextWithIgnoreBehavior() { Collections.emptyMap(), Collections.emptyMap(), null, - PropagationTags.factory() - .fromHeaderValue( - PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"), + propagationTagsWithOtelState(), null, DATADOG); DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(extractedContext).start(); @@ -420,6 +458,25 @@ void buildContextFromExtractedContextWithIgnoreBehavior() { assertNotEquals(extractedContext.getSpanId(), span.getParentId()); assertEquals(PrioritySampling.UNSET, span.samplingPriority()); assertTrue(span.getLinks().isEmpty()); + String initialTraceState = + span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); + assertTrue(initialTraceState == null || !initialTraceState.contains("ot=")); + + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, 0.5, LOCAL_USER_RULE, true); + + String freshTraceState = + span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); + assertFalse(freshTraceState.contains("ef284ace7a91e1")); + assertTrue(freshTraceState.contains(";th:8")); + } + + private static PropagationTags propagationTagsWithOtelState() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue( + PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"); + propagationTags.updateW3CTracestate("dd=s:0,ot=rv:ef284ace7a91e1;th:e6666666666668"); + return propagationTags; } @Test diff --git a/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java b/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java new file mode 100644 index 00000000000..094aad08d9e --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java @@ -0,0 +1,162 @@ +package datadog.trace.core; + +import static datadog.trace.api.config.TracerConfig.TRACE_RATE_LIMIT; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_RULES; +import static datadog.trace.api.sampling.PrioritySampling.USER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.LOCAL_USER_RULE; +import static datadog.trace.common.sampling.RuleBasedTraceSampler.SAMPLING_RULE_RATE; +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.common.sampling.PrioritySampler; +import datadog.trace.common.sampling.RateByServiceTraceSampler; +import datadog.trace.common.sampling.Sampler; +import datadog.trace.common.writer.ListWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class OtelSamplingDecisionTest extends DDCoreJavaSpecification { + + @Test + void initialAgentRateDoesNotEstablishOtelProbabilityState() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + + new RateByServiceTraceSampler().setSamplingPriority(span); + + assertFalse(w3cHeader(span).contains("ot=")); + } finally { + tracer.close(); + } + } + + @Test + void loadedAgentRateEstablishesOtelProbabilityState() { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + sampler.onResponse("traces", agentRates(0.5)); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + + sampler.setSamplingPriority(span); + + String header = w3cHeader(span); + assertTrue(header.contains("ot=rv:")); + assertTrue(header.contains(";th:8")); + } finally { + tracer.close(); + } + } + + @Test + void loadedZeroAgentRateDoesNotEstablishOtelProbabilityState() { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + sampler.onResponse("traces", agentRates(0)); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + + sampler.setSamplingPriority(span); + + assertFalse(w3cHeader(span).contains("ot=")); + } finally { + tracer.close(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void configuredRulesEstablishOtelProbabilityState(boolean traceRule) { + Properties properties = new Properties(); + if (traceRule) { + properties.setProperty(TRACE_SAMPLING_RULES, "[{\"sample_rate\": 0.5}]"); + } else { + properties.setProperty(TRACE_SAMPLE_RATE, "0.5"); + } + properties.setProperty(TRACE_RATE_LIMIT, "10000000"); + PrioritySampler sampler = (PrioritySampler) Sampler.Builder.forConfig(properties); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + + sampler.setSamplingPriority(span); + + String header = w3cHeader(span); + assertTrue(header.contains("ot=rv:")); + assertTrue(header.contains(";th:8")); + } finally { + tracer.close(); + } + } + + @Test + void limiterRejectionDoesNotTurnProbabilityKeepIntoOtelDrop() { + Properties properties = new Properties(); + properties.setProperty(TRACE_SAMPLING_RULES, "[{\"sample_rate\": 1}]"); + properties.setProperty(TRACE_RATE_LIMIT, "1"); + PrioritySampler sampler = (PrioritySampler) Sampler.Builder.forConfig(properties); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan allowed = newRootSpan(tracer); + DDSpan rejected = newRootSpan(tracer); + + sampler.setSamplingPriority(allowed); + sampler.setSamplingPriority(rejected); + + assertTrue(w3cHeader(allowed).contains("ot=rv:")); + assertEquals(USER_DROP, rejected.samplingPriority()); + assertFalse(w3cHeader(rejected).contains("ot=")); + } finally { + tracer.close(); + } + } + + @Test + void manualOverrideRemovesLocallyGeneratedProbabilityState() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, 0.5, LOCAL_USER_RULE, true); + assertTrue(w3cHeader(span).contains("ot=rv:")); + + span.spanContext().forceKeep(); + + assertFalse(w3cHeader(span).contains("ot=")); + } finally { + tracer.close(); + } + } + + private static DDSpan newRootSpan(CoreTracer tracer) { + return (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .ignoreActiveSpan() + .start(); + } + + private static String w3cHeader(DDSpan span) { + String header = span.spanContext().getPropagationTags().headerValue(W3C); + assertNotNull(header); + return header; + } + + private static Map> agentRates(double rate) { + Map rates = new HashMap<>(); + rates.put("service:,env:", rate); + Map> response = new HashMap<>(); + response.put("rate_by_service", rates); + return response; + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java new file mode 100644 index 00000000000..329e01d683b --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java @@ -0,0 +1,172 @@ +package datadog.trace.core.propagation; + +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.LOCAL_USER_RULE; +import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; +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 static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class OtelTraceStatePropagationTest { + private static final String RV = "ef284ace7a91e1"; + private static final String TH = "e6666666666668"; + + @ParameterizedTest + @MethodSource("inboundTraceState") + void normalizesAndForwardsFirstOtelMember(String header, String expected) { + PropagationTags propagationTags = PropagationTags.factory().fromHeaderValue(W3C, header); + + assertEquals(expected, propagationTags.headerValue(W3C)); + } + + static Stream inboundTraceState() { + return Stream.of( + arguments("ot=rv:" + RV + ";th:" + TH, "ot=rv:" + RV + ";th:" + TH), + arguments("ot=rv:" + RV, "ot=rv:" + RV), + arguments("ot=th:" + TH, "ot=th:" + TH), + arguments("ot=foo:bar", "ot=foo:bar"), + arguments( + "congo=state,ot=rv:invalid;th:" + TH + ";foo:bar", + "ot=th:" + TH + ";foo:bar,congo=state"), + arguments("congo=state,ot=rv:invalid;th:invalid", "congo=state"), + arguments("ot=rv:EF284ACE7A91E1;th:" + TH, "ot=th:" + TH), + arguments( + "ot=rv:" + RV + ";rv:1234567890abcd;th:" + TH, + "ot=rv:" + RV + ";rv:1234567890abcd;th:" + TH), + arguments( + "foo=bar,ot=rv:" + RV + ",ot=rv:1234567890abcd,other=state", + "foo=bar,ot=rv:" + RV + ",other=state"), + arguments("dd=s:1,dd=s:0,ot=rv:" + RV, "dd=s:1,ot=rv:" + RV)); + } + + @Test + void preservesUnchangedInheritedMemberPosition() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue(W3C, "dd=s:1,foo=bar,ot=rv:" + RV + ",something=else"); + + assertEquals( + "dd=s:1,foo=bar,ot=rv:" + RV + ",something=else", propagationTags.headerValue(W3C)); + } + + @Test + void movesNormalizedMemberImmediatelyAfterDatadog() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue(W3C, "dd=s:1,foo=bar,ot=rv:invalid;th:" + TH + ",something=else"); + + assertEquals( + "dd=s:1,ot=th:" + TH + ",foo=bar,something=else", propagationTags.headerValue(W3C)); + } + + @Test + void movesLocallyGeneratedMemberImmediatelyAfterDatadog() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue( + W3C, "foo=bar,ot=rv:" + RV + ";th:" + TH + ";future:value,other=state"); + + propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); + propagationTags.updateOtelTraceState(1, 0.5, true, USER_KEEP); + + assertEquals( + "dd=s:2;t.dm:-3,ot=rv:f0948a54d43b8e;th:8;future:value,foo=bar,other=state", + propagationTags.headerValue(W3C)); + } + + @Test + void manualKeepRetainsInheritedRandomnessAndUnknownFields() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue( + W3C, "foo=bar,ot=rv:" + RV + ";th:" + TH + ";future:value,other=state"); + + propagationTags.forceKeep(MANUAL); + + assertEquals( + "dd=s:2;t.dm:-4,ot=rv:" + RV + ";future:value,foo=bar,other=state", + propagationTags.headerValue(W3C)); + } + + @Test + void manualKeepRemovesLocallyGeneratedRandomness() { + PropagationTags propagationTags = PropagationTags.factory().empty(); + propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); + propagationTags.updateOtelTraceState(1, 0.5, true, USER_KEEP); + + propagationTags.forceKeep(MANUAL); + + assertEquals("dd=s:2;t.dm:-4", propagationTags.headerValue(W3C)); + } + + @Test + void sampledContextWithoutOtelStateDoesNotFabricateIt() { + PropagationTags propagationTags = + PropagationTags.factory().fromHeaderValue(W3C, "dd=s:1,foo=bar"); + + assertEquals("dd=s:1,foo=bar", propagationTags.headerValue(W3C)); + } + + @Test + void updatingW3cTraceStateCapturesOtelStateForMixedHeaderExtraction() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.dm=-3"); + + propagationTags.updateW3CTracestate("foo=bar,ot=rv:" + RV + ";th:" + TH + ",other=state"); + + assertEquals( + "dd=t.dm:-3,foo=bar,ot=rv:" + RV + ";th:" + TH + ",other=state", + propagationTags.headerValue(W3C)); + } + + @Test + void emitsOnlyCompleteOtelFieldsAtMemberSizeLimit() { + String unknownField = "x:" + repeat("v", 254); + PropagationTags propagationTags = + PropagationTags.factory().fromHeaderValue(W3C, "ot=" + unknownField); + + assertEquals("ot=" + unknownField, propagationTags.headerValue(W3C)); + + propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); + propagationTags.updateOtelTraceState(1, 0.5, true, USER_KEEP); + + String header = propagationTags.headerValue(W3C); + assertTrue(header.contains("ot=rv:f0948a54d43b8e;th:8")); + assertFalse(header.contains("x:")); + } + + @Test + void generatedOtelMemberHonorsMemberCountLimit() { + StringBuilder original = new StringBuilder("ot=rv:").append(RV); + for (int i = 0; i < 31; i++) { + original.append(",v").append(i).append("=state"); + } + PropagationTags propagationTags = + PropagationTags.factory().fromHeaderValue(W3C, original.toString()); + + propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); + propagationTags.updateOtelTraceState(1, 0.5, true, USER_KEEP); + + String header = propagationTags.headerValue(W3C); + assertEquals(32, header.split(",").length); + assertTrue(header.startsWith("dd=s:2;t.dm:-3,ot=rv:f0948a54d43b8e;th:8,")); + assertFalse(header.contains("v30=state")); + } + + private static String repeat(String value, int count) { + StringBuilder repeated = new StringBuilder(value.length() * count); + for (int i = 0; i < count; i++) { + repeated.append(value); + } + return repeated.toString(); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java new file mode 100644 index 00000000000..b28ee550d8b --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java @@ -0,0 +1,94 @@ +package datadog.trace.core.propagation.ptags; + +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.common.sampling.DeterministicSampler; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class OtelTraceStateTest { + + @ParameterizedTest + @MethodSource("goldenSamplingVectors") + void createsGoldenSamplingState(double sampleRate, boolean sampled, String expectedTraceState) { + OtelTraceState state = + OtelTraceState.updateProbability( + null, 1, sampleRate, sampled, sampled ? SAMPLER_KEEP : SAMPLER_DROP); + + assertEquals(expectedTraceState, state.getValue()); + } + + static Stream goldenSamplingVectors() { + return Stream.of( + arguments(0.01, false, "rv:f0948a54d43b8e;th:fd70a3d70a3d7"), + arguments(0.1, true, "rv:f0948a54d43b8e;th:e6666666666668"), + arguments(0.2, true, "rv:f0948a54d43b8e;th:ccccccccccccd"), + arguments(0.5, true, "rv:f0948a54d43b8e;th:8"), + arguments(0.99, true, "rv:f0948a54d43b8e;th:028f5c28f5c29")); + } + + @Test + void omitsStateAtRateZero() { + assertNull(OtelTraceState.updateProbability(null, 1, 0, false, SAMPLER_DROP)); + } + + @Test + void clampsTinyPositiveRateThreshold() { + OtelTraceState state = + OtelTraceState.updateProbability(null, 1, Double.MIN_VALUE, false, SAMPLER_DROP); + + assertEquals("rv:f0948a54d43b8e;th:ffffffffffffff", state.getValue()); + } + + @Test + void correctsKeepAtPrecisionBoundary() { + OtelTraceState state = + OtelTraceState.updateProbability(null, 0x03A93EE8B1999F00L, 0.1, true, SAMPLER_KEEP); + + assertEquals("rv:e6666666666668;th:e6666666666668", state.getValue()); + } + + @Test + void correctsDropAtPrecisionBoundary() { + OtelTraceState state = + OtelTraceState.updateProbability(null, 5401449561355763072L, 0.05, false, SAMPLER_DROP); + + assertEquals("rv:f333333333332f;th:f333333333333", state.getValue()); + } + + @Test + void limiterRejectionRemovesLocallyGeneratedProbability() { + OtelTraceState state = + OtelTraceState.updateProbability( + OtelTraceState.parse("foo:bar", 2), 1, 0.5, true, SAMPLER_KEEP); + + state = OtelTraceState.updateProbability(state, 1, 0.5, true, SAMPLER_DROP); + + assertEquals("foo:bar", state.getValue()); + assertEquals(0, state.getInheritedPosition()); + } + + @Test + void nonProbabilityDecisionRetainsOnlyInheritedRandomnessAndUnknownFields() { + OtelTraceState state = OtelTraceState.parse("th:e6666666666668;foo:bar;rv:ef284ace7a91e1", 2); + + state = state.removeForNonProbabilityDecision(); + + assertEquals("rv:ef284ace7a91e1;foo:bar", state.getValue()); + assertEquals(0, state.getInheritedPosition()); + } + + @Test + void deterministicSamplerRetainsDoublePrecisionRate() { + double rate = 0.123456789012345; + + assertEquals(rate, new DeterministicSampler.TraceSampler(rate).getSampleRate()); + } +} diff --git a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java index b349e937e68..5330ffe8b38 100644 --- a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java +++ b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java @@ -363,6 +363,16 @@ public PojoSpan setSamplingPriority( return this; } + @Override + public PojoSpan setSamplingPriority( + int samplingPriority, + CharSequence rate, + double sampleRate, + int samplingMechanism, + boolean sampled) { + return this; + } + @Override public PojoSpan setSpanSamplingPriority(double rate, int limit) { return this; From a7ddc988ead68b622661f902e2747b90d823566f Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Thu, 3 Sep 2026 16:44:10 +0200 Subject: [PATCH 2/7] clearer determination of ratelimiting and move th/rv computation in helpers --- .../sampling/RateByServiceTraceSampler.java | 21 +++++++------------ .../sampling/RuleBasedTraceSampler.java | 6 +++--- .../java/datadog/trace/core/CoreSpan.java | 5 +---- .../main/java/datadog/trace/core/DDSpan.java | 18 +++++++--------- .../propagation/ptags/OtelTraceState.java | 21 +++++++++++++++---- .../trace/common/metrics/SimpleSpan.java | 8 +------ .../trace/common/writer/TraceGenerator.java | 8 +------ .../propagation/ptags/OtelTraceStateTest.java | 11 ++++++++++ .../traceAgentTest/java/TraceGenerator.java | 8 +------ 9 files changed, 50 insertions(+), 56 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java index 7424dd91493..33a9f803433 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java @@ -61,20 +61,13 @@ public > void setSamplingPriority(final T span) { boolean sampled = sampler.sample(span); int samplingPriority = sampled ? PrioritySampling.SAMPLER_KEEP : PrioritySampling.SAMPLER_DROP; - if (rates.hasAgentRates()) { - span.setSamplingPriority( - samplingPriority, - SAMPLING_AGENT_RATE, - sampler.getSampleRate(), - SamplingMechanism.AGENT_RATE, - sampled); - } else { - span.setSamplingPriority( - samplingPriority, - SAMPLING_AGENT_RATE, - sampler.getSampleRate(), - SamplingMechanism.AGENT_RATE); - } + Boolean probabilitySamplingResult = rates.hasAgentRates() ? sampled : null; + span.setSamplingPriority( + samplingPriority, + SAMPLING_AGENT_RATE, + sampler.getSampleRate(), + SamplingMechanism.AGENT_RATE, + probabilitySamplingResult); } private > String getSpanEnv(final T span) { diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java index 18381d7d472..6fe8e754664 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java @@ -154,14 +154,14 @@ public > void setSamplingPriority(final T span) { SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), matchedRule.getMechanism(), - true); + Boolean.TRUE); } else { span.setSamplingPriority( PrioritySampling.USER_DROP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), matchedRule.getMechanism(), - true); + Boolean.TRUE); } span.setMetric(SAMPLING_LIMIT_RATE, rateLimit); } else { @@ -170,7 +170,7 @@ public > void setSamplingPriority(final T span) { SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), matchedRule.getMechanism(), - false); + Boolean.FALSE); } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java index 0f3de33b068..db068847fd7 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java @@ -121,15 +121,12 @@ default void processTagsAndBaggageWithStructuredLinks( T setSamplingPriority(int samplingPriority, int samplingMechanism); - T setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism); - T setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism, - boolean sampled); + Boolean probabilitySamplingResult); T setSpanSamplingPriority(double rate, int limit); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index 64b840729ce..fedc0b7ad58 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -644,24 +644,22 @@ public final DDSpan setSamplingPriority(final int newPriority, int samplingMecha return this; } - @Override - public DDSpan setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { - setSamplingPriorityWithRate(samplingPriority, rate, sampleRate, samplingMechanism); - return this; - } - @Override public DDSpan setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism, - boolean sampled) { - if (setSamplingPriorityWithRate(samplingPriority, rate, sampleRate, samplingMechanism)) { + Boolean probabilitySamplingResult) { + if (setSamplingPriorityWithRate(samplingPriority, rate, sampleRate, samplingMechanism) + && probabilitySamplingResult != null) { context .getPropagationTags() - .updateOtelTraceState(getTraceId().toLong(), sampleRate, sampled, samplingPriority); + .updateOtelTraceState( + getTraceId().toLong(), + sampleRate, + probabilitySamplingResult.booleanValue(), + samplingPriority); } return this; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java index e6a67398ebc..26bf366cd61 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java @@ -110,13 +110,17 @@ static OtelTraceState updateProbability( double sampleRate, boolean sampled, int samplingPriority) { - if (sampleRate <= 0 || sampled && samplingPriority <= 0) { + // `sampled` is the raw probability result; `samplingPriority` may be changed by rate limiting. + if (sampleRate <= 0) { return current == null ? null : current.removeLocalProbability(); } - long threshold = Math.round((1 - sampleRate) * THRESHOLD_RANGE); - threshold = Math.max(0, Math.min(threshold, MAX_THRESHOLD)); - long randomValue = (~(traceIdLowOrderBits * KNUTH_FACTOR)) >>> 8; + if (sampled && samplingPriority <= 0) { + return current == null ? null : current.removeForNonProbabilityDecision(); + } + + long threshold = computeThreshold(sampleRate); + long randomValue = computeRandomValue(traceIdLowOrderBits); if (sampled && randomValue < threshold) { randomValue = threshold; } else if (!sampled && randomValue >= threshold) { @@ -215,6 +219,15 @@ private static boolean isLowercaseHex(String value) { return true; } + private static long computeRandomValue(long traceIdLowOrderBits) { + return (~(traceIdLowOrderBits * KNUTH_FACTOR)) >>> 8; + } + + private static long computeThreshold(double sampleRate) { + long threshold = Math.round((1 - sampleRate) * THRESHOLD_RANGE); + return Math.max(0, Math.min(threshold, MAX_THRESHOLD)); + } + private static String formatRandomValue(long randomValue) { String hex = Long.toHexString(randomValue); if (hex.length() == HEX_DIGITS) { diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java index 2c27468f492..99ef1e1fbf4 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java @@ -331,19 +331,13 @@ public SimpleSpan setSamplingPriority(int samplingPriority, int samplingMechanis return this; } - @Override - public SimpleSpan setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { - return this; - } - @Override public SimpleSpan setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism, - boolean sampled) { + Boolean probabilitySamplingResult) { return this; } diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java index da02fac9e2b..fd19a32164b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java @@ -407,19 +407,13 @@ public PojoSpan setSamplingPriority(int samplingPriority, int samplingMechanism) return this; } - @Override - public PojoSpan setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { - return this; - } - @Override public PojoSpan setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism, - boolean sampled) { + Boolean probabilitySamplingResult) { return this; } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java index b28ee550d8b..eee9a880dae 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java @@ -75,6 +75,17 @@ void limiterRejectionRemovesLocallyGeneratedProbability() { assertEquals(0, state.getInheritedPosition()); } + @Test + void limiterRejectionRetainsInheritedRandomness() { + OtelTraceState state = + OtelTraceState.parse("rv:ef284ace7a91e1;th:8;foo:bar", 2); + + state = OtelTraceState.updateProbability(state, 1, 0.5, true, SAMPLER_DROP); + + assertEquals("rv:ef284ace7a91e1;foo:bar", state.getValue()); + assertEquals(0, state.getInheritedPosition()); + } + @Test void nonProbabilityDecisionRetainsOnlyInheritedRandomnessAndUnknownFields() { OtelTraceState state = OtelTraceState.parse("th:e6666666666668;foo:bar;rv:ef284ace7a91e1", 2); diff --git a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java index 5330ffe8b38..8b3e53b32f2 100644 --- a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java +++ b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java @@ -357,19 +357,13 @@ public PojoSpan setSamplingPriority(int samplingPriority, int samplingMechanism) return this; } - @Override - public PojoSpan setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { - return this; - } - @Override public PojoSpan setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism, - boolean sampled) { + Boolean probabilitySamplingResult) { return this; } From 6b14a215666b37cfb2617316076b0bca4c3d7c29 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Thu, 3 Sep 2026 17:03:02 +0200 Subject: [PATCH 3/7] centralize values used for testing --- .../DeterministicTraceSamplerTest.java | 11 +-- .../trace/core/CoreSpanBuilderTest.java | 35 ++++---- .../trace/core/OtelSamplingDecisionTest.java | 55 +++++++----- .../OtelTraceStatePropagationTest.java | 33 ++++--- .../propagation/ptags/OtelTraceStateTest.java | 90 +++++++++++++------ 5 files changed, 147 insertions(+), 77 deletions(-) diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java index c97a8056336..f5ab0baa29c 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java @@ -19,6 +19,10 @@ class DeterministicTraceSamplerTest { + private static final String SAMPLE_RATE_COLUMN = "rate "; + private static final String SAMPLE_RATE_0_123456789 = "0.123456789"; + private static final String SAMPLE_RATE_0_999999999 = "0.999999999"; + @TableTest({ "scenario | expected | traceId ", "10428415896243638596 f | false | 10428415896243638596", @@ -357,11 +361,8 @@ void testSamplingAll(String traceId) { assertTrue(sampler.sample(span)); } - @TableTest({ - "rate ", - "0.123456789", - "0.999999999" - }) + // These values fail if the configured rate is narrowed to a float. + @TableTest({SAMPLE_RATE_COLUMN, SAMPLE_RATE_0_123456789, SAMPLE_RATE_0_999999999}) void preservesConfiguredSampleRate(double rate) { assertEquals(rate, new DeterministicSampler.TraceSampler(rate).getSampleRate()); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java index 096d83f40d4..a09947bb3b3 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java @@ -60,6 +60,14 @@ public class CoreSpanBuilderTest extends DDCoreJavaSpecification { + // Propagation fixtures preserve the raw inbound vendor values as a single test vector. + private static final String INHERITED_RANDOM_VALUE = "ef284ace7a91e1"; + private static final String OTEL_TRACE_STATE = + "dd=s:0,ot=rv:" + INHERITED_RANDOM_VALUE + ";th:e6666666666668"; + private static final String OTEL_MEMBER = "ot="; + private static final String THRESHOLD_0_5 = ";th:8"; + private static final double SAMPLE_RATE_0_5 = 0.5; + private ListWriter writer; private CoreTracer tracer; @@ -370,9 +378,7 @@ void extractedContextShouldPopulateNewSpanDetails(ExtractedContext extractedCont @Test void extractedContextShouldPreserveOtelTraceState() { PropagationTags propagationTags = - PropagationTags.factory() - .fromHeaderValue( - PropagationTags.HeaderType.W3C, "dd=s:0,ot=rv:ef284ace7a91e1;th:e6666666666668"); + PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.W3C, OTEL_TRACE_STATE); ExtractedContext extractedContext = new ExtractedContext( DDTraceId.ONE, @@ -390,7 +396,7 @@ void extractedContextShouldPreserveOtelTraceState() { DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(extractedContext).start(); assertEquals( - "dd=s:0,ot=rv:ef284ace7a91e1;th:e6666666666668", + OTEL_TRACE_STATE, span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C)); } @@ -426,14 +432,14 @@ void buildContextFromExtractedContextWithRestartBehavior() { link.traceState()); String initialTraceState = span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); - assertTrue(initialTraceState == null || !initialTraceState.contains("ot=")); + assertTrue(initialTraceState == null || !initialTraceState.contains(OTEL_MEMBER)); - span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, 0.5, LOCAL_USER_RULE, true); + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, LOCAL_USER_RULE, true); String freshTraceState = span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); - assertFalse(freshTraceState.contains("ef284ace7a91e1")); - assertTrue(freshTraceState.contains(";th:8")); + assertFalse(freshTraceState.contains(INHERITED_RANDOM_VALUE)); + assertTrue(freshTraceState.contains(THRESHOLD_0_5)); } @Test @@ -460,22 +466,21 @@ void buildContextFromExtractedContextWithIgnoreBehavior() { assertTrue(span.getLinks().isEmpty()); String initialTraceState = span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); - assertTrue(initialTraceState == null || !initialTraceState.contains("ot=")); + assertTrue(initialTraceState == null || !initialTraceState.contains(OTEL_MEMBER)); - span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, 0.5, LOCAL_USER_RULE, true); + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, LOCAL_USER_RULE, true); String freshTraceState = span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); - assertFalse(freshTraceState.contains("ef284ace7a91e1")); - assertTrue(freshTraceState.contains(";th:8")); + assertFalse(freshTraceState.contains(INHERITED_RANDOM_VALUE)); + assertTrue(freshTraceState.contains(THRESHOLD_0_5)); } private static PropagationTags propagationTagsWithOtelState() { PropagationTags propagationTags = PropagationTags.factory() - .fromHeaderValue( - PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"); - propagationTags.updateW3CTracestate("dd=s:0,ot=rv:ef284ace7a91e1;th:e6666666666668"); + .fromHeaderValue(PropagationTags.HeaderType.DATADOG, DATADOG_TRACE_STATE); + propagationTags.updateW3CTracestate(OTEL_TRACE_STATE); return propagationTags; } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java b/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java index 094aad08d9e..e8b6607138a 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java @@ -26,6 +26,20 @@ class OtelSamplingDecisionTest extends DDCoreJavaSpecification { + private static final String AGENT_RATE_ENDPOINT = "traces"; + private static final String INSTRUMENTATION_NAME = "datadog"; + private static final String OPERATION_NAME = "operation"; + private static final String SERVICE_NAME = "service"; + private static final String OTEL_MEMBER = "ot="; + private static final String OTEL_RANDOM_VALUE_PREFIX = "ot=rv:"; + private static final String HALF_THRESHOLD = ";th:8"; + private static final double SAMPLE_RATE_0_5 = 0.5; + private static final String SAMPLE_RATE_0_5_RULE = "[{\"sample_rate\": 0.5}]"; + private static final String FULL_SAMPLE_RATE_RULE = "[{\"sample_rate\": 1}]"; + // Keeps configured-rule vectors out of the limiter path. + private static final String HIGH_RATE_LIMIT = "10000000"; + private static final String ONE_PER_SECOND_RATE_LIMIT = "1"; + @Test void initialAgentRateDoesNotEstablishOtelProbabilityState() { CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); @@ -34,7 +48,7 @@ void initialAgentRateDoesNotEstablishOtelProbabilityState() { new RateByServiceTraceSampler().setSamplingPriority(span); - assertFalse(w3cHeader(span).contains("ot=")); + assertFalse(w3cHeader(span).contains(OTEL_MEMBER)); } finally { tracer.close(); } @@ -43,7 +57,7 @@ void initialAgentRateDoesNotEstablishOtelProbabilityState() { @Test void loadedAgentRateEstablishesOtelProbabilityState() { RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); - sampler.onResponse("traces", agentRates(0.5)); + sampler.onResponse(AGENT_RATE_ENDPOINT, agentRates(SAMPLE_RATE_0_5)); CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); try { DDSpan span = newRootSpan(tracer); @@ -51,8 +65,8 @@ void loadedAgentRateEstablishesOtelProbabilityState() { sampler.setSamplingPriority(span); String header = w3cHeader(span); - assertTrue(header.contains("ot=rv:")); - assertTrue(header.contains(";th:8")); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(header.contains(HALF_THRESHOLD)); } finally { tracer.close(); } @@ -61,14 +75,14 @@ void loadedAgentRateEstablishesOtelProbabilityState() { @Test void loadedZeroAgentRateDoesNotEstablishOtelProbabilityState() { RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); - sampler.onResponse("traces", agentRates(0)); + sampler.onResponse(AGENT_RATE_ENDPOINT, agentRates(0)); CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); try { DDSpan span = newRootSpan(tracer); sampler.setSamplingPriority(span); - assertFalse(w3cHeader(span).contains("ot=")); + assertFalse(w3cHeader(span).contains(OTEL_MEMBER)); } finally { tracer.close(); } @@ -79,11 +93,11 @@ void loadedZeroAgentRateDoesNotEstablishOtelProbabilityState() { void configuredRulesEstablishOtelProbabilityState(boolean traceRule) { Properties properties = new Properties(); if (traceRule) { - properties.setProperty(TRACE_SAMPLING_RULES, "[{\"sample_rate\": 0.5}]"); + properties.setProperty(TRACE_SAMPLING_RULES, SAMPLE_RATE_0_5_RULE); } else { - properties.setProperty(TRACE_SAMPLE_RATE, "0.5"); + properties.setProperty(TRACE_SAMPLE_RATE, String.valueOf(SAMPLE_RATE_0_5)); } - properties.setProperty(TRACE_RATE_LIMIT, "10000000"); + properties.setProperty(TRACE_RATE_LIMIT, HIGH_RATE_LIMIT); PrioritySampler sampler = (PrioritySampler) Sampler.Builder.forConfig(properties); CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); try { @@ -92,8 +106,8 @@ void configuredRulesEstablishOtelProbabilityState(boolean traceRule) { sampler.setSamplingPriority(span); String header = w3cHeader(span); - assertTrue(header.contains("ot=rv:")); - assertTrue(header.contains(";th:8")); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(header.contains(HALF_THRESHOLD)); } finally { tracer.close(); } @@ -102,8 +116,8 @@ void configuredRulesEstablishOtelProbabilityState(boolean traceRule) { @Test void limiterRejectionDoesNotTurnProbabilityKeepIntoOtelDrop() { Properties properties = new Properties(); - properties.setProperty(TRACE_SAMPLING_RULES, "[{\"sample_rate\": 1}]"); - properties.setProperty(TRACE_RATE_LIMIT, "1"); + properties.setProperty(TRACE_SAMPLING_RULES, FULL_SAMPLE_RATE_RULE); + properties.setProperty(TRACE_RATE_LIMIT, ONE_PER_SECOND_RATE_LIMIT); PrioritySampler sampler = (PrioritySampler) Sampler.Builder.forConfig(properties); CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); try { @@ -113,9 +127,9 @@ void limiterRejectionDoesNotTurnProbabilityKeepIntoOtelDrop() { sampler.setSamplingPriority(allowed); sampler.setSamplingPriority(rejected); - assertTrue(w3cHeader(allowed).contains("ot=rv:")); + assertTrue(w3cHeader(allowed).contains(OTEL_RANDOM_VALUE_PREFIX)); assertEquals(USER_DROP, rejected.samplingPriority()); - assertFalse(w3cHeader(rejected).contains("ot=")); + assertFalse(w3cHeader(rejected).contains(OTEL_MEMBER)); } finally { tracer.close(); } @@ -126,12 +140,13 @@ void manualOverrideRemovesLocallyGeneratedProbabilityState() { CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); try { DDSpan span = newRootSpan(tracer); - span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, 0.5, LOCAL_USER_RULE, true); - assertTrue(w3cHeader(span).contains("ot=rv:")); + span.setSamplingPriority( + USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, LOCAL_USER_RULE, true); + assertTrue(w3cHeader(span).contains(OTEL_RANDOM_VALUE_PREFIX)); span.spanContext().forceKeep(); - assertFalse(w3cHeader(span).contains("ot=")); + assertFalse(w3cHeader(span).contains(OTEL_MEMBER)); } finally { tracer.close(); } @@ -140,8 +155,8 @@ void manualOverrideRemovesLocallyGeneratedProbabilityState() { private static DDSpan newRootSpan(CoreTracer tracer) { return (DDSpan) tracer - .buildSpan("datadog", "operation") - .withServiceName("service") + .buildSpan(INSTRUMENTATION_NAME, OPERATION_NAME) + .withServiceName(SERVICE_NAME) .ignoreActiveSpan() .start(); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java index 329e01d683b..f3bda1391b5 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java @@ -16,8 +16,14 @@ import org.junit.jupiter.params.provider.MethodSource; class OtelTraceStatePropagationTest { + private static final long TRACE_ID = 1; + private static final double SAMPLE_RATE_0_5 = 0.5; private static final String RV = "ef284ace7a91e1"; private static final String TH = "e6666666666668"; + private static final String GENERATED_RV = "f0948a54d43b8e"; + private static final String THRESHOLD_0_5 = "8"; + private static final int W3C_MEMBER_VALUE_MAX_LENGTH = 256; + private static final int W3C_TRACESTATE_MEMBER_LIMIT = 32; @ParameterizedTest @MethodSource("inboundTraceState") @@ -28,6 +34,7 @@ void normalizesAndForwardsFirstOtelMember(String header, String expected) { } static Stream inboundTraceState() { + // Covers valid, invalid, duplicate, and ordered ot list-members. return Stream.of( arguments("ot=rv:" + RV + ";th:" + TH, "ot=rv:" + RV + ";th:" + TH), arguments("ot=rv:" + RV, "ot=rv:" + RV), @@ -75,10 +82,14 @@ void movesLocallyGeneratedMemberImmediatelyAfterDatadog() { W3C, "foo=bar,ot=rv:" + RV + ";th:" + TH + ";future:value,other=state"); propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); - propagationTags.updateOtelTraceState(1, 0.5, true, USER_KEEP); + propagationTags.updateOtelTraceState(TRACE_ID, SAMPLE_RATE_0_5, true, USER_KEEP); assertEquals( - "dd=s:2;t.dm:-3,ot=rv:f0948a54d43b8e;th:8;future:value,foo=bar,other=state", + "dd=s:2;t.dm:-3,ot=rv:" + + GENERATED_RV + + ";th:" + + THRESHOLD_0_5 + + ";future:value,foo=bar,other=state", propagationTags.headerValue(W3C)); } @@ -100,7 +111,7 @@ void manualKeepRetainsInheritedRandomnessAndUnknownFields() { void manualKeepRemovesLocallyGeneratedRandomness() { PropagationTags propagationTags = PropagationTags.factory().empty(); propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); - propagationTags.updateOtelTraceState(1, 0.5, true, USER_KEEP); + propagationTags.updateOtelTraceState(TRACE_ID, SAMPLE_RATE_0_5, true, USER_KEEP); propagationTags.forceKeep(MANUAL); @@ -130,35 +141,37 @@ void updatingW3cTraceStateCapturesOtelStateForMixedHeaderExtraction() { @Test void emitsOnlyCompleteOtelFieldsAtMemberSizeLimit() { - String unknownField = "x:" + repeat("v", 254); + // W3C limits an individual tracestate member value to 256 characters. + String unknownField = "x:" + repeat("v", W3C_MEMBER_VALUE_MAX_LENGTH - 2); PropagationTags propagationTags = PropagationTags.factory().fromHeaderValue(W3C, "ot=" + unknownField); assertEquals("ot=" + unknownField, propagationTags.headerValue(W3C)); propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); - propagationTags.updateOtelTraceState(1, 0.5, true, USER_KEEP); + propagationTags.updateOtelTraceState(TRACE_ID, SAMPLE_RATE_0_5, true, USER_KEEP); String header = propagationTags.headerValue(W3C); - assertTrue(header.contains("ot=rv:f0948a54d43b8e;th:8")); + assertTrue(header.contains("ot=rv:" + GENERATED_RV + ";th:" + THRESHOLD_0_5)); assertFalse(header.contains("x:")); } @Test void generatedOtelMemberHonorsMemberCountLimit() { StringBuilder original = new StringBuilder("ot=rv:").append(RV); - for (int i = 0; i < 31; i++) { + for (int i = 0; i < W3C_TRACESTATE_MEMBER_LIMIT - 1; i++) { original.append(",v").append(i).append("=state"); } PropagationTags propagationTags = PropagationTags.factory().fromHeaderValue(W3C, original.toString()); propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); - propagationTags.updateOtelTraceState(1, 0.5, true, USER_KEEP); + propagationTags.updateOtelTraceState(TRACE_ID, SAMPLE_RATE_0_5, true, USER_KEEP); String header = propagationTags.headerValue(W3C); - assertEquals(32, header.split(",").length); - assertTrue(header.startsWith("dd=s:2;t.dm:-3,ot=rv:f0948a54d43b8e;th:8,")); + assertEquals(W3C_TRACESTATE_MEMBER_LIMIT, header.split(",").length); + assertTrue( + header.startsWith("dd=s:2;t.dm:-3,ot=rv:" + GENERATED_RV + ";th:" + THRESHOLD_0_5 + ",")); assertFalse(header.contains("v30=state")); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java index eee9a880dae..ea69a632271 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java @@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.params.provider.Arguments.arguments; -import datadog.trace.common.sampling.DeterministicSampler; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -15,91 +14,128 @@ class OtelTraceStateTest { + // A stable trace ID covers sampling decisions at representative rates. + private static final long TRACE_ID = 1; + private static final String LOCAL_RANDOM_VALUE = "f0948a54d43b8e"; + private static final double SAMPLE_RATE_0_01 = 0.01; + private static final String THRESHOLD_0_01 = "fd70a3d70a3d7"; + private static final double SAMPLE_RATE_0_1 = 0.1; + private static final String THRESHOLD_0_1 = "e6666666666668"; + private static final double SAMPLE_RATE_0_2 = 0.2; + private static final String THRESHOLD_0_2 = "ccccccccccccd"; + private static final double SAMPLE_RATE_0_5 = 0.5; + private static final String THRESHOLD_0_5 = "8"; + private static final double SAMPLE_RATE_0_99 = 0.99; + private static final String THRESHOLD_0_99 = "028f5c28f5c29"; + private static final double SAMPLE_RATE_0_05 = 0.05; + + // These IDs straddle thresholds that would otherwise lose precision when rounded. + private static final long KEEP_PRECISION_BOUNDARY_TRACE_ID = 0x03A93EE8B1999F00L; + private static final String KEEP_PRECISION_BOUNDARY_VALUE = "e6666666666668"; + private static final long DROP_PRECISION_BOUNDARY_TRACE_ID = 5401449561355763072L; + private static final String DROP_PRECISION_BOUNDARY_RANDOM_VALUE = "f333333333332f"; + private static final String DROP_PRECISION_BOUNDARY_THRESHOLD = "f333333333333"; + private static final String INHERITED_RANDOM_VALUE = "ef284ace7a91e1"; + private static final String UNKNOWN_FIELD = "foo:bar"; + private static final int INHERITED_POSITION = 2; + private static final String TINY_POSITIVE_THRESHOLD = "ffffffffffffff"; + @ParameterizedTest @MethodSource("goldenSamplingVectors") void createsGoldenSamplingState(double sampleRate, boolean sampled, String expectedTraceState) { OtelTraceState state = OtelTraceState.updateProbability( - null, 1, sampleRate, sampled, sampled ? SAMPLER_KEEP : SAMPLER_DROP); + null, TRACE_ID, sampleRate, sampled, sampled ? SAMPLER_KEEP : SAMPLER_DROP); assertEquals(expectedTraceState, state.getValue()); } static Stream goldenSamplingVectors() { return Stream.of( - arguments(0.01, false, "rv:f0948a54d43b8e;th:fd70a3d70a3d7"), - arguments(0.1, true, "rv:f0948a54d43b8e;th:e6666666666668"), - arguments(0.2, true, "rv:f0948a54d43b8e;th:ccccccccccccd"), - arguments(0.5, true, "rv:f0948a54d43b8e;th:8"), - arguments(0.99, true, "rv:f0948a54d43b8e;th:028f5c28f5c29")); + arguments(SAMPLE_RATE_0_01, false, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_01)), + arguments(SAMPLE_RATE_0_1, true, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_1)), + arguments(SAMPLE_RATE_0_2, true, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_2)), + arguments(SAMPLE_RATE_0_5, true, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_5)), + arguments(SAMPLE_RATE_0_99, true, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_99))); } @Test void omitsStateAtRateZero() { - assertNull(OtelTraceState.updateProbability(null, 1, 0, false, SAMPLER_DROP)); + assertNull(OtelTraceState.updateProbability(null, TRACE_ID, 0, false, SAMPLER_DROP)); } @Test void clampsTinyPositiveRateThreshold() { OtelTraceState state = - OtelTraceState.updateProbability(null, 1, Double.MIN_VALUE, false, SAMPLER_DROP); + OtelTraceState.updateProbability(null, TRACE_ID, Double.MIN_VALUE, false, SAMPLER_DROP); - assertEquals("rv:f0948a54d43b8e;th:ffffffffffffff", state.getValue()); + assertEquals(traceState(LOCAL_RANDOM_VALUE, TINY_POSITIVE_THRESHOLD), state.getValue()); } @Test void correctsKeepAtPrecisionBoundary() { OtelTraceState state = - OtelTraceState.updateProbability(null, 0x03A93EE8B1999F00L, 0.1, true, SAMPLER_KEEP); + OtelTraceState.updateProbability( + null, KEEP_PRECISION_BOUNDARY_TRACE_ID, SAMPLE_RATE_0_1, true, SAMPLER_KEEP); - assertEquals("rv:e6666666666668;th:e6666666666668", state.getValue()); + assertEquals( + traceState(KEEP_PRECISION_BOUNDARY_VALUE, KEEP_PRECISION_BOUNDARY_VALUE), state.getValue()); } @Test void correctsDropAtPrecisionBoundary() { OtelTraceState state = - OtelTraceState.updateProbability(null, 5401449561355763072L, 0.05, false, SAMPLER_DROP); + OtelTraceState.updateProbability( + null, DROP_PRECISION_BOUNDARY_TRACE_ID, SAMPLE_RATE_0_05, false, SAMPLER_DROP); - assertEquals("rv:f333333333332f;th:f333333333333", state.getValue()); + assertEquals( + traceState(DROP_PRECISION_BOUNDARY_RANDOM_VALUE, DROP_PRECISION_BOUNDARY_THRESHOLD), + state.getValue()); } @Test void limiterRejectionRemovesLocallyGeneratedProbability() { OtelTraceState state = OtelTraceState.updateProbability( - OtelTraceState.parse("foo:bar", 2), 1, 0.5, true, SAMPLER_KEEP); + OtelTraceState.parse(UNKNOWN_FIELD, INHERITED_POSITION), + TRACE_ID, + SAMPLE_RATE_0_5, + true, + SAMPLER_KEEP); - state = OtelTraceState.updateProbability(state, 1, 0.5, true, SAMPLER_DROP); + state = OtelTraceState.updateProbability(state, TRACE_ID, SAMPLE_RATE_0_5, true, SAMPLER_DROP); - assertEquals("foo:bar", state.getValue()); + assertEquals(UNKNOWN_FIELD, state.getValue()); assertEquals(0, state.getInheritedPosition()); } @Test void limiterRejectionRetainsInheritedRandomness() { OtelTraceState state = - OtelTraceState.parse("rv:ef284ace7a91e1;th:8;foo:bar", 2); + OtelTraceState.parse( + traceState(INHERITED_RANDOM_VALUE, THRESHOLD_0_5) + ";" + UNKNOWN_FIELD, + INHERITED_POSITION); - state = OtelTraceState.updateProbability(state, 1, 0.5, true, SAMPLER_DROP); + state = OtelTraceState.updateProbability(state, TRACE_ID, SAMPLE_RATE_0_5, true, SAMPLER_DROP); - assertEquals("rv:ef284ace7a91e1;foo:bar", state.getValue()); + assertEquals("rv:" + INHERITED_RANDOM_VALUE + ";" + UNKNOWN_FIELD, state.getValue()); assertEquals(0, state.getInheritedPosition()); } @Test void nonProbabilityDecisionRetainsOnlyInheritedRandomnessAndUnknownFields() { - OtelTraceState state = OtelTraceState.parse("th:e6666666666668;foo:bar;rv:ef284ace7a91e1", 2); + OtelTraceState state = + OtelTraceState.parse( + "th:" + THRESHOLD_0_1 + ";" + UNKNOWN_FIELD + ";rv:" + INHERITED_RANDOM_VALUE, + INHERITED_POSITION); state = state.removeForNonProbabilityDecision(); - assertEquals("rv:ef284ace7a91e1;foo:bar", state.getValue()); + assertEquals("rv:" + INHERITED_RANDOM_VALUE + ";" + UNKNOWN_FIELD, state.getValue()); assertEquals(0, state.getInheritedPosition()); } - @Test - void deterministicSamplerRetainsDoublePrecisionRate() { - double rate = 0.123456789012345; - - assertEquals(rate, new DeterministicSampler.TraceSampler(rate).getSampleRate()); + private static String traceState(String randomValue, String threshold) { + return "rv:" + randomValue + ";th:" + threshold; } } From d22f3fba9a3bf3f18256fe3a2f268e170e69b68e Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Thu, 3 Sep 2026 18:10:06 +0200 Subject: [PATCH 4/7] fix(propagation): align otel sampling state behavior --- .../sampling/RuleBasedTraceSampler.java | 31 +++---- .../propagation/ptags/OtelTraceState.java | 40 ++++++---- .../trace/core/CoreSpanBuilderTest.java | 1 + .../trace/core/OtelSamplingDecisionTest.java | 14 +++- .../W3COtelTraceStateContinuationTest.java | 80 +++++++++++++++++++ .../propagation/ptags/OtelTraceStateTest.java | 21 +++-- 6 files changed, 140 insertions(+), 47 deletions(-) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/propagation/W3COtelTraceStateContinuationTest.java diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java index 6fe8e754664..f419bce31d2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java @@ -147,31 +147,20 @@ public > void setSamplingPriority(final T span) { fallbackSampler.setSamplingPriority(span); } else { boolean sampled = matchedRule.sample(span); + int samplingPriority; if (sampled) { - if (rateLimiter.tryAcquire()) { - span.setSamplingPriority( - PrioritySampling.USER_KEEP, - SAMPLING_RULE_RATE, - matchedRule.getSampler().getSampleRate(), - matchedRule.getMechanism(), - Boolean.TRUE); - } else { - span.setSamplingPriority( - PrioritySampling.USER_DROP, - SAMPLING_RULE_RATE, - matchedRule.getSampler().getSampleRate(), - matchedRule.getMechanism(), - Boolean.TRUE); - } + samplingPriority = + rateLimiter.tryAcquire() ? PrioritySampling.USER_KEEP : PrioritySampling.USER_DROP; span.setMetric(SAMPLING_LIMIT_RATE, rateLimit); } else { - span.setSamplingPriority( - PrioritySampling.USER_DROP, - SAMPLING_RULE_RATE, - matchedRule.getSampler().getSampleRate(), - matchedRule.getMechanism(), - Boolean.FALSE); + samplingPriority = PrioritySampling.USER_DROP; } + span.setSamplingPriority( + samplingPriority, + SAMPLING_RULE_RATE, + matchedRule.getSampler().getSampleRate(), + matchedRule.getMechanism(), + Boolean.valueOf(sampled)); } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java index 26bf366cd61..0023c4fd067 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java @@ -10,6 +10,7 @@ final class OtelTraceState { private static final long KNUTH_FACTOR = 1111111111111111111L; private static final long MAX_THRESHOLD = (1L << 56) - 1; private static final double THRESHOLD_RANGE = 1L << 56; + private static final String[] EMPTY_UNKNOWN_FIELDS = new String[0]; private final String value; private final String randomValue; @@ -42,7 +43,7 @@ static OtelTraceState parse(String raw, int inheritedPosition) { } List fields = new ArrayList<>(); - List unknownFields = new ArrayList<>(); + List unknownFields = null; String randomValue = null; String threshold = null; int randomValueCount = 0; @@ -80,6 +81,9 @@ && isLowercaseHex(fieldValue)) { } } else if (!field.isEmpty()) { fields.add(field); + if (unknownFields == null) { + unknownFields = new ArrayList<>(); + } unknownFields.add(field); } else { changed = true; @@ -98,7 +102,7 @@ && isLowercaseHex(fieldValue)) { value, randomValue, threshold, - unknownFields.toArray(new String[0]), + unknownFields == null ? EMPTY_UNKNOWN_FIELDS : unknownFields.toArray(new String[0]), randomValueCount, changed ? 0 : inheritedPosition, false); @@ -110,13 +114,15 @@ static OtelTraceState updateProbability( double sampleRate, boolean sampled, int samplingPriority) { - // `sampled` is the raw probability result; `samplingPriority` may be changed by rate limiting. - if (sampleRate <= 0) { - return current == null ? null : current.removeLocalProbability(); - } + String[] unknownFields = current == null ? EMPTY_UNKNOWN_FIELDS : current.unknownFields; + // `sampled` is the raw probability result; `samplingPriority` may be changed by rate limiting. if (sampled && samplingPriority <= 0) { - return current == null ? null : current.removeForNonProbabilityDecision(); + if (current != null) { + return current.removeThresholdForLimiterDemotion(); + } + return create( + formatRandomValue(computeRandomValue(traceIdLowOrderBits)), null, unknownFields, true); } long threshold = computeThreshold(sampleRate); @@ -127,9 +133,7 @@ static OtelTraceState updateProbability( randomValue = threshold == 0 ? 0 : threshold - 1; } - String[] unknownFields = current == null ? new String[0] : current.unknownFields; - return create( - formatRandomValue(randomValue), formatThreshold(threshold), unknownFields, 0, true); + return create(formatRandomValue(randomValue), formatThreshold(threshold), unknownFields, true); } OtelTraceState removeForNonProbabilityDecision() { @@ -137,7 +141,14 @@ OtelTraceState removeForNonProbabilityDecision() { return this; } String retainedRandomValue = locallyGeneratedRandomValue ? null : randomValue; - return create(retainedRandomValue, null, unknownFields, 0, false); + return create(retainedRandomValue, null, unknownFields, false); + } + + OtelTraceState removeThresholdForLimiterDemotion() { + if (threshold == null) { + return this; + } + return create(randomValue, null, unknownFields, locallyGeneratedRandomValue); } String getValue() { @@ -152,15 +163,10 @@ int getInheritedPosition() { return inheritedPosition; } - private OtelTraceState removeLocalProbability() { - return locallyGeneratedRandomValue ? create(null, null, unknownFields, 0, false) : this; - } - private static OtelTraceState create( String randomValue, String threshold, String[] unknownFields, - int inheritedPosition, boolean locallyGeneratedRandomValue) { StringBuilder value = new StringBuilder(); append(value, randomValue == null ? null : "rv:" + randomValue); @@ -177,7 +183,7 @@ private static OtelTraceState create( threshold, unknownFields, randomValue == null ? 0 : 1, - inheritedPosition, + 0, locallyGeneratedRandomValue); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java index a09947bb3b3..c8bb8ff809f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java @@ -67,6 +67,7 @@ public class CoreSpanBuilderTest extends DDCoreJavaSpecification { private static final String OTEL_MEMBER = "ot="; private static final String THRESHOLD_0_5 = ";th:8"; private static final double SAMPLE_RATE_0_5 = 0.5; + private static final String DATADOG_TRACE_STATE = "_dd.p.dm=934086a686-4,_dd.p.anytag=value"; private ListWriter writer; private CoreTracer tracer; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java b/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java index e8b6607138a..de5290e4c93 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java @@ -33,6 +33,8 @@ class OtelSamplingDecisionTest extends DDCoreJavaSpecification { private static final String OTEL_MEMBER = "ot="; private static final String OTEL_RANDOM_VALUE_PREFIX = "ot=rv:"; private static final String HALF_THRESHOLD = ";th:8"; + private static final String MAX_THRESHOLD = ";th:ffffffffffffff"; + private static final String OTEL_THRESHOLD_PREFIX = ";th:"; private static final double SAMPLE_RATE_0_5 = 0.5; private static final String SAMPLE_RATE_0_5_RULE = "[{\"sample_rate\": 0.5}]"; private static final String FULL_SAMPLE_RATE_RULE = "[{\"sample_rate\": 1}]"; @@ -73,7 +75,7 @@ void loadedAgentRateEstablishesOtelProbabilityState() { } @Test - void loadedZeroAgentRateDoesNotEstablishOtelProbabilityState() { + void loadedZeroAgentRateEstablishesOtelProbabilityStateWithMaxThreshold() { RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); sampler.onResponse(AGENT_RATE_ENDPOINT, agentRates(0)); CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); @@ -82,7 +84,9 @@ void loadedZeroAgentRateDoesNotEstablishOtelProbabilityState() { sampler.setSamplingPriority(span); - assertFalse(w3cHeader(span).contains(OTEL_MEMBER)); + String header = w3cHeader(span); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(header.contains(MAX_THRESHOLD)); } finally { tracer.close(); } @@ -114,7 +118,7 @@ void configuredRulesEstablishOtelProbabilityState(boolean traceRule) { } @Test - void limiterRejectionDoesNotTurnProbabilityKeepIntoOtelDrop() { + void limiterRejectionStripsThresholdButKeepsOtelRandomValue() { Properties properties = new Properties(); properties.setProperty(TRACE_SAMPLING_RULES, FULL_SAMPLE_RATE_RULE); properties.setProperty(TRACE_RATE_LIMIT, ONE_PER_SECOND_RATE_LIMIT); @@ -129,7 +133,9 @@ void limiterRejectionDoesNotTurnProbabilityKeepIntoOtelDrop() { assertTrue(w3cHeader(allowed).contains(OTEL_RANDOM_VALUE_PREFIX)); assertEquals(USER_DROP, rejected.samplingPriority()); - assertFalse(w3cHeader(rejected).contains(OTEL_MEMBER)); + String rejectedHeader = w3cHeader(rejected); + assertTrue(rejectedHeader.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertFalse(rejectedHeader.contains(OTEL_THRESHOLD_PREFIX)); } finally { tracer.close(); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3COtelTraceStateContinuationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3COtelTraceStateContinuationTest.java new file mode 100644 index 00000000000..4c27fdeb7fb --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3COtelTraceStateContinuationTest.java @@ -0,0 +1,80 @@ +package datadog.trace.core.propagation; + +import static datadog.trace.bootstrap.instrumentation.api.ContextVisitors.stringValuesMap; +import static datadog.trace.core.propagation.HttpCodecTestHelper.headers; +import static datadog.trace.core.propagation.W3CHttpCodec.TRACE_PARENT_KEY; +import static datadog.trace.core.propagation.W3CHttpCodec.TRACE_STATE_KEY; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.Config; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.DDSpanContext; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Exercises the {@code ot=} tracestate member through the real {@link W3CHttpCodec} extractor and + * injector, continuing an inbound trace rather than parsing tracestate or building an {@link + * ExtractedContext} by hand. + */ +class W3COtelTraceStateContinuationTest extends DDCoreJavaSpecification { + + private static final String TRACE_PARENT = + "00-00000000000000000000000000000001-123456789abcdef0-01"; + private static final String OTEL_RANDOM_VALUE = "ef284ace7a91e1"; + private static final String OTEL_THRESHOLD = "e6666666666668"; + private static final String OTEL_MALFORMED_RANDOM_VALUE = "zz"; + private static final String DD_MEMBER = "dd=s:2;p:123456789abcdef0"; + + private final HttpCodec.Injector injector = W3CHttpCodec.newInjector(emptyMap()); + + @Test + void roundTripsOtelTraceState() { + String inboundTracestate = DD_MEMBER + ",ot=rv:" + OTEL_RANDOM_VALUE + ";th:" + OTEL_THRESHOLD; + String outboundTracestate = continueTraceAndReinject(inboundTracestate); + + assertTrue(outboundTracestate.contains("ot=rv:" + OTEL_RANDOM_VALUE + ";th:" + OTEL_THRESHOLD)); + } + + @Test + void removesMalformedOtelRandomValueButKeepsThreshold() { + String inboundTracestate = + DD_MEMBER + ",ot=rv:" + OTEL_MALFORMED_RANDOM_VALUE + ";th:" + OTEL_THRESHOLD; + String outboundTracestate = continueTraceAndReinject(inboundTracestate); + + assertTrue(outboundTracestate.contains("ot=th:" + OTEL_THRESHOLD)); + assertTrue(!outboundTracestate.contains("rv:" + OTEL_MALFORMED_RANDOM_VALUE)); + } + + private String continueTraceAndReinject(String inboundTracestate) { + Map inboundHeaders = + headers(TRACE_PARENT_KEY, TRACE_PARENT, TRACE_STATE_KEY, inboundTracestate); + + CoreTracer tracer = tracerBuilder().build(); + try { + HttpCodec.Extractor extractor = + W3CHttpCodec.newExtractor(Config.get(), tracer::captureTraceConfig); + Object extracted = extractor.extract(inboundHeaders, stringValuesMap()); + assertInstanceOf(ExtractedContext.class, extracted); + ExtractedContext extractedContext = (ExtractedContext) extracted; + + AgentSpan continuedSpan = + tracer.buildSpan("test", "continued").asChildOf(extractedContext).start(); + assertEquals(extractedContext.getSamplingPriority(), continuedSpan.getSamplingPriority()); + + Map outboundHeaders = new HashMap<>(); + this.injector.inject((DDSpanContext) continuedSpan.spanContext(), outboundHeaders, Map::put); + + continuedSpan.finish(); + return outboundHeaders.get(TRACE_STATE_KEY); + } finally { + tracer.close(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java index ea69a632271..f19279d24f0 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java @@ -3,7 +3,6 @@ import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.params.provider.Arguments.arguments; import java.util.stream.Stream; @@ -16,6 +15,7 @@ class OtelTraceStateTest { // A stable trace ID covers sampling decisions at representative rates. private static final long TRACE_ID = 1; + private static final long OTEL_SPEC_WORKED_EXAMPLE_TRACE_ID = 0xfff972474538efffL; private static final String LOCAL_RANDOM_VALUE = "f0948a54d43b8e"; private static final double SAMPLE_RATE_0_01 = 0.01; private static final String THRESHOLD_0_01 = "fd70a3d70a3d7"; @@ -60,8 +60,19 @@ static Stream goldenSamplingVectors() { } @Test - void omitsStateAtRateZero() { - assertNull(OtelTraceState.updateProbability(null, TRACE_ID, 0, false, SAMPLER_DROP)); + void matchesOtelSpecWorkedExample() { + OtelTraceState state = + OtelTraceState.updateProbability( + null, OTEL_SPEC_WORKED_EXAMPLE_TRACE_ID, SAMPLE_RATE_0_1, true, SAMPLER_KEEP); + + assertEquals(traceState(INHERITED_RANDOM_VALUE, THRESHOLD_0_1), state.getValue()); + } + + @Test + void emitsMaxThresholdAtRateZero() { + OtelTraceState state = OtelTraceState.updateProbability(null, TRACE_ID, 0, false, SAMPLER_DROP); + + assertEquals(traceState(LOCAL_RANDOM_VALUE, TINY_POSITIVE_THRESHOLD), state.getValue()); } @Test @@ -94,7 +105,7 @@ void correctsDropAtPrecisionBoundary() { } @Test - void limiterRejectionRemovesLocallyGeneratedProbability() { + void limiterRejectionRetainsLocallyGeneratedRandomness() { OtelTraceState state = OtelTraceState.updateProbability( OtelTraceState.parse(UNKNOWN_FIELD, INHERITED_POSITION), @@ -105,7 +116,7 @@ void limiterRejectionRemovesLocallyGeneratedProbability() { state = OtelTraceState.updateProbability(state, TRACE_ID, SAMPLE_RATE_0_5, true, SAMPLER_DROP); - assertEquals(UNKNOWN_FIELD, state.getValue()); + assertEquals("rv:" + LOCAL_RANDOM_VALUE + ";" + UNKNOWN_FIELD, state.getValue()); assertEquals(0, state.getInheritedPosition()); } From 69492df48ae6cb86c7b3259eac4b556825509bcb Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Fri, 4 Sep 2026 14:25:11 +0200 Subject: [PATCH 5/7] test(sampling): restore deterministic sampler table test --- .../sampling/DeterministicTraceSamplerTest.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java index f5ab0baa29c..c97a8056336 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java @@ -19,10 +19,6 @@ class DeterministicTraceSamplerTest { - private static final String SAMPLE_RATE_COLUMN = "rate "; - private static final String SAMPLE_RATE_0_123456789 = "0.123456789"; - private static final String SAMPLE_RATE_0_999999999 = "0.999999999"; - @TableTest({ "scenario | expected | traceId ", "10428415896243638596 f | false | 10428415896243638596", @@ -361,8 +357,11 @@ void testSamplingAll(String traceId) { assertTrue(sampler.sample(span)); } - // These values fail if the configured rate is narrowed to a float. - @TableTest({SAMPLE_RATE_COLUMN, SAMPLE_RATE_0_123456789, SAMPLE_RATE_0_999999999}) + @TableTest({ + "rate ", + "0.123456789", + "0.999999999" + }) void preservesConfiguredSampleRate(double rate) { assertEquals(rate, new DeterministicSampler.TraceSampler(rate).getSampleRate()); } From 8f9ec40ffa053fcf840d936ea4b7fe861180e64e Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Fri, 4 Sep 2026 14:45:35 +0200 Subject: [PATCH 6/7] perf(propagation): compact OTel trace state representation --- .../propagation/ptags/OtelTraceState.java | 286 ++++++++++-------- 1 file changed, 165 insertions(+), 121 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java index 0023c4fd067..3ad895659d3 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java @@ -1,8 +1,5 @@ package datadog.trace.core.propagation.ptags; -import java.util.ArrayList; -import java.util.List; - final class OtelTraceState { static final int MAX_VALUE_LENGTH = 256; @@ -10,31 +7,25 @@ final class OtelTraceState { private static final long KNUTH_FACTOR = 1111111111111111111L; private static final long MAX_THRESHOLD = (1L << 56) - 1; private static final double THRESHOLD_RANGE = 1L << 56; - private static final String[] EMPTY_UNKNOWN_FIELDS = new String[0]; + private static final long NO_VALUE = -1; + private static final int HAS_MULTIPLE_RANDOM_VALUES = 1; + private static final int HAS_LOCALLY_GENERATED_RANDOM_VALUE = 1 << 1; + private static final String RANDOM_VALUE_KEY = "rv:"; + private static final String THRESHOLD_KEY = "th:"; private final String value; - private final String randomValue; - private final String threshold; - private final String[] unknownFields; - private final int randomValueCount; + private final long randomValue; + private final long threshold; private final int inheritedPosition; - private final boolean locallyGeneratedRandomValue; + private final int flags; private OtelTraceState( - String value, - String randomValue, - String threshold, - String[] unknownFields, - int randomValueCount, - int inheritedPosition, - boolean locallyGeneratedRandomValue) { + String value, long randomValue, long threshold, int inheritedPosition, int flags) { this.value = value; this.randomValue = randomValue; this.threshold = threshold; - this.unknownFields = unknownFields; - this.randomValueCount = randomValueCount; this.inheritedPosition = inheritedPosition; - this.locallyGeneratedRandomValue = locallyGeneratedRandomValue; + this.flags = flags; } static OtelTraceState parse(String raw, int inheritedPosition) { @@ -42,70 +33,73 @@ static OtelTraceState parse(String raw, int inheritedPosition) { return null; } - List fields = new ArrayList<>(); - List unknownFields = null; - String randomValue = null; - String threshold = null; - int randomValueCount = 0; - boolean changed = false; + long randomValue = NO_VALUE; + long threshold = NO_VALUE; + int flags = 0; + StringBuilder normalized = null; int start = 0; while (start < raw.length()) { int end = raw.indexOf(';', start); if (end < 0) { end = raw.length(); } - String field = raw.substring(start, end); - int separator = field.indexOf(':'); - String key = separator > 0 ? field.substring(0, separator) : field; - String fieldValue = separator > 0 ? field.substring(separator + 1) : ""; - if ("rv".equals(key)) { - if (fieldValue.length() == HEX_DIGITS && isLowercaseHex(fieldValue)) { - fields.add(field); - if (randomValue == null) { - randomValue = fieldValue; + int separator = raw.indexOf(':', start); + if (separator >= end) { + separator = -1; + } + int fieldValueStart = separator < 0 ? end : separator + 1; + if (hasKey(raw, start, end, separator, 'r', 'v')) { + long parsedRandomValue = + end - fieldValueStart == HEX_DIGITS + ? parseLowercaseHex(raw, fieldValueStart, end) + : NO_VALUE; + if (parsedRandomValue != NO_VALUE) { + if (randomValue == NO_VALUE) { + randomValue = parsedRandomValue; + } else { + flags |= HAS_MULTIPLE_RANDOM_VALUES; + } + if (normalized != null) { + appendField(normalized, raw, start, end); } - randomValueCount++; } else { - changed = true; + normalized = startNormalizing(raw, normalized, start); } - } else if ("th".equals(key)) { - if (!fieldValue.isEmpty() - && fieldValue.length() <= HEX_DIGITS - && isLowercaseHex(fieldValue)) { - fields.add(field); - if (threshold == null) { - threshold = fieldValue; + } else if (hasKey(raw, start, end, separator, 't', 'h')) { + long parsedThreshold = + fieldValueStart < end && end - fieldValueStart <= HEX_DIGITS + ? parseLowercaseHex(raw, fieldValueStart, end) + : NO_VALUE; + if (parsedThreshold != NO_VALUE) { + if (threshold == NO_VALUE) { + threshold = parsedThreshold; + } + if (normalized != null) { + appendField(normalized, raw, start, end); } } else { - changed = true; + normalized = startNormalizing(raw, normalized, start); } - } else if (!field.isEmpty()) { - fields.add(field); - if (unknownFields == null) { - unknownFields = new ArrayList<>(); + } else if (start < end) { + if (normalized != null) { + appendField(normalized, raw, start, end); } - unknownFields.add(field); } else { - changed = true; + normalized = startNormalizing(raw, normalized, start); } start = end + 1; } - String value = join(fields); - if (value == null) { - return null; + if (raw.charAt(raw.length() - 1) == ';') { + normalized = startNormalizing(raw, normalized, raw.length()); } - if (!value.equals(raw)) { - changed = true; + + String value = normalized == null ? raw : normalized.toString(); + if (value.isEmpty()) { + return null; } return new OtelTraceState( - value, - randomValue, - threshold, - unknownFields == null ? EMPTY_UNKNOWN_FIELDS : unknownFields.toArray(new String[0]), - randomValueCount, - changed ? 0 : inheritedPosition, - false); + value, randomValue, threshold, normalized == null ? inheritedPosition : 0, flags); } static OtelTraceState updateProbability( @@ -114,15 +108,14 @@ static OtelTraceState updateProbability( double sampleRate, boolean sampled, int samplingPriority) { - String[] unknownFields = current == null ? EMPTY_UNKNOWN_FIELDS : current.unknownFields; + String currentValue = current == null ? null : current.value; // `sampled` is the raw probability result; `samplingPriority` may be changed by rate limiting. if (sampled && samplingPriority <= 0) { if (current != null) { return current.removeThresholdForLimiterDemotion(); } - return create( - formatRandomValue(computeRandomValue(traceIdLowOrderBits)), null, unknownFields, true); + return create(computeRandomValue(traceIdLowOrderBits), NO_VALUE, currentValue, true); } long threshold = computeThreshold(sampleRate); @@ -133,22 +126,22 @@ static OtelTraceState updateProbability( randomValue = threshold == 0 ? 0 : threshold - 1; } - return create(formatRandomValue(randomValue), formatThreshold(threshold), unknownFields, true); + return create(randomValue, threshold, currentValue, true); } OtelTraceState removeForNonProbabilityDecision() { - if (!locallyGeneratedRandomValue && threshold == null && randomValueCount <= 1) { + if (!hasLocallyGeneratedRandomValue() && threshold == NO_VALUE && !hasMultipleRandomValues()) { return this; } - String retainedRandomValue = locallyGeneratedRandomValue ? null : randomValue; - return create(retainedRandomValue, null, unknownFields, false); + long retainedRandomValue = hasLocallyGeneratedRandomValue() ? NO_VALUE : randomValue; + return create(retainedRandomValue, NO_VALUE, value, false); } OtelTraceState removeThresholdForLimiterDemotion() { - if (threshold == null) { + if (threshold == NO_VALUE) { return this; } - return create(randomValue, null, unknownFields, locallyGeneratedRandomValue); + return create(randomValue, NO_VALUE, value, hasLocallyGeneratedRandomValue()); } String getValue() { @@ -164,15 +157,16 @@ int getInheritedPosition() { } private static OtelTraceState create( - String randomValue, - String threshold, - String[] unknownFields, - boolean locallyGeneratedRandomValue) { + long randomValue, long threshold, String previousValue, boolean locallyGeneratedRandomValue) { StringBuilder value = new StringBuilder(); - append(value, randomValue == null ? null : "rv:" + randomValue); - append(value, threshold == null ? null : "th:" + threshold); - for (String field : unknownFields) { - append(value, field); + if (randomValue != NO_VALUE) { + appendRandomValue(value, randomValue); + } + if (threshold != NO_VALUE) { + appendThreshold(value, threshold); + } + if (previousValue != null) { + appendUnknownFields(value, previousValue); } if (value.length() == 0) { return null; @@ -181,48 +175,110 @@ private static OtelTraceState create( value.toString(), randomValue, threshold, - unknownFields, - randomValue == null ? 0 : 1, 0, - locallyGeneratedRandomValue); + locallyGeneratedRandomValue ? HAS_LOCALLY_GENERATED_RANDOM_VALUE : 0); + } + + private static StringBuilder startNormalizing(String raw, StringBuilder normalized, int start) { + if (normalized != null) { + return normalized; + } + normalized = new StringBuilder(raw.length()); + if (start > 0) { + normalized.append(raw, 0, start - 1); + } + return normalized; } - private static void append(StringBuilder value, String field) { - if (field == null || field.isEmpty()) { + private static void appendRandomValue(StringBuilder value, long randomValue) { + if (appendFieldPrefix(value, RANDOM_VALUE_KEY.length() + HEX_DIGITS)) { + value.append(RANDOM_VALUE_KEY); + appendHex(value, randomValue, HEX_DIGITS); + } + } + + private static void appendThreshold(StringBuilder value, long threshold) { + int hexDigits = thresholdHexDigits(threshold); + if (appendFieldPrefix(value, THRESHOLD_KEY.length() + hexDigits)) { + value.append(THRESHOLD_KEY); + appendHex(value, threshold, hexDigits); + } + } + + private static void appendUnknownFields(StringBuilder value, String previousValue) { + int start = 0; + while (start < previousValue.length()) { + int end = previousValue.indexOf(';', start); + if (end < 0) { + end = previousValue.length(); + } + int separator = previousValue.indexOf(':', start); + if (separator >= end) { + separator = -1; + } + if (!hasKey(previousValue, start, end, separator, 'r', 'v') + && !hasKey(previousValue, start, end, separator, 't', 'h')) { + appendField(value, previousValue, start, end); + } + start = end + 1; + } + } + + private static void appendField(StringBuilder value, String field, int start, int end) { + if (!appendFieldPrefix(value, end - start)) { return; } + value.append(field, start, end); + } + + private static boolean appendFieldPrefix(StringBuilder value, int fieldLength) { int separatorSize = value.length() == 0 ? 0 : 1; - if (value.length() + separatorSize + field.length() > MAX_VALUE_LENGTH) { - return; + if (value.length() + separatorSize + fieldLength > MAX_VALUE_LENGTH) { + return false; } if (separatorSize != 0) { value.append(';'); } - value.append(field); + return true; } - private static String join(List fields) { - if (fields.isEmpty()) { - return null; - } - StringBuilder value = new StringBuilder(); - for (String field : fields) { - if (value.length() != 0) { - value.append(';'); - } - value.append(field); + private static boolean hasKey( + String value, int start, int end, int separator, char first, char second) { + if (separator >= 0) { + return separator == start + 2 + && value.charAt(start) == first + && value.charAt(start + 1) == second; } - return value.toString(); + return end == start + 2 && value.charAt(start) == first && value.charAt(start + 1) == second; } - private static boolean isLowercaseHex(String value) { - for (int i = 0; i < value.length(); i++) { + private static long parseLowercaseHex(String value, int start, int end) { + long parsed = 0; + for (int i = start; i < end; i++) { char character = value.charAt(i); - if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) { - return false; + if (character >= '0' && character <= '9') { + parsed = (parsed << 4) | character - '0'; + } else if (character >= 'a' && character <= 'f') { + parsed = (parsed << 4) | character - 'a' + 10; + } else { + return NO_VALUE; } } - return true; + return parsed; + } + + private boolean hasMultipleRandomValues() { + return (flags & HAS_MULTIPLE_RANDOM_VALUES) != 0; + } + + private boolean hasLocallyGeneratedRandomValue() { + return (flags & HAS_LOCALLY_GENERATED_RANDOM_VALUE) != 0; + } + + private static void appendHex(StringBuilder value, long number, int digits) { + for (int shift = (HEX_DIGITS - 1) * 4; shift >= (HEX_DIGITS - digits) * 4; shift -= 4) { + value.append(Character.forDigit((int) (number >>> shift) & 0xF, 16)); + } } private static long computeRandomValue(long traceIdLowOrderBits) { @@ -234,24 +290,12 @@ private static long computeThreshold(double sampleRate) { return Math.max(0, Math.min(threshold, MAX_THRESHOLD)); } - private static String formatRandomValue(long randomValue) { - String hex = Long.toHexString(randomValue); - if (hex.length() == HEX_DIGITS) { - return hex; - } - StringBuilder padded = new StringBuilder(HEX_DIGITS); - for (int i = hex.length(); i < HEX_DIGITS; i++) { - padded.append('0'); - } - return padded.append(hex).toString(); - } - - private static String formatThreshold(long threshold) { - String hex = formatRandomValue(threshold); - int end = hex.length(); - while (end > 1 && hex.charAt(end - 1) == '0') { - end--; + private static int thresholdHexDigits(long threshold) { + int digits = HEX_DIGITS; + while (digits > 1 && (threshold & 0xF) == 0) { + digits--; + threshold >>>= 4; } - return hex.substring(0, end); + return digits; } } From 63c96913259c75a18834bd1d07426a11c4244c1a Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Fri, 4 Sep 2026 15:56:14 +0200 Subject: [PATCH 7/7] perf(propagation): pre-size OTel tracestate builder --- .../datadog/trace/core/propagation/ptags/OtelTraceState.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java index 3ad895659d3..a3b3c5e7760 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java @@ -12,6 +12,8 @@ final class OtelTraceState { private static final int HAS_LOCALLY_GENERATED_RANDOM_VALUE = 1 << 1; private static final String RANDOM_VALUE_KEY = "rv:"; private static final String THRESHOLD_KEY = "th:"; + private static final int DEFAULT_VALUE_CAPACITY = + RANDOM_VALUE_KEY.length() + HEX_DIGITS + 1 + THRESHOLD_KEY.length() + HEX_DIGITS; private final String value; private final long randomValue; @@ -158,7 +160,7 @@ int getInheritedPosition() { private static OtelTraceState create( long randomValue, long threshold, String previousValue, boolean locallyGeneratedRandomValue) { - StringBuilder value = new StringBuilder(); + StringBuilder value = new StringBuilder(DEFAULT_VALUE_CAPACITY); if (randomValue != NO_VALUE) { appendRandomValue(value, randomValue); }