From d2104b01e9c3ffb67ef086666d8da3e6e08cc3a3 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Mon, 14 Sep 2026 17:04:23 +0200 Subject: [PATCH 01/22] feat(propagation): model otel probability sampling state --- .../propagation/ptags/OtelTraceState.java | 435 +++++++++++++++++- .../ptags/OtelTraceStateParsingTest.java | 47 ++ .../propagation/ptags/OtelTraceStateTest.java | 88 ++++ 3 files changed, 562 insertions(+), 8 deletions(-) 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/core/propagation/ptags/OtelTraceState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java index dc6456e88ec..7b938646c3c 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,29 +1,234 @@ package datadog.trace.core.propagation.ptags; -final class OtelTraceState { +final class OtelTraceState implements CharSequence { + private static final String RANDOM_VALUE_KEY = "rv:"; + private static final String THRESHOLD_KEY = "th:"; + private static final long HASH_MULTIPLIER = 1_111_111_111_111_111_111L; + private static final long MAX_56_BIT_VALUE = 0x00ff_ffff_ffff_ffffL; + private static final double TWO_TO_56 = 0x1.0p56; + private static final int MAX_VALUE_LENGTH = 256; + private final CharSequence value; + private final CharSequence fields; private final int originalPosition; private final int originalSize; + private final long randomValue; + private final long threshold; + private final int randomValueStart; + private final int randomValueEnd; + private final int thresholdStart; + private final int thresholdEnd; + private final boolean includeRandomValue; + private final boolean includeThreshold; + private final boolean inheritedRandomValue; + private volatile String materializedValue; - private OtelTraceState(CharSequence value, int originalPosition, int originalSize) { + private OtelTraceState( + CharSequence value, + CharSequence fields, + int originalPosition, + int originalSize, + long randomValue, + long threshold, + int randomValueStart, + int randomValueEnd, + int thresholdStart, + int thresholdEnd, + boolean includeRandomValue, + boolean includeThreshold, + boolean inheritedRandomValue) { this.value = value; + this.fields = fields; this.originalPosition = originalPosition; this.originalSize = originalSize; + this.randomValue = randomValue; + this.threshold = threshold; + this.randomValueStart = randomValueStart; + this.randomValueEnd = randomValueEnd; + this.thresholdStart = thresholdStart; + this.thresholdEnd = thresholdEnd; + this.includeRandomValue = includeRandomValue; + this.includeThreshold = includeThreshold; + this.inheritedRandomValue = inheritedRandomValue; } static OtelTraceState parse(CharSequence raw, int originalPosition, int originalSize) { - if (raw == null || raw.length() == 0) { + if (raw == null || raw.length() == 0 || raw.length() > MAX_VALUE_LENGTH) { + return null; + } + + int randomValueStart = -1; + int randomValueEnd = -1; + int thresholdStart = -1; + int thresholdEnd = -1; + boolean randomValueSeen = false; + boolean invalidRandomValue = false; + boolean hasUnknownField = false; + boolean normalized = false; + int start = 0; + while (start <= raw.length()) { + int end = indexOf(raw, ';', start); + if (end < 0) { + end = raw.length(); + } + if (startsWith(raw, start, end, RANDOM_VALUE_KEY)) { + int candidateStart = start + RANDOM_VALUE_KEY.length(); + if (!randomValueSeen) { + randomValueSeen = true; + if (isLowerHex(raw, candidateStart, end, 14, 14)) { + randomValueStart = candidateStart; + randomValueEnd = end; + } else { + invalidRandomValue = true; + normalized = true; + } + } else { + normalized = true; + } + } else if (startsWith(raw, start, end, THRESHOLD_KEY)) { + int candidateStart = start + THRESHOLD_KEY.length(); + if (thresholdStart < 0 && isLowerHex(raw, candidateStart, end, 1, 14)) { + thresholdStart = candidateStart; + thresholdEnd = end; + } else { + normalized = true; + } + } else if (isUnknownField(raw, start, end)) { + hasUnknownField = true; + } else { + normalized = true; + } + if (end == raw.length()) { + break; + } + start = end + 1; + } + + if (invalidRandomValue) { + thresholdStart = -1; + thresholdEnd = -1; + } + + if (randomValueStart < 0 && thresholdStart < 0 && !hasUnknownField) { return null; } - return new OtelTraceState(raw, originalPosition, originalSize); + if (normalized) { + CharSequence normalizedValue = + normalize(raw, randomValueStart, randomValueEnd, thresholdStart, thresholdEnd); + return parseCanonical(normalizedValue, originalPosition, originalSize); + } + return new OtelTraceState( + raw, + raw, + originalPosition, + originalSize, + parseHex(raw, randomValueStart, randomValueEnd), + parseThreshold(raw, thresholdStart, thresholdEnd), + randomValueStart, + randomValueEnd, + thresholdStart, + thresholdEnd, + randomValueStart >= 0, + thresholdStart >= 0, + true); } - CharSequence getValue() { - return value; + private static OtelTraceState parseCanonical( + CharSequence value, int originalPosition, int originalSize) { + int randomValueStart = -1; + int randomValueEnd = -1; + int thresholdStart = -1; + int thresholdEnd = -1; + int start = 0; + while (start < value.length()) { + int end = indexOf(value, ';', start); + if (end < 0) { + end = value.length(); + } + if (startsWith(value, start, end, RANDOM_VALUE_KEY)) { + randomValueStart = start + RANDOM_VALUE_KEY.length(); + randomValueEnd = end; + } else if (startsWith(value, start, end, THRESHOLD_KEY)) { + thresholdStart = start + THRESHOLD_KEY.length(); + thresholdEnd = end; + } + start = end + 1; + } + return new OtelTraceState( + value, + value, + originalPosition, + originalSize, + parseHex(value, randomValueStart, randomValueEnd), + parseThreshold(value, thresholdStart, thresholdEnd), + randomValueStart, + randomValueEnd, + thresholdStart, + thresholdEnd, + randomValueStart >= 0, + thresholdStart >= 0, + true); } - int length() { - return value.length(); + static OtelTraceState fromProbabilityDecision( + long traceIdLowOrderBits, double rate, boolean sampled) { + long hash = traceIdLowOrderBits * HASH_MULTIPLIER; + long randomValue = (~hash) >>> 8; + long threshold = Math.round((1.0 - rate) * TWO_TO_56); + if (threshold > MAX_56_BIT_VALUE) { + threshold = MAX_56_BIT_VALUE; + } + if (sampled && randomValue < threshold) { + randomValue = threshold; + } else if (!sampled && randomValue >= threshold) { + randomValue = threshold == 0 ? 0 : threshold - 1; + } + + return new OtelTraceState( + null, null, 1, 0, randomValue, threshold, -1, -1, -1, -1, true, true, false); + } + + OtelTraceState withoutThreshold() { + if (!includeThreshold) { + return this; + } + return withFields(includeRandomValue, false, inheritedRandomValue); + } + + OtelTraceState forNonProbabilityDecision() { + return withFields(inheritedRandomValue && includeRandomValue, false, inheritedRandomValue); + } + + boolean isConsistentWith(boolean sampled) { + if (!includeRandomValue || !includeThreshold) { + return true; + } + return (randomValue >= threshold) == sampled; + } + + private OtelTraceState withFields( + boolean retainRandomValue, boolean retainThreshold, boolean randomValueIsInherited) { + if (!retainRandomValue && !retainThreshold && !hasUnknownFields()) { + return null; + } + return new OtelTraceState( + null, + fields, + 1, + 0, + randomValue, + threshold, + randomValueStart, + randomValueEnd, + thresholdStart, + thresholdEnd, + retainRandomValue, + retainThreshold, + randomValueIsInherited); + } + + CharSequence getValue() { + return value == null ? this : value; } int getOriginalPosition() { @@ -33,4 +238,218 @@ int getOriginalPosition() { int getOriginalSize() { return originalSize; } + + boolean isMaterialized() { + return materializedValue != null; + } + + @Override + public int length() { + return value == null ? materialize().length() : value.length(); + } + + @Override + public char charAt(int index) { + return value == null ? materialize().charAt(index) : value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return value == null ? materialize().subSequence(start, end) : value.subSequence(start, end); + } + + @Override + public String toString() { + return value == null ? materialize() : value.toString(); + } + + private String materialize() { + String current = materializedValue; + if (current != null) { + return current; + } + StringBuilder result = new StringBuilder(); + if (includeRandomValue) { + appendManagedField( + result, RANDOM_VALUE_KEY, randomValue, randomValueStart, randomValueEnd, false); + } + if (includeThreshold) { + appendManagedField(result, THRESHOLD_KEY, threshold, thresholdStart, thresholdEnd, true); + } + appendUnknownFields(result); + current = result.toString(); + materializedValue = current; + return current; + } + + private void appendManagedField( + StringBuilder result, + String key, + long numericValue, + int sourceStart, + int sourceEnd, + boolean trimTrailingZeros) { + appendSeparator(result); + result.append(key); + if (fields != null && sourceStart >= 0) { + result.append(fields, sourceStart, sourceEnd); + } else { + appendHex(result, numericValue, trimTrailingZeros); + } + } + + private void appendUnknownFields(StringBuilder result) { + if (fields == null) { + return; + } + int start = 0; + while (start < fields.length()) { + int end = indexOf(fields, ';', start); + if (end < 0) { + end = fields.length(); + } + if (!startsWith(fields, start, end, RANDOM_VALUE_KEY) + && !startsWith(fields, start, end, THRESHOLD_KEY)) { + appendSeparator(result); + result.append(fields, start, end); + } + start = end + 1; + } + } + + private boolean hasUnknownFields() { + if (fields == null) { + return false; + } + int start = 0; + while (start < fields.length()) { + int end = indexOf(fields, ';', start); + if (end < 0) { + end = fields.length(); + } + if (!startsWith(fields, start, end, RANDOM_VALUE_KEY) + && !startsWith(fields, start, end, THRESHOLD_KEY)) { + return true; + } + start = end + 1; + } + return false; + } + + private static CharSequence normalize( + CharSequence raw, + int randomValueStart, + int randomValueEnd, + int thresholdStart, + int thresholdEnd) { + StringBuilder result = new StringBuilder(raw.length()); + if (randomValueStart >= 0) { + appendRange(result, RANDOM_VALUE_KEY, raw, randomValueStart, randomValueEnd); + } + if (thresholdStart >= 0) { + appendRange(result, THRESHOLD_KEY, raw, thresholdStart, thresholdEnd); + } + int start = 0; + while (start < raw.length()) { + int end = indexOf(raw, ';', start); + if (end < 0) { + end = raw.length(); + } + if (!startsWith(raw, start, end, RANDOM_VALUE_KEY) + && !startsWith(raw, start, end, THRESHOLD_KEY) + && isUnknownField(raw, start, end)) { + appendSeparator(result); + result.append(raw, start, end); + } + start = end + 1; + } + return result.toString(); + } + + private static void appendRange( + StringBuilder result, String key, CharSequence source, int start, int end) { + appendSeparator(result); + result.append(key).append(source, start, end); + } + + private static void appendSeparator(StringBuilder result) { + if (result.length() > 0) { + result.append(';'); + } + } + + private static void appendHex(StringBuilder result, long value, boolean trimTrailingZeros) { + int digits = 14; + if (trimTrailingZeros) { + long remaining = value; + while (digits > 1 && (remaining & 0xf) == 0) { + remaining >>>= 4; + digits--; + } + } + for (int i = 13; i >= 14 - digits; i--) { + int digit = (int) ((value >>> (i * 4)) & 0xf); + result.append((char) (digit < 10 ? '0' + digit : 'a' + digit - 10)); + } + } + + private static int indexOf(CharSequence value, char target, int start) { + for (int i = start; i < value.length(); i++) { + if (value.charAt(i) == target) { + return i; + } + } + return -1; + } + + private static boolean startsWith(CharSequence value, int start, int end, CharSequence prefix) { + if (end - start < prefix.length()) { + return false; + } + for (int i = 0; i < prefix.length(); i++) { + if (value.charAt(start + i) != prefix.charAt(i)) { + return false; + } + } + return true; + } + + private static boolean isUnknownField(CharSequence value, int start, int end) { + int separator = indexOf(value, ':', start); + return separator > start && separator < end - 1; + } + + private static boolean isLowerHex( + CharSequence value, int start, int end, int minimumLength, int maximumLength) { + int length = end - start; + if (length < minimumLength || length > maximumLength) { + return false; + } + for (int i = start; i < end; i++) { + char c = value.charAt(i); + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { + return false; + } + } + return true; + } + + private static long parseHex(CharSequence value, int start, int end) { + if (start < 0) { + return -1; + } + long parsed = 0; + for (int i = start; i < end; i++) { + char c = value.charAt(i); + parsed = (parsed << 4) | (c <= '9' ? c - '0' : c - 'a' + 10); + } + return parsed; + } + + private static long parseThreshold(CharSequence value, int start, int end) { + if (start < 0) { + return -1; + } + return parseHex(value, start, end) << (4 * (14 - (end - start))); + } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java index 97a6accae40..b8280d2cece 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java @@ -1,6 +1,7 @@ package datadog.trace.core.propagation.ptags; 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.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -27,6 +28,7 @@ void retainsValueAndMemberMetadata() { assertNotNull(state); assertSame(value, state.getValue()); + assertFalse(state.isMaterialized()); assertEquals(VALUE.length(), state.length()); assertEquals(INHERITED_POSITION, state.getOriginalPosition()); assertEquals(ORIGINAL_MEMBER_CONTRIBUTION_SIZE, state.getOriginalSize()); @@ -40,4 +42,49 @@ void extractsOriginalMemberPosition() { assertNotNull(state); assertEquals(3, state.getOriginalPosition()); } + + @Test + void retainsValidThresholdWithoutRandomValue() { + OtelTraceState state = OtelTraceState.parse("th:8", 0, 0); + + assertEquals("th:8", state.getValue().toString()); + } + + @Test + void removesMalformedThresholdAndRetainsValidRandomValue() { + OtelTraceState state = OtelTraceState.parse("rv:0123456789abcd;th:not-hex;x:value", 0, 0); + + assertEquals("rv:0123456789abcd;x:value", state.getValue().toString()); + } + + @Test + void malformedRandomValueRemovesManagedPairButRetainsUnknownFields() { + OtelTraceState state = OtelTraceState.parse("rv:invalid;th:8;x:value", 0, 0); + + assertEquals("x:value", state.getValue().toString()); + } + + @Test + void malformedFirstRandomValuePreventsRecoveryFromLaterValues() { + OtelTraceState state = + OtelTraceState.parse("rv:invalid;rv:0123456789abcd;rv:ffffffffffffff;th:8;th:4", 0, 0); + + assertNull(state); + } + + @Test + void keepsFirstValidManagedFields() { + OtelTraceState state = + OtelTraceState.parse("rv:0123456789abcd;rv:ffffffffffffff;th:8;th:4", 0, 0); + + assertEquals("rv:0123456789abcd;th:8", state.getValue().toString()); + } + + @Test + void rejectsUppercaseAndOverlongManagedFields() { + OtelTraceState state = + OtelTraceState.parse("rv:0123456789ABCd;th:123456789abcdef;x:value", 0, 0); + + assertEquals("x:value", state.getValue().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..839f29237d7 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java @@ -0,0 +1,88 @@ +package datadog.trace.core.propagation.ptags; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class OtelTraceStateTest { + private static final long DROP_PRECISION_BOUNDARY_TRACE_ID = 5401449561355763072L; + private static final double DROP_PRECISION_BOUNDARY_RATE = 0.05; + private static final String DROP_PRECISION_BOUNDARY_RANDOM_VALUE = "f333333333332f"; + private static final String DROP_PRECISION_BOUNDARY_THRESHOLD = "f333333333333"; + + @Test + void convertsDatadogProbabilityDecision() { + OtelTraceState state = OtelTraceState.fromProbabilityDecision(0xfff972474538efffL, 0.1, true); + + assertSame(state, state.getValue()); + assertFalse(state.isMaterialized()); + assertEquals("rv:ef284ace7a91e1;th:e6666666666668", state.getValue().toString()); + assertTrue(state.isMaterialized()); + assertTrue(state.isConsistentWith(true)); + } + + @Test + void serializesThresholds() { + assertThreshold(0.01, "fd70a3d70a3d7"); + assertThreshold(0.1, "e6666666666668"); + assertThreshold(0.2, "ccccccccccccd"); + assertThreshold(0.5, "8"); + assertThreshold(0.99, "028f5c28f5c29"); + assertThreshold(1.0, "0"); + } + + @Test + void rateZeroUsesLargestWireThresholdAndRemainsDropConsistent() { + OtelTraceState state = OtelTraceState.fromProbabilityDecision(0L, 0.0, false); + + assertEquals("rv:fffffffffffffe;th:ffffffffffffff", state.getValue().toString()); + assertTrue(state.isConsistentWith(false)); + } + + @Test + void correctsOnlySerializedRandomValueAtKeepBoundary() { + OtelTraceState state = OtelTraceState.fromProbabilityDecision(0x03a93ee8b1999f00L, 0.1, true); + + assertEquals("rv:e6666666666668;th:e6666666666668", state.getValue().toString()); + assertTrue(state.isConsistentWith(true)); + } + + @Test + void correctsOnlySerializedRandomValueAtDropBoundary() { + OtelTraceState state = + OtelTraceState.fromProbabilityDecision( + DROP_PRECISION_BOUNDARY_TRACE_ID, DROP_PRECISION_BOUNDARY_RATE, false); + + assertEquals( + "rv:" + DROP_PRECISION_BOUNDARY_RANDOM_VALUE + ";th:" + DROP_PRECISION_BOUNDARY_THRESHOLD, + state.getValue().toString()); + assertTrue(state.isConsistentWith(false)); + } + + @Test + void removesLocalRandomnessForNonProbabilityDecision() { + OtelTraceState state = OtelTraceState.fromProbabilityDecision(1L, 1.0, true); + + assertNull(state.forNonProbabilityDecision()); + } + + @Test + void retainsInheritedRandomnessAndUnknownFieldsWithoutThreshold() { + OtelTraceState state = OtelTraceState.parse("rv:0123456789abcd;th:8;x:value", 0, 0); + + OtelTraceState transformed = state.forNonProbabilityDecision(); + + assertEquals("rv:0123456789abcd;x:value", transformed.getValue().toString()); + assertTrue(transformed.isConsistentWith(false)); + } + + private static void assertThreshold(double rate, String expectedThreshold) { + OtelTraceState state = OtelTraceState.fromProbabilityDecision(1L, rate, rate > 0.0); + String value = state.getValue().toString(); + assertEquals(expectedThreshold, value.substring(value.indexOf(";th:") + 4)); + } +} From 7025ae93213db5ef293b83f232fbd53fb20e02a5 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Mon, 14 Sep 2026 17:06:44 +0200 Subject: [PATCH 02/22] refactor(propagation): introduce immutable sampling snapshots --- .../core/propagation/PropagationTags.java | 48 ++++ .../propagation/ptags/DatadogPTagsCodec.java | 15 + .../core/propagation/ptags/PTagsCodec.java | 57 +++- .../core/propagation/ptags/PTagsFactory.java | 258 +++++++++++++----- .../core/propagation/ptags/W3CPTagsCodec.java | 19 +- 5 files changed, 310 insertions(+), 87 deletions(-) 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 0ebe630c87a..530c285291e 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 @@ -22,6 +22,47 @@ */ public abstract class PropagationTags { + public static final class SamplingState { + private final int samplingPriority; + private final String tracestate; + private final CharSequence otelTraceState; + private final CharSequence decisionMaker; + private final CharSequence knuthSamplingRate; + + public SamplingState( + int samplingPriority, + String tracestate, + CharSequence otelTraceState, + CharSequence decisionMaker, + CharSequence knuthSamplingRate) { + this.samplingPriority = samplingPriority; + this.tracestate = tracestate; + this.otelTraceState = otelTraceState; + this.decisionMaker = decisionMaker; + this.knuthSamplingRate = knuthSamplingRate; + } + + public int getSamplingPriority() { + return samplingPriority; + } + + public String getTracestate() { + return tracestate; + } + + public CharSequence getOtelTraceState() { + return otelTraceState; + } + + public CharSequence getDecisionMaker() { + return decisionMaker; + } + + public CharSequence getKnuthSamplingRate() { + return knuthSamplingRate; + } + } + public static PropagationTags.Factory factory(Config config) { return factory(config.getxDatadogTagsMaxLength()); } @@ -69,6 +110,8 @@ public interface Factory { public abstract int getSamplingPriority(); + public abstract SamplingState samplingState(); + public abstract void updateTraceOrigin(CharSequence origin); public abstract CharSequence getOrigin(); @@ -87,6 +130,8 @@ public interface Factory { */ public abstract String getW3CTracestate(); + public abstract String getW3CTracestate(SamplingState samplingState); + /** * Stores the original W3C * tracestate header value. @@ -116,6 +161,9 @@ public void updateW3CTracestateFrom(PropagationTags source) { */ public abstract String headerValue(HeaderType headerType, CharSequence lastParentIdOverride); + public abstract String headerValue( + HeaderType headerType, CharSequence lastParentIdOverride, SamplingState samplingState); + /** * Fills a provided tagMap with valid propagated _dd.p.* tags and possibly a new sampling decision * tags _dd.p.dm (root span only) based on the current state, or sets only an error tag if the diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java index 3ac0c7ad712..8dda4f1d7f3 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java @@ -3,6 +3,7 @@ import datadog.logging.RatelimitedLogger; import datadog.trace.api.ProductTraceSource; import datadog.trace.core.propagation.PropagationTags; +import datadog.trace.core.propagation.PropagationTags.SamplingState; import datadog.trace.core.propagation.ptags.PTagsFactory.PTags; import datadog.trace.core.propagation.ptags.TagElement.Encoding; import java.util.ArrayList; @@ -127,12 +128,26 @@ protected int estimateHeaderSize(PTags pTags) { return pTags.getXDatadogTagsSize(); } + @Override + protected int estimateHeaderSize(PTags pTags, SamplingState samplingState) { + return pTags.getXDatadogTagsSize(samplingState); + } + @Override protected int appendPrefix(StringBuilder sb, PTags ptags) { // Calculate the tag size here and return it. Don't do anything else since there is no prefix. return ptags.getXDatadogTagsSize(); } + @Override + protected int appendPrefix( + StringBuilder sb, + PTags ptags, + CharSequence lastParentIdOverride, + SamplingState samplingState) { + return ptags.getXDatadogTagsSize(samplingState); + } + @Override protected int appendTag(StringBuilder sb, TagElement key, TagElement value, int size) { if (size <= xDatadogTagsLimit) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java index e2c0658a1d2..82082a4e1ae 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java @@ -4,6 +4,7 @@ import datadog.trace.api.ProductTraceSource; import datadog.trace.core.propagation.PropagationTags; +import datadog.trace.core.propagation.PropagationTags.SamplingState; import datadog.trace.core.propagation.ptags.PTagsFactory.PTags; import datadog.trace.core.propagation.ptags.TagElement.Encoding; import java.util.Iterator; @@ -29,17 +30,26 @@ static String headerValue(PTagsCodec codec, PTags ptags) { } static String headerValue(PTagsCodec codec, PTags ptags, CharSequence lastParentIdOverride) { - int estimate = codec.estimateHeaderSize(ptags); + return headerValue(codec, ptags, lastParentIdOverride, ptags.samplingState()); + } + + static String headerValue( + PTagsCodec codec, + PTags ptags, + CharSequence lastParentIdOverride, + SamplingState samplingState) { + int estimate = codec.estimateHeaderSize(ptags, samplingState); if (estimate == 0) { return ""; } // No encoding validation here because we don't allow arbitrary tag change StringBuilder sb = new StringBuilder(estimate); - int size = codec.appendPrefix(sb, ptags, lastParentIdOverride); + int size = codec.appendPrefix(sb, ptags, lastParentIdOverride, samplingState); if (!ptags.isPropagationTagsDisabled()) { - if (ptags.getDecisionMakerTagValue() != null) { - size = codec.appendTag(sb, DECISION_MAKER_TAG, ptags.getDecisionMakerTagValue(), size); + TagValue decisionMakerTagValue = ptags.getDecisionMakerTagValue(samplingState); + if (decisionMakerTagValue != null) { + size = codec.appendTag(sb, DECISION_MAKER_TAG, decisionMakerTagValue, size); } if (ptags.getTraceIdHighOrderBitsHexTagValue() != null) { size = codec.appendTag(sb, TRACE_ID_TAG, ptags.getTraceIdHighOrderBitsHexTagValue(), size); @@ -55,10 +65,9 @@ static String headerValue(PTagsCodec codec, PTags ptags, CharSequence lastParent if (ptags.getDebugPropagation() != null) { size = codec.appendTag(sb, DEBUG_TAG, TagValue.from(ptags.getDebugPropagation()), size); } - if (ptags.getKnuthSamplingRateTagValue() != null) { - size = - codec.appendTag( - sb, KNUTH_SAMPLING_RATE_TAG, ptags.getKnuthSamplingRateTagValue(), size); + TagValue knuthSamplingRateTagValue = ptags.getKnuthSamplingRateTagValue(samplingState); + if (knuthSamplingRateTagValue != null) { + size = codec.appendTag(sb, KNUTH_SAMPLING_RATE_TAG, knuthSamplingRateTagValue, size); } if (ptags.getOrgPropagationMarkerTagValue() != null) { size = @@ -72,7 +81,7 @@ static String headerValue(PTagsCodec codec, PTags ptags, CharSequence lastParent size = codec.appendTag(sb, tagKey, tagValue, size); } } - size = codec.appendSuffix(sb, ptags, size); + size = codec.appendSuffix(sb, ptags, size, samplingState); if (codec.isTooLarge(sb, size)) { return null; } else { @@ -81,7 +90,8 @@ static String headerValue(PTagsCodec codec, PTags ptags, CharSequence lastParent } static void fillTagMap(PTags propagationTags, Map tagMap) { - int newSize = propagationTags.getXDatadogTagsSize(); + SamplingState samplingState = propagationTags.samplingState(); + int newSize = propagationTags.getXDatadogTagsSize(samplingState); if (newSize > propagationTags.getxDatadogTagsLimit()) { // Outgoing x-datadog-tags value length exceeds the configured limit @@ -103,10 +113,11 @@ static void fillTagMap(PTags propagationTags, Map tagMap) { tagKey.forType(Encoding.DATADOG).toString(), tagValue.forType(Encoding.DATADOG).toString()); } - if (propagationTags.getDecisionMakerTagValue() != null) { + TagValue decisionMakerTagValue = propagationTags.getDecisionMakerTagValue(samplingState); + if (decisionMakerTagValue != null) { tagMap.put( DECISION_MAKER_TAG.forType(Encoding.DATADOG).toString(), - propagationTags.getDecisionMakerTagValue().forType(Encoding.DATADOG).toString()); + decisionMakerTagValue.forType(Encoding.DATADOG).toString()); } if (propagationTags.getTraceSource() != ProductTraceSource.UNSET) { tagMap.put( @@ -119,10 +130,12 @@ static void fillTagMap(PTags propagationTags, Map tagMap) { tagMap.put( DEBUG_TAG.forType(Encoding.DATADOG).toString(), propagationTags.getDebugPropagation()); } - if (propagationTags.getKnuthSamplingRateTagValue() != null) { + TagValue knuthSamplingRateTagValue = + propagationTags.getKnuthSamplingRateTagValue(samplingState); + if (knuthSamplingRateTagValue != null) { tagMap.put( KNUTH_SAMPLING_RATE_TAG.forType(Encoding.DATADOG).toString(), - propagationTags.getKnuthSamplingRateTagValue().forType(Encoding.DATADOG).toString()); + knuthSamplingRateTagValue.forType(Encoding.DATADOG).toString()); } if (propagationTags.getOrgPropagationMarkerTagValue() != null) { tagMap.put( @@ -176,6 +189,10 @@ static int calcXDatadogTagsSize(int size, TagKey tagKey, TagValue tagValue) { protected abstract int estimateHeaderSize(PTags pTags); + protected int estimateHeaderSize(PTags pTags, SamplingState samplingState) { + return estimateHeaderSize(pTags); + } + protected abstract int appendPrefix(StringBuilder sb, PTags ptags); /** @@ -186,10 +203,22 @@ protected int appendPrefix(StringBuilder sb, PTags ptags, CharSequence lastParen return appendPrefix(sb, ptags); } + protected int appendPrefix( + StringBuilder sb, + PTags ptags, + CharSequence lastParentIdOverride, + SamplingState samplingState) { + return appendPrefix(sb, ptags, lastParentIdOverride); + } + protected abstract int appendTag(StringBuilder sb, TagElement key, TagElement value, int size); protected abstract int appendSuffix(StringBuilder sb, PTags ptags, int size); + protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingState samplingState) { + return appendSuffix(sb, ptags, size); + } + protected abstract boolean isTooLarge(StringBuilder sb, int size); protected abstract boolean isEmpty(StringBuilder sb, int size); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index 84f3c700269..35cde7277a0 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 @@ -98,21 +98,16 @@ static class PTags extends PropagationTags { justification = "This field is never accessed concurrently") private boolean canChangeDecisionMaker; - // extracted decision maker tag for easier updates - private volatile TagValue decisionMakerTagValue; - private static final AtomicIntegerFieldUpdater TRACE_SOURCE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(PTags.class, "traceSource"); private volatile int traceSource; private volatile String debugPropagation; - private volatile double knuthSamplingRate = Double.NaN; - private volatile TagValue knuthSamplingRateTagValue; - private volatile TagValue orgPropagationMarkerTagValue; - private volatile OtelTraceState otelTraceState; + private OtelTraceState otelTraceState; + private volatile SamplingState samplingState; // Static cache for the most-recently-seen rate → TagValue. In steady state a service uses one // rate, so this eliminates the char[] + String allocation on every new PTags instance. @@ -120,12 +115,11 @@ static class PTags extends PropagationTags { private static volatile double cachedKsrRate = Double.NaN; private static volatile TagValue cachedKsrTagValue; - // xDatadogTagsSize of the tagPairs, does not include the decision maker tag - private volatile int xDatadogTagsSize = -1; + private volatile SizeCacheEntry xDatadogTagsSizeCache; - private volatile int samplingPriority; private volatile CharSequence origin; - private volatile String[] headerCache = null; + private volatile HeaderCacheEntry datadogHeaderCache; + private volatile HeaderCacheEntry w3cHeaderCache; /** The high-order 64 bits of the trace id. */ private volatile long traceIdHighOrderBits; @@ -187,9 +181,9 @@ static class PTags extends PropagationTags { this.factory = factory; this.tagPairs = tagPairs; this.canChangeDecisionMaker = decisionMakerTagValue == null; - this.decisionMakerTagValue = decisionMakerTagValue; this.traceSource = traceSource; - this.samplingPriority = samplingPriority; + this.samplingState = + newSamplingState(samplingPriority, null, null, decisionMakerTagValue, null); this.origin = origin; this.lastParentId = lastParentId; this.orgPropagationMarkerTagValue = orgPropagationMarkerTagValue; @@ -220,7 +214,8 @@ static PTags withError(PTagsFactory factory, String error) { } @Override - public void updateTraceSamplingPriority(int samplingPriority, int samplingMechanism) { + public synchronized void updateTraceSamplingPriority( + int samplingPriority, int samplingMechanism) { if (samplingPriority != PrioritySampling.UNSET && canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) { doUpdateTraceSamplingPriority(samplingPriority, samplingMechanism); @@ -228,16 +223,16 @@ public void updateTraceSamplingPriority(int samplingPriority, int samplingMechan } @Override - public void forceKeep(int samplingMechanism) { + public synchronized void forceKeep(int samplingMechanism) { doUpdateTraceSamplingPriority(PrioritySampling.USER_KEEP, samplingMechanism); } private void doUpdateTraceSamplingPriority(int samplingPriority, int samplingMechanism) { - if (this.samplingPriority != samplingPriority) { - // This should invalidate any cached w3c header + SamplingState currentState = samplingState; + if (currentState.getSamplingPriority() != samplingPriority) { clearCachedHeader(W3C); } - this.samplingPriority = samplingPriority; + TagValue decisionMakerTagValue = getDecisionMakerTagValue(currentState); if (samplingPriority > 0) { // TODO should try to keep the old sampling mechanism if we override the value? if (samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) { @@ -264,6 +259,27 @@ private void doUpdateTraceSamplingPriority(int samplingPriority, int samplingMec } decisionMakerTagValue = null; } + samplingState = + newSamplingState( + samplingPriority, + tracestate, + otelTraceState, + decisionMakerTagValue, + getKnuthSamplingRateTagValue(currentState)); + } + + private static SamplingState newSamplingState( + int samplingPriority, + String tracestate, + OtelTraceState otelTraceState, + TagValue decisionMakerTagValue, + TagValue knuthSamplingRateTagValue) { + return new SamplingState( + samplingPriority, + tracestate, + otelTraceState, + decisionMakerTagValue, + knuthSamplingRateTagValue); } @Override @@ -301,27 +317,36 @@ public String getDebugPropagation() { } @Override - public void updateKnuthSamplingRate(double rate) { - if (Double.compare(knuthSamplingRate, rate) != 0) { + public synchronized void updateKnuthSamplingRate(double rate) { + TagValue current = getKnuthSamplingRateTagValue(); + TagValue next = knuthSamplingRateTagValue(rate); + if (!Objects.equals(current, next)) { clearCachedHeader(DATADOG); clearCachedHeader(W3C); - knuthSamplingRate = rate; - if (Double.isNaN(rate)) { - knuthSamplingRateTagValue = null; - } else { - TagValue tv; - if (Double.compare(cachedKsrRate, rate) == 0) { - tv = cachedKsrTagValue; - } else { - tv = TagValue.from(formatKnuthSamplingRate(rate)); - cachedKsrTagValue = tv; - cachedKsrRate = rate; - } - knuthSamplingRateTagValue = tv; - } + SamplingState currentState = samplingState; + samplingState = + newSamplingState( + currentState.getSamplingPriority(), + tracestate, + otelTraceState, + getDecisionMakerTagValue(currentState), + next); } } + private static TagValue knuthSamplingRateTagValue(double rate) { + if (Double.isNaN(rate)) { + return null; + } + if (Double.compare(cachedKsrRate, rate) == 0) { + return cachedKsrTagValue; + } + TagValue value = TagValue.from(formatKnuthSamplingRate(rate)); + cachedKsrTagValue = value; + cachedKsrRate = rate; + return value; + } + /** * Formats a sampling rate with up to 6 decimal digits of precision and no trailing zeros. * @@ -357,7 +382,11 @@ static String formatKnuthSamplingRate(double rate) { } TagValue getKnuthSamplingRateTagValue() { - return knuthSamplingRateTagValue; + return getKnuthSamplingRateTagValue(samplingState); + } + + TagValue getKnuthSamplingRateTagValue(SamplingState samplingState) { + return asTagValue(samplingState.getKnuthSamplingRate()); } @Override @@ -381,7 +410,12 @@ TagValue getOrgPropagationMarkerTagValue() { @Override public int getSamplingPriority() { - return samplingPriority; + return samplingState.getSamplingPriority(); + } + + @Override + public SamplingState samplingState() { + return samplingState; } @Override @@ -426,14 +460,17 @@ public CharSequence getLastParentId() { @SuppressWarnings("StringEquality") @SuppressFBWarnings("ES_COMPARING_STRINGS_WITH_EQ") public String headerValue(HeaderType headerType) { - String header = getCachedHeader(headerType); + SamplingState currentSamplingState = samplingState; + String header = getCachedHeader(headerType, currentSamplingState); if (header == null) { - header = PTagsCodec.headerValue(factory.getDecoderEncoder(headerType), this); + header = + PTagsCodec.headerValue( + factory.getDecoderEncoder(headerType), this, null, currentSamplingState); if (header != null) { - setCachedHeader(headerType, header); + setCachedHeader(headerType, currentSamplingState, header); } else { // We can still cache the fact that we got back null - setCachedHeader(headerType, EMPTY); + setCachedHeader(headerType, currentSamplingState, EMPTY); } } if (header == EMPTY) { @@ -447,43 +484,59 @@ public String headerValue(HeaderType headerType, CharSequence lastParentIdOverri if (lastParentIdOverride == null) { return headerValue(headerType); } - // Inject-time path: encode fresh with the override; do NOT cache — the W3C `p:` is - // per-injecting-span and these tags may be shared across sibling spans. String header = PTagsCodec.headerValue(factory.getDecoderEncoder(headerType), this, lastParentIdOverride); return (header == null || header.isEmpty()) ? null : header; } + @Override + public String headerValue( + HeaderType headerType, CharSequence lastParentIdOverride, SamplingState samplingState) { + String header = + PTagsCodec.headerValue( + factory.getDecoderEncoder(headerType), this, lastParentIdOverride, samplingState); + return (header == null || header.isEmpty()) ? null : header; + } + @Override public void fillTagMap(Map tagMap) { PTagsCodec.fillTagMap(this, tagMap); } - private String getCachedHeader(HeaderType headerType) { - String[] cache = headerCache; - if (cache == null) { - return null; - } - return cache[headerType.ordinal()]; + private String getCachedHeader(HeaderType headerType, SamplingState samplingState) { + HeaderCacheEntry cache = headerType == DATADOG ? datadogHeaderCache : w3cHeaderCache; + return cache != null && cache.samplingState == samplingState ? cache.header : null; } - private void setCachedHeader(HeaderType headerType, String header) { - String[] cache = headerCache; - if (cache == null) { - cache = headerCache = new String[HeaderType.getNumValues()]; + private void setCachedHeader( + HeaderType headerType, SamplingState samplingState, String header) { + HeaderCacheEntry entry = new HeaderCacheEntry(samplingState, header); + if (headerType == DATADOG) { + datadogHeaderCache = entry; + } else { + w3cHeaderCache = entry; } - cache[headerType.ordinal()] = header; } private void clearCachedHeader(HeaderType headerType) { if (headerType == DATADOG) { invalidateXDatadogTagsSize(); } - String[] cache = headerCache; - if (cache == null) { - return; + if (headerType == DATADOG) { + datadogHeaderCache = null; + } else { + w3cHeaderCache = null; + } + } + + private static final class HeaderCacheEntry { + private final SamplingState samplingState; + private final String header; + + private HeaderCacheEntry(SamplingState samplingState, String header) { + this.samplingState = samplingState; + this.header = header; } - cache[headerType.ordinal()] = null; } int getxDatadogTagsLimit() { @@ -499,18 +552,24 @@ List getTagPairs() { } private void invalidateXDatadogTagsSize() { - this.xDatadogTagsSize = -1; + xDatadogTagsSizeCache = null; } int getXDatadogTagsSize() { - int size = xDatadogTagsSize; - if (size == -1) { - size = PTagsCodec.calcXDatadogTagsSize(getTagPairs()); - size = PTagsCodec.calcXDatadogTagsSize(size, DECISION_MAKER_TAG, decisionMakerTagValue); + return getXDatadogTagsSize(samplingState); + } + + int getXDatadogTagsSize(SamplingState samplingState) { + SizeCacheEntry cache = xDatadogTagsSizeCache; + if (cache == null || cache.samplingState != samplingState) { + int size = PTagsCodec.calcXDatadogTagsSize(getTagPairs()); + size = + PTagsCodec.calcXDatadogTagsSize( + size, DECISION_MAKER_TAG, getDecisionMakerTagValue(samplingState)); size = PTagsCodec.calcXDatadogTagsSize(size, TRACE_ID_TAG, traceIdHighOrderBitsHexTagValue); size = PTagsCodec.calcXDatadogTagsSize( - size, KNUTH_SAMPLING_RATE_TAG, getKnuthSamplingRateTagValue()); + size, KNUTH_SAMPLING_RATE_TAG, getKnuthSamplingRateTagValue(samplingState)); size = PTagsCodec.calcXDatadogTagsSize( size, ORG_PROPAGATION_MARKER_TAG, getOrgPropagationMarkerTagValue()); @@ -522,9 +581,20 @@ int getXDatadogTagsSize() { TRACE_SOURCE_TAG, TagValue.from(ProductTraceSource.getBitfieldHex(currentProductTraceSource))); } - xDatadogTagsSize = size; + cache = new SizeCacheEntry(samplingState, size); + xDatadogTagsSizeCache = cache; + } + return cache.size; + } + + private static final class SizeCacheEntry { + private final SamplingState samplingState; + private final int size; + + private SizeCacheEntry(SamplingState samplingState, int size) { + this.samplingState = samplingState; + this.size = size; } - return size; } TagValue getTraceIdHighOrderBitsHexTagValue() { @@ -532,7 +602,18 @@ TagValue getTraceIdHighOrderBitsHexTagValue() { } TagValue getDecisionMakerTagValue() { - return decisionMakerTagValue; + return getDecisionMakerTagValue(samplingState); + } + + TagValue getDecisionMakerTagValue(SamplingState samplingState) { + return asTagValue(samplingState.getDecisionMaker()); + } + + private static TagValue asTagValue(CharSequence value) { + if (value == null) { + return null; + } + return value instanceof TagValue ? (TagValue) value : TagValue.from(value); } @Override @@ -540,6 +621,11 @@ public String getW3CTracestate() { return this.tracestate; } + @Override + public String getW3CTracestate(SamplingState samplingState) { + return samplingState.getTracestate(); + } + @Override public void updateW3CTracestate(String tracestate) { setW3CTracestate(tracestate, W3CPTagsCodec.extractOtelTraceState(tracestate)); @@ -552,13 +638,27 @@ public void updateW3CTracestateFrom(PropagationTags source) { return; } PTags sourcePTags = (PTags) source; - setW3CTracestate(sourcePTags.tracestate, sourcePTags.getOtelTraceState()); + SamplingState sourceState = sourcePTags.samplingState(); + CharSequence sourceOtelTraceState = sourceState.getOtelTraceState(); + setW3CTracestate( + sourceState.getTracestate(), + sourceOtelTraceState instanceof OtelTraceState + ? (OtelTraceState) sourceOtelTraceState + : W3CPTagsCodec.extractOtelTraceState(sourceState.getTracestate())); } - private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) { + private synchronized void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) { clearCachedHeader(W3C); + int samplingPriority = samplingState.getSamplingPriority(); this.tracestate = tracestate; this.otelTraceState = otelTraceState; + this.samplingState = + newSamplingState( + samplingPriority, + tracestate, + otelTraceState, + getDecisionMakerTagValue(), + getKnuthSamplingRateTagValue()); } OtelTraceState getOtelTraceState() { @@ -567,9 +667,17 @@ OtelTraceState getOtelTraceState() { void setOtelTraceState(OtelTraceState otelTraceState) { if (this.otelTraceState != otelTraceState) { - this.otelTraceState = otelTraceState; clearCachedHeader(W3C); } + this.otelTraceState = otelTraceState; + SamplingState currentState = samplingState; + this.samplingState = + newSamplingState( + currentState.getSamplingPriority(), + tracestate, + otelTraceState, + getDecisionMakerTagValue(currentState), + getKnuthSamplingRateTagValue(currentState)); } String getError() { @@ -577,14 +685,22 @@ String getError() { } @Override - public void updateAndLockDecisionMaker(PropagationTags source) { + public synchronized void updateAndLockDecisionMaker(PropagationTags source) { if (source instanceof PTags) { canChangeDecisionMaker = false; - decisionMakerTagValue = ((PTags) source).getDecisionMakerTagValue(); + TagValue decisionMakerTagValue = ((PTags) source).getDecisionMakerTagValue(); if (decisionMakerTagValue != null) { clearCachedHeader(DATADOG); clearCachedHeader(W3C); } + SamplingState currentState = samplingState; + samplingState = + newSamplingState( + currentState.getSamplingPriority(), + tracestate, + otelTraceState, + decisionMakerTagValue, + getKnuthSamplingRateTagValue(currentState)); } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index 9cccf3fd45c..9a3cf3245b2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -7,6 +7,7 @@ import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.core.propagation.PropagationTags; +import datadog.trace.core.propagation.PropagationTags.SamplingState; import datadog.trace.core.propagation.ptags.PTagsFactory.PTags; import datadog.trace.core.propagation.ptags.TagElement.Encoding; import datadog.trace.util.SubSequence; @@ -268,6 +269,11 @@ protected int estimateHeaderSize(PTags pTags) { return size; } + @Override + protected int estimateHeaderSize(PTags pTags, SamplingState samplingState) { + return MAX_HEADER_SIZE; + } + @Override protected int appendPrefix(StringBuilder sb, PTags ptags) { return appendPrefix(sb, ptags, null); @@ -275,11 +281,20 @@ protected int appendPrefix(StringBuilder sb, PTags ptags) { @Override protected int appendPrefix(StringBuilder sb, PTags ptags, CharSequence lastParentIdOverride) { + return appendPrefix(sb, ptags, lastParentIdOverride, ptags.samplingState()); + } + + @Override + protected int appendPrefix( + StringBuilder sb, + PTags ptags, + CharSequence lastParentIdOverride, + SamplingState samplingState) { sb.append(DATADOG_MEMBER_KEY); // Append sampling priority (s) - if (ptags.getSamplingPriority() != PrioritySampling.UNSET) { + if (samplingState.getSamplingPriority() != PrioritySampling.UNSET) { sb.append("s:"); - sb.append(ptags.getSamplingPriority()); + sb.append(samplingState.getSamplingPriority()); } // Append origin (o) CharSequence origin = ptags.getOrigin(); From 1c8a762096fbd7dd2e1be507de38037fec17dabd Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Mon, 14 Sep 2026 17:16:26 +0200 Subject: [PATCH 03/22] feat(sampling): publish probability decisions atomically --- .../sampling/RateByServiceTraceSampler.java | 21 +-- .../sampling/RuleBasedTraceSampler.java | 6 +- .../java/datadog/trace/core/CoreSpan.java | 6 +- .../main/java/datadog/trace/core/DDSpan.java | 19 +- .../datadog/trace/core/DDSpanContext.java | 56 +++--- .../core/propagation/PropagationTags.java | 11 ++ .../core/propagation/ptags/PTagsFactory.java | 105 ++++++++++-- .../trace/common/metrics/SimpleSpan.java | 6 +- .../RateByServiceTraceSamplerTest.java | 68 ++++++++ .../trace/common/writer/TraceGenerator.java | 6 +- .../trace/core/OtelSamplingDecisionTest.java | 162 ++++++++++++++++++ .../OtelTraceStatePropagationTest.java | 66 +++++++ .../traceAgentTest/java/TraceGenerator.java | 6 +- 13 files changed, 473 insertions(+), 65 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 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..ab24b2200b0 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 @@ -59,19 +59,14 @@ public > void setSamplingPriority(final T span) { final RateSamplersByEnvAndService rates = serviceRates; RateSampler sampler = rates.getSampler(env, serviceName); - if (sampler.sample(span)) { - span.setSamplingPriority( - PrioritySampling.SAMPLER_KEEP, - SAMPLING_AGENT_RATE, - sampler.getSampleRate(), - SamplingMechanism.AGENT_RATE); - } else { - span.setSamplingPriority( - PrioritySampling.SAMPLER_DROP, - SAMPLING_AGENT_RATE, - sampler.getSampleRate(), - SamplingMechanism.AGENT_RATE); - } + boolean sampled = sampler.sample(span); + int samplingPriority = sampled ? PrioritySampling.SAMPLER_KEEP : PrioritySampling.SAMPLER_DROP; + span.setSamplingPriority( + samplingPriority, + SAMPLING_AGENT_RATE, + sampler.getSampleRate(), + sampled, + SamplingMechanism.AGENT_RATE); } 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 1746a1a3c54..c66159607b1 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,18 +146,21 @@ 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(), + true, matchedRule.getMechanism()); } else { span.setSamplingPriority( PrioritySampling.USER_DROP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), + true, matchedRule.getMechanism()); } span.setMetric(SAMPLING_LIMIT_RATE, rateLimit); @@ -166,6 +169,7 @@ public > void setSamplingPriority(final T span) { PrioritySampling.USER_DROP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), + false, matchedRule.getMechanism()); } } 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..f7b5bcb7c3a 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 @@ -122,7 +122,11 @@ default void processTagsAndBaggageWithStructuredLinks( T setSamplingPriority(int samplingPriority, int samplingMechanism); T setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism); + int samplingPriority, + CharSequence rate, + double sampleRate, + boolean probabilitySamplingResult, + int samplingMechanism); 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 57809e76069..fa94775f214 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,15 +647,18 @@ public final DDSpan setSamplingPriority(final int newPriority, int samplingMecha @Override public DDSpan setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { - if (context.setSamplingPriority(samplingPriority, samplingMechanism)) { + int samplingPriority, + CharSequence rate, + double sampleRate, + boolean probabilitySamplingResult, + int samplingMechanism) { + if (context.setSamplingPriority( + samplingPriority, + samplingMechanism, + sampleRate, + probabilitySamplingResult, + getTraceId().toLong())) { setMetric(rate, sampleRate); - if (samplingMechanism == SamplingMechanism.AGENT_RATE - || samplingMechanism == SamplingMechanism.LOCAL_USER_RULE - || samplingMechanism == SamplingMechanism.REMOTE_USER_RULE - || samplingMechanism == SamplingMechanism.REMOTE_ADAPTIVE_RULE) { - context.getPropagationTags().updateKnuthSamplingRate(sampleRate); - } } return this; } 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 1b211b5fae1..e5b7552b2ef 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 @@ -46,7 +46,6 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.function.Function; import javax.annotation.Nonnull; import org.slf4j.Logger; @@ -161,11 +160,6 @@ public class DDSpanContext private volatile boolean topLevel; - private static final AtomicIntegerFieldUpdater SAMPLING_PRIORITY_UPDATER = - AtomicIntegerFieldUpdater.newUpdater(DDSpanContext.class, "samplingPriority"); - - private volatile int samplingPriority = PrioritySampling.UNSET; - /** The origin of the trace. (eg. Synthetics, CI App) */ private volatile CharSequence origin; @@ -667,10 +661,6 @@ public void forceKeep(byte samplingMechanism) { } private void forceKeepThisSpan(byte samplingMechanism) { - // if the user really wants to keep this trace chunk, we will let them, - // even if the old sampling priority and mechanism have already propagated - SAMPLING_PRIORITY_UPDATER.set(this, PrioritySampling.USER_KEEP); - // record force keep decision for future distributed trace propagation propagationTags.forceKeep(samplingMechanism); } @@ -710,26 +700,43 @@ private boolean setThisSpanSamplingPriority(final int newPriority, final int new if (!validateSamplingPriority(newPriority, newMechanism)) { return false; } - if (SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism)) { - SAMPLING_PRIORITY_UPDATER.set(this, newPriority); - propagationTags.updateTraceSamplingPriority(newPriority, newMechanism); - return true; - } - if (!SAMPLING_PRIORITY_UPDATER.compareAndSet(this, PrioritySampling.UNSET, newPriority)) { + boolean updated = + propagationTags.tryUpdateTraceSamplingPriority( + newPriority, + newMechanism, + SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism)); + if (!updated) { if (log.isDebugEnabled()) { log.debug( "samplingPriority locked at priority: {}. Refusing to set to priority: {} mechanism: {}", - samplingPriority, + propagationTags.getSamplingPriority(), newPriority, newMechanism); } return false; } - // set trace level sampling priority tag propagationTags - propagationTags.updateTraceSamplingPriority(newPriority, newMechanism); return true; } + public boolean setSamplingPriority( + final int newPriority, + final int newMechanism, + final double sampleRate, + final boolean probabilitySamplingResult, + final long traceIdLowOrderBits) { + DDSpanContext spanContext = getRootSpanContextOrThis(); + if (!spanContext.validateSamplingPriority(newPriority, newMechanism)) { + return false; + } + return spanContext.propagationTags.tryUpdateProbabilitySamplingDecision( + newPriority, + newMechanism, + sampleRate, + probabilitySamplingResult, + traceIdLowOrderBits, + SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism)); + } + private boolean validateSamplingPriority(final int newPriority, final int newMechanism) { if (newPriority == PrioritySampling.UNSET) { log.debug("{}: Refusing to set samplingPriority to UNSET", this); @@ -758,7 +765,7 @@ private boolean validateSamplingPriority(final int newPriority, final int newMec @Override public int getSamplingPriority() { - return getRootSpanContextOrThis().samplingPriority; + return getRootSpanContextOrThis().propagationTags.getSamplingPriority(); } public void setSpanSamplingPriority(double rate, int limit) { @@ -790,7 +797,7 @@ public boolean lockSamplingPriority() { return rootSpan.spanContext().lockSamplingPriority(); } - return SAMPLING_PRIORITY_UPDATER.get(this) != PrioritySampling.UNSET; + return propagationTags.getSamplingPriority() != PrioritySampling.UNSET; } public CharSequence getOrigin() { @@ -1258,8 +1265,9 @@ public TagMap getTags() { tags.put(DDTags.THREAD_ID, threadId); // maintain previously observable type of the thread name :| tags.put(DDTags.THREAD_NAME, threadName.toString()); - if (samplingPriority != PrioritySampling.UNSET) { - tags.put(SAMPLE_RATE_KEY, samplingPriority); + int currentSamplingPriority = getSamplingPriority(); + if (currentSamplingPriority != PrioritySampling.UNSET) { + tags.put(SAMPLE_RATE_KEY, currentSamplingPriority); } if (httpStatusCode != 0) { tags.put(Tags.HTTP_STATUS, (int) httpStatusCode); @@ -1408,7 +1416,7 @@ void processTagsAndBaggage( threadName, unsafeTags, baggageItemsWithPropagationTags, - samplingPriority != PrioritySampling.UNSET ? samplingPriority : getSamplingPriority(), + getSamplingPriority(), measured, topLevel, httpStatusCode, 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 530c285291e..e6f406c4f0e 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 @@ -106,6 +106,17 @@ public interface Factory { */ public abstract void updateTraceSamplingPriority(int samplingPriority, int samplingMechanism); + public abstract boolean tryUpdateTraceSamplingPriority( + int samplingPriority, int samplingMechanism, boolean allowOverride); + + public abstract boolean tryUpdateProbabilitySamplingDecision( + int samplingPriority, + int samplingMechanism, + double sampleRate, + boolean probabilitySamplingResult, + long traceIdLowOrderBits, + boolean allowOverride); + public abstract void forceKeep(int samplingMechanism); public abstract int getSamplingPriority(); 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 35cde7277a0..f6bf1a444d7 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 @@ -218,21 +218,95 @@ public synchronized void updateTraceSamplingPriority( int samplingPriority, int samplingMechanism) { if (samplingPriority != PrioritySampling.UNSET && canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) { - doUpdateTraceSamplingPriority(samplingPriority, samplingMechanism); + OtelTraceState nextOtelTraceState = otelTraceState; + if (nextOtelTraceState != null) { + if (samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE + && !nextOtelTraceState.isConsistentWith(samplingPriority > 0)) { + nextOtelTraceState = nextOtelTraceState.withoutThreshold(); + } else if (samplingMechanism != SamplingMechanism.UNKNOWN + && samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE) { + nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); + } + } + installSamplingState(samplingPriority, samplingMechanism, nextOtelTraceState); } } @Override - public synchronized void forceKeep(int samplingMechanism) { - doUpdateTraceSamplingPriority(PrioritySampling.USER_KEEP, samplingMechanism); + public synchronized boolean tryUpdateTraceSamplingPriority( + int samplingPriority, int samplingMechanism, boolean allowOverride) { + if (samplingPriority == PrioritySampling.UNSET) { + return false; + } + SamplingState current = samplingState; + if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { + return false; + } + OtelTraceState nextOtelTraceState = otelTraceState; + if (nextOtelTraceState != null) { + if ((samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE + || samplingMechanism == SamplingMechanism.UNKNOWN) + && !nextOtelTraceState.isConsistentWith(samplingPriority > 0)) { + nextOtelTraceState = nextOtelTraceState.withoutThreshold(); + } else if (samplingMechanism != SamplingMechanism.UNKNOWN) { + nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); + } + } + installSamplingState(samplingPriority, samplingMechanism, nextOtelTraceState); + return true; } - private void doUpdateTraceSamplingPriority(int samplingPriority, int samplingMechanism) { - SamplingState currentState = samplingState; - if (currentState.getSamplingPriority() != samplingPriority) { - clearCachedHeader(W3C); + @Override + public synchronized boolean tryUpdateProbabilitySamplingDecision( + int samplingPriority, + int samplingMechanism, + double sampleRate, + boolean probabilitySamplingResult, + long traceIdLowOrderBits, + boolean allowOverride) { + SamplingState current = samplingState; + if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { + return false; } - TagValue decisionMakerTagValue = getDecisionMakerTagValue(currentState); + OtelTraceState nextOtelTraceState = otelTraceState; + if (nextOtelTraceState == null) { + boolean limiterDemotion = probabilitySamplingResult && samplingPriority <= 0; + if (!limiterDemotion) { + nextOtelTraceState = + OtelTraceState.fromProbabilityDecision( + traceIdLowOrderBits, sampleRate, probabilitySamplingResult); + } + } else if (probabilitySamplingResult && samplingPriority <= 0) { + nextOtelTraceState = nextOtelTraceState.withoutThreshold(); + } + TagValue nextKnuthSamplingRate = knuthSamplingRateTagValue(sampleRate); + installSamplingState( + samplingPriority, samplingMechanism, nextOtelTraceState, nextKnuthSamplingRate); + return true; + } + + @Override + public synchronized void forceKeep(int samplingMechanism) { + OtelTraceState nextOtelTraceState = otelTraceState; + if (nextOtelTraceState != null) { + nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); + } + installSamplingState(PrioritySampling.USER_KEEP, samplingMechanism, nextOtelTraceState); + } + + private void installSamplingState( + int samplingPriority, int samplingMechanism, OtelTraceState nextOtelTraceState) { + installSamplingState( + samplingPriority, samplingMechanism, nextOtelTraceState, getKnuthSamplingRateTagValue()); + } + + private void installSamplingState( + int samplingPriority, + int samplingMechanism, + OtelTraceState nextOtelTraceState, + TagValue nextKnuthSamplingRateTagValue) { + clearCachedHeader(W3C); + TagValue nextDecisionMakerTagValue = getDecisionMakerTagValue(); if (samplingPriority > 0) { // TODO should try to keep the old sampling mechanism if we override the value? if (samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) { @@ -243,29 +317,30 @@ private void doUpdateTraceSamplingPriority(int samplingPriority, int samplingMec // format if (samplingMechanism >= 0) { TagValue newDM = TagValue.from("-" + samplingMechanism); - if (!newDM.equals(decisionMakerTagValue)) { + if (!newDM.equals(nextDecisionMakerTagValue)) { // This should invalidate any cached w3c and datadog header clearCachedHeader(DATADOG); clearCachedHeader(W3C); } - decisionMakerTagValue = newDM; + nextDecisionMakerTagValue = newDM; } } else { // Drop the decision maker tag - if (decisionMakerTagValue != null) { + if (nextDecisionMakerTagValue != null) { // This should invalidate any cached w3c and datadog header clearCachedHeader(DATADOG); clearCachedHeader(W3C); } - decisionMakerTagValue = null; + nextDecisionMakerTagValue = null; } + otelTraceState = nextOtelTraceState; samplingState = newSamplingState( samplingPriority, tracestate, - otelTraceState, - decisionMakerTagValue, - getKnuthSamplingRateTagValue(currentState)); + nextOtelTraceState, + nextDecisionMakerTagValue, + nextKnuthSamplingRateTagValue); } private static SamplingState newSamplingState( 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..d6463298000 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 @@ -333,7 +333,11 @@ public SimpleSpan setSamplingPriority(int samplingPriority, int samplingMechanis @Override public SimpleSpan setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { + int samplingPriority, + CharSequence rate, + double sampleRate, + boolean probabilitySamplingResult, + int samplingMechanism) { return this; } diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java index 6918cf9c31f..5f7f0735e0c 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java @@ -216,6 +216,74 @@ void samplingPrioritySetOnContext() { } } + @Test + void defaultFallbackEmitsConsistentProbabilityStateBeforeAgentResponse() { + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = + (DDSpan) + tracer + .buildSpan("datadog", "fallback") + .withServiceName("spock") + .ignoreActiveSpan() + .start(); + + serviceSampler.setSamplingPriority(span); + + String otelTraceState = + span.spanContext() + .getPropagationTags() + .samplingState() + .getOtelTraceState() + .toString(); + assertEquals(SAMPLER_KEEP, span.getSamplingPriority()); + assertTrue(otelTraceState.matches("rv:[0-9a-f]{14};th:0"), otelTraceState); + } finally { + tracer.close(); + } + } + + @Test + void emptyAndNullOnlyResponsesKeepDefaultProbabilityFallback() { + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + serviceSampler.onResponse("traces", rateResponse(new String[0][0])); + assertEquals(1.0, serviceSampler.fallbackSampleRate()); + assertDefaultFallbackSampling(serviceSampler, tracer); + + serviceSampler.onResponse("traces", rateResponse("service:,env:", null)); + assertEquals(1.0, serviceSampler.fallbackSampleRate()); + assertDefaultFallbackSampling(serviceSampler, tracer); + } finally { + tracer.close(); + } + } + + private static void assertDefaultFallbackSampling( + RateByServiceTraceSampler serviceSampler, CoreTracer tracer) { + DDSpan span = + (DDSpan) + tracer + .buildSpan("datadog", "fallback") + .withServiceName("spock") + .ignoreActiveSpan() + .start(); + + serviceSampler.setSamplingPriority(span); + + assertEquals(SAMPLER_KEEP, span.getSamplingPriority()); + String otelTraceState = + span.spanContext() + .getPropagationTags() + .samplingState() + .getOtelTraceState() + .toString(); + assertTrue(otelTraceState.matches("rv:[0-9a-f]{14};th:0"), otelTraceState); + span.finish(); + } + @Test void samplingPrioritySetWhenServiceLater() throws Exception { RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); 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 e777235fef3..3a476c48719 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 @@ -409,7 +409,11 @@ public PojoSpan setSamplingPriority(int samplingPriority, int samplingMechanism) @Override public PojoSpan setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { + int samplingPriority, + CharSequence rate, + double sampleRate, + boolean probabilitySamplingResult, + int samplingMechanism) { return this; } 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..8030b3e7636 --- /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 org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +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 { + private static final String AGENT_RATE_ENDPOINT = "traces"; + private static final String OTEL_RANDOM_VALUE_PREFIX = "rv:"; + private static final String HALF_THRESHOLD = ";th:8"; + private static final String MAX_THRESHOLD = ";th:ffffffffffffff"; + private static final double HALF_RATE = 0.5; + private static final String HALF_RATE_RULE = "[{\"sample_rate\": 0.5}]"; + private static final String FULL_RATE_RULE = "[{\"sample_rate\": 1}]"; + + @Test + void initialAgentRateEstablishesDefaultProbabilityState() { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + withRootSpan( + span -> { + sampler.setSamplingPriority(span); + + String state = otelTraceState(span); + assertTrue(state.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(state.contains(";th:0")); + }); + } + + @Test + void loadedAgentRateEstablishesProbabilityState() { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + sampler.onResponse(AGENT_RATE_ENDPOINT, agentRates(HALF_RATE)); + withRootSpan( + span -> { + sampler.setSamplingPriority(span); + + String state = otelTraceState(span); + assertTrue(state.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(state.contains(HALF_THRESHOLD)); + }); + } + + @Test + void zeroAgentRateUsesDropConsistentMaximumThreshold() { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + sampler.onResponse(AGENT_RATE_ENDPOINT, agentRates(0)); + withRootSpan( + span -> { + sampler.setSamplingPriority(span); + + String state = otelTraceState(span); + assertTrue(state.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertFalse(state.contains("rv:ffffffffffffff")); + assertTrue(state.contains(MAX_THRESHOLD)); + }); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void configuredRulesEstablishProbabilityState(boolean traceRule) { + Properties properties = new Properties(); + properties.setProperty( + traceRule ? TRACE_SAMPLING_RULES : TRACE_SAMPLE_RATE, + traceRule ? HALF_RATE_RULE : String.valueOf(HALF_RATE)); + properties.setProperty(TRACE_RATE_LIMIT, "10000000"); + PrioritySampler sampler = (PrioritySampler) Sampler.Builder.forConfig(properties); + withRootSpan( + span -> { + sampler.setSamplingPriority(span); + + String state = otelTraceState(span); + assertTrue(state.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(state.contains(HALF_THRESHOLD)); + }); + } + + @Test + void limiterRejectionDoesNotFabricateProbabilityState() { + Properties properties = new Properties(); + properties.setProperty(TRACE_SAMPLING_RULES, FULL_RATE_RULE); + 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(otelTraceState(allowed).contains(OTEL_RANDOM_VALUE_PREFIX)); + assertEquals(USER_DROP, rejected.samplingPriority()); + assertNull(otelTraceState(rejected)); + } finally { + tracer.close(); + } + } + + @Test + void manualOverrideRemovesLocallyGeneratedProbabilityState() { + withRootSpan( + span -> { + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, HALF_RATE, true, LOCAL_USER_RULE); + assertTrue(otelTraceState(span).contains(OTEL_RANDOM_VALUE_PREFIX)); + + span.spanContext().forceKeep(); + + assertNull(otelTraceState(span)); + }); + } + + private void withRootSpan(java.util.function.Consumer test) { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + test.accept(newRootSpan(tracer)); + } finally { + tracer.close(); + } + } + + private static DDSpan newRootSpan(CoreTracer tracer) { + return (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .ignoreActiveSpan() + .start(); + } + + private static String otelTraceState(DDSpan span) { + CharSequence state = + span.spanContext().getPropagationTags().samplingState().getOtelTraceState(); + return state == null ? null : state.toString(); + } + + 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..033012d9172 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java @@ -0,0 +1,66 @@ +package datadog.trace.core.propagation; + +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.AGENT_RATE; +import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.core.propagation.PropagationTags.SamplingState; +import org.junit.jupiter.api.Test; + +class OtelTraceStatePropagationTest { + + @Test + void publishesProbabilityPriorityAndOtelStateTogether() { + PropagationTags tags = PropagationTags.factory().empty(); + + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, true, 1L, false)); + SamplingState state = tags.samplingState(); + + assertEquals(SAMPLER_KEEP, state.getSamplingPriority()); + assertEquals("-1", state.getDecisionMaker().toString()); + assertEquals("1", state.getKnuthSamplingRate().toString()); + assertTrue(state.getOtelTraceState().toString().matches("rv:[0-9a-f]{14};th:0")); + } + + @Test + void rejectedSamplingAttemptCannotReplaceProbabilityState() { + PropagationTags tags = PropagationTags.factory().empty(); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, true, 1L, false)); + SamplingState established = tags.samplingState(); + + assertFalse( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.1, false, 2L, false)); + + assertEquals(established, tags.samplingState()); + } + + @Test + void forceKeepRemovesLocallyGeneratedProbabilityState() { + PropagationTags tags = PropagationTags.factory().empty(); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.0, false, 1L, false)); + + tags.forceKeep(MANUAL); + + assertEquals(USER_KEEP, tags.samplingState().getSamplingPriority()); + assertNull(tags.samplingState().getOtelTraceState()); + } + + @Test + void limiterDemotionDoesNotFabricateState() { + PropagationTags tags = PropagationTags.factory().empty(); + + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 1.0, true, 1L, false)); + + assertNull(tags.samplingState().getOtelTraceState()); + } +} diff --git a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java index 1e1a9c58fe3..d3fc463cbf3 100644 --- a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java +++ b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java @@ -359,7 +359,11 @@ public PojoSpan setSamplingPriority(int samplingPriority, int samplingMechanism) @Override public PojoSpan setSamplingPriority( - int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { + int samplingPriority, + CharSequence rate, + double sampleRate, + boolean probabilitySamplingResult, + int samplingMechanism) { return this; } From e470bfcb33b658aefa8e2711fb189a1836cd7e4b Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Mon, 14 Sep 2026 17:19:59 +0200 Subject: [PATCH 04/22] feat(propagation): inject consistent otel sampling state --- .../trace/core/propagation/W3CHttpCodec.java | 23 +- .../core/propagation/ptags/PTagsFactory.java | 7 +- .../core/propagation/ptags/W3CPTagsCodec.java | 227 +++++++++++++++--- .../RateByServiceTraceSamplerTest.java | 19 +- .../trace/core/CoreSpanBuilderTest.java | 48 +++- .../trace/core/OtelSamplingDecisionTest.java | 48 ++-- .../OtelTraceStatePropagationTest.java | 97 +++++++- .../core/propagation/W3CHttpInjectorTest.java | 65 +++++ .../W3COtelTraceStateContinuationTest.java | 137 +++++++++++ 9 files changed, 578 insertions(+), 93 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/core/propagation/W3CHttpCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/W3CHttpCodec.java index 86ea382b482..a6dd7e7b65f 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/W3CHttpCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/W3CHttpCodec.java @@ -23,6 +23,7 @@ import datadog.trace.api.sampling.SamplingMechanism; import datadog.trace.bootstrap.instrumentation.api.TagContext; import datadog.trace.core.DDSpanContext; +import datadog.trace.core.propagation.PropagationTags.SamplingState; import java.util.Map; import java.util.function.Supplier; import org.slf4j.Logger; @@ -65,25 +66,33 @@ public Injector(Map invertedBaggageMapping) { @Override public void inject( final DDSpanContext context, final C carrier, final CarrierSetter setter) { - injectTraceParent(context, carrier, setter); - injectTraceState(context, carrier, setter); + PropagationTags propagationTags = context.getPropagationTags(); + SamplingState samplingState = propagationTags.samplingState(); + injectTraceParent(context, samplingState, carrier, setter); + injectTraceState(context, propagationTags, samplingState, carrier, setter); injectBaggage(context, carrier, setter); } - private void injectTraceParent(DDSpanContext context, C carrier, CarrierSetter setter) { + private void injectTraceParent( + DDSpanContext context, SamplingState samplingState, C carrier, CarrierSetter setter) { String traceparent = W3CTraceParent.from( - context.getTraceId(), context.getSpanId(), context.getSamplingPriority() > 0); + context.getTraceId(), context.getSpanId(), samplingState.getSamplingPriority() > 0); setter.set(carrier, TRACE_PARENT_KEY, traceparent); } - private void injectTraceState(DDSpanContext context, C carrier, CarrierSetter setter) { - PropagationTags propagationTags = context.getPropagationTags(); + private void injectTraceState( + DDSpanContext context, + PropagationTags propagationTags, + SamplingState samplingState, + C carrier, + CarrierSetter setter) { // Supply the injecting span's id for the W3C `p:` as a parameter rather than mutating it into // the (possibly trace-level, shared) tags — keeps transient per-injection identity out of // shared state, so concurrent sibling injects can't race on it. String tracestate = - propagationTags.headerValue(W3C, DDSpanId.toHexStringPadded(context.getSpanId())); + propagationTags.headerValue( + W3C, DDSpanId.toHexStringPadded(context.getSpanId()), samplingState); if (tracestate != null && !tracestate.isEmpty()) { setter.set(carrier, TRACE_STATE_KEY, tracestate); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index f6bf1a444d7..7fd4801421c 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 @@ -698,7 +698,7 @@ public String getW3CTracestate() { @Override public String getW3CTracestate(SamplingState samplingState) { - return samplingState.getTracestate(); + return W3CPTagsCodec.rebuildTracestate(samplingState); } @Override @@ -725,6 +725,11 @@ public void updateW3CTracestateFrom(PropagationTags source) { private synchronized void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) { clearCachedHeader(W3C); int samplingPriority = samplingState.getSamplingPriority(); + if (otelTraceState != null + && samplingPriority != PrioritySampling.UNSET + && !otelTraceState.isConsistentWith(samplingPriority > 0)) { + otelTraceState = otelTraceState.withoutThreshold(); + } this.tracestate = tracestate; this.otelTraceState = otelTraceState; this.samplingState = diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index 9a3cf3245b2..67185560467 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -333,6 +333,11 @@ protected int appendTag(StringBuilder sb, TagElement key, TagElement value, int @Override protected int appendSuffix(StringBuilder sb, PTags ptags, int size) { + return appendSuffix(sb, ptags, size, ptags.samplingState()); + } + + @Override + protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingState samplingState) { // If there is room for appending unknown from W3CPTags if (size < MAX_HEADER_SIZE && ptags instanceof W3CPTags) { W3CPTags w3cPTags = (W3CPTags) ptags; @@ -344,8 +349,12 @@ protected int appendSuffix(StringBuilder sb, PTags ptags, int size) { sb.setLength(0); size = 0; } + if (size == 0 && canForwardRawTracestate(samplingState)) { + sb.append(samplingState.getTracestate().trim()); + return EMPTY_SIZE + 1; + } // Append the managed OTel member and all other non-Datadog list-members - if (appendOtelAndVendorMembers(sb, ptags, size != 0)) { + if (appendOtelAndVendorMembers(sb, samplingState, size != 0)) { // We don't care about the total size in bytes here, but only the fact that we added something // that should be returned size = Math.max(size, EMPTY_SIZE + 1); @@ -353,6 +362,53 @@ protected int appendSuffix(StringBuilder sb, PTags ptags, int size) { return size; } + private static boolean canForwardRawTracestate(SamplingState samplingState) { + String original = samplingState.getTracestate(); + if (original == null || original.isEmpty()) { + return false; + } + String trimmed = original.trim(); + if (trimmed.isEmpty() || findNextMember(trimmed, 0) != 0) { + return false; + } + CharSequence otelTraceState = samplingState.getOtelTraceState(); + int otelMemberCount = 0; + int memberStart = 0; + while (memberStart < trimmed.length()) { + int memberEnd = trimmed.indexOf(MEMBER_SEPARATOR, memberStart); + if (memberEnd < 0) { + memberEnd = trimmed.length(); + } + if (trimmed.startsWith(DATADOG_MEMBER_KEY, memberStart)) { + return false; + } + if (trimmed.startsWith(OTEL_MEMBER_KEY, memberStart)) { + if (++otelMemberCount > 1 || otelTraceState == null) { + return false; + } + int valueStart = memberStart + OTEL_MEMBER_KEY.length(); + int valueEnd = stripTrailingOWC(trimmed, valueStart, memberEnd); + if (!contentEquals(trimmed, valueStart, valueEnd, otelTraceState)) { + return false; + } + } + memberStart = findNextMember(trimmed, memberEnd + 1); + } + return (otelTraceState == null) == (otelMemberCount == 0); + } + + private static boolean contentEquals(String value, int start, int end, CharSequence expected) { + if (end - start != expected.length()) { + return false; + } + for (int i = 0; i < expected.length(); i++) { + if (value.charAt(start + i) != expected.charAt(i)) { + return false; + } + } + return true; + } + @Override protected boolean isTooLarge(StringBuilder sb, int size) { return size > MAX_HEADER_SIZE; @@ -752,60 +808,139 @@ private static int cleanUpAndAppendUnknown(StringBuilder sb, W3CPTags w3CPTags, } private static boolean appendOtelAndVendorMembers( - StringBuilder sb, PTags ptags, boolean hasDatadogMember) { - String original = ptags.tracestate; - OtelTraceState otelTraceState = ptags.getOtelTraceState(); + StringBuilder sb, SamplingState samplingState, boolean hasDatadogMember) { + String original = samplingState.getTracestate(); + CharSequence otelTraceState = samplingState.getOtelTraceState(); int remainingMembers = MAX_MEMBER_COUNT - (hasDatadogMember ? 1 : 0); - int otherMemberPosition = 0; - int originalMemberPosition = 0; - int otelMemberPositionOffset = 0; - int otelMemberOriginalPosition = - otelTraceState == null ? -1 : otelTraceState.getOriginalPosition(); - boolean otelTraceStateAppended = false; boolean memberAppended = false; + boolean preserveOtelPosition = isUnchangedInheritedOtelMember(original, otelTraceState); + if (!preserveOtelPosition + && otelTraceState != null + && remainingMembers > 0 + && appendMemberIfFits(sb, OTEL_MEMBER_KEY, otelTraceState)) { + remainingMembers--; + memberAppended = true; + } int len = original == null ? 0 : original.length(); int memberStart = original == null ? 0 : findNextMember(original, 0); while (memberStart < len && remainingMembers > 0) { - // Look for member end position int memberEnd = original.indexOf(MEMBER_SEPARATOR, memberStart); if (memberEnd < 0) { memberEnd = len; } boolean datadogMember = original.startsWith(DATADOG_MEMBER_KEY, memberStart); - boolean managedMember = datadogMember || original.startsWith(OTEL_MEMBER_KEY, memberStart); - // offset to correct for dd members that were dropped/relocated before ot's original position - if (datadogMember && originalMemberPosition < otelMemberOriginalPosition) { - otelMemberPositionOffset++; - } - if (!managedMember) { - if (otelTraceState != null - && !otelTraceStateAppended - && otelMemberOriginalPosition - otelMemberPositionOffset == otherMemberPosition) { - appendMember(sb, OTEL_MEMBER_KEY, otelTraceState.getValue()); - remainingMembers--; - otelTraceStateAppended = true; - memberAppended = true; - if (remainingMembers == 0) { - break; - } - } + boolean otelMember = original.startsWith(OTEL_MEMBER_KEY, memberStart); + if (!datadogMember && (!otelMember || preserveOtelPosition)) { int end = stripTrailingOWC(original, memberStart, memberEnd); - appendMember(sb, original, memberStart, end); + if (!appendMemberIfFits(sb, original, memberStart, end)) { + break; + } remainingMembers--; - otherMemberPosition++; memberAppended = true; } - originalMemberPosition++; memberStart = findNextMember(original, memberEnd + 1); } + return memberAppended; + } + + private static boolean isUnchangedInheritedOtelMember( + String original, CharSequence otelTraceState) { + if (original == null || otelTraceState == null) { + return false; + } + int otelMemberCount = 0; + int memberStart = findNextMember(original, 0); + while (memberStart < original.length()) { + int memberEnd = original.indexOf(MEMBER_SEPARATOR, memberStart); + if (memberEnd < 0) { + memberEnd = original.length(); + } + if (original.startsWith(OTEL_MEMBER_KEY, memberStart)) { + if (++otelMemberCount > 1) { + return false; + } + int valueStart = memberStart + OTEL_MEMBER_KEY.length(); + int valueEnd = stripTrailingOWC(original, valueStart, memberEnd); + if (!contentEquals(original, valueStart, valueEnd, otelTraceState)) { + return false; + } + } + memberStart = findNextMember(original, memberEnd + 1); + } + return otelMemberCount == 1; + } + + public static String rebuildTracestate(SamplingState samplingState) { + String original = samplingState.getTracestate(); + CharSequence otelTraceState = samplingState.getOtelTraceState(); + if (original != null + && (isUnchangedInheritedOtelMember(original, otelTraceState) + || otelTraceState == null && !containsMember(original, OTEL_MEMBER_KEY))) { + return original; + } + StringBuilder result = new StringBuilder(MAX_HEADER_SIZE); + int memberCount = 0; + + if (original != null) { + int memberStart = findNextMember(original, 0); + while (memberStart < original.length()) { + int memberEnd = original.indexOf(MEMBER_SEPARATOR, memberStart); + if (memberEnd < 0) { + memberEnd = original.length(); + } + if (original.startsWith(DATADOG_MEMBER_KEY, memberStart)) { + int end = stripTrailingOWC(original, memberStart, memberEnd); + if (appendMemberIfFits(result, original, memberStart, end)) { + memberCount++; + } + break; + } + memberStart = findNextMember(original, memberEnd + 1); + } + } + if (otelTraceState != null - && !otelTraceStateAppended - && remainingMembers > 0 - && otelMemberOriginalPosition - otelMemberPositionOffset == otherMemberPosition) { - appendMember(sb, OTEL_MEMBER_KEY, otelTraceState.getValue()); - memberAppended = true; + && memberCount < MAX_MEMBER_COUNT + && appendMemberIfFits(result, OTEL_MEMBER_KEY, otelTraceState)) { + memberCount++; } - return memberAppended; + + if (original != null) { + int memberStart = findNextMember(original, 0); + while (memberStart < original.length() && memberCount < MAX_MEMBER_COUNT) { + int memberEnd = original.indexOf(MEMBER_SEPARATOR, memberStart); + if (memberEnd < 0) { + memberEnd = original.length(); + } + boolean managed = + original.startsWith(DATADOG_MEMBER_KEY, memberStart) + || original.startsWith(OTEL_MEMBER_KEY, memberStart); + if (!managed) { + int end = stripTrailingOWC(original, memberStart, memberEnd); + if (!appendMemberIfFits(result, original, memberStart, end)) { + break; + } + memberCount++; + } + memberStart = findNextMember(original, memberEnd + 1); + } + } + return result.length() == 0 ? null : result.toString(); + } + + private static boolean containsMember(String tracestate, String memberKey) { + int memberStart = findNextMember(tracestate, 0); + while (memberStart < tracestate.length()) { + int memberEnd = tracestate.indexOf(MEMBER_SEPARATOR, memberStart); + if (memberEnd < 0) { + memberEnd = tracestate.length(); + } + if (tracestate.startsWith(memberKey, memberStart)) { + return true; + } + memberStart = findNextMember(tracestate, memberEnd + 1); + } + return false; } private static void appendMember(StringBuilder sb, String member, int start, int end) { @@ -822,6 +957,24 @@ private static void appendMember(StringBuilder sb, String key, CharSequence valu sb.append(key).append(value); } + private static boolean appendMemberIfFits(StringBuilder sb, String member, int start, int end) { + int addedSize = end - start + (sb.length() == 0 ? 0 : 1); + if (sb.length() + addedSize > MAX_HEADER_SIZE) { + return false; + } + appendMember(sb, member, start, end); + return true; + } + + private static boolean appendMemberIfFits(StringBuilder sb, String key, CharSequence value) { + int addedSize = key.length() + value.length() + (sb.length() == 0 ? 0 : 1); + if (sb.length() + addedSize > MAX_HEADER_SIZE) { + return false; + } + appendMember(sb, key, value); + return true; + } + static OtelTraceState extractOtelTraceState(String tracestate) { if (tracestate == null || tracestate.isEmpty()) { return null; diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java index 5f7f0735e0c..cfd76f46734 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java @@ -2,6 +2,7 @@ import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +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.assertNull; @@ -231,14 +232,9 @@ void defaultFallbackEmitsConsistentProbabilityStateBeforeAgentResponse() { serviceSampler.setSamplingPriority(span); - String otelTraceState = - span.spanContext() - .getPropagationTags() - .samplingState() - .getOtelTraceState() - .toString(); + String tracestate = span.spanContext().getPropagationTags().headerValue(W3C); assertEquals(SAMPLER_KEEP, span.getSamplingPriority()); - assertTrue(otelTraceState.matches("rv:[0-9a-f]{14};th:0"), otelTraceState); + assertTrue(tracestate.matches(".*ot=rv:[0-9a-f]{14};th:0.*"), tracestate); } finally { tracer.close(); } @@ -274,13 +270,8 @@ private static void assertDefaultFallbackSampling( serviceSampler.setSamplingPriority(span); assertEquals(SAMPLER_KEEP, span.getSamplingPriority()); - String otelTraceState = - span.spanContext() - .getPropagationTags() - .samplingState() - .getOtelTraceState() - .toString(); - assertTrue(otelTraceState.matches("rv:[0-9a-f]{14};th:0"), otelTraceState); + String tracestate = span.spanContext().getPropagationTags().headerValue(W3C); + assertTrue(tracestate.matches(".*ot=rv:[0-9a-f]{14};th:0.*"), tracestate); span.finish(); } 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..547eeff9531 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,14 @@ 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.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; @@ -55,6 +59,14 @@ public class CoreSpanBuilderTest extends DDCoreJavaSpecification { + 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 static final String DATADOG_TRACE_STATE = "_dd.p.dm=934086a686-4,_dd.p.anytag=value"; + private ListWriter writer; private CoreTracer tracer; @@ -375,9 +387,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 +404,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(OTEL_MEMBER)); + + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, true, LOCAL_USER_RULE); + + String freshTraceState = + span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); + assertFalse(freshTraceState.contains(INHERITED_RANDOM_VALUE)); + assertTrue(freshTraceState.contains(THRESHOLD_0_5)); } @Test @@ -409,9 +429,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 +438,24 @@ 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(OTEL_MEMBER)); + + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, true, LOCAL_USER_RULE); + + String freshTraceState = + span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); + assertFalse(freshTraceState.contains(INHERITED_RANDOM_VALUE)); + assertTrue(freshTraceState.contains(THRESHOLD_0_5)); + } + + private static PropagationTags propagationTagsWithOtelState() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue(PropagationTags.HeaderType.DATADOG, DATADOG_TRACE_STATE); + propagationTags.updateW3CTracestate(OTEL_TRACE_STATE); + 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 index 8030b3e7636..12e489f21a6 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 @@ -7,9 +7,10 @@ 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.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import datadog.trace.common.sampling.PrioritySampler; @@ -25,7 +26,8 @@ class OtelSamplingDecisionTest extends DDCoreJavaSpecification { private static final String AGENT_RATE_ENDPOINT = "traces"; - private static final String OTEL_RANDOM_VALUE_PREFIX = "rv:"; + 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 double HALF_RATE = 0.5; @@ -39,9 +41,9 @@ void initialAgentRateEstablishesDefaultProbabilityState() { span -> { sampler.setSamplingPriority(span); - String state = otelTraceState(span); - assertTrue(state.contains(OTEL_RANDOM_VALUE_PREFIX)); - assertTrue(state.contains(";th:0")); + String header = w3cHeader(span); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(header.contains(";th:0")); }); } @@ -53,9 +55,9 @@ void loadedAgentRateEstablishesProbabilityState() { span -> { sampler.setSamplingPriority(span); - String state = otelTraceState(span); - assertTrue(state.contains(OTEL_RANDOM_VALUE_PREFIX)); - assertTrue(state.contains(HALF_THRESHOLD)); + String header = w3cHeader(span); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(header.contains(HALF_THRESHOLD)); }); } @@ -67,10 +69,10 @@ void zeroAgentRateUsesDropConsistentMaximumThreshold() { span -> { sampler.setSamplingPriority(span); - String state = otelTraceState(span); - assertTrue(state.contains(OTEL_RANDOM_VALUE_PREFIX)); - assertFalse(state.contains("rv:ffffffffffffff")); - assertTrue(state.contains(MAX_THRESHOLD)); + String header = w3cHeader(span); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertFalse(header.contains("ot=rv:ffffffffffffff")); + assertTrue(header.contains(MAX_THRESHOLD)); }); } @@ -87,9 +89,9 @@ void configuredRulesEstablishProbabilityState(boolean traceRule) { span -> { sampler.setSamplingPriority(span); - String state = otelTraceState(span); - assertTrue(state.contains(OTEL_RANDOM_VALUE_PREFIX)); - assertTrue(state.contains(HALF_THRESHOLD)); + String header = w3cHeader(span); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(header.contains(HALF_THRESHOLD)); }); } @@ -107,9 +109,9 @@ void limiterRejectionDoesNotFabricateProbabilityState() { sampler.setSamplingPriority(allowed); sampler.setSamplingPriority(rejected); - assertTrue(otelTraceState(allowed).contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(w3cHeader(allowed).contains(OTEL_RANDOM_VALUE_PREFIX)); assertEquals(USER_DROP, rejected.samplingPriority()); - assertNull(otelTraceState(rejected)); + assertFalse(w3cHeader(rejected).contains(OTEL_MEMBER)); } finally { tracer.close(); } @@ -120,11 +122,11 @@ void manualOverrideRemovesLocallyGeneratedProbabilityState() { withRootSpan( span -> { span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, HALF_RATE, true, LOCAL_USER_RULE); - assertTrue(otelTraceState(span).contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(w3cHeader(span).contains(OTEL_RANDOM_VALUE_PREFIX)); span.spanContext().forceKeep(); - assertNull(otelTraceState(span)); + assertFalse(w3cHeader(span).contains(OTEL_MEMBER)); }); } @@ -146,10 +148,10 @@ private static DDSpan newRootSpan(CoreTracer tracer) { .start(); } - private static String otelTraceState(DDSpan span) { - CharSequence state = - span.spanContext().getPropagationTags().samplingState().getOtelTraceState(); - return state == null ? null : state.toString(); + private static String w3cHeader(DDSpan span) { + String header = span.spanContext().getPropagationTags().headerValue(W3C); + assertNotNull(header); + return header; } private static Map> agentRates(double rate) { 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 033012d9172..6251d1413c6 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 @@ -4,29 +4,66 @@ import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; import static datadog.trace.api.sampling.SamplingMechanism.AGENT_RATE; +import static datadog.trace.api.sampling.SamplingMechanism.EXTERNAL_OVERRIDE; 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.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; import datadog.trace.core.propagation.PropagationTags.SamplingState; +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("inboundTracestates") + void normalizesAndForwardsOnlyFirstManagedOtelMember(String header, String expected) { + PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, header); + + assertEquals(expected, tags.headerValue(W3C)); + } + + static Stream inboundTracestates() { + 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=future:value", "ot=future:value"), + arguments( + "vendor=state,ot=rv:invalid;th:" + TH + ";future:value", + "ot=future:value,vendor=state"), + arguments("vendor=state,ot=rv:invalid;th:invalid", "vendor=state"), + arguments("ot=rv:EF284ACE7A91E1;th:" + TH, null), + arguments( + "vendor=state,ot=rv:" + RV + ",ot=rv:1234567890abcd,other=state", + "ot=rv:" + RV + ",vendor=state,other=state"), + arguments("dd=s:1,dd=s:0,ot=rv:" + RV, "dd=s:1,ot=rv:" + RV)); + } @Test void publishesProbabilityPriorityAndOtelStateTogether() { PropagationTags tags = PropagationTags.factory().empty(); + SamplingState before = tags.samplingState(); assertTrue( tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, true, 1L, false)); - SamplingState state = tags.samplingState(); + SamplingState after = tags.samplingState(); - assertEquals(SAMPLER_KEEP, state.getSamplingPriority()); - assertEquals("-1", state.getDecisionMaker().toString()); - assertEquals("1", state.getKnuthSamplingRate().toString()); - assertTrue(state.getOtelTraceState().toString().matches("rv:[0-9a-f]{14};th:0")); + assertEquals(SAMPLER_KEEP, after.getSamplingPriority()); + assertEquals("-1", after.getDecisionMaker().toString()); + assertEquals("1", after.getKnuthSamplingRate().toString()); + assertTrue(after.getOtelTraceState().toString().matches("rv:[0-9a-f]{14};th:0")); + assertNull(tags.getW3CTracestate(before)); + assertEquals("ot=" + after.getOtelTraceState(), tags.getW3CTracestate(after)); } @Test @@ -42,6 +79,39 @@ void rejectedSamplingAttemptCannotReplaceProbabilityState() { assertEquals(established, tags.samplingState()); } + @Test + void inheritedStateRetainsItsPositionRelativeToVendors() { + PropagationTags tags = + PropagationTags.factory() + .fromHeaderValue(W3C, "vendor=state,ot=rv:ef284ace7a91e1;th:e6666666666668,dd=s:1"); + tags.updateTraceSamplingPriority(SAMPLER_KEEP, EXTERNAL_OVERRIDE); + + assertEquals( + "dd=s:1;t.dm:-0,vendor=state,ot=rv:ef284ace7a91e1;th:e6666666666668", + tags.headerValue(W3C)); + } + + @Test + void preservesFinalUnchangedInheritedOtelMember() { + PropagationTags tags = + PropagationTags.factory().fromHeaderValue(W3C, "dd=s:1,first=value,sec=value,ot=rv:" + RV); + + assertEquals("dd=s:1,first=value,sec=value,ot=rv:" + RV, tags.headerValue(W3C)); + assertEquals( + "dd=s:1,first=value,sec=value,ot=rv:" + RV, tags.getW3CTracestate(tags.samplingState())); + } + + @Test + void compoundConflictRemovesThresholdAndRetainsRandomValue() { + PropagationTags tags = + PropagationTags.factory() + .fromHeaderValue(W3C, "dd=s:1,ot=rv:00000000000001;th:8,vendor=state"); + + tags.updateTraceSamplingPriority(SAMPLER_KEEP, EXTERNAL_OVERRIDE); + + assertEquals("dd=s:1;t.dm:-0,ot=rv:00000000000001,vendor=state", tags.headerValue(W3C)); + } + @Test void forceKeepRemovesLocallyGeneratedProbabilityState() { PropagationTags tags = PropagationTags.factory().empty(); @@ -63,4 +133,21 @@ void limiterDemotionDoesNotFabricateState() { assertNull(tags.samplingState().getOtelTraceState()); } + + @Test + void generatedManagedMembersDisplaceRightmostVendorAtMemberLimit() { + StringBuilder original = new StringBuilder("v0=state"); + for (int i = 1; i < 31; i++) { + original.append(",v").append(i).append("=state"); + } + PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, original.toString()); + + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, true, 1L, false)); + + String header = tags.headerValue(W3C); + assertTrue(header.split(",").length <= 32); + assertTrue(header.startsWith("dd=s:1;t.dm:-1;t.ksr:0.5,ot=rv:")); + assertFalse(header.contains("v30=state")); + } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java index 9903867e1c4..9af53d7c4c3 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java @@ -1,14 +1,20 @@ package datadog.trace.core.propagation; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; import static datadog.trace.api.sampling.PrioritySampling.UNSET; import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.AGENT_RATE; import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; import static datadog.trace.core.propagation.PropagationTags.HeaderType.DATADOG; +import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; import static datadog.trace.core.propagation.W3CHttpCodec.OT_BAGGAGE_PREFIX; 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.singletonMap; 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 datadog.trace.api.DDSpanId; import datadog.trace.api.DDTraceId; @@ -19,6 +25,9 @@ import datadog.trace.test.junit.utils.converter.TraceIdConverter; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.converter.ConvertWith; import org.tabletest.junit.TableTest; @@ -123,6 +132,62 @@ void injectTheDecisionMakerTag() { assertEquals(expected, carrier); } + @Test + void injectUsesSingleSamplingStateAcrossHeaders() throws InterruptedException { + PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, "ot=rv:ef284ace7a91e1"); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, true, 1L, true)); + DDSpanContext context = + mockSpanContext( + DDTraceId.from("1"), DDSpanId.from("2"), SAMPLER_KEEP, null, new HashMap<>(), tags); + Map carrier = new HashMap<>(); + CountDownLatch traceparentWritten = new CountDownLatch(1); + CountDownLatch samplingUpdated = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + Thread updater = + new Thread( + () -> { + try { + assertTrue(traceparentWritten.await(5, TimeUnit.SECONDS)); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision( + SAMPLER_DROP, AGENT_RATE, 0.0, false, 1L, true)); + } catch (Throwable throwable) { + failure.set(throwable); + } finally { + samplingUpdated.countDown(); + } + }); + updater.start(); + + injector.inject( + context, + carrier, + (headers, key, value) -> { + headers.put(key, value); + if (TRACE_PARENT_KEY.equals(key)) { + traceparentWritten.countDown(); + try { + assertTrue(samplingUpdated.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new AssertionError(interrupted); + } + } + }); + updater.join(TimeUnit.SECONDS.toMillis(5)); + + assertFalse(updater.isAlive()); + if (failure.get() != null) { + throw new AssertionError(failure.get()); + } + assertTrue(carrier.get(TRACE_PARENT_KEY).endsWith("-01")); + assertEquals( + "dd=s:1;p:0000000000000002;t.dm:-1;t.ksr:1,ot=rv:ef284ace7a91e1", + carrier.get(TRACE_STATE_KEY)); + assertEquals("dd=s:0;t.ksr:0,ot=rv:ef284ace7a91e1", tags.headerValue(W3C)); + } + @Test void updateLastParentIdOnChildSpan() { Map carrier = new HashMap<>(); 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..f444c1ded04 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3COtelTraceStateContinuationTest.java @@ -0,0 +1,137 @@ +package datadog.trace.core.propagation; + +import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_X_DATADOG_TAGS_MAX_LENGTH; +import static datadog.trace.api.TracePropagationStyle.DATADOG; +import static datadog.trace.api.TracePropagationStyle.TRACECONTEXT; +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.Arrays.asList; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +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.LinkedHashSet; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** Exercises {@code ot=} through real W3C extraction, continuation, and reinjection. */ +class W3COtelTraceStateContinuationTest extends DDCoreJavaSpecification { + private static final String TRACE_PARENT = + "00-00000000000000000000000000000001-123456789abcdef0-01"; + private static final String RANDOM_VALUE = "ef284ace7a91e1"; + private static final String THRESHOLD = "e6666666666668"; + private static final String DD_MEMBER = "dd=s:2;p:123456789abcdef0"; + + private final HttpCodec.Injector injector = W3CHttpCodec.newInjector(emptyMap()); + + @Test + void roundTripsValidOtelState() { + String inbound = DD_MEMBER + ",ot=rv:" + RANDOM_VALUE + ";th:" + THRESHOLD; + + assertTrue(continueTraceAndReinject(inbound).contains("ot=rv:" + RANDOM_VALUE + ";th:")); + } + + @Test + void malformedRandomValueRemovesManagedPair() { + String inbound = DD_MEMBER + ",ot=rv:zz;th:" + THRESHOLD; + String outbound = continueTraceAndReinject(inbound); + + assertFalse(outbound.contains("ot=")); + assertFalse(outbound.contains("th:" + THRESHOLD)); + } + + @ParameterizedTest + @CsvSource({ + "0, 01, ef284ace7a91e1, 00, false", + "2, 00, 00000000000000, 01, false", + "0, 00, 00000000000000, 00, true", + "2, 01, ef284ace7a91e1, 01, true" + }) + void compoundExtractionKeepsFirstPriorityAndReconcilesOtelState( + int datadogPriority, + String inboundFlags, + String randomValue, + String outboundFlags, + boolean thresholdExpected) { + String traceParent = TRACE_PARENT.substring(0, TRACE_PARENT.length() - 2) + inboundFlags; + String inboundTracestate = + DD_MEMBER + ",ot=rv:" + randomValue + ";th:" + THRESHOLD + ",vendor=state"; + Map inboundHeaders = + headers( + DatadogHttpCodec.TRACE_ID_KEY, + "1", + DatadogHttpCodec.SPAN_ID_KEY, + "2", + DatadogHttpCodec.SAMPLING_PRIORITY_KEY, + String.valueOf(datadogPriority), + TRACE_PARENT_KEY, + traceParent, + TRACE_STATE_KEY, + inboundTracestate); + Config config = mock(Config.class); + when(config.getTracePropagationStylesToExtract()) + .thenReturn(new LinkedHashSet<>(asList(DATADOG, TRACECONTEXT))); + when(config.getxDatadogTagsMaxLength()).thenReturn(DEFAULT_TRACE_X_DATADOG_TAGS_MAX_LENGTH); + + CoreTracer tracer = tracerBuilder().build(); + try { + HttpCodec.Extractor extractor = HttpCodec.createExtractor(config, tracer::captureTraceConfig); + ExtractedContext extracted = + assertInstanceOf( + ExtractedContext.class, extractor.extract(inboundHeaders, stringValuesMap())); + assertEquals(inboundTracestate, extracted.getPropagationTags().getW3CTracestate()); + + AgentSpan span = tracer.buildSpan("test", "continued").asChildOf(extracted).start(); + Map outboundHeaders = new HashMap<>(); + injector.inject((DDSpanContext) span.spanContext(), outboundHeaders, Map::put); + span.finish(); + + assertTrue(outboundHeaders.get(TRACE_PARENT_KEY).endsWith("-" + outboundFlags)); + String outboundTracestate = outboundHeaders.get(TRACE_STATE_KEY); + assertTrue(outboundTracestate.startsWith("dd=s:" + datadogPriority)); + assertTrue(outboundTracestate.contains("ot=rv:" + randomValue)); + assertTrue(outboundTracestate.endsWith("vendor=state")); + assertEquals(thresholdExpected, outboundTracestate.contains("th:" + THRESHOLD)); + } finally { + tracer.close(); + } + } + + 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); + ExtractedContext extracted = + assertInstanceOf( + ExtractedContext.class, extractor.extract(inboundHeaders, stringValuesMap())); + + AgentSpan span = tracer.buildSpan("test", "continued").asChildOf(extracted).start(); + assertEquals(extracted.getSamplingPriority(), span.getSamplingPriority()); + + Map outboundHeaders = new HashMap<>(); + injector.inject((DDSpanContext) span.spanContext(), outboundHeaders, Map::put); + span.finish(); + return outboundHeaders.get(TRACE_STATE_KEY); + } finally { + tracer.close(); + } + } +} From 27cc740913844324f5f3dae4d17e09e1c448e5b4 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Mon, 14 Sep 2026 17:20:54 +0200 Subject: [PATCH 05/22] feat(otlp): export consistent otel sampling state --- .../trace/core/otlp/trace/OtlpTraceJson.java | 6 +- .../trace/core/otlp/trace/OtlpTraceProto.java | 6 +- .../trace/OtlpTraceJsonCollectorTest.java | 99 ++++++++++++- .../core/otlp/trace/OtlpTraceProtoTest.java | 140 ++++++++++++++++++ 4 files changed, 243 insertions(+), 8 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java index a56498d0a23..845f870b965 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java @@ -34,6 +34,7 @@ import datadog.trace.core.MetadataConsumer; import datadog.trace.core.PendingTrace; import datadog.trace.core.propagation.PropagationTags; +import datadog.trace.core.propagation.PropagationTags.SamplingState; import java.util.List; import java.util.Map; @@ -51,13 +52,14 @@ private OtlpTraceJson() {} public static void writeSpan( JsonWriter writer, DDSpan span, MetaWriter metaWriter, List links) { PropagationTags propagationTags = span.spanContext().getPropagationTags(); + SamplingState samplingState = propagationTags.samplingState(); writer.beginObject(); writer.name("traceId").value(hexTraceId(span.getTraceId())); writer.name("spanId").value(hexSpanId(span.getSpanId())); - String tracestate = propagationTags.getW3CTracestate(); + String tracestate = propagationTags.getW3CTracestate(samplingState); if (tracestate != null) { writer.name("traceState").value(tracestate); } @@ -67,7 +69,7 @@ public static void writeSpan( } int traceFlags = NO_TRACE_FLAGS; - if (span.samplingPriority() > 0) { + if (samplingState.getSamplingPriority() > 0) { traceFlags |= SAMPLED_TRACE_FLAG; } if (span.spanContext().isRemote()) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java index 258cfb73669..707225cf5a6 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java @@ -48,6 +48,7 @@ import datadog.trace.core.PendingTrace; import datadog.trace.core.otlp.common.OtlpProtoBuffer; import datadog.trace.core.propagation.PropagationTags; +import datadog.trace.core.propagation.PropagationTags.SamplingState; /** Provides optimized writers for OpenTelemetry's "trace.proto" wire protocol. */ public final class OtlpTraceProto { @@ -84,6 +85,7 @@ public static int recordSpanMessage( int nestedSpanLinkBytes, OtlpProtoBuffer protobuf) { PropagationTags propagationTags = span.spanContext().getPropagationTags(); + SamplingState samplingState = propagationTags.samplingState(); writeTag(buf, 1, LEN_WIRE_TYPE); writeTraceId(buf, span.getTraceId()); @@ -91,7 +93,7 @@ public static int recordSpanMessage( writeTag(buf, 2, LEN_WIRE_TYPE); writeSpanId(buf, span.getSpanId()); - String tracestate = propagationTags.getW3CTracestate(); + String tracestate = propagationTags.getW3CTracestate(samplingState); if (tracestate != null) { writeTag(buf, 3, LEN_WIRE_TYPE); writeString(buf, tracestate); @@ -103,7 +105,7 @@ public static int recordSpanMessage( } int traceFlags = NO_TRACE_FLAGS; - if (span.samplingPriority() > 0) { + if (samplingState.getSamplingPriority() > 0) { traceFlags |= SAMPLED_TRACE_FLAG; } if (span.spanContext().isRemote()) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index 17ec0c0d17f..acdea5b89c2 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -118,15 +118,16 @@ void nonErrorSpanHasNoStatusObject() throws IOException { } @Test - void spanTraceStateOmittedWhenNotPropagated() throws IOException { + void spanTraceStateIncludesDefaultProbabilityDecision() throws IOException { DDSpan span = startAndFinish("op.notracestate", "GET /no-tracestate", null); OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); collector.addTrace(asList((CoreSpan) span)); Map parsedSpan = onlySpan(collector.collectTraces()); - assertFalse( - parsedSpan.containsKey("traceState"), "no W3C tracestate propagated should be omitted"); + assertTrue( + parsedSpan.get("traceState").toString().matches("ot=rv:[0-9a-f]{14};th:0"), + parsedSpan.get("traceState").toString()); } @Test @@ -150,7 +151,9 @@ void spanTraceStateIncludedWhenPropagated() throws IOException { collector.addTrace(asList((CoreSpan) agentSpan)); Map parsedSpan = onlySpan(collector.collectTraces()); - assertEquals("vendor=state", parsedSpan.get("traceState")); + assertTrue( + parsedSpan.get("traceState").toString().matches("ot=rv:[0-9a-f]{14};th:0,vendor=state"), + parsedSpan.get("traceState").toString()); } @Test @@ -181,6 +184,46 @@ void spanFlagsIncludeSampledBitWhenSampled() throws IOException { assertEquals(SAMPLED_TRACE_FLAG, ((Number) parsedSpan.get("flags")).intValue()); } + @Test + void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { + Map localFallback = exportSamplingSpan(localProbabilitySpan(1.0, true)); + assertTrue(localFallback.get("traceState").toString().matches("ot=rv:[0-9a-f]{14};th:0")); + assertEquals(SAMPLED_TRACE_FLAG, ((Number) localFallback.get("flags")).intValue()); + + Map inherited = exportSamplingSpan(inheritedSamplingSpan()); + assertEquals("dd=s:1,vendor=state,ot=rv:ef284ace7a91e1;th:8", inherited.get("traceState")); + assertEquals(SAMPLED_TRACE_FLAG, ((Number) inherited.get("flags")).intValue()); + + Map probabilityDrop = exportSamplingSpan(localProbabilitySpan(0.0, false)); + assertTrue( + probabilityDrop + .get("traceState") + .toString() + .matches("ot=rv:[0-9a-f]{14};th:ffffffffffffff")); + assertFalse(probabilityDrop.containsKey("flags")); + + DDSpan limiterDrop = localSamplingSpan(); + limiterDrop + .spanContext() + .getPropagationTags() + .tryUpdateProbabilitySamplingDecision( + PrioritySampling.SAMPLER_DROP, + SamplingMechanism.AGENT_RATE, + 1.0, + true, + limiterDrop.getTraceId().toLong(), + true); + Map limiter = exportSamplingSpan(limiterDrop); + assertNull(limiter.get("traceState")); + assertFalse(limiter.containsKey("flags")); + + DDSpan nonProbabilityKeep = localProbabilitySpan(0.0, false); + nonProbabilityKeep.spanContext().getPropagationTags().forceKeep(SamplingMechanism.MANUAL); + Map nonProbability = exportSamplingSpan(nonProbabilityKeep); + assertNull(nonProbability.get("traceState")); + assertEquals(SAMPLED_TRACE_FLAG, ((Number) nonProbability.get("flags")).intValue()); + } + @Test void multipleSpansInATraceAreAllWritten() throws IOException { AgentSpan parent = TRACER.startSpan("test", "op.parent"); @@ -283,6 +326,54 @@ private static DDSpan startAndFinish(String operationName, String resourceName, return (DDSpan) agentSpan; } + private static DDSpan localSamplingSpan() { + AgentSpan span = TRACER.startSpan("test", "op.sampling"); + span.setResourceName("op.sampling"); + return (DDSpan) span; + } + + private static DDSpan localProbabilitySpan(double rate, boolean sampled) { + DDSpan span = localSamplingSpan(); + span.spanContext() + .getPropagationTags() + .tryUpdateProbabilitySamplingDecision( + sampled ? PrioritySampling.SAMPLER_KEEP : PrioritySampling.SAMPLER_DROP, + SamplingMechanism.AGENT_RATE, + rate, + sampled, + span.getTraceId().toLong(), + true); + return span; + } + + private static DDSpan inheritedSamplingSpan() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue( + PropagationTags.HeaderType.W3C, "dd=s:1,vendor=state,ot=rv:ef284ace7a91e1;th:8"); + ExtractedContext parent = + new ExtractedContext( + DDTraceId.ONE, + 0L, + PrioritySampling.SAMPLER_KEEP, + null, + propagationTags, + TracePropagationStyle.TRACECONTEXT); + AgentSpan span = TRACER.startSpan("test", "op.inherited", parent); + span.setResourceName("op.inherited"); + return (DDSpan) span; + } + + private static Map exportSamplingSpan(DDSpan span) throws IOException { + if (span.getSamplingPriority() <= 0) { + span.setTag(SPAN_SAMPLING_MECHANISM_TAG, SamplingMechanism.SPAN_SAMPLING_RATE); + } + span.finish(); + OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); + collector.addTrace(asList((CoreSpan) span)); + return onlySpan(collector.collectTraces()); + } + @SuppressWarnings("unchecked") private static Map onlySpan(OtlpPayload payload) throws IOException { List> spans = allSpans(payload); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java index aa9d7c7022b..9fbb6a3c3fe 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java @@ -6,6 +6,7 @@ import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_INTERNAL; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_PRODUCER; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER; +import static datadog.trace.core.DDSpanContext.SPAN_SAMPLING_MECHANISM_TAG; import static datadog.trace.core.otlp.common.OtlpTraceFlags.SAMPLED_TRACE_FLAG; import static java.util.Arrays.asList; import static java.util.Arrays.copyOfRange; @@ -632,6 +633,42 @@ void testCollectMultipleTraces() throws IOException { "payload must contain spans with all three distinct trace IDs"); } + @Test + void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { + EncodedSamplingState localFallback = exportSamplingState(localProbabilitySpan(1.0, true)); + assertTrue(localFallback.traceState.matches("ot=rv:[0-9a-f]{14};th:0")); + assertEquals(SAMPLED_TRACE_FLAG, localFallback.flags); + + EncodedSamplingState inherited = exportSamplingState(inheritedSamplingSpan()); + assertEquals("dd=s:1,vendor=state,ot=rv:ef284ace7a91e1;th:8", inherited.traceState); + assertEquals(SAMPLED_TRACE_FLAG, inherited.flags); + + EncodedSamplingState probabilityDrop = exportSamplingState(localProbabilitySpan(0.0, false)); + assertTrue(probabilityDrop.traceState.matches("ot=rv:[0-9a-f]{14};th:ffffffffffffff")); + assertEquals(0, probabilityDrop.flags); + + DDSpan limiterDrop = localSamplingSpan(); + limiterDrop + .spanContext() + .getPropagationTags() + .tryUpdateProbabilitySamplingDecision( + PrioritySampling.SAMPLER_DROP, + SamplingMechanism.AGENT_RATE, + 1.0, + true, + limiterDrop.getTraceId().toLong(), + true); + EncodedSamplingState limiter = exportSamplingState(limiterDrop); + assertNull(limiter.traceState); + assertEquals(0, limiter.flags); + + DDSpan nonProbabilityKeep = localProbabilitySpan(0.0, false); + nonProbabilityKeep.spanContext().getPropagationTags().forceKeep(SamplingMechanism.MANUAL); + EncodedSamplingState nonProbability = exportSamplingState(nonProbabilityKeep); + assertNull(nonProbability.traceState); + assertEquals(SAMPLED_TRACE_FLAG, nonProbability.flags); + } + @Test void poisonedSpanResetsCollectorForNextTrace() { // mid-trace exception (e.g. from a malformed span) must not leave partial state behind @@ -715,6 +752,109 @@ private static List parseSpanNamesFromPayload(OtlpPayload payload) throw return names; } + private static DDSpan localSamplingSpan() { + AgentSpan span = TRACER.startSpan("test", "op.sampling"); + span.setResourceName("op.sampling"); + return (DDSpan) span; + } + + private static DDSpan localProbabilitySpan(double rate, boolean sampled) { + DDSpan span = localSamplingSpan(); + span.spanContext() + .getPropagationTags() + .tryUpdateProbabilitySamplingDecision( + sampled ? PrioritySampling.SAMPLER_KEEP : PrioritySampling.SAMPLER_DROP, + SamplingMechanism.AGENT_RATE, + rate, + sampled, + span.getTraceId().toLong(), + true); + return span; + } + + private static DDSpan inheritedSamplingSpan() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue( + PropagationTags.HeaderType.W3C, "dd=s:1,vendor=state,ot=rv:ef284ace7a91e1;th:8"); + ExtractedContext parent = + new ExtractedContext( + DDTraceId.ONE, + 0L, + PrioritySampling.SAMPLER_KEEP, + null, + propagationTags, + TracePropagationStyle.TRACECONTEXT); + AgentSpan span = TRACER.startSpan("test", "op.inherited", parent); + span.setResourceName("op.inherited"); + return (DDSpan) span; + } + + private static EncodedSamplingState exportSamplingState(DDSpan span) throws IOException { + if (span.getSamplingPriority() <= 0) { + span.setTag(SPAN_SAMPLING_MECHANISM_TAG, SamplingMechanism.SPAN_SAMPLING_RATE); + } + span.finish(); + OtlpTraceProtoCollector collector = new OtlpTraceProtoCollector(); + collector.addTrace(asList((CoreSpan) span)); + return parseOnlySpanSamplingState(collector.collectTraces()); + } + + private static EncodedSamplingState parseOnlySpanSamplingState(OtlpPayload payload) + throws IOException { + CodedInputStream tracesData = CodedInputStream.newInstance(payload.getContent()); + tracesData.readTag(); + CodedInputStream resourceSpans = tracesData.readBytes().newCodedInput(); + CodedInputStream scopeSpans = null; + while (!resourceSpans.isAtEnd()) { + int tag = resourceSpans.readTag(); + if (WireFormat.getTagFieldNumber(tag) == 2) { + scopeSpans = resourceSpans.readBytes().newCodedInput(); + } else { + resourceSpans.skipField(tag); + } + } + assertNotNull(scopeSpans); + + CodedInputStream spanData = null; + while (!scopeSpans.isAtEnd()) { + int tag = scopeSpans.readTag(); + if (WireFormat.getTagFieldNumber(tag) == 2) { + spanData = scopeSpans.readBytes().newCodedInput(); + break; + } + scopeSpans.skipField(tag); + } + assertNotNull(spanData); + + String traceState = null; + int flags = 0; + while (!spanData.isAtEnd()) { + int tag = spanData.readTag(); + switch (WireFormat.getTagFieldNumber(tag)) { + case 3: + traceState = spanData.readString(); + break; + case 16: + flags = spanData.readFixed32(); + break; + default: + spanData.skipField(tag); + } + } + return new EncodedSamplingState(traceState, flags); + } + + private static final class EncodedSamplingState { + private final String traceState; + private final int flags; + + private EncodedSamplingState(String traceState, int flags) { + this.traceState = traceState; + this.flags = flags; + } + } + // ── span construction ───────────────────────────────────────────────────── /** Builds {@link DDSpan} instances from the given specs, collecting them in order. */ From a5a43ee6ca67c7392027f567d6d5b4180fee5abb Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Mon, 14 Sep 2026 17:48:08 +0200 Subject: [PATCH 06/22] perf(propagation): reduce sampling state allocations Reuse the immutable empty sampling state across propagation tags. Cache effective W3C tracestate by immutable sampling state identity. Size W3C encoding buffers from the captured sampling state. --- .../core/propagation/ptags/PTagsCodec.java | 7 +++- .../core/propagation/ptags/PTagsFactory.java | 32 +++++++++++++-- .../core/propagation/ptags/W3CPTagsCodec.java | 40 ++++++++++++++++++- .../OtelTraceStatePropagationTest.java | 12 +++++- 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java index 82082a4e1ae..7dfd0455d23 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java @@ -38,7 +38,7 @@ static String headerValue( PTags ptags, CharSequence lastParentIdOverride, SamplingState samplingState) { - int estimate = codec.estimateHeaderSize(ptags, samplingState); + int estimate = codec.estimateHeaderSize(ptags, lastParentIdOverride, samplingState); if (estimate == 0) { return ""; } @@ -193,6 +193,11 @@ protected int estimateHeaderSize(PTags pTags, SamplingState samplingState) { return estimateHeaderSize(pTags); } + protected int estimateHeaderSize( + PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState) { + return estimateHeaderSize(pTags, samplingState); + } + protected abstract int appendPrefix(StringBuilder sb, PTags ptags); /** 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 7fd4801421c..679b9901b42 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 @@ -87,6 +87,8 @@ PropagationTags createInvalid(String error) { static class PTags extends PropagationTags { private static final String EMPTY = ""; + private static final SamplingState EMPTY_SAMPLING_STATE = + new SamplingState(PrioritySampling.UNSET, null, null, null, null); protected final PTagsFactory factory; @@ -120,6 +122,7 @@ static class PTags extends PropagationTags { private volatile CharSequence origin; private volatile HeaderCacheEntry datadogHeaderCache; private volatile HeaderCacheEntry w3cHeaderCache; + private volatile TracestateCacheEntry tracestateCache; /** The high-order 64 bits of the trace id. */ private volatile long traceIdHighOrderBits; @@ -182,8 +185,7 @@ static class PTags extends PropagationTags { this.tagPairs = tagPairs; this.canChangeDecisionMaker = decisionMakerTagValue == null; this.traceSource = traceSource; - this.samplingState = - newSamplingState(samplingPriority, null, null, decisionMakerTagValue, null); + this.samplingState = initialSamplingState(samplingPriority, decisionMakerTagValue); this.origin = origin; this.lastParentId = lastParentId; this.orgPropagationMarkerTagValue = orgPropagationMarkerTagValue; @@ -357,6 +359,14 @@ private static SamplingState newSamplingState( knuthSamplingRateTagValue); } + private static SamplingState initialSamplingState( + int samplingPriority, TagValue decisionMakerTagValue) { + if (samplingPriority == PrioritySampling.UNSET && decisionMakerTagValue == null) { + return EMPTY_SAMPLING_STATE; + } + return newSamplingState(samplingPriority, null, null, decisionMakerTagValue, null); + } + @Override public void addTraceSource(final int product) { TRACE_SOURCE_UPDATER.updateAndGet( @@ -698,7 +708,23 @@ public String getW3CTracestate() { @Override public String getW3CTracestate(SamplingState samplingState) { - return W3CPTagsCodec.rebuildTracestate(samplingState); + TracestateCacheEntry cache = tracestateCache; + if (cache == null || cache.samplingState != samplingState) { + cache = + new TracestateCacheEntry(samplingState, W3CPTagsCodec.rebuildTracestate(samplingState)); + tracestateCache = cache; + } + return cache.tracestate; + } + + private static final class TracestateCacheEntry { + private final SamplingState samplingState; + private final String tracestate; + + private TracestateCacheEntry(SamplingState samplingState, String tracestate) { + this.samplingState = samplingState; + this.tracestate = tracestate; + } } @Override diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index 67185560467..49fbb519b31 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -270,8 +270,44 @@ protected int estimateHeaderSize(PTags pTags) { } @Override - protected int estimateHeaderSize(PTags pTags, SamplingState samplingState) { - return MAX_HEADER_SIZE; + protected int estimateHeaderSize( + PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState) { + int size = EMPTY_SIZE + 1; + size += pTags.getXDatadogTagsSize(samplingState); + if (pTags.getOrigin() != null) { + size += pTags.getOrigin().length() + 3; + } + if (samplingState.getSamplingPriority() != PrioritySampling.UNSET) { + size += 5; + } + CharSequence lastParent = + lastParentIdOverride != null ? lastParentIdOverride : pTags.getLastParentId(); + if (lastParent != null) { + size += lastParent.length() + 3; + } + String originalTracestate = samplingState.getTracestate(); + boolean includesOriginalTracestate = false; + if (originalTracestate != null + && pTags instanceof W3CPTags + && originalTracestate == pTags.tracestate) { + W3CPTags w3CPTags = (W3CPTags) pTags; + size += w3CPTags.maxUnknownSize; + if (w3CPTags.ddMemberStart != -1) { + size += originalTracestate.length() - (w3CPTags.ddMemberValueEnd - w3CPTags.ddMemberStart); + includesOriginalTracestate = true; + } + } else if (originalTracestate != null) { + size += originalTracestate.length(); + includesOriginalTracestate = true; + } + CharSequence otelTraceState = samplingState.getOtelTraceState(); + if (otelTraceState != null) { + if (includesOriginalTracestate && otelTraceState instanceof OtelTraceState) { + size -= ((OtelTraceState) otelTraceState).getOriginalSize(); + } + size += OTEL_MEMBER_KEY.length() + otelTraceState.length() + 1; + } + return Math.min(size, MAX_HEADER_SIZE); } @Override 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 6251d1413c6..5e5daffabae 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 @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; @@ -24,6 +25,13 @@ class OtelTraceStatePropagationTest { private static final String RV = "ef284ace7a91e1"; private static final String TH = "e6666666666668"; + @Test + void reusesEmptySamplingState() { + PropagationTags.Factory factory = PropagationTags.factory(); + + assertSame(factory.empty().samplingState(), factory.empty().samplingState()); + } + @ParameterizedTest @MethodSource("inboundTracestates") void normalizesAndForwardsOnlyFirstManagedOtelMember(String header, String expected) { @@ -63,7 +71,9 @@ void publishesProbabilityPriorityAndOtelStateTogether() { assertEquals("1", after.getKnuthSamplingRate().toString()); assertTrue(after.getOtelTraceState().toString().matches("rv:[0-9a-f]{14};th:0")); assertNull(tags.getW3CTracestate(before)); - assertEquals("ot=" + after.getOtelTraceState(), tags.getW3CTracestate(after)); + String tracestate = tags.getW3CTracestate(after); + assertEquals("ot=" + after.getOtelTraceState(), tracestate); + assertSame(tracestate, tags.getW3CTracestate(after)); } @Test From b5b84b2000963d7731b4fc65679f66c33de15f78 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Mon, 14 Sep 2026 18:01:57 +0200 Subject: [PATCH 07/22] fix(propagation): preserve locked decision maker --- .../core/propagation/ptags/PTagsFactory.java | 26 +++++++++++++---- .../OtelTraceStatePropagationTest.java | 28 +++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) 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 679b9901b42..ce810c5a895 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 @@ -254,7 +254,12 @@ public synchronized boolean tryUpdateTraceSamplingPriority( nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); } } - installSamplingState(samplingPriority, samplingMechanism, nextOtelTraceState); + installSamplingState( + samplingPriority, + samplingMechanism, + nextOtelTraceState, + getKnuthSamplingRateTagValue(), + canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE); return true; } @@ -283,7 +288,11 @@ public synchronized boolean tryUpdateProbabilitySamplingDecision( } TagValue nextKnuthSamplingRate = knuthSamplingRateTagValue(sampleRate); installSamplingState( - samplingPriority, samplingMechanism, nextOtelTraceState, nextKnuthSamplingRate); + samplingPriority, + samplingMechanism, + nextOtelTraceState, + nextKnuthSamplingRate, + canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE); return true; } @@ -299,17 +308,22 @@ public synchronized void forceKeep(int samplingMechanism) { private void installSamplingState( int samplingPriority, int samplingMechanism, OtelTraceState nextOtelTraceState) { installSamplingState( - samplingPriority, samplingMechanism, nextOtelTraceState, getKnuthSamplingRateTagValue()); + samplingPriority, + samplingMechanism, + nextOtelTraceState, + getKnuthSamplingRateTagValue(), + true); } private void installSamplingState( int samplingPriority, int samplingMechanism, OtelTraceState nextOtelTraceState, - TagValue nextKnuthSamplingRateTagValue) { + TagValue nextKnuthSamplingRateTagValue, + boolean updateDecisionMaker) { clearCachedHeader(W3C); TagValue nextDecisionMakerTagValue = getDecisionMakerTagValue(); - if (samplingPriority > 0) { + if (updateDecisionMaker && samplingPriority > 0) { // TODO should try to keep the old sampling mechanism if we override the value? if (samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) { // There is no specific value for the EXTERNAL_OVERRIDE, so say that it's the DEFAULT @@ -326,7 +340,7 @@ private void installSamplingState( } nextDecisionMakerTagValue = newDM; } - } else { + } else if (updateDecisionMaker) { // Drop the decision maker tag if (nextDecisionMakerTagValue != null) { // This should invalidate any cached w3c and datadog header 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 5e5daffabae..a3e0db7467c 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 @@ -6,6 +6,7 @@ import static datadog.trace.api.sampling.SamplingMechanism.AGENT_RATE; import static datadog.trace.api.sampling.SamplingMechanism.EXTERNAL_OVERRIDE; import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; +import static datadog.trace.core.propagation.PropagationTags.HeaderType.DATADOG; 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; @@ -89,6 +90,33 @@ void rejectedSamplingAttemptCannotReplaceProbabilityState() { assertEquals(established, tags.samplingState()); } + @Test + void atomicPriorityUpdatePreservesLockedDecisionMaker() { + PropagationTags tags = + PropagationTags.factory().fromHeaderValue(DATADOG, "_dd.p.dm=934086a686-4"); + + assertTrue(tags.tryUpdateTraceSamplingPriority(SAMPLER_KEEP, AGENT_RATE, false)); + + SamplingState state = tags.samplingState(); + assertEquals(SAMPLER_KEEP, state.getSamplingPriority()); + assertEquals("934086a686-4", state.getDecisionMaker().toString()); + } + + @Test + void atomicProbabilityUpdatePreservesLockedDecisionMaker() { + PropagationTags tags = + PropagationTags.factory().fromHeaderValue(DATADOG, "_dd.p.dm=934086a686-4"); + + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.5, false, 1L, false)); + + SamplingState state = tags.samplingState(); + assertEquals(SAMPLER_DROP, state.getSamplingPriority()); + assertEquals("934086a686-4", state.getDecisionMaker().toString()); + assertEquals("0.5", state.getKnuthSamplingRate().toString()); + assertTrue(state.getOtelTraceState().toString().matches("rv:[0-9a-f]{14};th:8")); + } + @Test void inheritedStateRetainsItsPositionRelativeToVendors() { PropagationTags tags = From de7920c71b6b4f0cdbcd9e8f42f271cfb225d768 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Mon, 14 Sep 2026 18:14:03 +0200 Subject: [PATCH 08/22] refactor(propagation): simplify otel sampling state --- .../propagation/ptags/DatadogPTagsCodec.java | 16 +-- .../propagation/ptags/OtelTraceState.java | 23 +--- .../core/propagation/ptags/PTagsCodec.java | 43 +------- .../core/propagation/ptags/PTagsFactory.java | 33 +++--- .../core/propagation/ptags/W3CPTagsCodec.java | 103 +++--------------- .../OtelTraceStatePropagationTest.java | 2 +- .../ptags/OtelTraceStateParsingTest.java | 46 +++----- .../propagation/ptags/OtelTraceStateTest.java | 16 ++- 8 files changed, 66 insertions(+), 216 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java index 8dda4f1d7f3..f8e745ec10c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java @@ -124,21 +124,11 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { } @Override - protected int estimateHeaderSize(PTags pTags) { - return pTags.getXDatadogTagsSize(); - } - - @Override - protected int estimateHeaderSize(PTags pTags, SamplingState samplingState) { + protected int estimateHeaderSize( + PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState) { return pTags.getXDatadogTagsSize(samplingState); } - @Override - protected int appendPrefix(StringBuilder sb, PTags ptags) { - // Calculate the tag size here and return it. Don't do anything else since there is no prefix. - return ptags.getXDatadogTagsSize(); - } - @Override protected int appendPrefix( StringBuilder sb, @@ -162,7 +152,7 @@ protected int appendTag(StringBuilder sb, TagElement key, TagElement value, int } @Override - protected int appendSuffix(StringBuilder sb, PTags ptags, int size) { + protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingState samplingState) { return size; } 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 7b938646c3c..b7fc01fdbb1 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,7 +10,6 @@ final class OtelTraceState implements CharSequence { private final CharSequence value; private final CharSequence fields; - private final int originalPosition; private final int originalSize; private final long randomValue; private final long threshold; @@ -26,7 +25,6 @@ final class OtelTraceState implements CharSequence { private OtelTraceState( CharSequence value, CharSequence fields, - int originalPosition, int originalSize, long randomValue, long threshold, @@ -39,7 +37,6 @@ private OtelTraceState( boolean inheritedRandomValue) { this.value = value; this.fields = fields; - this.originalPosition = originalPosition; this.originalSize = originalSize; this.randomValue = randomValue; this.threshold = threshold; @@ -52,7 +49,7 @@ private OtelTraceState( this.inheritedRandomValue = inheritedRandomValue; } - static OtelTraceState parse(CharSequence raw, int originalPosition, int originalSize) { + static OtelTraceState parse(CharSequence raw, int originalSize) { if (raw == null || raw.length() == 0 || raw.length() > MAX_VALUE_LENGTH) { return null; } @@ -115,12 +112,11 @@ static OtelTraceState parse(CharSequence raw, int originalPosition, int original if (normalized) { CharSequence normalizedValue = normalize(raw, randomValueStart, randomValueEnd, thresholdStart, thresholdEnd); - return parseCanonical(normalizedValue, originalPosition, originalSize); + return parseCanonical(normalizedValue, originalSize); } return new OtelTraceState( raw, raw, - originalPosition, originalSize, parseHex(raw, randomValueStart, randomValueEnd), parseThreshold(raw, thresholdStart, thresholdEnd), @@ -133,8 +129,7 @@ static OtelTraceState parse(CharSequence raw, int originalPosition, int original true); } - private static OtelTraceState parseCanonical( - CharSequence value, int originalPosition, int originalSize) { + private static OtelTraceState parseCanonical(CharSequence value, int originalSize) { int randomValueStart = -1; int randomValueEnd = -1; int thresholdStart = -1; @@ -157,7 +152,6 @@ private static OtelTraceState parseCanonical( return new OtelTraceState( value, value, - originalPosition, originalSize, parseHex(value, randomValueStart, randomValueEnd), parseThreshold(value, thresholdStart, thresholdEnd), @@ -185,7 +179,7 @@ static OtelTraceState fromProbabilityDecision( } return new OtelTraceState( - null, null, 1, 0, randomValue, threshold, -1, -1, -1, -1, true, true, false); + null, null, 0, randomValue, threshold, -1, -1, -1, -1, true, true, false); } OtelTraceState withoutThreshold() { @@ -214,7 +208,6 @@ private OtelTraceState withFields( return new OtelTraceState( null, fields, - 1, 0, randomValue, threshold, @@ -227,14 +220,6 @@ private OtelTraceState withFields( randomValueIsInherited); } - CharSequence getValue() { - return value == null ? this : value; - } - - int getOriginalPosition() { - return originalPosition; - } - int getOriginalSize() { return originalSize; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java index 7dfd0455d23..f09e0f33171 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java @@ -25,14 +25,6 @@ abstract class PTagsCodec { protected static final String PROPAGATION_ERROR_INCONSISTENT_TID = "inconsistent_tid "; protected static final TagKey UPSTREAM_SERVICES_DEPRECATED_TAG = TagKey.from("upstream_services"); - static String headerValue(PTagsCodec codec, PTags ptags) { - return headerValue(codec, ptags, null); - } - - static String headerValue(PTagsCodec codec, PTags ptags, CharSequence lastParentIdOverride) { - return headerValue(codec, ptags, lastParentIdOverride, ptags.samplingState()); - } - static String headerValue( PTagsCodec codec, PTags ptags, @@ -187,42 +179,19 @@ static int calcXDatadogTagsSize(int size, TagKey tagKey, TagValue tagValue) { abstract PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value); - protected abstract int estimateHeaderSize(PTags pTags); - - protected int estimateHeaderSize(PTags pTags, SamplingState samplingState) { - return estimateHeaderSize(pTags); - } - - protected int estimateHeaderSize( - PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState) { - return estimateHeaderSize(pTags, samplingState); - } - - protected abstract int appendPrefix(StringBuilder sb, PTags ptags); + protected abstract int estimateHeaderSize( + PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState); - /** - * Encode the prefix, using {@code lastParentIdOverride} for the W3C {@code p:} when non-null - * (inject-time). Codecs without a last-parent-id (e.g. Datadog) ignore the override. - */ - protected int appendPrefix(StringBuilder sb, PTags ptags, CharSequence lastParentIdOverride) { - return appendPrefix(sb, ptags); - } - - protected int appendPrefix( + protected abstract int appendPrefix( StringBuilder sb, PTags ptags, CharSequence lastParentIdOverride, - SamplingState samplingState) { - return appendPrefix(sb, ptags, lastParentIdOverride); - } + SamplingState samplingState); protected abstract int appendTag(StringBuilder sb, TagElement key, TagElement value, int size); - protected abstract int appendSuffix(StringBuilder sb, PTags ptags, int size); - - protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingState samplingState) { - return appendSuffix(sb, ptags, size); - } + protected abstract int appendSuffix( + StringBuilder sb, PTags ptags, int size, SamplingState samplingState); protected abstract boolean isTooLarge(StringBuilder sb, int size); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index ce810c5a895..217bd9d8761 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 @@ -108,7 +108,6 @@ static class PTags extends PropagationTags { private volatile TagValue orgPropagationMarkerTagValue; - private OtelTraceState otelTraceState; private volatile SamplingState samplingState; // Static cache for the most-recently-seen rate → TagValue. In steady state a service uses one @@ -220,7 +219,7 @@ public synchronized void updateTraceSamplingPriority( int samplingPriority, int samplingMechanism) { if (samplingPriority != PrioritySampling.UNSET && canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) { - OtelTraceState nextOtelTraceState = otelTraceState; + OtelTraceState nextOtelTraceState = getOtelTraceState(); if (nextOtelTraceState != null) { if (samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE && !nextOtelTraceState.isConsistentWith(samplingPriority > 0)) { @@ -244,7 +243,7 @@ public synchronized boolean tryUpdateTraceSamplingPriority( if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { return false; } - OtelTraceState nextOtelTraceState = otelTraceState; + OtelTraceState nextOtelTraceState = getOtelTraceState(); if (nextOtelTraceState != null) { if ((samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE || samplingMechanism == SamplingMechanism.UNKNOWN) @@ -275,7 +274,7 @@ public synchronized boolean tryUpdateProbabilitySamplingDecision( if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { return false; } - OtelTraceState nextOtelTraceState = otelTraceState; + OtelTraceState nextOtelTraceState = getOtelTraceState(); if (nextOtelTraceState == null) { boolean limiterDemotion = probabilitySamplingResult && samplingPriority <= 0; if (!limiterDemotion) { @@ -298,7 +297,7 @@ public synchronized boolean tryUpdateProbabilitySamplingDecision( @Override public synchronized void forceKeep(int samplingMechanism) { - OtelTraceState nextOtelTraceState = otelTraceState; + OtelTraceState nextOtelTraceState = getOtelTraceState(); if (nextOtelTraceState != null) { nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); } @@ -349,7 +348,6 @@ private void installSamplingState( } nextDecisionMakerTagValue = null; } - otelTraceState = nextOtelTraceState; samplingState = newSamplingState( samplingPriority, @@ -427,7 +425,7 @@ public synchronized void updateKnuthSamplingRate(double rate) { newSamplingState( currentState.getSamplingPriority(), tracestate, - otelTraceState, + getOtelTraceState(), getDecisionMakerTagValue(currentState), next); } @@ -583,8 +581,13 @@ public String headerValue(HeaderType headerType, CharSequence lastParentIdOverri if (lastParentIdOverride == null) { return headerValue(headerType); } + SamplingState currentSamplingState = samplingState; String header = - PTagsCodec.headerValue(factory.getDecoderEncoder(headerType), this, lastParentIdOverride); + PTagsCodec.headerValue( + factory.getDecoderEncoder(headerType), + this, + lastParentIdOverride, + currentSamplingState); return (header == null || header.isEmpty()) ? null : header; } @@ -654,10 +657,6 @@ private void invalidateXDatadogTagsSize() { xDatadogTagsSizeCache = null; } - int getXDatadogTagsSize() { - return getXDatadogTagsSize(samplingState); - } - int getXDatadogTagsSize(SamplingState samplingState) { SizeCacheEntry cache = xDatadogTagsSizeCache; if (cache == null || cache.samplingState != samplingState) { @@ -771,7 +770,6 @@ private synchronized void setW3CTracestate(String tracestate, OtelTraceState ote otelTraceState = otelTraceState.withoutThreshold(); } this.tracestate = tracestate; - this.otelTraceState = otelTraceState; this.samplingState = newSamplingState( samplingPriority, @@ -781,15 +779,14 @@ private synchronized void setW3CTracestate(String tracestate, OtelTraceState ote getKnuthSamplingRateTagValue()); } - OtelTraceState getOtelTraceState() { - return otelTraceState; + private OtelTraceState getOtelTraceState() { + return (OtelTraceState) samplingState.getOtelTraceState(); } void setOtelTraceState(OtelTraceState otelTraceState) { - if (this.otelTraceState != otelTraceState) { + if (getOtelTraceState() != otelTraceState) { clearCachedHeader(W3C); } - this.otelTraceState = otelTraceState; SamplingState currentState = samplingState; this.samplingState = newSamplingState( @@ -818,7 +815,7 @@ public synchronized void updateAndLockDecisionMaker(PropagationTags source) { newSamplingState( currentState.getSamplingPriority(), tracestate, - otelTraceState, + getOtelTraceState(), decisionMakerTagValue, getKnuthSamplingRateTagValue(currentState)); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index 49fbb519b31..ccca86d1776 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -11,6 +11,7 @@ import datadog.trace.core.propagation.ptags.PTagsFactory.PTags; import datadog.trace.core.propagation.ptags.TagElement.Encoding; import datadog.trace.util.SubSequence; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -55,7 +56,6 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { int otelMemberStart = -1; int otelMemberValueStart = -1; int otelMemberValueEnd = -1; - int otelMemberPosition = -1; while (memberStart < len) { if (memberIndex == MAX_MEMBER_COUNT) { // TODO should we return one with an error? @@ -88,7 +88,6 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { otelMemberStart = memberStart; otelMemberValueStart = memberValueStart; otelMemberValueEnd = memberValueEnd; - otelMemberPosition = memberIndex; } memberIndex++; @@ -105,7 +104,6 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { otelTraceState = OtelTraceState.parse( SubSequence.of(value, otelMemberValueStart, valueEnd), - otelMemberPosition, memberContributionSize(value, firstMemberStart, otelMemberStart, otelMemberValueEnd)); } @@ -237,39 +235,12 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { } @Override - protected int estimateHeaderSize(PTags pTags) { - int size = EMPTY_SIZE + 1; // 'dd=' and delimiter; - // Yes, this is a bit much, but better safe than sorry - size += pTags.getXDatadogTagsSize(); - if (pTags.getOrigin() != null) { - size += pTags.getOrigin().length() + 3; // 'o:' + delimiter - } - if (pTags.getSamplingPriority() != PrioritySampling.UNSET) { - size += 5; // 's:-?[0-9]' + delimiter - } - boolean includesOriginalTracestate = false; - if (pTags instanceof W3CPTags) { - W3CPTags w3CPTags = (W3CPTags) pTags; - size += w3CPTags.maxUnknownSize; - if (w3CPTags.ddMemberStart != -1) { - size += - (w3CPTags.tracestate.length() - (w3CPTags.ddMemberValueEnd - w3CPTags.ddMemberStart)); - includesOriginalTracestate = true; - } - } else if (pTags.tracestate != null) { - // We assume there is no Datadog list-member - size += pTags.tracestate.length(); - includesOriginalTracestate = true; - } - OtelTraceState otelTraceState = pTags.getOtelTraceState(); - if (otelTraceState != null) { - size -= includesOriginalTracestate ? otelTraceState.getOriginalSize() : 0; - size += OTEL_MEMBER_KEY.length() + otelTraceState.length() + 1; - } - return size; - } - - @Override + @SuppressWarnings("StringEquality") + @SuppressFBWarnings( + value = "ES_COMPARING_STRINGS_WITH_EQ", + justification = + "Identity determines whether the sampling state retains this PTags instance's raw " + + "tracestate, allowing its parsed size metadata to be reused.") protected int estimateHeaderSize( PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState) { int size = EMPTY_SIZE + 1; @@ -310,16 +281,6 @@ protected int estimateHeaderSize( return Math.min(size, MAX_HEADER_SIZE); } - @Override - protected int appendPrefix(StringBuilder sb, PTags ptags) { - return appendPrefix(sb, ptags, null); - } - - @Override - protected int appendPrefix(StringBuilder sb, PTags ptags, CharSequence lastParentIdOverride) { - return appendPrefix(sb, ptags, lastParentIdOverride, ptags.samplingState()); - } - @Override protected int appendPrefix( StringBuilder sb, @@ -367,11 +328,6 @@ protected int appendTag(StringBuilder sb, TagElement key, TagElement value, int return appendTag(sb, key, value, Encoding.W3C, size); } - @Override - protected int appendSuffix(StringBuilder sb, PTags ptags, int size) { - return appendSuffix(sb, ptags, size, ptags.samplingState()); - } - @Override protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingState samplingState) { // If there is room for appending unknown from W3CPTags @@ -850,10 +806,8 @@ private static boolean appendOtelAndVendorMembers( int remainingMembers = MAX_MEMBER_COUNT - (hasDatadogMember ? 1 : 0); boolean memberAppended = false; boolean preserveOtelPosition = isUnchangedInheritedOtelMember(original, otelTraceState); - if (!preserveOtelPosition - && otelTraceState != null - && remainingMembers > 0 - && appendMemberIfFits(sb, OTEL_MEMBER_KEY, otelTraceState)) { + if (!preserveOtelPosition && otelTraceState != null && remainingMembers > 0) { + appendMember(sb, OTEL_MEMBER_KEY, otelTraceState); remainingMembers--; memberAppended = true; } @@ -868,9 +822,7 @@ && appendMemberIfFits(sb, OTEL_MEMBER_KEY, otelTraceState)) { boolean otelMember = original.startsWith(OTEL_MEMBER_KEY, memberStart); if (!datadogMember && (!otelMember || preserveOtelPosition)) { int end = stripTrailingOWC(original, memberStart, memberEnd); - if (!appendMemberIfFits(sb, original, memberStart, end)) { - break; - } + appendMember(sb, original, memberStart, end); remainingMembers--; memberAppended = true; } @@ -926,18 +878,16 @@ public static String rebuildTracestate(SamplingState samplingState) { } if (original.startsWith(DATADOG_MEMBER_KEY, memberStart)) { int end = stripTrailingOWC(original, memberStart, memberEnd); - if (appendMemberIfFits(result, original, memberStart, end)) { - memberCount++; - } + appendMember(result, original, memberStart, end); + memberCount++; break; } memberStart = findNextMember(original, memberEnd + 1); } } - if (otelTraceState != null - && memberCount < MAX_MEMBER_COUNT - && appendMemberIfFits(result, OTEL_MEMBER_KEY, otelTraceState)) { + if (otelTraceState != null && memberCount < MAX_MEMBER_COUNT) { + appendMember(result, OTEL_MEMBER_KEY, otelTraceState); memberCount++; } @@ -953,9 +903,7 @@ && appendMemberIfFits(result, OTEL_MEMBER_KEY, otelTraceState)) { || original.startsWith(OTEL_MEMBER_KEY, memberStart); if (!managed) { int end = stripTrailingOWC(original, memberStart, memberEnd); - if (!appendMemberIfFits(result, original, memberStart, end)) { - break; - } + appendMember(result, original, memberStart, end); memberCount++; } memberStart = findNextMember(original, memberEnd + 1); @@ -993,29 +941,10 @@ private static void appendMember(StringBuilder sb, String key, CharSequence valu sb.append(key).append(value); } - private static boolean appendMemberIfFits(StringBuilder sb, String member, int start, int end) { - int addedSize = end - start + (sb.length() == 0 ? 0 : 1); - if (sb.length() + addedSize > MAX_HEADER_SIZE) { - return false; - } - appendMember(sb, member, start, end); - return true; - } - - private static boolean appendMemberIfFits(StringBuilder sb, String key, CharSequence value) { - int addedSize = key.length() + value.length() + (sb.length() == 0 ? 0 : 1); - if (sb.length() + addedSize > MAX_HEADER_SIZE) { - return false; - } - appendMember(sb, key, value); - return true; - } - static OtelTraceState extractOtelTraceState(String tracestate) { if (tracestate == null || tracestate.isEmpty()) { return null; } - int memberPosition = 0; int firstMemberStart = findNextMember(tracestate, 0); int memberStart = firstMemberStart; int otelMemberStart = -1; @@ -1036,7 +965,6 @@ static OtelTraceState extractOtelTraceState(String tracestate) { otelMemberValueEnd = memberValueEnd; break; } - memberPosition++; memberStart = findNextMember(tracestate, memberValueEnd); } if (otelMemberStart == -1) { @@ -1045,7 +973,6 @@ static OtelTraceState extractOtelTraceState(String tracestate) { int valueEnd = stripTrailingOWC(tracestate, otelMemberValueStart, otelMemberValueEnd); return OtelTraceState.parse( SubSequence.of(tracestate, otelMemberValueStart, valueEnd), - memberPosition, memberContributionSize(tracestate, firstMemberStart, otelMemberStart, otelMemberValueEnd)); } 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 a3e0db7467c..9c8e825b223 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 @@ -184,7 +184,7 @@ void generatedManagedMembersDisplaceRightmostVendorAtMemberLimit() { tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, true, 1L, false)); String header = tags.headerValue(W3C); - assertTrue(header.split(",").length <= 32); + assertEquals(32, header.split(",").length); assertTrue(header.startsWith("dd=s:1;t.dm:-1;t.ksr:0.5,ot=rv:")); assertFalse(header.contains("v30=state")); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java index b8280d2cece..83e81477ac5 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java @@ -4,87 +4,71 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; import datadog.trace.util.SubSequence; import org.junit.jupiter.api.Test; class OtelTraceStateParsingTest { private static final String VALUE = "rv:0123456789abcd"; - private static final int INHERITED_POSITION = 2; private static final int ORIGINAL_MEMBER_CONTRIBUTION_SIZE = 21; @Test void ignoresAbsentValues() { - assertNull(OtelTraceState.parse(null, INHERITED_POSITION, ORIGINAL_MEMBER_CONTRIBUTION_SIZE)); - assertNull(OtelTraceState.parse("", INHERITED_POSITION, ORIGINAL_MEMBER_CONTRIBUTION_SIZE)); + assertNull(OtelTraceState.parse(null, ORIGINAL_MEMBER_CONTRIBUTION_SIZE)); + assertNull(OtelTraceState.parse("", ORIGINAL_MEMBER_CONTRIBUTION_SIZE)); } @Test - void retainsValueAndMemberMetadata() { + void retainsValueAndMemberSize() { SubSequence value = SubSequence.of(VALUE, 0, VALUE.length()); - OtelTraceState state = - OtelTraceState.parse(value, INHERITED_POSITION, ORIGINAL_MEMBER_CONTRIBUTION_SIZE); + OtelTraceState state = OtelTraceState.parse(value, ORIGINAL_MEMBER_CONTRIBUTION_SIZE); assertNotNull(state); - assertSame(value, state.getValue()); assertFalse(state.isMaterialized()); assertEquals(VALUE.length(), state.length()); - assertEquals(INHERITED_POSITION, state.getOriginalPosition()); assertEquals(ORIGINAL_MEMBER_CONTRIBUTION_SIZE, state.getOriginalSize()); } - @Test - void extractsOriginalMemberPosition() { - OtelTraceState state = - W3CPTagsCodec.extractOtelTraceState("first=value,dd=s:1,dd=s:0,ot=" + VALUE); - - assertNotNull(state); - assertEquals(3, state.getOriginalPosition()); - } - @Test void retainsValidThresholdWithoutRandomValue() { - OtelTraceState state = OtelTraceState.parse("th:8", 0, 0); + OtelTraceState state = OtelTraceState.parse("th:8", 0); - assertEquals("th:8", state.getValue().toString()); + assertEquals("th:8", state.toString()); } @Test void removesMalformedThresholdAndRetainsValidRandomValue() { - OtelTraceState state = OtelTraceState.parse("rv:0123456789abcd;th:not-hex;x:value", 0, 0); + OtelTraceState state = OtelTraceState.parse("rv:0123456789abcd;th:not-hex;x:value", 0); - assertEquals("rv:0123456789abcd;x:value", state.getValue().toString()); + assertEquals("rv:0123456789abcd;x:value", state.toString()); } @Test void malformedRandomValueRemovesManagedPairButRetainsUnknownFields() { - OtelTraceState state = OtelTraceState.parse("rv:invalid;th:8;x:value", 0, 0); + OtelTraceState state = OtelTraceState.parse("rv:invalid;th:8;x:value", 0); - assertEquals("x:value", state.getValue().toString()); + assertEquals("x:value", state.toString()); } @Test void malformedFirstRandomValuePreventsRecoveryFromLaterValues() { OtelTraceState state = - OtelTraceState.parse("rv:invalid;rv:0123456789abcd;rv:ffffffffffffff;th:8;th:4", 0, 0); + OtelTraceState.parse("rv:invalid;rv:0123456789abcd;rv:ffffffffffffff;th:8;th:4", 0); assertNull(state); } @Test void keepsFirstValidManagedFields() { - OtelTraceState state = - OtelTraceState.parse("rv:0123456789abcd;rv:ffffffffffffff;th:8;th:4", 0, 0); + OtelTraceState state = OtelTraceState.parse("rv:0123456789abcd;rv:ffffffffffffff;th:8;th:4", 0); - assertEquals("rv:0123456789abcd;th:8", state.getValue().toString()); + assertEquals("rv:0123456789abcd;th:8", state.toString()); } @Test void rejectsUppercaseAndOverlongManagedFields() { - OtelTraceState state = - OtelTraceState.parse("rv:0123456789ABCd;th:123456789abcdef;x:value", 0, 0); + OtelTraceState state = OtelTraceState.parse("rv:0123456789ABCd;th:123456789abcdef;x:value", 0); - assertEquals("x:value", state.getValue().toString()); + assertEquals("x:value", state.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 index 839f29237d7..0acd0ff21d3 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 org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -18,9 +17,8 @@ class OtelTraceStateTest { void convertsDatadogProbabilityDecision() { OtelTraceState state = OtelTraceState.fromProbabilityDecision(0xfff972474538efffL, 0.1, true); - assertSame(state, state.getValue()); assertFalse(state.isMaterialized()); - assertEquals("rv:ef284ace7a91e1;th:e6666666666668", state.getValue().toString()); + assertEquals("rv:ef284ace7a91e1;th:e6666666666668", state.toString()); assertTrue(state.isMaterialized()); assertTrue(state.isConsistentWith(true)); } @@ -39,7 +37,7 @@ void serializesThresholds() { void rateZeroUsesLargestWireThresholdAndRemainsDropConsistent() { OtelTraceState state = OtelTraceState.fromProbabilityDecision(0L, 0.0, false); - assertEquals("rv:fffffffffffffe;th:ffffffffffffff", state.getValue().toString()); + assertEquals("rv:fffffffffffffe;th:ffffffffffffff", state.toString()); assertTrue(state.isConsistentWith(false)); } @@ -47,7 +45,7 @@ void rateZeroUsesLargestWireThresholdAndRemainsDropConsistent() { void correctsOnlySerializedRandomValueAtKeepBoundary() { OtelTraceState state = OtelTraceState.fromProbabilityDecision(0x03a93ee8b1999f00L, 0.1, true); - assertEquals("rv:e6666666666668;th:e6666666666668", state.getValue().toString()); + assertEquals("rv:e6666666666668;th:e6666666666668", state.toString()); assertTrue(state.isConsistentWith(true)); } @@ -59,7 +57,7 @@ void correctsOnlySerializedRandomValueAtDropBoundary() { assertEquals( "rv:" + DROP_PRECISION_BOUNDARY_RANDOM_VALUE + ";th:" + DROP_PRECISION_BOUNDARY_THRESHOLD, - state.getValue().toString()); + state.toString()); assertTrue(state.isConsistentWith(false)); } @@ -72,17 +70,17 @@ void removesLocalRandomnessForNonProbabilityDecision() { @Test void retainsInheritedRandomnessAndUnknownFieldsWithoutThreshold() { - OtelTraceState state = OtelTraceState.parse("rv:0123456789abcd;th:8;x:value", 0, 0); + OtelTraceState state = OtelTraceState.parse("rv:0123456789abcd;th:8;x:value", 0); OtelTraceState transformed = state.forNonProbabilityDecision(); - assertEquals("rv:0123456789abcd;x:value", transformed.getValue().toString()); + assertEquals("rv:0123456789abcd;x:value", transformed.toString()); assertTrue(transformed.isConsistentWith(false)); } private static void assertThreshold(double rate, String expectedThreshold) { OtelTraceState state = OtelTraceState.fromProbabilityDecision(1L, rate, rate > 0.0); - String value = state.getValue().toString(); + String value = state.toString(); assertEquals(expectedThreshold, value.substring(value.indexOf(";th:") + 4)); } } From d324e16da2c5c15e173a174aac85ab61deeab580 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Tue, 15 Sep 2026 11:22:03 +0200 Subject: [PATCH 09/22] fix(propagation): reset otel state for Org Guard resampling --- .../core/propagation/ptags/W3CPTagsCodec.java | 3 +- .../propagation/opg/OrgGuardEnforcerTest.java | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index ccca86d1776..10a85ed7a20 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -983,8 +983,9 @@ private static int memberContributionSize( return isOnlyMember ? memberSize : memberSize + 1; } + /** Creates tags that preserve unmanaged W3C tracestate members without sampling state. */ static W3CPTags empty(PTagsFactory factory, String original) { - return empty(factory, original, extractOtelTraceState(original)); + return empty(factory, original, null); } private static W3CPTags empty( diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java index 65d7ee271d4..4eb636264d1 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java @@ -3,6 +3,7 @@ import static datadog.trace.api.TracePropagationStyle.DATADOG; import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; import static datadog.trace.api.sampling.PrioritySampling.UNSET; +import static datadog.trace.api.sampling.SamplingMechanism.AGENT_RATE; import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; import static java.util.Collections.emptySet; @@ -152,6 +153,34 @@ void stripPreservesNonDdVendors() { assertTrue(reEncoded.contains("vendor2=def"), "vendor2 missing: " + reEncoded); } + @Test + @DisplayName("strip replaces inherited OTel sampling state after local resampling") + void stripReplacesOtelSamplingStateAfterLocalResampling() { + OrgGuardEnforcer enforcer = enforcer(false, emptySet(), () -> "L"); + PropagationTags tags = + factory.fromHeaderValue( + W3C, "dd=s:1;o:foo;t.opm:upstream-X,ot=rv:00000000000000;th:1,vendor1=abc"); + ExtractedContext ctx = + new ExtractedContext( + DDTraceId.from(123L), + 456L, + SAMPLER_KEEP, + "origin", + tags, + TracePropagationStyle.TRACECONTEXT); + + ExtractedContext stripped = (ExtractedContext) enforcer.enforce(ctx); + assertTrue( + stripped + .getPropagationTags() + .tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, true, 1L, false)); + + String reEncoded = stripped.getPropagationTags().headerValue(W3C); + assertNotNull(reEncoded); + assertTrue( + reEncoded.matches("dd=s:1;t.dm:-1;t.ksr:1,ot=rv:[0-9a-f]{14};th:0,vendor1=abc"), reEncoded); + } + // ---- helpers ---- private OrgGuardEnforcer enforcer( From a51831b5f42e1d3cdb384e1260f46429d929ac34 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Tue, 15 Sep 2026 14:23:40 +0200 Subject: [PATCH 10/22] update test values across tests which were hardcoding the tracestate value --- .../src/test/groovy/OpenTelemetryTest.groovy | 1 + .../src/test/groovy/OpenTracing31Test.groovy | 1 + .../src/test/groovy/OpenTracing32Test.groovy | 1 + .../java/datadog/opentracing/OT31ApiTest.java | 5 ++++- .../java/datadog/opentracing/OT33ApiTest.java | 5 ++++- 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/OpenTelemetryTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/OpenTelemetryTest.groovy index 62f315e6191..e222e462a83 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/OpenTelemetryTest.groovy +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/OpenTelemetryTest.groovy @@ -287,6 +287,7 @@ class OpenTelemetryTest extends InstrumentationSpecification { } if (contextPriority == UNSET) { expectedTracestate += ";t.ksr:1" + expectedTracestate += ",ot=${span.delegate.spanContext().propagationTags.samplingState().otelTraceState}" } if (traceId.toHighOrderLong() != 0) { expectedDataTags << "_dd.p.tid=" + traceId.toHexStringPadded(32).substring(0, 16) diff --git a/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/OpenTracing31Test.groovy b/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/OpenTracing31Test.groovy index 53f4d0a62fc..a9bcbd17f0c 100644 --- a/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/OpenTracing31Test.groovy +++ b/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/OpenTracing31Test.groovy @@ -295,6 +295,7 @@ class OpenTracing31Test extends InstrumentationSpecification { } if (contextPriority == UNSET) { expectedTracestate += ";t.ksr:1" + expectedTracestate += ",ot=${context.delegate.propagationTags.samplingState().otelTraceState}" datadogTags << "_dd.p.ksr=1" } def expectedTextMap = [ diff --git a/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/OpenTracing32Test.groovy b/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/OpenTracing32Test.groovy index fe452134938..0709d70f832 100644 --- a/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/OpenTracing32Test.groovy +++ b/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/OpenTracing32Test.groovy @@ -311,6 +311,7 @@ class OpenTracing32Test extends InstrumentationSpecification { } if (contextPriority == UNSET) { expectedTracestate+= ";t.ksr:1" + expectedTracestate+= ",ot=${context.delegate.propagationTags.samplingState().otelTraceState}" datadogTags << "_dd.p.ksr=1" } def expectedTextMap = [ diff --git a/dd-trace-ot/src/ot31CompatibilityTest/java/datadog/opentracing/OT31ApiTest.java b/dd-trace-ot/src/ot31CompatibilityTest/java/datadog/opentracing/OT31ApiTest.java index 5f1e873fb8c..db6dd5ff569 100644 --- a/dd-trace-ot/src/ot31CompatibilityTest/java/datadog/opentracing/OT31ApiTest.java +++ b/dd-trace-ot/src/ot31CompatibilityTest/java/datadog/opentracing/OT31ApiTest.java @@ -127,7 +127,10 @@ void testInjectExtract( + (propagatedPriority > 0 ? ";t.dm:-" + effectiveSamplingMechanism : "") + ";t.tid:" + traceId.toHexStringPadded(32).substring(0, 16) - + (contextPriority == UNSET ? ";t.ksr:1" : ""); + + (contextPriority == UNSET + ? ";t.ksr:1,ot=" + + ddContext.getPropagationTags().samplingState().getOtelTraceState() + : ""); Map expectedTextMap = new HashMap<>(); OTSpanContext otContext = (OTSpanContext) context; diff --git a/dd-trace-ot/src/ot33CompatibilityTest/java/datadog/opentracing/OT33ApiTest.java b/dd-trace-ot/src/ot33CompatibilityTest/java/datadog/opentracing/OT33ApiTest.java index a2d6270bb3a..2b9cfdca200 100644 --- a/dd-trace-ot/src/ot33CompatibilityTest/java/datadog/opentracing/OT33ApiTest.java +++ b/dd-trace-ot/src/ot33CompatibilityTest/java/datadog/opentracing/OT33ApiTest.java @@ -113,7 +113,10 @@ void testInjectExtract( + (propagatedPriority > 0 ? ";t.dm:-" + effectiveSamplingMechanism : "") + ";t.tid:" + traceId.toHexStringPadded(32).substring(0, 16) - + (contextPriority == UNSET ? ";t.ksr:1" : ""); + + (contextPriority == UNSET + ? ";t.ksr:1,ot=" + + ddContext.getPropagationTags().samplingState().getOtelTraceState() + : ""); Map expectedTextMap = new HashMap<>(); expectedTextMap.put("x-datadog-trace-id", context.toTraceId()); From 255fcb2b9d15e8e78e3c4e80e44a5ea3b09b6a7f Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Tue, 15 Sep 2026 15:53:12 +0200 Subject: [PATCH 11/22] remove fast path for OTel rebuildTracestate as it requires checking no duplicate dd and probably more things + regression test --- .../core/propagation/ptags/W3CPTagsCodec.java | 21 +------------------ .../OtelTraceStatePropagationTest.java | 10 +++++++-- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index 10a85ed7a20..1ad40b88f2b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -861,11 +861,7 @@ private static boolean isUnchangedInheritedOtelMember( public static String rebuildTracestate(SamplingState samplingState) { String original = samplingState.getTracestate(); CharSequence otelTraceState = samplingState.getOtelTraceState(); - if (original != null - && (isUnchangedInheritedOtelMember(original, otelTraceState) - || otelTraceState == null && !containsMember(original, OTEL_MEMBER_KEY))) { - return original; - } + // TODO Consider a raw passthrough for unchanged state after checking dd= is not duplicated. StringBuilder result = new StringBuilder(MAX_HEADER_SIZE); int memberCount = 0; @@ -912,21 +908,6 @@ public static String rebuildTracestate(SamplingState samplingState) { return result.length() == 0 ? null : result.toString(); } - private static boolean containsMember(String tracestate, String memberKey) { - int memberStart = findNextMember(tracestate, 0); - while (memberStart < tracestate.length()) { - int memberEnd = tracestate.indexOf(MEMBER_SEPARATOR, memberStart); - if (memberEnd < 0) { - memberEnd = tracestate.length(); - } - if (tracestate.startsWith(memberKey, memberStart)) { - return true; - } - memberStart = findNextMember(tracestate, memberEnd + 1); - } - return false; - } - private static void appendMember(StringBuilder sb, String member, int start, int end) { if (sb.length() != 0) { sb.append(MEMBER_SEPARATOR); 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 9c8e825b223..d74ab5f2746 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 @@ -58,6 +58,14 @@ static Stream inboundTracestates() { arguments("dd=s:1,dd=s:0,ot=rv:" + RV, "dd=s:1,ot=rv:" + RV)); } + @Test + void rebuildsDuplicateDatadogMembers() { + PropagationTags tags = + PropagationTags.factory().fromHeaderValue(W3C, "dd=s:1,dd=s:0,ot=rv:" + RV); + + assertEquals("dd=s:1,ot=rv:" + RV, tags.getW3CTracestate(tags.samplingState())); + } + @Test void publishesProbabilityPriorityAndOtelStateTogether() { PropagationTags tags = PropagationTags.factory().empty(); @@ -135,8 +143,6 @@ void preservesFinalUnchangedInheritedOtelMember() { PropagationTags.factory().fromHeaderValue(W3C, "dd=s:1,first=value,sec=value,ot=rv:" + RV); assertEquals("dd=s:1,first=value,sec=value,ot=rv:" + RV, tags.headerValue(W3C)); - assertEquals( - "dd=s:1,first=value,sec=value,ot=rv:" + RV, tags.getW3CTracestate(tags.samplingState())); } @Test From ac097ca2a4ee746b3cea4dd7d25cdd001dc08f00 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Tue, 15 Sep 2026 17:32:15 +0200 Subject: [PATCH 12/22] fix(ptags): use private lock for sampling updates --- .../core/propagation/ptags/PTagsFactory.java | 232 +++++++++--------- 1 file changed, 122 insertions(+), 110 deletions(-) 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 217bd9d8761..c1758c60dce 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 @@ -95,9 +95,8 @@ static class PTags extends PropagationTags { // tags that don't require any modifications and propagated as-is private final List tagPairs; - @SuppressFBWarnings( - value = "AT_STALE_THREAD_WRITE_OF_PRIMITIVE", - justification = "This field is never accessed concurrently") + private final Object samplingStateLock = new Object(); + private boolean canChangeDecisionMaker; private static final AtomicIntegerFieldUpdater TRACE_SOURCE_UPDATER = @@ -215,93 +214,100 @@ static PTags withError(PTagsFactory factory, String error) { } @Override - public synchronized void updateTraceSamplingPriority( - int samplingPriority, int samplingMechanism) { - if (samplingPriority != PrioritySampling.UNSET && canChangeDecisionMaker - || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) { - OtelTraceState nextOtelTraceState = getOtelTraceState(); - if (nextOtelTraceState != null) { - if (samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE - && !nextOtelTraceState.isConsistentWith(samplingPriority > 0)) { - nextOtelTraceState = nextOtelTraceState.withoutThreshold(); - } else if (samplingMechanism != SamplingMechanism.UNKNOWN - && samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE) { - nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); + public void updateTraceSamplingPriority(int samplingPriority, int samplingMechanism) { + synchronized (samplingStateLock) { + if (samplingPriority != PrioritySampling.UNSET && canChangeDecisionMaker + || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) { + OtelTraceState nextOtelTraceState = getOtelTraceState(); + if (nextOtelTraceState != null) { + if (samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE + && !nextOtelTraceState.isConsistentWith(samplingPriority > 0)) { + nextOtelTraceState = nextOtelTraceState.withoutThreshold(); + } else if (samplingMechanism != SamplingMechanism.UNKNOWN + && samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE) { + nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); + } } + installSamplingState(samplingPriority, samplingMechanism, nextOtelTraceState); } - installSamplingState(samplingPriority, samplingMechanism, nextOtelTraceState); } } @Override - public synchronized boolean tryUpdateTraceSamplingPriority( + public boolean tryUpdateTraceSamplingPriority( int samplingPriority, int samplingMechanism, boolean allowOverride) { - if (samplingPriority == PrioritySampling.UNSET) { - return false; - } - SamplingState current = samplingState; - if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { - return false; - } - OtelTraceState nextOtelTraceState = getOtelTraceState(); - if (nextOtelTraceState != null) { - if ((samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE - || samplingMechanism == SamplingMechanism.UNKNOWN) - && !nextOtelTraceState.isConsistentWith(samplingPriority > 0)) { - nextOtelTraceState = nextOtelTraceState.withoutThreshold(); - } else if (samplingMechanism != SamplingMechanism.UNKNOWN) { - nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); + synchronized (samplingStateLock) { + if (samplingPriority == PrioritySampling.UNSET) { + return false; + } + SamplingState current = samplingState; + if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { + return false; } + OtelTraceState nextOtelTraceState = getOtelTraceState(); + if (nextOtelTraceState != null) { + if ((samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE + || samplingMechanism == SamplingMechanism.UNKNOWN) + && !nextOtelTraceState.isConsistentWith(samplingPriority > 0)) { + nextOtelTraceState = nextOtelTraceState.withoutThreshold(); + } else if (samplingMechanism != SamplingMechanism.UNKNOWN) { + nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); + } + } + installSamplingState( + samplingPriority, + samplingMechanism, + nextOtelTraceState, + getKnuthSamplingRateTagValue(), + canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE); + return true; } - installSamplingState( - samplingPriority, - samplingMechanism, - nextOtelTraceState, - getKnuthSamplingRateTagValue(), - canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE); - return true; } @Override - public synchronized boolean tryUpdateProbabilitySamplingDecision( + public boolean tryUpdateProbabilitySamplingDecision( int samplingPriority, int samplingMechanism, double sampleRate, boolean probabilitySamplingResult, long traceIdLowOrderBits, boolean allowOverride) { - SamplingState current = samplingState; - if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { - return false; - } - OtelTraceState nextOtelTraceState = getOtelTraceState(); - if (nextOtelTraceState == null) { - boolean limiterDemotion = probabilitySamplingResult && samplingPriority <= 0; - if (!limiterDemotion) { - nextOtelTraceState = - OtelTraceState.fromProbabilityDecision( - traceIdLowOrderBits, sampleRate, probabilitySamplingResult); + synchronized (samplingStateLock) { + SamplingState current = samplingState; + if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { + return false; } - } else if (probabilitySamplingResult && samplingPriority <= 0) { - nextOtelTraceState = nextOtelTraceState.withoutThreshold(); + OtelTraceState nextOtelTraceState = getOtelTraceState(); + if (nextOtelTraceState == null) { + boolean limiterDemotion = probabilitySamplingResult && samplingPriority <= 0; + if (!limiterDemotion) { + nextOtelTraceState = + OtelTraceState.fromProbabilityDecision( + traceIdLowOrderBits, sampleRate, probabilitySamplingResult); + } + } else if (probabilitySamplingResult && samplingPriority <= 0) { + nextOtelTraceState = nextOtelTraceState.withoutThreshold(); + } + TagValue nextKnuthSamplingRate = knuthSamplingRateTagValue(sampleRate); + installSamplingState( + samplingPriority, + samplingMechanism, + nextOtelTraceState, + nextKnuthSamplingRate, + canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE); + return true; } - TagValue nextKnuthSamplingRate = knuthSamplingRateTagValue(sampleRate); - installSamplingState( - samplingPriority, - samplingMechanism, - nextOtelTraceState, - nextKnuthSamplingRate, - canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE); - return true; } @Override - public synchronized void forceKeep(int samplingMechanism) { - OtelTraceState nextOtelTraceState = getOtelTraceState(); - if (nextOtelTraceState != null) { - nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); + public void forceKeep(int samplingMechanism) { + synchronized (samplingStateLock) { + OtelTraceState nextOtelTraceState = getOtelTraceState(); + if (nextOtelTraceState != null) { + nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); + } + installSamplingState(PrioritySampling.USER_KEEP, samplingMechanism, nextOtelTraceState); } - installSamplingState(PrioritySampling.USER_KEEP, samplingMechanism, nextOtelTraceState); } private void installSamplingState( @@ -414,20 +420,22 @@ public String getDebugPropagation() { } @Override - public synchronized void updateKnuthSamplingRate(double rate) { - TagValue current = getKnuthSamplingRateTagValue(); - TagValue next = knuthSamplingRateTagValue(rate); - if (!Objects.equals(current, next)) { - clearCachedHeader(DATADOG); - clearCachedHeader(W3C); - SamplingState currentState = samplingState; - samplingState = - newSamplingState( - currentState.getSamplingPriority(), - tracestate, - getOtelTraceState(), - getDecisionMakerTagValue(currentState), - next); + public void updateKnuthSamplingRate(double rate) { + synchronized (samplingStateLock) { + TagValue current = getKnuthSamplingRateTagValue(); + TagValue next = knuthSamplingRateTagValue(rate); + if (!Objects.equals(current, next)) { + clearCachedHeader(DATADOG); + clearCachedHeader(W3C); + SamplingState currentState = samplingState; + samplingState = + newSamplingState( + currentState.getSamplingPriority(), + tracestate, + getOtelTraceState(), + getDecisionMakerTagValue(currentState), + next); + } } } @@ -761,22 +769,24 @@ public void updateW3CTracestateFrom(PropagationTags source) { : W3CPTagsCodec.extractOtelTraceState(sourceState.getTracestate())); } - private synchronized void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) { - clearCachedHeader(W3C); - int samplingPriority = samplingState.getSamplingPriority(); - if (otelTraceState != null - && samplingPriority != PrioritySampling.UNSET - && !otelTraceState.isConsistentWith(samplingPriority > 0)) { - otelTraceState = otelTraceState.withoutThreshold(); + private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) { + synchronized (samplingStateLock) { + clearCachedHeader(W3C); + int samplingPriority = samplingState.getSamplingPriority(); + if (otelTraceState != null + && samplingPriority != PrioritySampling.UNSET + && !otelTraceState.isConsistentWith(samplingPriority > 0)) { + otelTraceState = otelTraceState.withoutThreshold(); + } + this.tracestate = tracestate; + this.samplingState = + newSamplingState( + samplingPriority, + tracestate, + otelTraceState, + getDecisionMakerTagValue(), + getKnuthSamplingRateTagValue()); } - this.tracestate = tracestate; - this.samplingState = - newSamplingState( - samplingPriority, - tracestate, - otelTraceState, - getDecisionMakerTagValue(), - getKnuthSamplingRateTagValue()); } private OtelTraceState getOtelTraceState() { @@ -802,22 +812,24 @@ String getError() { } @Override - public synchronized void updateAndLockDecisionMaker(PropagationTags source) { - if (source instanceof PTags) { - canChangeDecisionMaker = false; - TagValue decisionMakerTagValue = ((PTags) source).getDecisionMakerTagValue(); - if (decisionMakerTagValue != null) { - clearCachedHeader(DATADOG); - clearCachedHeader(W3C); + public void updateAndLockDecisionMaker(PropagationTags source) { + synchronized (samplingStateLock) { + if (source instanceof PTags) { + canChangeDecisionMaker = false; + TagValue decisionMakerTagValue = ((PTags) source).getDecisionMakerTagValue(); + if (decisionMakerTagValue != null) { + clearCachedHeader(DATADOG); + clearCachedHeader(W3C); + } + SamplingState currentState = samplingState; + samplingState = + newSamplingState( + currentState.getSamplingPriority(), + tracestate, + getOtelTraceState(), + decisionMakerTagValue, + getKnuthSamplingRateTagValue(currentState)); } - SamplingState currentState = samplingState; - samplingState = - newSamplingState( - currentState.getSamplingPriority(), - tracestate, - getOtelTraceState(), - decisionMakerTagValue, - getKnuthSamplingRateTagValue(currentState)); } } } From 10af99959e28d88fc7794928d75a2f32c583d7db Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Wed, 16 Sep 2026 11:25:18 +0200 Subject: [PATCH 13/22] test(otlp): normalize inherited tracestate assertions --- .../trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java | 2 +- .../java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index acdea5b89c2..6879cc976ef 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -191,7 +191,7 @@ void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { assertEquals(SAMPLED_TRACE_FLAG, ((Number) localFallback.get("flags")).intValue()); Map inherited = exportSamplingSpan(inheritedSamplingSpan()); - assertEquals("dd=s:1,vendor=state,ot=rv:ef284ace7a91e1;th:8", inherited.get("traceState")); + assertEquals("dd=s:1,ot=rv:ef284ace7a91e1;th:8,vendor=state", inherited.get("traceState")); assertEquals(SAMPLED_TRACE_FLAG, ((Number) inherited.get("flags")).intValue()); Map probabilityDrop = exportSamplingSpan(localProbabilitySpan(0.0, false)); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java index 9fbb6a3c3fe..3bb6e7d6eff 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java @@ -640,7 +640,7 @@ void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { assertEquals(SAMPLED_TRACE_FLAG, localFallback.flags); EncodedSamplingState inherited = exportSamplingState(inheritedSamplingSpan()); - assertEquals("dd=s:1,vendor=state,ot=rv:ef284ace7a91e1;th:8", inherited.traceState); + assertEquals("dd=s:1,ot=rv:ef284ace7a91e1;th:8,vendor=state", inherited.traceState); assertEquals(SAMPLED_TRACE_FLAG, inherited.flags); EncodedSamplingState probabilityDrop = exportSamplingState(localProbabilitySpan(0.0, false)); From efcf28a9a88e319ca66699dd1e6a57621e22557a Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Wed, 16 Sep 2026 14:12:27 +0200 Subject: [PATCH 14/22] fix(sampling): serialize rate only on root spans --- .../src/main/java/datadog/trace/core/DDSpanContext.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 e5b7552b2ef..6359e8ece2d 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 @@ -1266,7 +1266,9 @@ public TagMap getTags() { // maintain previously observable type of the thread name :| tags.put(DDTags.THREAD_NAME, threadName.toString()); int currentSamplingPriority = getSamplingPriority(); - if (currentSamplingPriority != PrioritySampling.UNSET) { + // add _sample_rate tag only on the root/local span owning the decision + if (getRootSpanContextIfDifferent() == null + && currentSamplingPriority != PrioritySampling.UNSET) { tags.put(SAMPLE_RATE_KEY, currentSamplingPriority); } if (httpStatusCode != 0) { From af7e3077f8e58829a5fa95d1b3f1bf86fe14ebe5 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Tue, 22 Sep 2026 11:07:19 +0200 Subject: [PATCH 15/22] pass the override info in the samplingMechanism instead of passing the sampling result --- .../sampling/RateByServiceTraceSampler.java | 1 - .../sampling/RuleBasedTraceSampler.java | 5 +---- .../java/datadog/trace/core/CoreSpan.java | 6 +----- .../main/java/datadog/trace/core/DDSpan.java | 12 ++--------- .../datadog/trace/core/DDSpanContext.java | 2 -- .../core/propagation/PropagationTags.java | 1 - .../propagation/ptags/OtelTraceState.java | 6 +++--- .../core/propagation/ptags/PTagsFactory.java | 10 ++++----- .../trace/common/metrics/SimpleSpan.java | 6 +----- .../trace/common/writer/TraceGenerator.java | 6 +----- .../trace/core/CoreSpanBuilderTest.java | 4 ++-- .../trace/core/OtelSamplingDecisionTest.java | 2 +- .../trace/OtlpTraceJsonCollectorTest.java | 4 +--- .../core/otlp/trace/OtlpTraceProtoTest.java | 4 +--- .../OtelTraceStatePropagationTest.java | 21 ++++++++----------- .../core/propagation/W3CHttpInjectorTest.java | 5 ++--- .../propagation/opg/OrgGuardEnforcerTest.java | 2 +- .../propagation/ptags/OtelTraceStateTest.java | 17 +++++++++------ .../traceAgentTest/java/TraceGenerator.java | 6 +----- .../trace/api/sampling/SamplingMechanism.java | 21 ++++++++++++++++++- 20 files changed, 63 insertions(+), 78 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 ab24b2200b0..0401da982ba 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 @@ -65,7 +65,6 @@ public > void setSamplingPriority(final T span) { samplingPriority, SAMPLING_AGENT_RATE, sampler.getSampleRate(), - sampled, SamplingMechanism.AGENT_RATE); } 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 c66159607b1..ec44eec63e6 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 @@ -153,15 +153,13 @@ public > void setSamplingPriority(final T span) { PrioritySampling.USER_KEEP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), - true, matchedRule.getMechanism()); } else { span.setSamplingPriority( PrioritySampling.USER_DROP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), - true, - matchedRule.getMechanism()); + SamplingMechanism.markRateLimiterRejected(matchedRule.getMechanism())); } span.setMetric(SAMPLING_LIMIT_RATE, rateLimit); } else { @@ -169,7 +167,6 @@ public > void setSamplingPriority(final T span) { PrioritySampling.USER_DROP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), - false, matchedRule.getMechanism()); } } 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 f7b5bcb7c3a..b2ab55c8e25 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 @@ -122,11 +122,7 @@ default void processTagsAndBaggageWithStructuredLinks( T setSamplingPriority(int samplingPriority, int samplingMechanism); T setSamplingPriority( - int samplingPriority, - CharSequence rate, - double sampleRate, - boolean probabilitySamplingResult, - int samplingMechanism); + int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism); 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 fa94775f214..e9873f147fe 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,17 +647,9 @@ public final DDSpan setSamplingPriority(final int newPriority, int samplingMecha @Override public DDSpan setSamplingPriority( - int samplingPriority, - CharSequence rate, - double sampleRate, - boolean probabilitySamplingResult, - int samplingMechanism) { + int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { if (context.setSamplingPriority( - samplingPriority, - samplingMechanism, - sampleRate, - probabilitySamplingResult, - getTraceId().toLong())) { + samplingPriority, samplingMechanism, sampleRate, getTraceId().toLong())) { setMetric(rate, sampleRate); } return this; 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 6359e8ece2d..65d70323fd6 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 @@ -722,7 +722,6 @@ public boolean setSamplingPriority( final int newPriority, final int newMechanism, final double sampleRate, - final boolean probabilitySamplingResult, final long traceIdLowOrderBits) { DDSpanContext spanContext = getRootSpanContextOrThis(); if (!spanContext.validateSamplingPriority(newPriority, newMechanism)) { @@ -732,7 +731,6 @@ public boolean setSamplingPriority( newPriority, newMechanism, sampleRate, - probabilitySamplingResult, traceIdLowOrderBits, SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism)); } 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 e6f406c4f0e..6d0ae49e4dd 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 @@ -113,7 +113,6 @@ public abstract boolean tryUpdateProbabilitySamplingDecision( int samplingPriority, int samplingMechanism, double sampleRate, - boolean probabilitySamplingResult, long traceIdLowOrderBits, boolean allowOverride); 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 b7fc01fdbb1..4125dbc8d53 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 @@ -165,16 +165,16 @@ private static OtelTraceState parseCanonical(CharSequence value, int originalSiz } static OtelTraceState fromProbabilityDecision( - long traceIdLowOrderBits, double rate, boolean sampled) { + long traceIdLowOrderBits, double rate, int samplingPriority) { long hash = traceIdLowOrderBits * HASH_MULTIPLIER; long randomValue = (~hash) >>> 8; long threshold = Math.round((1.0 - rate) * TWO_TO_56); if (threshold > MAX_56_BIT_VALUE) { threshold = MAX_56_BIT_VALUE; } - if (sampled && randomValue < threshold) { + if (samplingPriority > 0 && randomValue < threshold) { randomValue = threshold; - } else if (!sampled && randomValue >= threshold) { + } else if (samplingPriority <= 0 && randomValue >= threshold) { randomValue = threshold == 0 ? 0 : threshold - 1; } 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 c1758c60dce..5261806dbc0 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 @@ -269,7 +269,6 @@ public boolean tryUpdateProbabilitySamplingDecision( int samplingPriority, int samplingMechanism, double sampleRate, - boolean probabilitySamplingResult, long traceIdLowOrderBits, boolean allowOverride) { synchronized (samplingStateLock) { @@ -277,15 +276,16 @@ public boolean tryUpdateProbabilitySamplingDecision( if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { return false; } + boolean rateLimiterRejected = SamplingMechanism.isRateLimiterRejected(samplingMechanism); + samplingMechanism = SamplingMechanism.clearRateLimiterRejected(samplingMechanism); OtelTraceState nextOtelTraceState = getOtelTraceState(); if (nextOtelTraceState == null) { - boolean limiterDemotion = probabilitySamplingResult && samplingPriority <= 0; - if (!limiterDemotion) { + if (!rateLimiterRejected) { nextOtelTraceState = OtelTraceState.fromProbabilityDecision( - traceIdLowOrderBits, sampleRate, probabilitySamplingResult); + traceIdLowOrderBits, sampleRate, samplingPriority); } - } else if (probabilitySamplingResult && samplingPriority <= 0) { + } else if (rateLimiterRejected) { nextOtelTraceState = nextOtelTraceState.withoutThreshold(); } TagValue nextKnuthSamplingRate = knuthSamplingRateTagValue(sampleRate); 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 d6463298000..41a2a5a0d14 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 @@ -333,11 +333,7 @@ public SimpleSpan setSamplingPriority(int samplingPriority, int samplingMechanis @Override public SimpleSpan setSamplingPriority( - int samplingPriority, - CharSequence rate, - double sampleRate, - boolean probabilitySamplingResult, - int samplingMechanism) { + int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { 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 3a476c48719..e777235fef3 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 @@ -409,11 +409,7 @@ public PojoSpan setSamplingPriority(int samplingPriority, int samplingMechanism) @Override public PojoSpan setSamplingPriority( - int samplingPriority, - CharSequence rate, - double sampleRate, - boolean probabilitySamplingResult, - int samplingMechanism) { + int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { 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 547eeff9531..2825910b901 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 @@ -408,7 +408,7 @@ void buildContextFromExtractedContextWithRestartBehavior() { span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); assertTrue(initialTraceState == null || !initialTraceState.contains(OTEL_MEMBER)); - span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, true, LOCAL_USER_RULE); + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, LOCAL_USER_RULE); String freshTraceState = span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); @@ -442,7 +442,7 @@ void buildContextFromExtractedContextWithIgnoreBehavior() { span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); assertTrue(initialTraceState == null || !initialTraceState.contains(OTEL_MEMBER)); - span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, true, LOCAL_USER_RULE); + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, LOCAL_USER_RULE); String freshTraceState = span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); 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 12e489f21a6..84662abd620 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 @@ -121,7 +121,7 @@ void limiterRejectionDoesNotFabricateProbabilityState() { void manualOverrideRemovesLocallyGeneratedProbabilityState() { withRootSpan( span -> { - span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, HALF_RATE, true, LOCAL_USER_RULE); + span.setSamplingPriority(USER_KEEP, SAMPLING_RULE_RATE, HALF_RATE, LOCAL_USER_RULE); assertTrue(w3cHeader(span).contains(OTEL_RANDOM_VALUE_PREFIX)); span.spanContext().forceKeep(); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index 6879cc976ef..2aa3e62f6c4 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -208,9 +208,8 @@ void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { .getPropagationTags() .tryUpdateProbabilitySamplingDecision( PrioritySampling.SAMPLER_DROP, - SamplingMechanism.AGENT_RATE, + SamplingMechanism.markRateLimiterRejected(SamplingMechanism.AGENT_RATE), 1.0, - true, limiterDrop.getTraceId().toLong(), true); Map limiter = exportSamplingSpan(limiterDrop); @@ -340,7 +339,6 @@ private static DDSpan localProbabilitySpan(double rate, boolean sampled) { sampled ? PrioritySampling.SAMPLER_KEEP : PrioritySampling.SAMPLER_DROP, SamplingMechanism.AGENT_RATE, rate, - sampled, span.getTraceId().toLong(), true); return span; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java index 3bb6e7d6eff..a88aed60af5 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java @@ -653,9 +653,8 @@ void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { .getPropagationTags() .tryUpdateProbabilitySamplingDecision( PrioritySampling.SAMPLER_DROP, - SamplingMechanism.AGENT_RATE, + SamplingMechanism.markRateLimiterRejected(SamplingMechanism.AGENT_RATE), 1.0, - true, limiterDrop.getTraceId().toLong(), true); EncodedSamplingState limiter = exportSamplingState(limiterDrop); @@ -766,7 +765,6 @@ private static DDSpan localProbabilitySpan(double rate, boolean sampled) { sampled ? PrioritySampling.SAMPLER_KEEP : PrioritySampling.SAMPLER_DROP, SamplingMechanism.AGENT_RATE, rate, - sampled, span.getTraceId().toLong(), true); return span; 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 d74ab5f2746..d59499acaa1 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 @@ -6,6 +6,7 @@ import static datadog.trace.api.sampling.SamplingMechanism.AGENT_RATE; import static datadog.trace.api.sampling.SamplingMechanism.EXTERNAL_OVERRIDE; import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; +import static datadog.trace.api.sampling.SamplingMechanism.markRateLimiterRejected; import static datadog.trace.core.propagation.PropagationTags.HeaderType.DATADOG; import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -71,8 +72,7 @@ void publishesProbabilityPriorityAndOtelStateTogether() { PropagationTags tags = PropagationTags.factory().empty(); SamplingState before = tags.samplingState(); - assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, true, 1L, false)); + assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, false)); SamplingState after = tags.samplingState(); assertEquals(SAMPLER_KEEP, after.getSamplingPriority()); @@ -88,12 +88,11 @@ void publishesProbabilityPriorityAndOtelStateTogether() { @Test void rejectedSamplingAttemptCannotReplaceProbabilityState() { PropagationTags tags = PropagationTags.factory().empty(); - assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, true, 1L, false)); + assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, 1L, false)); SamplingState established = tags.samplingState(); assertFalse( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.1, false, 2L, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.1, 2L, false)); assertEquals(established, tags.samplingState()); } @@ -115,8 +114,7 @@ void atomicProbabilityUpdatePreservesLockedDecisionMaker() { PropagationTags tags = PropagationTags.factory().fromHeaderValue(DATADOG, "_dd.p.dm=934086a686-4"); - assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.5, false, 1L, false)); + assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.5, 1L, false)); SamplingState state = tags.samplingState(); assertEquals(SAMPLER_DROP, state.getSamplingPriority()); @@ -159,8 +157,7 @@ void compoundConflictRemovesThresholdAndRetainsRandomValue() { @Test void forceKeepRemovesLocallyGeneratedProbabilityState() { PropagationTags tags = PropagationTags.factory().empty(); - assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.0, false, 1L, false)); + assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.0, 1L, false)); tags.forceKeep(MANUAL); @@ -173,7 +170,8 @@ void limiterDemotionDoesNotFabricateState() { PropagationTags tags = PropagationTags.factory().empty(); assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 1.0, true, 1L, false)); + tags.tryUpdateProbabilitySamplingDecision( + SAMPLER_DROP, markRateLimiterRejected(AGENT_RATE), 1.0, 1L, false)); assertNull(tags.samplingState().getOtelTraceState()); } @@ -186,8 +184,7 @@ void generatedManagedMembersDisplaceRightmostVendorAtMemberLimit() { } PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, original.toString()); - assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, true, 1L, false)); + assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, 1L, false)); String header = tags.headerValue(W3C); assertEquals(32, header.split(",").length); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java index 9af53d7c4c3..f890c2eb1c8 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java @@ -135,8 +135,7 @@ void injectTheDecisionMakerTag() { @Test void injectUsesSingleSamplingStateAcrossHeaders() throws InterruptedException { PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, "ot=rv:ef284ace7a91e1"); - assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, true, 1L, true)); + assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, true)); DDSpanContext context = mockSpanContext( DDTraceId.from("1"), DDSpanId.from("2"), SAMPLER_KEEP, null, new HashMap<>(), tags); @@ -151,7 +150,7 @@ void injectUsesSingleSamplingStateAcrossHeaders() throws InterruptedException { assertTrue(traceparentWritten.await(5, TimeUnit.SECONDS)); assertTrue( tags.tryUpdateProbabilitySamplingDecision( - SAMPLER_DROP, AGENT_RATE, 0.0, false, 1L, true)); + SAMPLER_DROP, AGENT_RATE, 0.0, 1L, true)); } catch (Throwable throwable) { failure.set(throwable); } finally { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java index 4eb636264d1..4df5c106ece 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java @@ -173,7 +173,7 @@ void stripReplacesOtelSamplingStateAfterLocalResampling() { assertTrue( stripped .getPropagationTags() - .tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, true, 1L, false)); + .tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, false)); String reEncoded = stripped.getPropagationTags().headerValue(W3C); assertNotNull(reEncoded); 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 0acd0ff21d3..127feaca2f5 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 @@ -1,5 +1,7 @@ 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.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -15,7 +17,8 @@ class OtelTraceStateTest { @Test void convertsDatadogProbabilityDecision() { - OtelTraceState state = OtelTraceState.fromProbabilityDecision(0xfff972474538efffL, 0.1, true); + OtelTraceState state = + OtelTraceState.fromProbabilityDecision(0xfff972474538efffL, 0.1, SAMPLER_KEEP); assertFalse(state.isMaterialized()); assertEquals("rv:ef284ace7a91e1;th:e6666666666668", state.toString()); @@ -35,7 +38,7 @@ void serializesThresholds() { @Test void rateZeroUsesLargestWireThresholdAndRemainsDropConsistent() { - OtelTraceState state = OtelTraceState.fromProbabilityDecision(0L, 0.0, false); + OtelTraceState state = OtelTraceState.fromProbabilityDecision(0L, 0.0, SAMPLER_DROP); assertEquals("rv:fffffffffffffe;th:ffffffffffffff", state.toString()); assertTrue(state.isConsistentWith(false)); @@ -43,7 +46,8 @@ void rateZeroUsesLargestWireThresholdAndRemainsDropConsistent() { @Test void correctsOnlySerializedRandomValueAtKeepBoundary() { - OtelTraceState state = OtelTraceState.fromProbabilityDecision(0x03a93ee8b1999f00L, 0.1, true); + OtelTraceState state = + OtelTraceState.fromProbabilityDecision(0x03a93ee8b1999f00L, 0.1, SAMPLER_KEEP); assertEquals("rv:e6666666666668;th:e6666666666668", state.toString()); assertTrue(state.isConsistentWith(true)); @@ -53,7 +57,7 @@ void correctsOnlySerializedRandomValueAtKeepBoundary() { void correctsOnlySerializedRandomValueAtDropBoundary() { OtelTraceState state = OtelTraceState.fromProbabilityDecision( - DROP_PRECISION_BOUNDARY_TRACE_ID, DROP_PRECISION_BOUNDARY_RATE, false); + DROP_PRECISION_BOUNDARY_TRACE_ID, DROP_PRECISION_BOUNDARY_RATE, SAMPLER_DROP); assertEquals( "rv:" + DROP_PRECISION_BOUNDARY_RANDOM_VALUE + ";th:" + DROP_PRECISION_BOUNDARY_THRESHOLD, @@ -63,7 +67,7 @@ void correctsOnlySerializedRandomValueAtDropBoundary() { @Test void removesLocalRandomnessForNonProbabilityDecision() { - OtelTraceState state = OtelTraceState.fromProbabilityDecision(1L, 1.0, true); + OtelTraceState state = OtelTraceState.fromProbabilityDecision(1L, 1.0, SAMPLER_KEEP); assertNull(state.forNonProbabilityDecision()); } @@ -79,7 +83,8 @@ void retainsInheritedRandomnessAndUnknownFieldsWithoutThreshold() { } private static void assertThreshold(double rate, String expectedThreshold) { - OtelTraceState state = OtelTraceState.fromProbabilityDecision(1L, rate, rate > 0.0); + int samplingPriority = rate > 0.0 ? SAMPLER_KEEP : SAMPLER_DROP; + OtelTraceState state = OtelTraceState.fromProbabilityDecision(1L, rate, samplingPriority); String value = state.toString(); assertEquals(expectedThreshold, value.substring(value.indexOf(";th:") + 4)); } diff --git a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java index d3fc463cbf3..1e1a9c58fe3 100644 --- a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java +++ b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java @@ -359,11 +359,7 @@ public PojoSpan setSamplingPriority(int samplingPriority, int samplingMechanism) @Override public PojoSpan setSamplingPriority( - int samplingPriority, - CharSequence rate, - double sampleRate, - boolean probabilitySamplingResult, - int samplingMechanism) { + int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { return this; } diff --git a/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java b/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java index 6b0cd397626..818a23808a7 100644 --- a/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java +++ b/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java @@ -8,6 +8,12 @@ import datadog.trace.api.Config; public class SamplingMechanism { + /** + * Internal flag to combine with a positive value to indicate the base result was overriden by + * rate limiter + */ + private static final int RATE_LIMITER_REJECTED = 1 << 30; + /** Not encouraged to use */ public static final byte UNKNOWN = -1; @@ -45,8 +51,20 @@ public class SamplingMechanism { /** Force override sampling decision from external source, like W3C traceparent. */ public static final byte EXTERNAL_OVERRIDE = Byte.MIN_VALUE; + public static int markRateLimiterRejected(int mechanism) { + return mechanism | RATE_LIMITER_REJECTED; + } + + public static boolean isRateLimiterRejected(int mechanism) { + return (mechanism & RATE_LIMITER_REJECTED) != 0; + } + + public static int clearRateLimiterRejected(int mechanism) { + return mechanism & ~RATE_LIMITER_REJECTED; + } + public static boolean validateWithSamplingPriority(int mechanism, int priority) { - switch (mechanism) { + switch (clearRateLimiterRejected(mechanism)) { case UNKNOWN: return true; @@ -82,6 +100,7 @@ public static boolean validateWithSamplingPriority(int mechanism, int priority) * @return {@code true} if the sampling priority lock can be avoided, {@code false} otherwise */ public static boolean canAvoidSamplingPriorityLock(int priority, int mechanism) { + mechanism = clearRateLimiterRejected(mechanism); return (!Config.get().isApmTracingEnabled() && mechanism == SamplingMechanism.APPSEC) || (Config.get().isDataJobsEnabled() && mechanism == DATA_JOBS); } From 88f374c7b757fe23240e117fce5d60c80ef25848 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Tue, 22 Sep 2026 11:18:50 +0200 Subject: [PATCH 16/22] refactor(propagation): remove redundant codec overrides --- .../propagation/ptags/DatadogPTagsCodec.java | 22 ------------------- .../core/propagation/ptags/PTagsCodec.java | 17 +++++++++----- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java index f8e745ec10c..3be0d091672 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java @@ -3,8 +3,6 @@ import datadog.logging.RatelimitedLogger; import datadog.trace.api.ProductTraceSource; import datadog.trace.core.propagation.PropagationTags; -import datadog.trace.core.propagation.PropagationTags.SamplingState; -import datadog.trace.core.propagation.ptags.PTagsFactory.PTags; import datadog.trace.core.propagation.ptags.TagElement.Encoding; import java.util.ArrayList; import java.util.List; @@ -123,21 +121,6 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { orgPropagationMarkerTagValue); } - @Override - protected int estimateHeaderSize( - PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState) { - return pTags.getXDatadogTagsSize(samplingState); - } - - @Override - protected int appendPrefix( - StringBuilder sb, - PTags ptags, - CharSequence lastParentIdOverride, - SamplingState samplingState) { - return ptags.getXDatadogTagsSize(samplingState); - } - @Override protected int appendTag(StringBuilder sb, TagElement key, TagElement value, int size) { if (size <= xDatadogTagsLimit) { @@ -151,11 +134,6 @@ protected int appendTag(StringBuilder sb, TagElement key, TagElement value, int return size; } - @Override - protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingState samplingState) { - return size; - } - @Override protected boolean isTooLarge(StringBuilder sb, int size) { return size > xDatadogTagsLimit; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java index f09e0f33171..57e713cadef 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java @@ -179,19 +179,24 @@ static int calcXDatadogTagsSize(int size, TagKey tagKey, TagValue tagValue) { abstract PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value); - protected abstract int estimateHeaderSize( - PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState); + protected int estimateHeaderSize( + PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState) { + return pTags.getXDatadogTagsSize(samplingState); + } - protected abstract int appendPrefix( + protected int appendPrefix( StringBuilder sb, PTags ptags, CharSequence lastParentIdOverride, - SamplingState samplingState); + SamplingState samplingState) { + return ptags.getXDatadogTagsSize(samplingState); + } protected abstract int appendTag(StringBuilder sb, TagElement key, TagElement value, int size); - protected abstract int appendSuffix( - StringBuilder sb, PTags ptags, int size, SamplingState samplingState); + protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingState samplingState) { + return size; + } protected abstract boolean isTooLarge(StringBuilder sb, int size); From 2fe4d46acc27d4c69e33f22a130e4f5ee5b91ed5 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Tue, 22 Sep 2026 13:41:56 +0200 Subject: [PATCH 17/22] fix(sampling): preserve negative mechanisms --- .../datadog/trace/api/sampling/SamplingMechanism.java | 8 +++++++- .../trace/api/sampling/SamplingMechanismTest.groovy | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java b/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java index 818a23808a7..8473163255c 100644 --- a/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java +++ b/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java @@ -52,14 +52,20 @@ public class SamplingMechanism { public static final byte EXTERNAL_OVERRIDE = Byte.MIN_VALUE; public static int markRateLimiterRejected(int mechanism) { + if (mechanism < 0) { + return mechanism; + } return mechanism | RATE_LIMITER_REJECTED; } public static boolean isRateLimiterRejected(int mechanism) { - return (mechanism & RATE_LIMITER_REJECTED) != 0; + return mechanism >= 0 && (mechanism & RATE_LIMITER_REJECTED) != 0; } public static int clearRateLimiterRejected(int mechanism) { + if (mechanism < 0) { + return mechanism; + } return mechanism & ~RATE_LIMITER_REJECTED; } diff --git a/internal-api/src/test/groovy/datadog/trace/api/sampling/SamplingMechanismTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/sampling/SamplingMechanismTest.groovy index 4a4890435c1..c9bf4564149 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/sampling/SamplingMechanismTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/sampling/SamplingMechanismTest.groovy @@ -117,4 +117,11 @@ class SamplingMechanismTest extends DDSpecification { DATA_JOBS | SAMPLER_KEEP | false EXTERNAL_OVERRIDE | SAMPLER_KEEP | false } + + void 'rate limiter rejection marker ignores negative mechanisms'() { + expect: + markRateLimiterRejected(EXTERNAL_OVERRIDE) == EXTERNAL_OVERRIDE + !isRateLimiterRejected(EXTERNAL_OVERRIDE) + clearRateLimiterRejected(EXTERNAL_OVERRIDE) == EXTERNAL_OVERRIDE + } } From 3728627a35f26962f9389ebaaee38f213345f713 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 23 Sep 2026 23:21:21 +0100 Subject: [PATCH 18/22] Small simplifications/re-use --- .../propagation/ptags/OtelTraceState.java | 3 +-- .../core/propagation/ptags/PTagsFactory.java | 26 +++++-------------- 2 files changed, 7 insertions(+), 22 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 4125dbc8d53..8b8b4e670f3 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 @@ -411,8 +411,7 @@ private static boolean isLowerHex( return false; } for (int i = start; i < end; i++) { - char c = value.charAt(i); - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { + if (!PTagsCodec.isHexDigit(value.charAt(i))) { return false; } } 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 5261806dbc0..60d50cd87e2 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 @@ -355,7 +355,7 @@ private void installSamplingState( nextDecisionMakerTagValue = null; } samplingState = - newSamplingState( + new SamplingState( samplingPriority, tracestate, nextOtelTraceState, @@ -363,26 +363,12 @@ private void installSamplingState( nextKnuthSamplingRateTagValue); } - private static SamplingState newSamplingState( - int samplingPriority, - String tracestate, - OtelTraceState otelTraceState, - TagValue decisionMakerTagValue, - TagValue knuthSamplingRateTagValue) { - return new SamplingState( - samplingPriority, - tracestate, - otelTraceState, - decisionMakerTagValue, - knuthSamplingRateTagValue); - } - private static SamplingState initialSamplingState( int samplingPriority, TagValue decisionMakerTagValue) { if (samplingPriority == PrioritySampling.UNSET && decisionMakerTagValue == null) { return EMPTY_SAMPLING_STATE; } - return newSamplingState(samplingPriority, null, null, decisionMakerTagValue, null); + return new SamplingState(samplingPriority, null, null, decisionMakerTagValue, null); } @Override @@ -429,7 +415,7 @@ public void updateKnuthSamplingRate(double rate) { clearCachedHeader(W3C); SamplingState currentState = samplingState; samplingState = - newSamplingState( + new SamplingState( currentState.getSamplingPriority(), tracestate, getOtelTraceState(), @@ -780,7 +766,7 @@ private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) } this.tracestate = tracestate; this.samplingState = - newSamplingState( + new SamplingState( samplingPriority, tracestate, otelTraceState, @@ -799,7 +785,7 @@ void setOtelTraceState(OtelTraceState otelTraceState) { } SamplingState currentState = samplingState; this.samplingState = - newSamplingState( + new SamplingState( currentState.getSamplingPriority(), tracestate, otelTraceState, @@ -823,7 +809,7 @@ public void updateAndLockDecisionMaker(PropagationTags source) { } SamplingState currentState = samplingState; samplingState = - newSamplingState( + new SamplingState( currentState.getSamplingPriority(), tracestate, getOtelTraceState(), From cbf8c321ac2e9cbbac12ead306ae4cb0933f9e11 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 23 Sep 2026 23:55:12 +0100 Subject: [PATCH 19/22] Move RATE_LIMITER_REJECTED back to being a boolean parameter, but use default method + delegation to minimize impact --- .../sampling/RuleBasedTraceSampler.java | 6 ++--- .../java/datadog/trace/core/CoreSpan.java | 9 +++++++ .../main/java/datadog/trace/core/DDSpan.java | 16 ++++++++++- .../datadog/trace/core/DDSpanContext.java | 6 +++-- .../core/propagation/PropagationTags.java | 3 ++- .../core/propagation/ptags/PTagsFactory.java | 5 ++-- .../trace/OtlpTraceJsonCollectorTest.java | 6 +++-- .../core/otlp/trace/OtlpTraceProtoTest.java | 6 +++-- .../OtelTraceStatePropagationTest.java | 21 ++++++++------- .../core/propagation/W3CHttpInjectorTest.java | 5 ++-- .../propagation/opg/OrgGuardEnforcerTest.java | 2 +- .../trace/api/sampling/SamplingMechanism.java | 27 +------------------ .../api/sampling/SamplingMechanismTest.groovy | 9 +------ 13 files changed, 61 insertions(+), 60 deletions(-) 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 ec44eec63e6..e419432589d 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,8 +146,7 @@ public > void setSamplingPriority(final T span) { if (matchedRule == null) { fallbackSampler.setSamplingPriority(span); } else { - boolean sampled = matchedRule.sample(span); - if (sampled) { + if (matchedRule.sample(span)) { if (rateLimiter.tryAcquire()) { span.setSamplingPriority( PrioritySampling.USER_KEEP, @@ -159,7 +158,8 @@ public > void setSamplingPriority(final T span) { PrioritySampling.USER_DROP, SAMPLING_RULE_RATE, matchedRule.getSampler().getSampleRate(), - SamplingMechanism.markRateLimiterRejected(matchedRule.getMechanism())); + matchedRule.getMechanism(), + true); } span.setMetric(SAMPLING_LIMIT_RATE, rateLimit); } else { 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..51365a1fed2 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,15 @@ default void processTagsAndBaggageWithStructuredLinks( T setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism); + default T setSamplingPriority( + int samplingPriority, + CharSequence rate, + double sampleRate, + int samplingMechanism, + boolean rateLimiterRejected) { + return setSamplingPriority(samplingPriority, rate, sampleRate, samplingMechanism); + } + 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 e9873f147fe..b0cc812aadc 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 @@ -648,8 +648,22 @@ public final DDSpan setSamplingPriority(final int newPriority, int samplingMecha @Override public DDSpan setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { + return setSamplingPriority(samplingPriority, rate, sampleRate, samplingMechanism, false); + } + + @Override + public DDSpan setSamplingPriority( + int samplingPriority, + CharSequence rate, + double sampleRate, + int samplingMechanism, + boolean rateLimiterRejected) { if (context.setSamplingPriority( - samplingPriority, samplingMechanism, sampleRate, getTraceId().toLong())) { + samplingPriority, + samplingMechanism, + sampleRate, + getTraceId().toLong(), + rateLimiterRejected)) { setMetric(rate, sampleRate); } return this; 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 65d70323fd6..65c86596726 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 @@ -722,7 +722,8 @@ public boolean setSamplingPriority( final int newPriority, final int newMechanism, final double sampleRate, - final long traceIdLowOrderBits) { + final long traceIdLowOrderBits, + final boolean rateLimiterRejected) { DDSpanContext spanContext = getRootSpanContextOrThis(); if (!spanContext.validateSamplingPriority(newPriority, newMechanism)) { return false; @@ -732,7 +733,8 @@ public boolean setSamplingPriority( newMechanism, sampleRate, traceIdLowOrderBits, - SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism)); + SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism), + rateLimiterRejected); } private boolean validateSamplingPriority(final int newPriority, final int newMechanism) { 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 6d0ae49e4dd..8ff442fa336 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 @@ -114,7 +114,8 @@ public abstract boolean tryUpdateProbabilitySamplingDecision( int samplingMechanism, double sampleRate, long traceIdLowOrderBits, - boolean allowOverride); + boolean allowOverride, + boolean rateLimiterRejected); public abstract void forceKeep(int samplingMechanism); 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 60d50cd87e2..a85c6e324ce 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 @@ -270,14 +270,13 @@ public boolean tryUpdateProbabilitySamplingDecision( int samplingMechanism, double sampleRate, long traceIdLowOrderBits, - boolean allowOverride) { + boolean allowOverride, + boolean rateLimiterRejected) { synchronized (samplingStateLock) { SamplingState current = samplingState; if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { return false; } - boolean rateLimiterRejected = SamplingMechanism.isRateLimiterRejected(samplingMechanism); - samplingMechanism = SamplingMechanism.clearRateLimiterRejected(samplingMechanism); OtelTraceState nextOtelTraceState = getOtelTraceState(); if (nextOtelTraceState == null) { if (!rateLimiterRejected) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index 2aa3e62f6c4..1263757fe74 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -208,9 +208,10 @@ void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { .getPropagationTags() .tryUpdateProbabilitySamplingDecision( PrioritySampling.SAMPLER_DROP, - SamplingMechanism.markRateLimiterRejected(SamplingMechanism.AGENT_RATE), + SamplingMechanism.AGENT_RATE, 1.0, limiterDrop.getTraceId().toLong(), + true, true); Map limiter = exportSamplingSpan(limiterDrop); assertNull(limiter.get("traceState")); @@ -340,7 +341,8 @@ private static DDSpan localProbabilitySpan(double rate, boolean sampled) { SamplingMechanism.AGENT_RATE, rate, span.getTraceId().toLong(), - true); + true, + false); return span; } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java index a88aed60af5..4dda625c9ee 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java @@ -653,9 +653,10 @@ void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { .getPropagationTags() .tryUpdateProbabilitySamplingDecision( PrioritySampling.SAMPLER_DROP, - SamplingMechanism.markRateLimiterRejected(SamplingMechanism.AGENT_RATE), + SamplingMechanism.AGENT_RATE, 1.0, limiterDrop.getTraceId().toLong(), + true, true); EncodedSamplingState limiter = exportSamplingState(limiterDrop); assertNull(limiter.traceState); @@ -766,7 +767,8 @@ private static DDSpan localProbabilitySpan(double rate, boolean sampled) { SamplingMechanism.AGENT_RATE, rate, span.getTraceId().toLong(), - true); + true, + false); return span; } 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 d59499acaa1..f1029dbaa0f 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 @@ -6,7 +6,6 @@ import static datadog.trace.api.sampling.SamplingMechanism.AGENT_RATE; import static datadog.trace.api.sampling.SamplingMechanism.EXTERNAL_OVERRIDE; import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; -import static datadog.trace.api.sampling.SamplingMechanism.markRateLimiterRejected; import static datadog.trace.core.propagation.PropagationTags.HeaderType.DATADOG; import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -72,7 +71,8 @@ void publishesProbabilityPriorityAndOtelStateTogether() { PropagationTags tags = PropagationTags.factory().empty(); SamplingState before = tags.samplingState(); - assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, false)); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, false, false)); SamplingState after = tags.samplingState(); assertEquals(SAMPLER_KEEP, after.getSamplingPriority()); @@ -88,11 +88,12 @@ void publishesProbabilityPriorityAndOtelStateTogether() { @Test void rejectedSamplingAttemptCannotReplaceProbabilityState() { PropagationTags tags = PropagationTags.factory().empty(); - assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, 1L, false)); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, 1L, false, false)); SamplingState established = tags.samplingState(); assertFalse( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.1, 2L, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.1, 2L, false, false)); assertEquals(established, tags.samplingState()); } @@ -114,7 +115,8 @@ void atomicProbabilityUpdatePreservesLockedDecisionMaker() { PropagationTags tags = PropagationTags.factory().fromHeaderValue(DATADOG, "_dd.p.dm=934086a686-4"); - assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.5, 1L, false)); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.5, 1L, false, false)); SamplingState state = tags.samplingState(); assertEquals(SAMPLER_DROP, state.getSamplingPriority()); @@ -157,7 +159,8 @@ void compoundConflictRemovesThresholdAndRetainsRandomValue() { @Test void forceKeepRemovesLocallyGeneratedProbabilityState() { PropagationTags tags = PropagationTags.factory().empty(); - assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.0, 1L, false)); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.0, 1L, false, false)); tags.forceKeep(MANUAL); @@ -170,8 +173,7 @@ void limiterDemotionDoesNotFabricateState() { PropagationTags tags = PropagationTags.factory().empty(); assertTrue( - tags.tryUpdateProbabilitySamplingDecision( - SAMPLER_DROP, markRateLimiterRejected(AGENT_RATE), 1.0, 1L, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 1.0, 1L, false, true)); assertNull(tags.samplingState().getOtelTraceState()); } @@ -184,7 +186,8 @@ void generatedManagedMembersDisplaceRightmostVendorAtMemberLimit() { } PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, original.toString()); - assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, 1L, false)); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, 1L, false, false)); String header = tags.headerValue(W3C); assertEquals(32, header.split(",").length); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java index f890c2eb1c8..f3b274eae7f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java @@ -135,7 +135,8 @@ void injectTheDecisionMakerTag() { @Test void injectUsesSingleSamplingStateAcrossHeaders() throws InterruptedException { PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, "ot=rv:ef284ace7a91e1"); - assertTrue(tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, true)); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, true, false)); DDSpanContext context = mockSpanContext( DDTraceId.from("1"), DDSpanId.from("2"), SAMPLER_KEEP, null, new HashMap<>(), tags); @@ -150,7 +151,7 @@ void injectUsesSingleSamplingStateAcrossHeaders() throws InterruptedException { assertTrue(traceparentWritten.await(5, TimeUnit.SECONDS)); assertTrue( tags.tryUpdateProbabilitySamplingDecision( - SAMPLER_DROP, AGENT_RATE, 0.0, 1L, true)); + SAMPLER_DROP, AGENT_RATE, 0.0, 1L, true, false)); } catch (Throwable throwable) { failure.set(throwable); } finally { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java index 4df5c106ece..089547447c3 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java @@ -173,7 +173,7 @@ void stripReplacesOtelSamplingStateAfterLocalResampling() { assertTrue( stripped .getPropagationTags() - .tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, false)); + .tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, false, false)); String reEncoded = stripped.getPropagationTags().headerValue(W3C); assertNotNull(reEncoded); diff --git a/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java b/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java index 8473163255c..6b0cd397626 100644 --- a/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java +++ b/internal-api/src/main/java/datadog/trace/api/sampling/SamplingMechanism.java @@ -8,12 +8,6 @@ import datadog.trace.api.Config; public class SamplingMechanism { - /** - * Internal flag to combine with a positive value to indicate the base result was overriden by - * rate limiter - */ - private static final int RATE_LIMITER_REJECTED = 1 << 30; - /** Not encouraged to use */ public static final byte UNKNOWN = -1; @@ -51,26 +45,8 @@ public class SamplingMechanism { /** Force override sampling decision from external source, like W3C traceparent. */ public static final byte EXTERNAL_OVERRIDE = Byte.MIN_VALUE; - public static int markRateLimiterRejected(int mechanism) { - if (mechanism < 0) { - return mechanism; - } - return mechanism | RATE_LIMITER_REJECTED; - } - - public static boolean isRateLimiterRejected(int mechanism) { - return mechanism >= 0 && (mechanism & RATE_LIMITER_REJECTED) != 0; - } - - public static int clearRateLimiterRejected(int mechanism) { - if (mechanism < 0) { - return mechanism; - } - return mechanism & ~RATE_LIMITER_REJECTED; - } - public static boolean validateWithSamplingPriority(int mechanism, int priority) { - switch (clearRateLimiterRejected(mechanism)) { + switch (mechanism) { case UNKNOWN: return true; @@ -106,7 +82,6 @@ public static boolean validateWithSamplingPriority(int mechanism, int priority) * @return {@code true} if the sampling priority lock can be avoided, {@code false} otherwise */ public static boolean canAvoidSamplingPriorityLock(int priority, int mechanism) { - mechanism = clearRateLimiterRejected(mechanism); return (!Config.get().isApmTracingEnabled() && mechanism == SamplingMechanism.APPSEC) || (Config.get().isDataJobsEnabled() && mechanism == DATA_JOBS); } diff --git a/internal-api/src/test/groovy/datadog/trace/api/sampling/SamplingMechanismTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/sampling/SamplingMechanismTest.groovy index c9bf4564149..9ad58eb1e34 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/sampling/SamplingMechanismTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/sampling/SamplingMechanismTest.groovy @@ -96,7 +96,7 @@ class SamplingMechanismTest extends DDSpecification { EXTERNAL_OVERRIDE | userKeepX | false } - void 'Test canAvoidSamplingPriorityLock'(){ + void 'Test canAvoidSamplingPriorityLock'() { setup: injectSysConfig("dd.apm.tracing.enabled", "false") @@ -117,11 +117,4 @@ class SamplingMechanismTest extends DDSpecification { DATA_JOBS | SAMPLER_KEEP | false EXTERNAL_OVERRIDE | SAMPLER_KEEP | false } - - void 'rate limiter rejection marker ignores negative mechanisms'() { - expect: - markRateLimiterRejected(EXTERNAL_OVERRIDE) == EXTERNAL_OVERRIDE - !isRateLimiterRejected(EXTERNAL_OVERRIDE) - clearRateLimiterRejected(EXTERNAL_OVERRIDE) == EXTERNAL_OVERRIDE - } } From 1a0f06f7c60dc2ee08404fe0c616885244f6becb Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Thu, 24 Sep 2026 00:46:26 +0100 Subject: [PATCH 20/22] Update tryUpdateProbabilitySamplingDecision parameter order --- .../java/datadog/trace/core/DDSpanContext.java | 4 ++-- .../trace/core/propagation/PropagationTags.java | 4 ++-- .../trace/core/propagation/ptags/PTagsFactory.java | 4 ++-- .../otlp/trace/OtlpTraceJsonCollectorTest.java | 6 +++--- .../trace/core/otlp/trace/OtlpTraceProtoTest.java | 6 +++--- .../propagation/OtelTraceStatePropagationTest.java | 14 +++++++------- .../core/propagation/W3CHttpInjectorTest.java | 4 ++-- .../core/propagation/opg/OrgGuardEnforcerTest.java | 2 +- 8 files changed, 22 insertions(+), 22 deletions(-) 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 65c86596726..cf4aa20aa09 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 @@ -732,9 +732,9 @@ public boolean setSamplingPriority( newPriority, newMechanism, sampleRate, + rateLimiterRejected, traceIdLowOrderBits, - SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism), - rateLimiterRejected); + SamplingMechanism.canAvoidSamplingPriorityLock(newPriority, newMechanism)); } private boolean validateSamplingPriority(final int newPriority, final int newMechanism) { 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 8ff442fa336..8df2d2cd150 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 @@ -113,9 +113,9 @@ public abstract boolean tryUpdateProbabilitySamplingDecision( int samplingPriority, int samplingMechanism, double sampleRate, + boolean rateLimiterRejected, long traceIdLowOrderBits, - boolean allowOverride, - boolean rateLimiterRejected); + boolean allowOverride); public abstract void forceKeep(int samplingMechanism); 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 a85c6e324ce..1f592419fb4 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 @@ -269,9 +269,9 @@ public boolean tryUpdateProbabilitySamplingDecision( int samplingPriority, int samplingMechanism, double sampleRate, + boolean rateLimiterRejected, long traceIdLowOrderBits, - boolean allowOverride, - boolean rateLimiterRejected) { + boolean allowOverride) { synchronized (samplingStateLock) { SamplingState current = samplingState; if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index 1263757fe74..3f836227a39 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -210,8 +210,8 @@ void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { PrioritySampling.SAMPLER_DROP, SamplingMechanism.AGENT_RATE, 1.0, - limiterDrop.getTraceId().toLong(), true, + limiterDrop.getTraceId().toLong(), true); Map limiter = exportSamplingSpan(limiterDrop); assertNull(limiter.get("traceState")); @@ -340,9 +340,9 @@ private static DDSpan localProbabilitySpan(double rate, boolean sampled) { sampled ? PrioritySampling.SAMPLER_KEEP : PrioritySampling.SAMPLER_DROP, SamplingMechanism.AGENT_RATE, rate, + false, span.getTraceId().toLong(), - true, - false); + true); return span; } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java index 4dda625c9ee..e326a6958b8 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java @@ -655,8 +655,8 @@ void traceStateAndFlagsStayPairedAcrossSamplingDecisions() throws IOException { PrioritySampling.SAMPLER_DROP, SamplingMechanism.AGENT_RATE, 1.0, - limiterDrop.getTraceId().toLong(), true, + limiterDrop.getTraceId().toLong(), true); EncodedSamplingState limiter = exportSamplingState(limiterDrop); assertNull(limiter.traceState); @@ -766,9 +766,9 @@ private static DDSpan localProbabilitySpan(double rate, boolean sampled) { sampled ? PrioritySampling.SAMPLER_KEEP : PrioritySampling.SAMPLER_DROP, SamplingMechanism.AGENT_RATE, rate, + false, span.getTraceId().toLong(), - true, - false); + true); return span; } 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 f1029dbaa0f..25cb364cf25 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 @@ -72,7 +72,7 @@ void publishesProbabilityPriorityAndOtelStateTogether() { SamplingState before = tags.samplingState(); assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, false, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, false, 1L, false)); SamplingState after = tags.samplingState(); assertEquals(SAMPLER_KEEP, after.getSamplingPriority()); @@ -89,11 +89,11 @@ void publishesProbabilityPriorityAndOtelStateTogether() { void rejectedSamplingAttemptCannotReplaceProbabilityState() { PropagationTags tags = PropagationTags.factory().empty(); assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, 1L, false, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, false, 1L, false)); SamplingState established = tags.samplingState(); assertFalse( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.1, 2L, false, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.1, false, 2L, false)); assertEquals(established, tags.samplingState()); } @@ -116,7 +116,7 @@ void atomicProbabilityUpdatePreservesLockedDecisionMaker() { PropagationTags.factory().fromHeaderValue(DATADOG, "_dd.p.dm=934086a686-4"); assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.5, 1L, false, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.5, false, 1L, false)); SamplingState state = tags.samplingState(); assertEquals(SAMPLER_DROP, state.getSamplingPriority()); @@ -160,7 +160,7 @@ void compoundConflictRemovesThresholdAndRetainsRandomValue() { void forceKeepRemovesLocallyGeneratedProbabilityState() { PropagationTags tags = PropagationTags.factory().empty(); assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.0, 1L, false, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 0.0, false, 1L, false)); tags.forceKeep(MANUAL); @@ -173,7 +173,7 @@ void limiterDemotionDoesNotFabricateState() { PropagationTags tags = PropagationTags.factory().empty(); assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 1.0, 1L, false, true)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 1.0, true, 1L, false)); assertNull(tags.samplingState().getOtelTraceState()); } @@ -187,7 +187,7 @@ void generatedManagedMembersDisplaceRightmostVendorAtMemberLimit() { PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, original.toString()); assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, 1L, false, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, false, 1L, false)); String header = tags.headerValue(W3C); assertEquals(32, header.split(",").length); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java index 16101d2a94a..c5439e2eeb4 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CHttpInjectorTest.java @@ -136,7 +136,7 @@ void injectTheDecisionMakerTag() { void injectUsesSingleSamplingStateAcrossHeaders() throws InterruptedException { PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, "ot=rv:ef284ace7a91e1"); assertTrue( - tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, true, false)); + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, false, 1L, true)); DDSpanContext context = mockSpanContext( DDTraceId.from("1"), DDSpanId.from("2"), SAMPLER_KEEP, null, new HashMap<>(), tags); @@ -151,7 +151,7 @@ void injectUsesSingleSamplingStateAcrossHeaders() throws InterruptedException { assertTrue(traceparentWritten.await(5, TimeUnit.SECONDS)); assertTrue( tags.tryUpdateProbabilitySamplingDecision( - SAMPLER_DROP, AGENT_RATE, 0.0, 1L, true, false)); + SAMPLER_DROP, AGENT_RATE, 0.0, false, 1L, true)); } catch (Throwable throwable) { failure.set(throwable); } finally { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java index 089547447c3..bd56aeb298f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java @@ -173,7 +173,7 @@ void stripReplacesOtelSamplingStateAfterLocalResampling() { assertTrue( stripped .getPropagationTags() - .tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, 1L, false, false)); + .tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, false, 1L, false)); String reEncoded = stripped.getPropagationTags().headerValue(W3C); assertNotNull(reEncoded); From b4a6c300ba9884ac4f9445fbffda14ad93f570d9 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Thu, 24 Sep 2026 11:46:05 +0200 Subject: [PATCH 21/22] fix(propagation): reconcile unknown otel state --- .../core/propagation/ptags/PTagsFactory.java | 39 +++++++++---------- .../OtelTraceStatePropagationTest.java | 12 ++++++ 2 files changed, 31 insertions(+), 20 deletions(-) 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 1f592419fb4..808533ebdbf 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 @@ -218,16 +218,8 @@ public void updateTraceSamplingPriority(int samplingPriority, int samplingMechan synchronized (samplingStateLock) { if (samplingPriority != PrioritySampling.UNSET && canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) { - OtelTraceState nextOtelTraceState = getOtelTraceState(); - if (nextOtelTraceState != null) { - if (samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE - && !nextOtelTraceState.isConsistentWith(samplingPriority > 0)) { - nextOtelTraceState = nextOtelTraceState.withoutThreshold(); - } else if (samplingMechanism != SamplingMechanism.UNKNOWN - && samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE) { - nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); - } - } + OtelTraceState nextOtelTraceState = + reconcileOtelTraceState(getOtelTraceState(), samplingPriority, samplingMechanism); installSamplingState(samplingPriority, samplingMechanism, nextOtelTraceState); } } @@ -244,16 +236,8 @@ public boolean tryUpdateTraceSamplingPriority( if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) { return false; } - OtelTraceState nextOtelTraceState = getOtelTraceState(); - if (nextOtelTraceState != null) { - if ((samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE - || samplingMechanism == SamplingMechanism.UNKNOWN) - && !nextOtelTraceState.isConsistentWith(samplingPriority > 0)) { - nextOtelTraceState = nextOtelTraceState.withoutThreshold(); - } else if (samplingMechanism != SamplingMechanism.UNKNOWN) { - nextOtelTraceState = nextOtelTraceState.forNonProbabilityDecision(); - } - } + OtelTraceState nextOtelTraceState = + reconcileOtelTraceState(getOtelTraceState(), samplingPriority, samplingMechanism); installSamplingState( samplingPriority, samplingMechanism, @@ -370,6 +354,21 @@ private static SamplingState initialSamplingState( return new SamplingState(samplingPriority, null, null, decisionMakerTagValue, null); } + private static OtelTraceState reconcileOtelTraceState( + OtelTraceState otelTraceState, int samplingPriority, int samplingMechanism) { + if (otelTraceState == null) { + return null; + } + if (samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE + && samplingMechanism != SamplingMechanism.UNKNOWN) { + return otelTraceState.forNonProbabilityDecision(); + } + if (!otelTraceState.isConsistentWith(samplingPriority > 0)) { + return otelTraceState.withoutThreshold(); + } + return otelTraceState; + } + @Override public void addTraceSource(final int product) { TRACE_SOURCE_UPDATER.updateAndGet( 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 25cb364cf25..7285681e2de 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 @@ -6,6 +6,7 @@ import static datadog.trace.api.sampling.SamplingMechanism.AGENT_RATE; import static datadog.trace.api.sampling.SamplingMechanism.EXTERNAL_OVERRIDE; import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; +import static datadog.trace.api.sampling.SamplingMechanism.UNKNOWN; import static datadog.trace.core.propagation.PropagationTags.HeaderType.DATADOG; import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -156,6 +157,17 @@ void compoundConflictRemovesThresholdAndRetainsRandomValue() { assertEquals("dd=s:1;t.dm:-0,ot=rv:00000000000001,vendor=state", tags.headerValue(W3C)); } + @Test + void unknownPriorityUpdateRemovesInconsistentThreshold() { + PropagationTags tags = + PropagationTags.factory() + .fromHeaderValue(W3C, "dd=s:0,ot=rv:00000000000001;th:8,vendor=state"); + + tags.updateTraceSamplingPriority(SAMPLER_KEEP, UNKNOWN); + + assertEquals("dd=s:1,ot=rv:00000000000001,vendor=state", tags.headerValue(W3C)); + } + @Test void forceKeepRemovesLocallyGeneratedProbabilityState() { PropagationTags tags = PropagationTags.factory().empty(); From b1022f8a5ee33e8ef15d73b7dad615f24fba53d6 Mon Sep 17 00:00:00 2001 From: MilanGarnier Date: Thu, 24 Sep 2026 12:08:47 +0200 Subject: [PATCH 22/22] perf(propagation): reuse unchanged sampling state --- .../core/propagation/ptags/PTagsFactory.java | 10 +++++++++- .../OtelTraceStatePropagationTest.java | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index 808533ebdbf..3092281df57 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 @@ -309,7 +309,7 @@ private void installSamplingState( OtelTraceState nextOtelTraceState, TagValue nextKnuthSamplingRateTagValue, boolean updateDecisionMaker) { - clearCachedHeader(W3C); + SamplingState currentState = samplingState; TagValue nextDecisionMakerTagValue = getDecisionMakerTagValue(); if (updateDecisionMaker && samplingPriority > 0) { // TODO should try to keep the old sampling mechanism if we override the value? @@ -337,6 +337,14 @@ private void installSamplingState( } nextDecisionMakerTagValue = null; } + if (currentState.getSamplingPriority() == samplingPriority + && currentState.getTracestate() == tracestate + && currentState.getOtelTraceState() == nextOtelTraceState + && Objects.equals(currentState.getDecisionMaker(), nextDecisionMakerTagValue) + && Objects.equals(currentState.getKnuthSamplingRate(), nextKnuthSamplingRateTagValue)) { + return; + } + clearCachedHeader(W3C); samplingState = new SamplingState( samplingPriority, 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 7285681e2de..46d84e905e2 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 @@ -86,6 +86,25 @@ void publishesProbabilityPriorityAndOtelStateTogether() { assertSame(tracestate, tags.getW3CTracestate(after)); } + @Test + void reusesSamplingStateAndHeaderCachesForUnchangedProbabilityDecision() { + PropagationTags tags = PropagationTags.factory().empty(); + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, false, 1L, false)); + SamplingState expectedState = tags.samplingState(); + String expectedDatadogHeader = tags.headerValue(DATADOG); + String expectedW3CHeader = tags.headerValue(W3C); + String expectedTracestate = tags.getW3CTracestate(expectedState); + + assertTrue( + tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, false, 1L, true)); + + assertSame(expectedState, tags.samplingState()); + assertSame(expectedDatadogHeader, tags.headerValue(DATADOG)); + assertSame(expectedW3CHeader, tags.headerValue(W3C)); + assertSame(expectedTracestate, tags.getW3CTracestate(expectedState)); + } + @Test void rejectedSamplingAttemptCannotReplaceProbabilityState() { PropagationTags tags = PropagationTags.factory().empty();