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 dd7647676ae..3f24dad8eb5 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 @@ -271,24 +271,38 @@ class OpenTelemetryTest extends InstrumentationSpecification { httpPropagator.inject(context, textMap, new TextMapSetter()) then: - def expectedTraceparent = "00-${span.delegate.traceId.toHexStringPadded(32)}" + - "-${DDSpanId.toHexStringPadded(span.delegate.spanId)}" + + def traceId = span.delegate.traceId as DDTraceId + def spanId = span.delegate.spanId + def expectedTraceparent = "00-${traceId.toHexStringPadded(32)}" + + "-${DDSpanId.toHexStringPadded(spanId)}" + "-" + (propagatedPriority > 0 ? "01" : "00") - def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(span.delegate.spanId)}" - def expectedDatadogTags = null + def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(spanId)}" + def expectedDataTags = [] if (propagatedMechanism != UNKNOWN) { - expectedDatadogTags = "_dd.p.dm=-" + propagatedMechanism - expectedTracestate+= ";t.dm:-" + propagatedMechanism + expectedDataTags << "_dd.p.dm=-" + propagatedMechanism + expectedTracestate += ";t.dm:-" + propagatedMechanism + } + if (traceId.toHighOrderLong() != 0) { + expectedTracestate += ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}" + } + if (contextPriority == UNSET) { + expectedTracestate += ";t.ksr:1" + } + if (traceId.toHighOrderLong() != 0) { + expectedDataTags << "_dd.p.tid=" + traceId.toHexStringPadded(32).substring(0, 16) + } + if (contextPriority == UNSET) { + expectedDataTags << "_dd.p.ksr=1" } def expectedTextMap = [ - "x-datadog-trace-id" : "$span.delegate.traceId", - "x-datadog-parent-id" : "$span.delegate.spanId", + "x-datadog-trace-id" : "$traceId", + "x-datadog-parent-id" : "$spanId", "x-datadog-sampling-priority": propagatedPriority.toString(), "traceparent" : expectedTraceparent, "tracestate" : expectedTracestate, ] - if (expectedDatadogTags != null) { - expectedTextMap.put("x-datadog-tags", expectedDatadogTags) + if (!expectedDataTags.empty) { + expectedTextMap.put("x-datadog-tags", expectedDataTags.join(',')) } textMap == expectedTextMap diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy index 061138bb76e..43355b189cb 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy @@ -37,6 +37,9 @@ class DatadogPropagatorTest extends AgentPropagatorTest { if (traceId.length() == 32) { tags+= '_dd.p.tid='+ traceId.substring(0, 16) } + if (sampling == UNSET) { + tags+= '_dd.p.ksr=1' + } assert headers['x-datadog-tags'] == tags.join(',') assert headers['x-datadog-sampling-priority'] == samplingPriority } 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 e17b06ad232..2c41794bcaa 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 @@ -284,13 +284,21 @@ class OpenTracing31Test extends InstrumentationSpecification { "-${DDSpanId.toHexStringPadded(context.delegate.spanId)}" + "-" + (propagatedPriority > 0 ? "01" : "00") def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(context.delegate.spanId)}" - def expectedDatadogTags = null + def datadogTags = [] if (propagatedPriority > 0) { def effectiveSamplingMechanism = contextPriority == UNSET ? AGENT_RATE : samplingMechanism - expectedDatadogTags = "_dd.p.dm=-" + effectiveSamplingMechanism - expectedTracestate+= ";t.dm:-" + effectiveSamplingMechanism + datadogTags << "_dd.p.dm=-" + effectiveSamplingMechanism + expectedTracestate += ";t.dm:-" + effectiveSamplingMechanism + } + def traceId = context.delegate.traceId as DDTraceId + if (traceId.toHighOrderLong() != 0) { + expectedTracestate += ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}" + datadogTags << "_dd.p.tid=" + traceId.toHexStringPadded(32).substring(0, 16) + } + if (contextPriority == UNSET) { + expectedTracestate += ";t.ksr:1" + datadogTags << "_dd.p.ksr=1" } - def expectedTextMap = [ "x-datadog-trace-id" : "$context.delegate.traceId", "x-datadog-parent-id" : "$context.delegate.spanId", @@ -298,8 +306,8 @@ class OpenTracing31Test extends InstrumentationSpecification { "traceparent" : expectedTraceparent, "tracestate" : expectedTracestate, ] - if (expectedDatadogTags != null) { - expectedTextMap.put("x-datadog-tags", expectedDatadogTags) + if (!datadogTags.empty) { + expectedTextMap.put("x-datadog-tags", datadogTags.join(',')) } textMap == 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 09e01fad297..e262f947a14 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 @@ -2,6 +2,7 @@ import datadog.trace.agent.test.InstrumentationSpecification import datadog.trace.api.DDSpanId import datadog.trace.api.DDTags import datadog.trace.api.DDTraceId +import datadog.trace.api.internal.util.LongStringUtils import datadog.trace.api.interceptor.MutableSpan import datadog.trace.bootstrap.instrumentation.api.AgentTracer import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities @@ -299,12 +300,21 @@ class OpenTracing32Test extends InstrumentationSpecification { "-${DDSpanId.toHexStringPadded(context.delegate.spanId)}" + "-" + (propagatedPriority > 0 ? "01" : "00") def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(context.delegate.spanId)}" - def expectedDatadogTags = null + def datadogTags = [] if (propagatedPriority > 0) { def effectiveSamplingMechanism = contextPriority == UNSET ? AGENT_RATE : samplingMechanism - expectedDatadogTags = "_dd.p.dm=-" + effectiveSamplingMechanism + datadogTags << "_dd.p.dm=-" + effectiveSamplingMechanism expectedTracestate+= ";t.dm:-" + effectiveSamplingMechanism } + def traceId = context.delegate.traceId as DDTraceId + if (traceId.toHighOrderLong() != 0) { + expectedTracestate+= ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}" + datadogTags << "_dd.p.tid=" + LongStringUtils.toHexStringPadded(traceId.toHighOrderLong(), 16) + } + if (contextPriority == UNSET) { + expectedTracestate+= ";t.ksr:1" + datadogTags << "_dd.p.ksr=1" + } def expectedTextMap = [ "x-datadog-trace-id" : "$context.delegate.traceId", "x-datadog-parent-id" : "$context.delegate.spanId", @@ -312,8 +322,8 @@ class OpenTracing32Test extends InstrumentationSpecification { "traceparent" : expectedTraceparent, "tracestate" : expectedTracestate ] - if (expectedDatadogTags != null) { - expectedTextMap.put("x-datadog-tags", expectedDatadogTags) + if (!datadogTags.empty) { + expectedTextMap.put("x-datadog-tags", datadogTags.join(',')) } textMap == expectedTextMap diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/ptags/KnuthSamplingRateFormatBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/ptags/KnuthSamplingRateFormatBenchmark.java new file mode 100644 index 00000000000..61577dca41f --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/ptags/KnuthSamplingRateFormatBenchmark.java @@ -0,0 +1,93 @@ +package datadog.trace.core.propagation.ptags; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import datadog.trace.core.propagation.PropagationTags; +import java.util.Locale; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Benchmarks for formatting the Knuth sampling rate (_dd.p.ksr tag value). + * + *
The format requirement is %.6g semantics: 6 significant figures, no trailing zeros, using + * fixed notation for values in [1e-4, 1] and scientific notation for smaller values. + * + *
Run with: + * + *
+ * ./gradlew :dd-trace-core:jmhJar + * java -jar dd-trace-core/build/libs/dd-trace-core-*-jmh.jar KnuthSamplingRateFormatBenchmark + *+ */ +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 10, timeUnit = SECONDS) +@Measurement(iterations = 5, time = 10, timeUnit = SECONDS) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(NANOSECONDS) +@Fork(value = 1) +public class KnuthSamplingRateFormatBenchmark { + + /** + * Representative sampling rates. Most real-world rates are in [0.001, 1.0]. The 0.0001 value + * exercises the edge of the fixed-notation range. + */ + @Param({"0.5", "0.1", "0.01", "0.001", "0.0001", "0.123456789", "0.999999"}) + double rate; + + PTagsFactory.PTags ptags; + + @Setup(Level.Trial) + public void setUp() { + ptags = (PTagsFactory.PTags) PropagationTags.factory().empty(); + ptags.updateKnuthSamplingRate(rate); + } + + /** Baseline: old implementation using String.format + substring trimming. */ + @Benchmark + public void stringFormat(Blackhole bh) { + bh.consume(stringFormatImpl(rate)); + } + + /** Custom formatter: char-array arithmetic, no Formatter allocation. */ + @Benchmark + public void customFormat(Blackhole bh) { + bh.consume(PTagsFactory.PTags.formatKnuthSamplingRate(rate)); + } + + /** Cached TagValue: the full getKnuthSamplingRateTagValue() hot-path after caching. */ + @Benchmark + public void cachedTagValue(Blackhole bh) { + bh.consume(ptags.getKnuthSamplingRateTagValue()); + } + + // ---- old implementation for comparison ---- + + static String stringFormatImpl(double rate) { + String formatted = String.format(Locale.ROOT, "%.6g", rate); + int dotIndex = formatted.indexOf('.'); + if (dotIndex >= 0) { + int end = formatted.length(); + while (end > dotIndex + 1 && formatted.charAt(end - 1) == '0') { + end--; + } + if (formatted.charAt(end - 1) == '.') { + end--; + } + formatted = formatted.substring(0, end); + } + return formatted; + } +} 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 d460d5ea1bb..772cbe90866 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,12 @@ public DDSpan setSamplingPriority( int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { if (context.setSamplingPriority(samplingPriority, samplingMechanism)) { 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/propagation/PropagationTags.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java index aea99b16f77..05576f9cceb 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 @@ -130,6 +130,16 @@ public interface Factory { public abstract String getDebugPropagation(); + /** + * Updates the Knuth sampling rate (_dd.p.ksr) propagated tag. This records the sampling rate that + * was applied when making an agent-based or rule-based sampling decision. The rate is formatted + * with up to 6 significant digits and no trailing zeros, matching the Go/Python reference + * implementations (%.6g format). + * + * @param rate the sampling rate value + */ + public abstract void updateKnuthSamplingRate(double rate); + public HashMap
Uses char-array arithmetic to avoid {@link java.util.Formatter} allocations entirely.
+ */
+ static String formatKnuthSamplingRate(double rate) {
+ if (rate <= 0.0) return "0";
+ if (rate >= 1.0) return "1";
+
+ if (rate < 1e-4) {
+ return formatScientific6g(rate);
+ }
+
+ return formatFixed6g(rate);
+ }
+
+ /** Fixed notation for rates in [1e-4, 1): "0.DDDDDDDDD" with trailing zeros trimmed. */
+ private static String formatFixed6g(double rate) {
+ // Choose a multiplier so Math.round(rate * multiplier) is a 6-significant-figure integer.
+ // For rate in [10^-k, 10^-(k-1)) the first sig fig is at decimal position k, so we need
+ // k+5 total fractional digits:
+ // [0.1, 1.0) -> scale=6, multiplier=1e6
+ // [0.01, 0.1) -> scale=7, multiplier=1e7
+ // [0.001, 0.01) -> scale=8, multiplier=1e8
+ // [1e-4, 0.001) -> scale=9, multiplier=1e9
+ final int scale;
+ final long multiplier;
+ if (rate >= 0.1) {
+ scale = 6;
+ multiplier = 1_000_000L;
+ } else if (rate >= 0.01) {
+ scale = 7;
+ multiplier = 10_000_000L;
+ } else if (rate >= 0.001) {
+ scale = 8;
+ multiplier = 100_000_000L;
+ } else {
+ scale = 9;
+ multiplier = 1_000_000_000L;
+ }
+
+ long rounded = Math.round(rate * multiplier);
+ if (rounded == 0) return "0";
+ if (rounded >= multiplier) return "1"; // rounding pushed value to 1.0
+
+ // Build "0." +