diff --git a/ddtrace/internal/sampling.py b/ddtrace/internal/sampling.py index 53a63dc2770..8a2706c7e0b 100644 --- a/ddtrace/internal/sampling.py +++ b/ddtrace/internal/sampling.py @@ -1,4 +1,5 @@ import json +import math from typing import Any from typing import Optional from typing import TypedDict @@ -46,6 +47,17 @@ class PriorityCategory(object): KNUTH_SAMPLE_RATE_KEY = "_dd.p.ksr" + +def _format_ksr(rate: float) -> str: + """Format a sampling rate for _dd.p.ksr: up to 6 decimal digits, trailing zeros stripped. + + Uses explicit rounding via math.floor(x + 0.5) to avoid Python's banker's rounding + which would round 0.0000005 down to 0 instead of up to 0.000001. + """ + rounded = math.floor(rate * 1e6 + 0.5) / 1e6 + return f"{rounded:.6f}".rstrip("0").rstrip(".") + + SpanSamplingRules = TypedDict( "SpanSamplingRules", { @@ -243,10 +255,10 @@ def _set_sampling_tags(span: Span, sampled: bool, sample_rate: float, mechanism: SamplingMechanism.REMOTE_DYNAMIC_TRACE_SAMPLING_RULE, ): span._set_attribute(_SAMPLING_RULE_DECISION, sample_rate) - span._set_attribute(KNUTH_SAMPLE_RATE_KEY, f"{sample_rate:.6g}") + span._set_attribute(KNUTH_SAMPLE_RATE_KEY, _format_ksr(sample_rate)) elif mechanism == SamplingMechanism.AGENT_RATE_BY_SERVICE: span._set_attribute(_SAMPLING_AGENT_DECISION, sample_rate) - span._set_attribute(KNUTH_SAMPLE_RATE_KEY, f"{sample_rate:.6g}") + span._set_attribute(KNUTH_SAMPLE_RATE_KEY, _format_ksr(sample_rate)) # Set the sampling priority priorities = SAMPLING_MECHANISM_TO_PRIORITIES[mechanism] priority_index = _KEEP_PRIORITY_INDEX if sampled else _REJECT_PRIORITY_INDEX diff --git a/releasenotes/notes/fix-ksr-scientific-notation-e7d1adabe548eb40.yaml b/releasenotes/notes/fix-ksr-scientific-notation-e7d1adabe548eb40.yaml new file mode 100644 index 00000000000..42c78b185c6 --- /dev/null +++ b/releasenotes/notes/fix-ksr-scientific-notation-e7d1adabe548eb40.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + tracing: Fix ``_dd.p.ksr`` span tag formatting for very small sampling rates. + Previously, rates below 0.001 could be output in scientific notation (e.g. + ``1e-06``). Now always uses decimal notation with up to 6 decimal digits. diff --git a/tests/tracer/test_sampler.py b/tests/tracer/test_sampler.py index 9c43c5d60d7..a4c7d59273f 100644 --- a/tests/tracer/test_sampler.py +++ b/tests/tracer/test_sampler.py @@ -889,3 +889,32 @@ def span(): def test_trace_tag(span, sampling_mechanism, expected): span._set_sampling_decision_maker(sampling_mechanism) assert span.context._meta["_dd.p.dm"] == expected + + +@pytest.mark.parametrize( + "sample_rate,expected_ksr", + [ + (1.0, "1"), + (0.5, "0.5"), + (0.000001, "0.000001"), + (0.0000001, "0"), + (0.0000005, "0.000001"), + (0.7654321, "0.765432"), + ], + ids=[ + "rate_1_strips_trailing_zeros", + "simple_rate", + "six_decimal_precision_boundary", + "below_precision_rounds_to_zero", + "rounds_up_to_one_millionth", + "truncation_at_six_decimals", + ], +) +def test_ksr_formatting(span, sample_rate, expected_ksr): + """_dd.p.ksr is formatted with up to 6 decimal digits, trailing zeros stripped, no scientific notation.""" + from ddtrace.internal.sampling import KNUTH_SAMPLE_RATE_KEY + from ddtrace.internal.sampling import SamplingMechanism + from ddtrace.internal.sampling import _set_sampling_tags + + _set_sampling_tags(span, True, sample_rate, SamplingMechanism.LOCAL_USER_TRACE_SAMPLING_RULE) + assert span._meta.get(KNUTH_SAMPLE_RATE_KEY) == expected_ksr