From 9f7317ece105125f1a7a95c55384abe939ae681c Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Thu, 11 Sep 2025 16:55:57 -0400 Subject: [PATCH 1/4] WIP: chore(sampling): track knuth sampling in distributed trace --- .../common/sampling/RateByServiceTraceSampler.java | 10 ++++++++++ .../trace/common/sampling/RuleBasedTraceSampler.java | 10 ++++++++++ 2 files changed, 20 insertions(+) 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 52957b643a0..72f3a9c9d4a 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 @@ -22,6 +22,7 @@ public class RateByServiceTraceSampler implements Sampler, PrioritySampler, Remo private static final Logger log = LoggerFactory.getLogger(RateByServiceTraceSampler.class); public static final String SAMPLING_AGENT_RATE = "_dd.agent_psr"; + public static final String KNUTH_SAMPLING_RATE = "_dd.p.ksr"; private static final double DEFAULT_RATE = 1.0; @@ -56,12 +57,21 @@ public > void setSamplingPriority(final T span) { sampler.getSampleRate(), SamplingMechanism.AGENT_RATE); } + + // Set Knuth sampling rate tag + String ksrRate = formatKnuthSamplingRate(sampler.getSampleRate()); + span.setTag(KNUTH_SAMPLING_RATE, ksrRate); } private > String getSpanEnv(final T span) { return span.getTag("env", ""); } + private String formatKnuthSamplingRate(double rate) { + // Format to up to 6 decimal places, removing trailing zeros + return String.format("%.6f", rate).replaceAll("0*$", "").replaceAll("\\.$", ""); + } + @Override public void onResponse( final String endpoint, final Map> responseJson) { 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..ff176a9302f 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 @@ -22,6 +22,7 @@ public class RuleBasedTraceSampler> implements Sampler, Pr public static final String SAMPLING_RULE_RATE = "_dd.rule_psr"; public static final String SAMPLING_LIMIT_RATE = "_dd.limit_psr"; + public static final String KNUTH_SAMPLING_RATE = "_dd.p.ksr"; public RuleBasedTraceSampler( final List samplingRules, @@ -168,6 +169,15 @@ public > void setSamplingPriority(final T span) { matchedRule.getSampler().getSampleRate(), matchedRule.getMechanism()); } + + // Set Knuth sampling rate tag + String ksrRate = formatKnuthSamplingRate(matchedRule.getSampler().getSampleRate()); + span.setTag(KNUTH_SAMPLING_RATE, ksrRate); } } + + private String formatKnuthSamplingRate(double rate) { + // Format to up to 6 decimal places, removing trailing zeros + return String.format("%.6f", rate).replaceAll("0*$", "").replaceAll("\\.$", ""); + } } From b6e1df9c7ae0c79f6052807267e4afdb858572e9 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Mon, 15 Sep 2025 15:58:22 -0400 Subject: [PATCH 2/4] lint build errors --- .../common/sampling/RateByServiceTraceSampler.java | 11 ++++++++++- .../trace/common/sampling/RuleBasedTraceSampler.java | 12 +++++++++++- 2 files changed, 21 insertions(+), 2 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 72f3a9c9d4a..68b817cb39b 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 @@ -6,7 +6,10 @@ import datadog.trace.api.sampling.SamplingMechanism; import datadog.trace.common.writer.RemoteResponseListener; import datadog.trace.core.CoreSpan; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.Collections; +import java.util.Locale; import java.util.Map; import java.util.TreeMap; import java.util.function.Function; @@ -25,6 +28,12 @@ public class RateByServiceTraceSampler implements Sampler, PrioritySampler, Remo public static final String KNUTH_SAMPLING_RATE = "_dd.p.ksr"; private static final double DEFAULT_RATE = 1.0; + private static final DecimalFormat DECIMAL_FORMAT; + + static { + DECIMAL_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + DECIMAL_FORMAT.setMaximumFractionDigits(6); + } private volatile RateSamplersByEnvAndService serviceRates = new RateSamplersByEnvAndService(); @@ -69,7 +78,7 @@ private > String getSpanEnv(final T span) { private String formatKnuthSamplingRate(double rate) { // Format to up to 6 decimal places, removing trailing zeros - return String.format("%.6f", rate).replaceAll("0*$", "").replaceAll("\\.$", ""); + return DECIMAL_FORMAT.format(rate); } @Override 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 ff176a9302f..0ce9f3d1acf 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 @@ -5,8 +5,11 @@ import datadog.trace.api.sampling.SamplingRule; import datadog.trace.core.CoreSpan; import datadog.trace.core.util.SimpleRateLimiter; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import org.slf4j.Logger; @@ -15,6 +18,13 @@ public class RuleBasedTraceSampler> implements Sampler, PrioritySampler { private static final Logger log = LoggerFactory.getLogger(RuleBasedTraceSampler.class); + private static final DecimalFormat DECIMAL_FORMAT; + + static { + DECIMAL_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + DECIMAL_FORMAT.setMaximumFractionDigits(6); + } + private final List samplingRules; private final PrioritySampler fallbackSampler; private final SimpleRateLimiter rateLimiter; @@ -178,6 +188,6 @@ public > void setSamplingPriority(final T span) { private String formatKnuthSamplingRate(double rate) { // Format to up to 6 decimal places, removing trailing zeros - return String.format("%.6f", rate).replaceAll("0*$", "").replaceAll("\\.$", ""); + return DECIMAL_FORMAT.format(rate); } } From c6ef17347624786de7ab18bff6421efa1bd057c5 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Mon, 15 Sep 2025 16:00:03 -0400 Subject: [PATCH 3/4] lint build errors --- .../trace/common/sampling/RateByServiceTraceSampler.java | 2 +- .../datadog/trace/common/sampling/RuleBasedTraceSampler.java | 4 ++-- 2 files changed, 3 insertions(+), 3 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 68b817cb39b..654e646be5c 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 @@ -29,7 +29,7 @@ public class RateByServiceTraceSampler implements Sampler, PrioritySampler, Remo private static final double DEFAULT_RATE = 1.0; private static final DecimalFormat DECIMAL_FORMAT; - + static { DECIMAL_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); DECIMAL_FORMAT.setMaximumFractionDigits(6); 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 0ce9f3d1acf..3cecf68d9bc 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 @@ -19,12 +19,12 @@ public class RuleBasedTraceSampler> implements Sampler, Pr private static final Logger log = LoggerFactory.getLogger(RuleBasedTraceSampler.class); private static final DecimalFormat DECIMAL_FORMAT; - + static { DECIMAL_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); DECIMAL_FORMAT.setMaximumFractionDigits(6); } - + private final List samplingRules; private final PrioritySampler fallbackSampler; private final SimpleRateLimiter rateLimiter; From 2ac621f164a8f0963ca7c062a609ce2b7ecff136 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Wed, 1 Oct 2025 16:46:37 -0400 Subject: [PATCH 4/4] fix formatting --- .../agent/test/asserts/TagsAssert.groovy | 1 + .../common/sampling/DeterministicSampler.java | 38 ++++++++++++ .../trace/common/sampling/RateSampler.java | 7 +++ .../sampling/RuleBasedTraceSampler.java | 17 +----- .../DeterministicTraceSamplerTest.groovy | 58 +++++++++++++++++++ 5 files changed, 105 insertions(+), 16 deletions(-) diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy index 4179677a12a..bc68e73f63f 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy @@ -86,6 +86,7 @@ class TagsAssert { assertedTags.add(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) assertedTags.add(TraceMapper.SAMPLING_PRIORITY_KEY.toString()) assertedTags.add("_sample_rate") + assertedTags.add(RateByServiceTraceSampler.KNUTH_SAMPLING_RATE) assertedTags.add(DDTags.PID_TAG) assertedTags.add(DDTags.SCHEMA_VERSION_TAG_KEY) assertedTags.add(DDTags.PROFILING_ENABLED) diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/DeterministicSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/DeterministicSampler.java index c1da7cf25d5..5efb23a480e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/DeterministicSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/DeterministicSampler.java @@ -40,10 +40,12 @@ protected > long getSamplingId(T span) { private final float rate; private final long threshold; + private final String knuthSampleRate; public DeterministicSampler(final double rate) { this.rate = (float) rate; this.threshold = cutoff(rate); + this.knuthSampleRate = formatKnuthSamplingRate(rate); } @Override @@ -59,6 +61,11 @@ public double getSampleRate() { return rate; } + @Override + public String getKnuthSampleRate() { + return knuthSampleRate; + } + public static long cutoff(double rate) { if (rate < 0.5) { return (long) (rate * MAX) + Long.MIN_VALUE; @@ -68,4 +75,35 @@ public static long cutoff(double rate) { } return Long.MAX_VALUE; } + + /** + * Thread-safe custom formatter for Knuth sampling rates. Formats rates to up to 6 decimal places, + * removing trailing zeros. Assumes the value is between 0 and 1 (inclusive). + */ + private static String formatKnuthSamplingRate(double value) { + if (value <= 0) { + return "0"; + } else if (value >= 1) { + return "1"; + } else { + // Scale to 6 decimal places and round + long scaled = Math.round(value * 1_000_000); + + // Handle rounding to 1.0 case + if (scaled >= 1_000_000) { + return "1"; + } + + // Convert back to string with proper decimal formatting + String result = "0." + String.format("%06d", scaled); + + // Remove trailing zeros + int endIndex = result.length(); + while (endIndex > 2 && result.charAt(endIndex - 1) == '0') { + endIndex--; + } + + return result.substring(0, endIndex); + } + } } diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateSampler.java index d8691a66426..24b37b1a4d6 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateSampler.java @@ -2,4 +2,11 @@ public interface RateSampler extends Sampler { double getSampleRate(); + + /** + * Returns the pre-formatted Knuth sampling rate as a string. This is computed once at + * construction time to avoid thread-safety issues and performance overhead of formatting on each + * sampling operation. + */ + String getKnuthSampleRate(); } 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 3cecf68d9bc..1d38e6cd9c0 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 @@ -5,11 +5,8 @@ import datadog.trace.api.sampling.SamplingRule; import datadog.trace.core.CoreSpan; import datadog.trace.core.util.SimpleRateLimiter; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import org.slf4j.Logger; @@ -18,12 +15,6 @@ public class RuleBasedTraceSampler> implements Sampler, PrioritySampler { private static final Logger log = LoggerFactory.getLogger(RuleBasedTraceSampler.class); - private static final DecimalFormat DECIMAL_FORMAT; - - static { - DECIMAL_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); - DECIMAL_FORMAT.setMaximumFractionDigits(6); - } private final List samplingRules; private final PrioritySampler fallbackSampler; @@ -181,13 +172,7 @@ public > void setSamplingPriority(final T span) { } // Set Knuth sampling rate tag - String ksrRate = formatKnuthSamplingRate(matchedRule.getSampler().getSampleRate()); - span.setTag(KNUTH_SAMPLING_RATE, ksrRate); + span.setTag(KNUTH_SAMPLING_RATE, matchedRule.getSampler().getKnuthSampleRate()); } } - - private String formatKnuthSamplingRate(double rate) { - // Format to up to 6 decimal places, removing trailing zeros - return DECIMAL_FORMAT.format(rate); - } } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicTraceSamplerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicTraceSamplerTest.groovy index daa8240cd92..438fc9dd6db 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicTraceSamplerTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicTraceSamplerTest.groovy @@ -376,4 +376,62 @@ class DeterministicTraceSamplerTest extends DDSpecification { where: rate << (0..100) } + + def "test getKnuthSampleRate formatting: #rate -> #expected"() { + given: + DeterministicSampler sampler = new DeterministicSampler.TraceSampler(rate) + + when: + String formatted = sampler.getKnuthSampleRate() + + then: + formatted == expected + + where: + rate | expected + 0.0 | "0" + 0.000001 | "0.000001" + 0.000010 | "0.00001" + 0.000100 | "0.0001" + 0.001000 | "0.001" + 0.010000 | "0.01" + 0.100000 | "0.1" + 0.123456 | "0.123456" + 0.123450 | "0.12345" + 0.123400 | "0.1234" + 0.123000 | "0.123" + 0.120000 | "0.12" + 0.100000 | "0.1" + 0.500000 | "0.5" + 0.999999 | "0.999999" + 1.0 | "1" + 1.5 | "1" // Values > 1 are clamped to 1 + -0.5 | "0" // Values < 0 are clamped to 0 + } + + def "test getKnuthSampleRate precision and rounding"() { + given: + // Test edge cases around rounding + DeterministicSampler sampler1 = new DeterministicSampler.TraceSampler(0.1234564) // Should round down + DeterministicSampler sampler2 = new DeterministicSampler.TraceSampler(0.1234565) // Should round up + DeterministicSampler sampler3 = new DeterministicSampler.TraceSampler(0.9999995) // Should round up to 1 + + expect: + sampler1.getKnuthSampleRate() == "0.123456" + sampler2.getKnuthSampleRate() == "0.123457" + sampler3.getKnuthSampleRate() == "1" + } + + def "test getKnuthSampleRate is consistent"() { + given: + DeterministicSampler sampler = new DeterministicSampler.TraceSampler(0.123456) + + when: + String rate1 = sampler.getKnuthSampleRate() + String rate2 = sampler.getKnuthSampleRate() + + then: + rate1 == rate2 + rate1 == "0.123456" + } }