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..33a9f803433 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java @@ -58,20 +58,16 @@ 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; + + Boolean probabilitySamplingResult = rates.hasAgentRates() ? sampled : null; + span.setSamplingPriority( + samplingPriority, + SAMPLING_AGENT_RATE, + sampler.getSampleRate(), + SamplingMechanism.AGENT_RATE, + probabilitySamplingResult); } private > String getSpanEnv(final T span) { @@ -161,7 +157,7 @@ public void onResponse( if (canIncrease && anyCapped) { lastCappedNanos = now; } - serviceRates = new RateSamplersByEnvAndService(updatedEnvServiceRates, fallbackSampler); + serviceRates = new RateSamplersByEnvAndService(updatedEnvServiceRates, fallbackSampler, true); } private static RateSampler createRateSampler(final double sampleRate) { @@ -183,15 +179,23 @@ private static final class RateSamplersByEnvAndService { private final Map> envServiceRates; private final RateSampler fallbackSampler; + private final boolean hasAgentRates; RateSamplersByEnvAndService() { - this(Collections.emptyMap(), DEFAULT_SAMPLER); + this(Collections.emptyMap(), DEFAULT_SAMPLER, false); } RateSamplersByEnvAndService( - Map> envServiceRates, RateSampler fallbackSampler) { + Map> envServiceRates, + RateSampler fallbackSampler, + boolean hasAgentRates) { this.envServiceRates = envServiceRates; this.fallbackSampler = fallbackSampler; + this.hasAgentRates = hasAgentRates; + } + + boolean hasAgentRates() { + return hasAgentRates; } RateSampler getFallbackSampler() { diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java index 1746a1a3c54..f419bce31d2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java @@ -146,28 +146,21 @@ public > void setSamplingPriority(final T span) { if (matchedRule == null) { fallbackSampler.setSamplingPriority(span); } else { - if (matchedRule.sample(span)) { - if (rateLimiter.tryAcquire()) { - span.setSamplingPriority( - PrioritySampling.USER_KEEP, - SAMPLING_RULE_RATE, - matchedRule.getSampler().getSampleRate(), - matchedRule.getMechanism()); - } else { - span.setSamplingPriority( - PrioritySampling.USER_DROP, - SAMPLING_RULE_RATE, - matchedRule.getSampler().getSampleRate(), - matchedRule.getMechanism()); - } + boolean sampled = matchedRule.sample(span); + int samplingPriority; + if (sampled) { + samplingPriority = + rateLimiter.tryAcquire() ? PrioritySampling.USER_KEEP : PrioritySampling.USER_DROP; span.setMetric(SAMPLING_LIMIT_RATE, rateLimit); } else { - span.setSamplingPriority( - PrioritySampling.USER_DROP, - SAMPLING_RULE_RATE, - matchedRule.getSampler().getSampleRate(), - matchedRule.getMechanism()); + samplingPriority = PrioritySampling.USER_DROP; } + span.setSamplingPriority( + samplingPriority, + SAMPLING_RULE_RATE, + matchedRule.getSampler().getSampleRate(), + matchedRule.getMechanism(), + Boolean.valueOf(sampled)); } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java index b2ab55c8e25..db068847fd7 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java @@ -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, + int samplingMechanism, + Boolean probabilitySamplingResult); T setSpanSamplingPriority(double rate, int limit); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index 5517952358e..fedc0b7ad58 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -646,6 +646,25 @@ public final DDSpan setSamplingPriority(final int newPriority, int samplingMecha @Override public DDSpan setSamplingPriority( + int samplingPriority, + CharSequence rate, + double sampleRate, + int samplingMechanism, + Boolean probabilitySamplingResult) { + if (setSamplingPriorityWithRate(samplingPriority, rate, sampleRate, samplingMechanism) + && probabilitySamplingResult != null) { + context + .getPropagationTags() + .updateOtelTraceState( + getTraceId().toLong(), + sampleRate, + probabilitySamplingResult.booleanValue(), + samplingPriority); + } + return this; + } + + private boolean setSamplingPriorityWithRate( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { if (context.setSamplingPriority(samplingPriority, samplingMechanism)) { setMetric(rate, sampleRate); @@ -655,8 +674,9 @@ public DDSpan setSamplingPriority( || samplingMechanism == SamplingMechanism.REMOTE_ADAPTIVE_RULE) { context.getPropagationTags().updateKnuthSamplingRate(sampleRate); } + return true; } - return this; + return false; } @Override diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index adf4cd66156..88f257dc66c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -445,7 +445,13 @@ public DDSpanContext( setOrigin(origin); } if (samplingPriority != PrioritySampling.UNSET) { - setSamplingPriority(samplingPriority, SamplingMechanism.UNKNOWN); + if (this.propagationTags.getSamplingPriority() == samplingPriority) { + // Extractors already applied this priority to the propagation tags. Initialize the local + // field without reapplying the decision as an unknown local override. + SAMPLING_PRIORITY_UPDATER.set(this, samplingPriority); + } else { + setSamplingPriority(samplingPriority, SamplingMechanism.UNKNOWN); + } } setTag(PARENT_ID, this.propagationTags.getLastParentId()); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java index 3a0c57a4dd8..80b7d697dbd 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java @@ -67,6 +67,9 @@ public interface Factory { public abstract void forceKeep(int samplingMechanism); + public abstract void updateOtelTraceState( + long traceIdLowOrderBits, double sampleRate, boolean sampled, int samplingPriority); + public abstract int getSamplingPriority(); public abstract void updateTraceOrigin(CharSequence origin); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java index 3b8e837b5b9..a3b3c5e7760 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java @@ -1,19 +1,149 @@ package datadog.trace.core.propagation.ptags; final class OtelTraceState { + static final int MAX_VALUE_LENGTH = 256; + + private static final int HEX_DIGITS = 14; + private static final long KNUTH_FACTOR = 1111111111111111111L; + private static final long MAX_THRESHOLD = (1L << 56) - 1; + private static final double THRESHOLD_RANGE = 1L << 56; + private static final long NO_VALUE = -1; + private static final int HAS_MULTIPLE_RANDOM_VALUES = 1; + private static final int HAS_LOCALLY_GENERATED_RANDOM_VALUE = 1 << 1; + private static final String RANDOM_VALUE_KEY = "rv:"; + private static final String THRESHOLD_KEY = "th:"; + private static final int DEFAULT_VALUE_CAPACITY = + RANDOM_VALUE_KEY.length() + HEX_DIGITS + 1 + THRESHOLD_KEY.length() + HEX_DIGITS; + private final String value; + private final long randomValue; + private final long threshold; private final int inheritedPosition; + private final int flags; - private OtelTraceState(String value, int inheritedPosition) { + private OtelTraceState( + String value, long randomValue, long threshold, int inheritedPosition, int flags) { this.value = value; + this.randomValue = randomValue; + this.threshold = threshold; this.inheritedPosition = inheritedPosition; + this.flags = flags; } static OtelTraceState parse(String raw, int inheritedPosition) { if (raw == null || raw.isEmpty()) { return null; } - return new OtelTraceState(raw, inheritedPosition); + + long randomValue = NO_VALUE; + long threshold = NO_VALUE; + int flags = 0; + StringBuilder normalized = null; + int start = 0; + while (start < raw.length()) { + int end = raw.indexOf(';', start); + if (end < 0) { + end = raw.length(); + } + int separator = raw.indexOf(':', start); + if (separator >= end) { + separator = -1; + } + int fieldValueStart = separator < 0 ? end : separator + 1; + if (hasKey(raw, start, end, separator, 'r', 'v')) { + long parsedRandomValue = + end - fieldValueStart == HEX_DIGITS + ? parseLowercaseHex(raw, fieldValueStart, end) + : NO_VALUE; + if (parsedRandomValue != NO_VALUE) { + if (randomValue == NO_VALUE) { + randomValue = parsedRandomValue; + } else { + flags |= HAS_MULTIPLE_RANDOM_VALUES; + } + if (normalized != null) { + appendField(normalized, raw, start, end); + } + } else { + normalized = startNormalizing(raw, normalized, start); + } + } else if (hasKey(raw, start, end, separator, 't', 'h')) { + long parsedThreshold = + fieldValueStart < end && end - fieldValueStart <= HEX_DIGITS + ? parseLowercaseHex(raw, fieldValueStart, end) + : NO_VALUE; + if (parsedThreshold != NO_VALUE) { + if (threshold == NO_VALUE) { + threshold = parsedThreshold; + } + if (normalized != null) { + appendField(normalized, raw, start, end); + } + } else { + normalized = startNormalizing(raw, normalized, start); + } + } else if (start < end) { + if (normalized != null) { + appendField(normalized, raw, start, end); + } + } else { + normalized = startNormalizing(raw, normalized, start); + } + start = end + 1; + } + + if (raw.charAt(raw.length() - 1) == ';') { + normalized = startNormalizing(raw, normalized, raw.length()); + } + + String value = normalized == null ? raw : normalized.toString(); + if (value.isEmpty()) { + return null; + } + return new OtelTraceState( + value, randomValue, threshold, normalized == null ? inheritedPosition : 0, flags); + } + + static OtelTraceState updateProbability( + OtelTraceState current, + long traceIdLowOrderBits, + double sampleRate, + boolean sampled, + int samplingPriority) { + String currentValue = current == null ? null : current.value; + + // `sampled` is the raw probability result; `samplingPriority` may be changed by rate limiting. + if (sampled && samplingPriority <= 0) { + if (current != null) { + return current.removeThresholdForLimiterDemotion(); + } + return create(computeRandomValue(traceIdLowOrderBits), NO_VALUE, currentValue, true); + } + + long threshold = computeThreshold(sampleRate); + long randomValue = computeRandomValue(traceIdLowOrderBits); + if (sampled && randomValue < threshold) { + randomValue = threshold; + } else if (!sampled && randomValue >= threshold) { + randomValue = threshold == 0 ? 0 : threshold - 1; + } + + return create(randomValue, threshold, currentValue, true); + } + + OtelTraceState removeForNonProbabilityDecision() { + if (!hasLocallyGeneratedRandomValue() && threshold == NO_VALUE && !hasMultipleRandomValues()) { + return this; + } + long retainedRandomValue = hasLocallyGeneratedRandomValue() ? NO_VALUE : randomValue; + return create(retainedRandomValue, NO_VALUE, value, false); + } + + OtelTraceState removeThresholdForLimiterDemotion() { + if (threshold == NO_VALUE) { + return this; + } + return create(randomValue, NO_VALUE, value, hasLocallyGeneratedRandomValue()); } String getValue() { @@ -27,4 +157,147 @@ int length() { int getInheritedPosition() { return inheritedPosition; } + + private static OtelTraceState create( + long randomValue, long threshold, String previousValue, boolean locallyGeneratedRandomValue) { + StringBuilder value = new StringBuilder(DEFAULT_VALUE_CAPACITY); + if (randomValue != NO_VALUE) { + appendRandomValue(value, randomValue); + } + if (threshold != NO_VALUE) { + appendThreshold(value, threshold); + } + if (previousValue != null) { + appendUnknownFields(value, previousValue); + } + if (value.length() == 0) { + return null; + } + return new OtelTraceState( + value.toString(), + randomValue, + threshold, + 0, + locallyGeneratedRandomValue ? HAS_LOCALLY_GENERATED_RANDOM_VALUE : 0); + } + + private static StringBuilder startNormalizing(String raw, StringBuilder normalized, int start) { + if (normalized != null) { + return normalized; + } + normalized = new StringBuilder(raw.length()); + if (start > 0) { + normalized.append(raw, 0, start - 1); + } + return normalized; + } + + private static void appendRandomValue(StringBuilder value, long randomValue) { + if (appendFieldPrefix(value, RANDOM_VALUE_KEY.length() + HEX_DIGITS)) { + value.append(RANDOM_VALUE_KEY); + appendHex(value, randomValue, HEX_DIGITS); + } + } + + private static void appendThreshold(StringBuilder value, long threshold) { + int hexDigits = thresholdHexDigits(threshold); + if (appendFieldPrefix(value, THRESHOLD_KEY.length() + hexDigits)) { + value.append(THRESHOLD_KEY); + appendHex(value, threshold, hexDigits); + } + } + + private static void appendUnknownFields(StringBuilder value, String previousValue) { + int start = 0; + while (start < previousValue.length()) { + int end = previousValue.indexOf(';', start); + if (end < 0) { + end = previousValue.length(); + } + int separator = previousValue.indexOf(':', start); + if (separator >= end) { + separator = -1; + } + if (!hasKey(previousValue, start, end, separator, 'r', 'v') + && !hasKey(previousValue, start, end, separator, 't', 'h')) { + appendField(value, previousValue, start, end); + } + start = end + 1; + } + } + + private static void appendField(StringBuilder value, String field, int start, int end) { + if (!appendFieldPrefix(value, end - start)) { + return; + } + value.append(field, start, end); + } + + private static boolean appendFieldPrefix(StringBuilder value, int fieldLength) { + int separatorSize = value.length() == 0 ? 0 : 1; + if (value.length() + separatorSize + fieldLength > MAX_VALUE_LENGTH) { + return false; + } + if (separatorSize != 0) { + value.append(';'); + } + return true; + } + + private static boolean hasKey( + String value, int start, int end, int separator, char first, char second) { + if (separator >= 0) { + return separator == start + 2 + && value.charAt(start) == first + && value.charAt(start + 1) == second; + } + return end == start + 2 && value.charAt(start) == first && value.charAt(start + 1) == second; + } + + private static long parseLowercaseHex(String value, int start, int end) { + long parsed = 0; + for (int i = start; i < end; i++) { + char character = value.charAt(i); + if (character >= '0' && character <= '9') { + parsed = (parsed << 4) | character - '0'; + } else if (character >= 'a' && character <= 'f') { + parsed = (parsed << 4) | character - 'a' + 10; + } else { + return NO_VALUE; + } + } + return parsed; + } + + private boolean hasMultipleRandomValues() { + return (flags & HAS_MULTIPLE_RANDOM_VALUES) != 0; + } + + private boolean hasLocallyGeneratedRandomValue() { + return (flags & HAS_LOCALLY_GENERATED_RANDOM_VALUE) != 0; + } + + private static void appendHex(StringBuilder value, long number, int digits) { + for (int shift = (HEX_DIGITS - 1) * 4; shift >= (HEX_DIGITS - digits) * 4; shift -= 4) { + value.append(Character.forDigit((int) (number >>> shift) & 0xF, 16)); + } + } + + private static long computeRandomValue(long traceIdLowOrderBits) { + return (~(traceIdLowOrderBits * KNUTH_FACTOR)) >>> 8; + } + + private static long computeThreshold(double sampleRate) { + long threshold = Math.round((1 - sampleRate) * THRESHOLD_RANGE); + return Math.max(0, Math.min(threshold, MAX_THRESHOLD)); + } + + private static int thresholdHexDigits(long threshold) { + int digits = HEX_DIGITS; + while (digits > 1 && (threshold & 0xF) == 0) { + digits--; + threshold >>>= 4; + } + return digits; + } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index 1661398c819..c01e8fd2aee 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java @@ -233,6 +233,9 @@ public void forceKeep(int samplingMechanism) { } private void doUpdateTraceSamplingPriority(int samplingPriority, int samplingMechanism) { + boolean removeOtelProbability = + samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE + && !isProbabilitySamplingMechanism(samplingMechanism); if (this.samplingPriority != samplingPriority) { // This should invalidate any cached w3c header clearCachedHeader(W3C); @@ -264,6 +267,17 @@ private void doUpdateTraceSamplingPriority(int samplingPriority, int samplingMec } decisionMakerTagValue = null; } + if (removeOtelProbability && otelTraceState != null) { + setOtelTraceState(otelTraceState.removeForNonProbabilityDecision()); + } + } + + @Override + public void updateOtelTraceState( + long traceIdLowOrderBits, double sampleRate, boolean sampled, int samplingPriority) { + setOtelTraceState( + OtelTraceState.updateProbability( + otelTraceState, traceIdLowOrderBits, sampleRate, sampled, samplingPriority)); } @Override @@ -573,5 +587,19 @@ public void updateAndLockDecisionMaker(PropagationTags source) { } } } + + private static boolean isProbabilitySamplingMechanism(int samplingMechanism) { + switch (samplingMechanism) { + case SamplingMechanism.AGENT_RATE: + case SamplingMechanism.REMOTE_AUTO_RATE: + case SamplingMechanism.LOCAL_USER_RULE: + case SamplingMechanism.REMOTE_USER_RATE: + case SamplingMechanism.REMOTE_USER_RULE: + case SamplingMechanism.REMOTE_ADAPTIVE_RULE: + return true; + default: + return false; + } + } } } diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java index 41a2a5a0d14..99ef1e1fbf4 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java @@ -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, + int samplingMechanism, + Boolean probabilitySamplingResult) { return this; } diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java index c8fad4ab0ee..fd19a32164b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java @@ -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, + int samplingMechanism, + Boolean probabilitySamplingResult) { return this; } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java index 9d9367551a7..c8bb8ff809f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java @@ -12,10 +12,15 @@ import static datadog.trace.api.DDTags.THREAD_ID; import static datadog.trace.api.DDTags.THREAD_NAME; import static datadog.trace.api.TracePropagationStyle.DATADOG; +import static datadog.trace.api.TracePropagationStyle.TRACECONTEXT; +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.LOCAL_USER_RULE; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpan; +import static datadog.trace.common.sampling.RuleBasedTraceSampler.SAMPLING_RULE_RATE; import static datadog.trace.test.junit.utils.config.WithConfigExtension.injectSysConfig; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -55,6 +60,15 @@ public class CoreSpanBuilderTest extends DDCoreJavaSpecification { + // Propagation fixtures preserve the raw inbound vendor values as a single test vector. + private static final String INHERITED_RANDOM_VALUE = "ef284ace7a91e1"; + private static final String OTEL_TRACE_STATE = + "dd=s:0,ot=rv:" + INHERITED_RANDOM_VALUE + ";th:e6666666666668"; + private static final String OTEL_MEMBER = "ot="; + private static final String THRESHOLD_0_5 = ";th:8"; + private static final double SAMPLE_RATE_0_5 = 0.5; + private static final String DATADOG_TRACE_STATE = "_dd.p.dm=934086a686-4,_dd.p.anytag=value"; + private ListWriter writer; private CoreTracer tracer; @@ -362,6 +376,31 @@ void extractedContextShouldPopulateNewSpanDetails(ExtractedContext extractedCont span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.DATADOG)); } + @Test + void extractedContextShouldPreserveOtelTraceState() { + PropagationTags propagationTags = + PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.W3C, OTEL_TRACE_STATE); + ExtractedContext extractedContext = + new ExtractedContext( + DDTraceId.ONE, + 2, + PrioritySampling.SAMPLER_DROP, + null, + 0, + Collections.emptyMap(), + Collections.emptyMap(), + null, + propagationTags, + null, + TRACECONTEXT); + + DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(extractedContext).start(); + + assertEquals( + OTEL_TRACE_STATE, + span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C)); + } + @Test @WithConfig(key = "trace.propagation.behavior.extract", value = "restart") void buildContextFromExtractedContextWithRestartBehavior() { @@ -375,9 +414,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 +431,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, LOCAL_USER_RULE, true); + + String freshTraceState = + span.spanContext().getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); + assertFalse(freshTraceState.contains(INHERITED_RANDOM_VALUE)); + assertTrue(freshTraceState.contains(THRESHOLD_0_5)); } @Test @@ -409,9 +456,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 +465,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, LOCAL_USER_RULE, true); + + 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 new file mode 100644 index 00000000000..de5290e4c93 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/OtelSamplingDecisionTest.java @@ -0,0 +1,183 @@ +package datadog.trace.core; + +import static datadog.trace.api.config.TracerConfig.TRACE_RATE_LIMIT; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_RULES; +import static datadog.trace.api.sampling.PrioritySampling.USER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.LOCAL_USER_RULE; +import static datadog.trace.common.sampling.RuleBasedTraceSampler.SAMPLING_RULE_RATE; +import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.common.sampling.PrioritySampler; +import datadog.trace.common.sampling.RateByServiceTraceSampler; +import datadog.trace.common.sampling.Sampler; +import datadog.trace.common.writer.ListWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class OtelSamplingDecisionTest extends DDCoreJavaSpecification { + + private static final String AGENT_RATE_ENDPOINT = "traces"; + private static final String INSTRUMENTATION_NAME = "datadog"; + private static final String OPERATION_NAME = "operation"; + private static final String SERVICE_NAME = "service"; + private static final String OTEL_MEMBER = "ot="; + private static final String OTEL_RANDOM_VALUE_PREFIX = "ot=rv:"; + private static final String HALF_THRESHOLD = ";th:8"; + private static final String MAX_THRESHOLD = ";th:ffffffffffffff"; + private static final String OTEL_THRESHOLD_PREFIX = ";th:"; + private static final double SAMPLE_RATE_0_5 = 0.5; + private static final String SAMPLE_RATE_0_5_RULE = "[{\"sample_rate\": 0.5}]"; + private static final String FULL_SAMPLE_RATE_RULE = "[{\"sample_rate\": 1}]"; + // Keeps configured-rule vectors out of the limiter path. + private static final String HIGH_RATE_LIMIT = "10000000"; + private static final String ONE_PER_SECOND_RATE_LIMIT = "1"; + + @Test + void initialAgentRateDoesNotEstablishOtelProbabilityState() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + + new RateByServiceTraceSampler().setSamplingPriority(span); + + assertFalse(w3cHeader(span).contains(OTEL_MEMBER)); + } finally { + tracer.close(); + } + } + + @Test + void loadedAgentRateEstablishesOtelProbabilityState() { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + sampler.onResponse(AGENT_RATE_ENDPOINT, agentRates(SAMPLE_RATE_0_5)); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + + sampler.setSamplingPriority(span); + + String header = w3cHeader(span); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(header.contains(HALF_THRESHOLD)); + } finally { + tracer.close(); + } + } + + @Test + void loadedZeroAgentRateEstablishesOtelProbabilityStateWithMaxThreshold() { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + sampler.onResponse(AGENT_RATE_ENDPOINT, agentRates(0)); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + + sampler.setSamplingPriority(span); + + String header = w3cHeader(span); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(header.contains(MAX_THRESHOLD)); + } finally { + tracer.close(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void configuredRulesEstablishOtelProbabilityState(boolean traceRule) { + Properties properties = new Properties(); + if (traceRule) { + properties.setProperty(TRACE_SAMPLING_RULES, SAMPLE_RATE_0_5_RULE); + } else { + properties.setProperty(TRACE_SAMPLE_RATE, String.valueOf(SAMPLE_RATE_0_5)); + } + properties.setProperty(TRACE_RATE_LIMIT, HIGH_RATE_LIMIT); + PrioritySampler sampler = (PrioritySampler) Sampler.Builder.forConfig(properties); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + + sampler.setSamplingPriority(span); + + String header = w3cHeader(span); + assertTrue(header.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertTrue(header.contains(HALF_THRESHOLD)); + } finally { + tracer.close(); + } + } + + @Test + void limiterRejectionStripsThresholdButKeepsOtelRandomValue() { + Properties properties = new Properties(); + properties.setProperty(TRACE_SAMPLING_RULES, FULL_SAMPLE_RATE_RULE); + properties.setProperty(TRACE_RATE_LIMIT, ONE_PER_SECOND_RATE_LIMIT); + PrioritySampler sampler = (PrioritySampler) Sampler.Builder.forConfig(properties); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan allowed = newRootSpan(tracer); + DDSpan rejected = newRootSpan(tracer); + + sampler.setSamplingPriority(allowed); + sampler.setSamplingPriority(rejected); + + assertTrue(w3cHeader(allowed).contains(OTEL_RANDOM_VALUE_PREFIX)); + assertEquals(USER_DROP, rejected.samplingPriority()); + String rejectedHeader = w3cHeader(rejected); + assertTrue(rejectedHeader.contains(OTEL_RANDOM_VALUE_PREFIX)); + assertFalse(rejectedHeader.contains(OTEL_THRESHOLD_PREFIX)); + } finally { + tracer.close(); + } + } + + @Test + void manualOverrideRemovesLocallyGeneratedProbabilityState() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = newRootSpan(tracer); + span.setSamplingPriority( + USER_KEEP, SAMPLING_RULE_RATE, SAMPLE_RATE_0_5, LOCAL_USER_RULE, true); + assertTrue(w3cHeader(span).contains(OTEL_RANDOM_VALUE_PREFIX)); + + span.spanContext().forceKeep(); + + assertFalse(w3cHeader(span).contains(OTEL_MEMBER)); + } finally { + tracer.close(); + } + } + + private static DDSpan newRootSpan(CoreTracer tracer) { + return (DDSpan) + tracer + .buildSpan(INSTRUMENTATION_NAME, OPERATION_NAME) + .withServiceName(SERVICE_NAME) + .ignoreActiveSpan() + .start(); + } + + private static String w3cHeader(DDSpan span) { + String header = span.spanContext().getPropagationTags().headerValue(W3C); + assertNotNull(header); + return header; + } + + private static Map> agentRates(double rate) { + Map rates = new HashMap<>(); + rates.put("service:,env:", rate); + Map> response = new HashMap<>(); + response.put("rate_by_service", rates); + return response; + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java new file mode 100644 index 00000000000..f3bda1391b5 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java @@ -0,0 +1,185 @@ +package datadog.trace.core.propagation; + +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.LOCAL_USER_RULE; +import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; +import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class OtelTraceStatePropagationTest { + private static final long TRACE_ID = 1; + private static final double SAMPLE_RATE_0_5 = 0.5; + private static final String RV = "ef284ace7a91e1"; + private static final String TH = "e6666666666668"; + private static final String GENERATED_RV = "f0948a54d43b8e"; + private static final String THRESHOLD_0_5 = "8"; + private static final int W3C_MEMBER_VALUE_MAX_LENGTH = 256; + private static final int W3C_TRACESTATE_MEMBER_LIMIT = 32; + + @ParameterizedTest + @MethodSource("inboundTraceState") + void normalizesAndForwardsFirstOtelMember(String header, String expected) { + PropagationTags propagationTags = PropagationTags.factory().fromHeaderValue(W3C, header); + + assertEquals(expected, propagationTags.headerValue(W3C)); + } + + static Stream inboundTraceState() { + // Covers valid, invalid, duplicate, and ordered ot list-members. + return Stream.of( + arguments("ot=rv:" + RV + ";th:" + TH, "ot=rv:" + RV + ";th:" + TH), + arguments("ot=rv:" + RV, "ot=rv:" + RV), + arguments("ot=th:" + TH, "ot=th:" + TH), + arguments("ot=foo:bar", "ot=foo:bar"), + arguments( + "congo=state,ot=rv:invalid;th:" + TH + ";foo:bar", + "ot=th:" + TH + ";foo:bar,congo=state"), + arguments("congo=state,ot=rv:invalid;th:invalid", "congo=state"), + arguments("ot=rv:EF284ACE7A91E1;th:" + TH, "ot=th:" + TH), + arguments( + "ot=rv:" + RV + ";rv:1234567890abcd;th:" + TH, + "ot=rv:" + RV + ";rv:1234567890abcd;th:" + TH), + arguments( + "foo=bar,ot=rv:" + RV + ",ot=rv:1234567890abcd,other=state", + "foo=bar,ot=rv:" + RV + ",other=state"), + arguments("dd=s:1,dd=s:0,ot=rv:" + RV, "dd=s:1,ot=rv:" + RV)); + } + + @Test + void preservesUnchangedInheritedMemberPosition() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue(W3C, "dd=s:1,foo=bar,ot=rv:" + RV + ",something=else"); + + assertEquals( + "dd=s:1,foo=bar,ot=rv:" + RV + ",something=else", propagationTags.headerValue(W3C)); + } + + @Test + void movesNormalizedMemberImmediatelyAfterDatadog() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue(W3C, "dd=s:1,foo=bar,ot=rv:invalid;th:" + TH + ",something=else"); + + assertEquals( + "dd=s:1,ot=th:" + TH + ",foo=bar,something=else", propagationTags.headerValue(W3C)); + } + + @Test + void movesLocallyGeneratedMemberImmediatelyAfterDatadog() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue( + W3C, "foo=bar,ot=rv:" + RV + ";th:" + TH + ";future:value,other=state"); + + propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); + propagationTags.updateOtelTraceState(TRACE_ID, SAMPLE_RATE_0_5, true, USER_KEEP); + + assertEquals( + "dd=s:2;t.dm:-3,ot=rv:" + + GENERATED_RV + + ";th:" + + THRESHOLD_0_5 + + ";future:value,foo=bar,other=state", + propagationTags.headerValue(W3C)); + } + + @Test + void manualKeepRetainsInheritedRandomnessAndUnknownFields() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue( + W3C, "foo=bar,ot=rv:" + RV + ";th:" + TH + ";future:value,other=state"); + + propagationTags.forceKeep(MANUAL); + + assertEquals( + "dd=s:2;t.dm:-4,ot=rv:" + RV + ";future:value,foo=bar,other=state", + propagationTags.headerValue(W3C)); + } + + @Test + void manualKeepRemovesLocallyGeneratedRandomness() { + PropagationTags propagationTags = PropagationTags.factory().empty(); + propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); + propagationTags.updateOtelTraceState(TRACE_ID, SAMPLE_RATE_0_5, true, USER_KEEP); + + propagationTags.forceKeep(MANUAL); + + assertEquals("dd=s:2;t.dm:-4", propagationTags.headerValue(W3C)); + } + + @Test + void sampledContextWithoutOtelStateDoesNotFabricateIt() { + PropagationTags propagationTags = + PropagationTags.factory().fromHeaderValue(W3C, "dd=s:1,foo=bar"); + + assertEquals("dd=s:1,foo=bar", propagationTags.headerValue(W3C)); + } + + @Test + void updatingW3cTraceStateCapturesOtelStateForMixedHeaderExtraction() { + PropagationTags propagationTags = + PropagationTags.factory() + .fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.dm=-3"); + + propagationTags.updateW3CTracestate("foo=bar,ot=rv:" + RV + ";th:" + TH + ",other=state"); + + assertEquals( + "dd=t.dm:-3,foo=bar,ot=rv:" + RV + ";th:" + TH + ",other=state", + propagationTags.headerValue(W3C)); + } + + @Test + void emitsOnlyCompleteOtelFieldsAtMemberSizeLimit() { + // W3C limits an individual tracestate member value to 256 characters. + String unknownField = "x:" + repeat("v", W3C_MEMBER_VALUE_MAX_LENGTH - 2); + PropagationTags propagationTags = + PropagationTags.factory().fromHeaderValue(W3C, "ot=" + unknownField); + + assertEquals("ot=" + unknownField, propagationTags.headerValue(W3C)); + + propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); + propagationTags.updateOtelTraceState(TRACE_ID, SAMPLE_RATE_0_5, true, USER_KEEP); + + String header = propagationTags.headerValue(W3C); + assertTrue(header.contains("ot=rv:" + GENERATED_RV + ";th:" + THRESHOLD_0_5)); + assertFalse(header.contains("x:")); + } + + @Test + void generatedOtelMemberHonorsMemberCountLimit() { + StringBuilder original = new StringBuilder("ot=rv:").append(RV); + for (int i = 0; i < W3C_TRACESTATE_MEMBER_LIMIT - 1; i++) { + original.append(",v").append(i).append("=state"); + } + PropagationTags propagationTags = + PropagationTags.factory().fromHeaderValue(W3C, original.toString()); + + propagationTags.updateTraceSamplingPriority(USER_KEEP, LOCAL_USER_RULE); + propagationTags.updateOtelTraceState(TRACE_ID, SAMPLE_RATE_0_5, true, USER_KEEP); + + String header = propagationTags.headerValue(W3C); + assertEquals(W3C_TRACESTATE_MEMBER_LIMIT, header.split(",").length); + assertTrue( + header.startsWith("dd=s:2;t.dm:-3,ot=rv:" + GENERATED_RV + ";th:" + THRESHOLD_0_5 + ",")); + assertFalse(header.contains("v30=state")); + } + + private static String repeat(String value, int count) { + StringBuilder repeated = new StringBuilder(value.length() * count); + for (int i = 0; i < count; i++) { + repeated.append(value); + } + return repeated.toString(); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3COtelTraceStateContinuationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3COtelTraceStateContinuationTest.java new file mode 100644 index 00000000000..4c27fdeb7fb --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3COtelTraceStateContinuationTest.java @@ -0,0 +1,80 @@ +package datadog.trace.core.propagation; + +import static datadog.trace.bootstrap.instrumentation.api.ContextVisitors.stringValuesMap; +import static datadog.trace.core.propagation.HttpCodecTestHelper.headers; +import static datadog.trace.core.propagation.W3CHttpCodec.TRACE_PARENT_KEY; +import static datadog.trace.core.propagation.W3CHttpCodec.TRACE_STATE_KEY; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.Config; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.DDSpanContext; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Exercises the {@code ot=} tracestate member through the real {@link W3CHttpCodec} extractor and + * injector, continuing an inbound trace rather than parsing tracestate or building an {@link + * ExtractedContext} by hand. + */ +class W3COtelTraceStateContinuationTest extends DDCoreJavaSpecification { + + private static final String TRACE_PARENT = + "00-00000000000000000000000000000001-123456789abcdef0-01"; + private static final String OTEL_RANDOM_VALUE = "ef284ace7a91e1"; + private static final String OTEL_THRESHOLD = "e6666666666668"; + private static final String OTEL_MALFORMED_RANDOM_VALUE = "zz"; + private static final String DD_MEMBER = "dd=s:2;p:123456789abcdef0"; + + private final HttpCodec.Injector injector = W3CHttpCodec.newInjector(emptyMap()); + + @Test + void roundTripsOtelTraceState() { + String inboundTracestate = DD_MEMBER + ",ot=rv:" + OTEL_RANDOM_VALUE + ";th:" + OTEL_THRESHOLD; + String outboundTracestate = continueTraceAndReinject(inboundTracestate); + + assertTrue(outboundTracestate.contains("ot=rv:" + OTEL_RANDOM_VALUE + ";th:" + OTEL_THRESHOLD)); + } + + @Test + void removesMalformedOtelRandomValueButKeepsThreshold() { + String inboundTracestate = + DD_MEMBER + ",ot=rv:" + OTEL_MALFORMED_RANDOM_VALUE + ";th:" + OTEL_THRESHOLD; + String outboundTracestate = continueTraceAndReinject(inboundTracestate); + + assertTrue(outboundTracestate.contains("ot=th:" + OTEL_THRESHOLD)); + assertTrue(!outboundTracestate.contains("rv:" + OTEL_MALFORMED_RANDOM_VALUE)); + } + + private String continueTraceAndReinject(String inboundTracestate) { + Map inboundHeaders = + headers(TRACE_PARENT_KEY, TRACE_PARENT, TRACE_STATE_KEY, inboundTracestate); + + CoreTracer tracer = tracerBuilder().build(); + try { + HttpCodec.Extractor extractor = + W3CHttpCodec.newExtractor(Config.get(), tracer::captureTraceConfig); + Object extracted = extractor.extract(inboundHeaders, stringValuesMap()); + assertInstanceOf(ExtractedContext.class, extracted); + ExtractedContext extractedContext = (ExtractedContext) extracted; + + AgentSpan continuedSpan = + tracer.buildSpan("test", "continued").asChildOf(extractedContext).start(); + assertEquals(extractedContext.getSamplingPriority(), continuedSpan.getSamplingPriority()); + + Map outboundHeaders = new HashMap<>(); + this.injector.inject((DDSpanContext) continuedSpan.spanContext(), outboundHeaders, Map::put); + + continuedSpan.finish(); + return outboundHeaders.get(TRACE_STATE_KEY); + } finally { + tracer.close(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java new file mode 100644 index 00000000000..f19279d24f0 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateTest.java @@ -0,0 +1,152 @@ +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.params.provider.Arguments.arguments; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class OtelTraceStateTest { + + // A stable trace ID covers sampling decisions at representative rates. + private static final long TRACE_ID = 1; + private static final long OTEL_SPEC_WORKED_EXAMPLE_TRACE_ID = 0xfff972474538efffL; + private static final String LOCAL_RANDOM_VALUE = "f0948a54d43b8e"; + private static final double SAMPLE_RATE_0_01 = 0.01; + private static final String THRESHOLD_0_01 = "fd70a3d70a3d7"; + private static final double SAMPLE_RATE_0_1 = 0.1; + private static final String THRESHOLD_0_1 = "e6666666666668"; + private static final double SAMPLE_RATE_0_2 = 0.2; + private static final String THRESHOLD_0_2 = "ccccccccccccd"; + private static final double SAMPLE_RATE_0_5 = 0.5; + private static final String THRESHOLD_0_5 = "8"; + private static final double SAMPLE_RATE_0_99 = 0.99; + private static final String THRESHOLD_0_99 = "028f5c28f5c29"; + private static final double SAMPLE_RATE_0_05 = 0.05; + + // These IDs straddle thresholds that would otherwise lose precision when rounded. + private static final long KEEP_PRECISION_BOUNDARY_TRACE_ID = 0x03A93EE8B1999F00L; + private static final String KEEP_PRECISION_BOUNDARY_VALUE = "e6666666666668"; + private static final long DROP_PRECISION_BOUNDARY_TRACE_ID = 5401449561355763072L; + private static final String DROP_PRECISION_BOUNDARY_RANDOM_VALUE = "f333333333332f"; + private static final String DROP_PRECISION_BOUNDARY_THRESHOLD = "f333333333333"; + private static final String INHERITED_RANDOM_VALUE = "ef284ace7a91e1"; + private static final String UNKNOWN_FIELD = "foo:bar"; + private static final int INHERITED_POSITION = 2; + private static final String TINY_POSITIVE_THRESHOLD = "ffffffffffffff"; + + @ParameterizedTest + @MethodSource("goldenSamplingVectors") + void createsGoldenSamplingState(double sampleRate, boolean sampled, String expectedTraceState) { + OtelTraceState state = + OtelTraceState.updateProbability( + null, TRACE_ID, sampleRate, sampled, sampled ? SAMPLER_KEEP : SAMPLER_DROP); + + assertEquals(expectedTraceState, state.getValue()); + } + + static Stream goldenSamplingVectors() { + return Stream.of( + arguments(SAMPLE_RATE_0_01, false, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_01)), + arguments(SAMPLE_RATE_0_1, true, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_1)), + arguments(SAMPLE_RATE_0_2, true, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_2)), + arguments(SAMPLE_RATE_0_5, true, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_5)), + arguments(SAMPLE_RATE_0_99, true, traceState(LOCAL_RANDOM_VALUE, THRESHOLD_0_99))); + } + + @Test + void matchesOtelSpecWorkedExample() { + OtelTraceState state = + OtelTraceState.updateProbability( + null, OTEL_SPEC_WORKED_EXAMPLE_TRACE_ID, SAMPLE_RATE_0_1, true, SAMPLER_KEEP); + + assertEquals(traceState(INHERITED_RANDOM_VALUE, THRESHOLD_0_1), state.getValue()); + } + + @Test + void emitsMaxThresholdAtRateZero() { + OtelTraceState state = OtelTraceState.updateProbability(null, TRACE_ID, 0, false, SAMPLER_DROP); + + assertEquals(traceState(LOCAL_RANDOM_VALUE, TINY_POSITIVE_THRESHOLD), state.getValue()); + } + + @Test + void clampsTinyPositiveRateThreshold() { + OtelTraceState state = + OtelTraceState.updateProbability(null, TRACE_ID, Double.MIN_VALUE, false, SAMPLER_DROP); + + assertEquals(traceState(LOCAL_RANDOM_VALUE, TINY_POSITIVE_THRESHOLD), state.getValue()); + } + + @Test + void correctsKeepAtPrecisionBoundary() { + OtelTraceState state = + OtelTraceState.updateProbability( + null, KEEP_PRECISION_BOUNDARY_TRACE_ID, SAMPLE_RATE_0_1, true, SAMPLER_KEEP); + + assertEquals( + traceState(KEEP_PRECISION_BOUNDARY_VALUE, KEEP_PRECISION_BOUNDARY_VALUE), state.getValue()); + } + + @Test + void correctsDropAtPrecisionBoundary() { + OtelTraceState state = + OtelTraceState.updateProbability( + null, DROP_PRECISION_BOUNDARY_TRACE_ID, SAMPLE_RATE_0_05, false, SAMPLER_DROP); + + assertEquals( + traceState(DROP_PRECISION_BOUNDARY_RANDOM_VALUE, DROP_PRECISION_BOUNDARY_THRESHOLD), + state.getValue()); + } + + @Test + void limiterRejectionRetainsLocallyGeneratedRandomness() { + OtelTraceState state = + OtelTraceState.updateProbability( + OtelTraceState.parse(UNKNOWN_FIELD, INHERITED_POSITION), + TRACE_ID, + SAMPLE_RATE_0_5, + true, + SAMPLER_KEEP); + + state = OtelTraceState.updateProbability(state, TRACE_ID, SAMPLE_RATE_0_5, true, SAMPLER_DROP); + + assertEquals("rv:" + LOCAL_RANDOM_VALUE + ";" + UNKNOWN_FIELD, state.getValue()); + assertEquals(0, state.getInheritedPosition()); + } + + @Test + void limiterRejectionRetainsInheritedRandomness() { + OtelTraceState state = + OtelTraceState.parse( + traceState(INHERITED_RANDOM_VALUE, THRESHOLD_0_5) + ";" + UNKNOWN_FIELD, + INHERITED_POSITION); + + state = OtelTraceState.updateProbability(state, TRACE_ID, SAMPLE_RATE_0_5, true, SAMPLER_DROP); + + assertEquals("rv:" + INHERITED_RANDOM_VALUE + ";" + UNKNOWN_FIELD, state.getValue()); + assertEquals(0, state.getInheritedPosition()); + } + + @Test + void nonProbabilityDecisionRetainsOnlyInheritedRandomnessAndUnknownFields() { + OtelTraceState state = + OtelTraceState.parse( + "th:" + THRESHOLD_0_1 + ";" + UNKNOWN_FIELD + ";rv:" + INHERITED_RANDOM_VALUE, + INHERITED_POSITION); + + state = state.removeForNonProbabilityDecision(); + + assertEquals("rv:" + INHERITED_RANDOM_VALUE + ";" + UNKNOWN_FIELD, state.getValue()); + assertEquals(0, state.getInheritedPosition()); + } + + private static String traceState(String randomValue, String threshold) { + return "rv:" + randomValue + ";th:" + threshold; + } +} diff --git a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java index b349e937e68..8b3e53b32f2 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, + int samplingMechanism, + Boolean probabilitySamplingResult) { return this; }