Skip to content

Commit c3d0156

Browse files
committed
dont round incoming sample_rand
1 parent 9380196 commit c3d0156

File tree

4 files changed

+62
-62
lines changed

4 files changed

+62
-62
lines changed

sentry_sdk/integrations/opentelemetry/sampler.py

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import sentry_sdk
99
from sentry_sdk.tracing_utils import (
1010
_generate_sample_rand,
11-
_round_sample_rand,
1211
has_tracing_enabled,
1312
)
1413
from sentry_sdk.utils import is_valid_sample_rate, logger
@@ -87,12 +86,12 @@ def get_parent_sample_rand(parent_context, trace_id):
8786
if parent_sample_rand is None:
8887
return None
8988

90-
return _round_sample_rand(parent_sample_rand)
89+
return Decimal(parent_sample_rand)
9190

9291
return None
9392

9493

95-
def dropped_result(parent_span_context, attributes, sample_rate=None, sample_rand=None):
94+
def dropped_result(span_context, attributes, sample_rate=None, sample_rand=None):
9695
# type: (SpanContext, Attributes, Optional[float], Optional[Decimal]) -> SamplingResult
9796
"""
9897
React to a span getting unsampled and return a DROP SamplingResult.
@@ -104,26 +103,11 @@ def dropped_result(parent_span_context, attributes, sample_rate=None, sample_ran
104103
See for more info about OTel sampling:
105104
https://opentelemetry-python.readthedocs.io/en/latest/sdk/trace.sampling.html
106105
"""
107-
# these will only be added the first time in a root span sampling decision
108-
# if sample_rate or sample_rand is provided, they'll be updated in trace state
109-
trace_state = parent_span_context.trace_state
110-
111-
if TRACESTATE_SAMPLED_KEY not in trace_state:
112-
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, "false")
113-
elif trace_state.get(TRACESTATE_SAMPLED_KEY) == "deferred":
114-
trace_state = trace_state.update(TRACESTATE_SAMPLED_KEY, "false")
115-
116-
if sample_rate is not None:
117-
trace_state = trace_state.update(TRACESTATE_SAMPLE_RATE_KEY, str(sample_rate))
118-
119-
if sample_rand is not None:
120-
trace_state = trace_state.update(
121-
TRACESTATE_SAMPLE_RAND_KEY, f"{sample_rand:.6f}" # noqa: E231
122-
)
123-
124-
is_root_span = not (
125-
parent_span_context.is_valid and not parent_span_context.is_remote
106+
trace_state = _update_trace_state(
107+
span_context, sampled=False, sample_rate=sample_rate, sample_rand=sample_rand
126108
)
109+
110+
is_root_span = not (span_context.is_valid and not span_context.is_remote)
127111
if is_root_span:
128112
# Tell Sentry why we dropped the transaction/root-span
129113
client = sentry_sdk.get_client()
@@ -156,28 +140,38 @@ def sampled_result(span_context, attributes, sample_rate=None, sample_rand=None)
156140
See for more info about OTel sampling:
157141
https://opentelemetry-python.readthedocs.io/en/latest/sdk/trace.sampling.html
158142
"""
159-
# these will only be added the first time in a root span sampling decision
160-
# if sample_rate or sample_rand is provided, they'll be updated in trace state
143+
trace_state = _update_trace_state(
144+
span_context, sampled=True, sample_rate=sample_rate, sample_rand=sample_rand
145+
)
146+
147+
return SamplingResult(
148+
Decision.RECORD_AND_SAMPLE,
149+
attributes=attributes,
150+
trace_state=trace_state,
151+
)
152+
153+
154+
def _update_trace_state(span_context, sampled, sample_rate=None, sample_rand=None):
155+
# type: (..., bool, Optional[float], Optional[Decimal]) -> TraceState
161156
trace_state = span_context.trace_state
162157

158+
sampled = "true" if sampled else "false"
163159
if TRACESTATE_SAMPLED_KEY not in trace_state:
164-
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, "true")
160+
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, sampled)
165161
elif trace_state.get(TRACESTATE_SAMPLED_KEY) == "deferred":
166-
trace_state = trace_state.update(TRACESTATE_SAMPLED_KEY, "true")
162+
trace_state = trace_state.update(TRACESTATE_SAMPLED_KEY, sampled)
167163

164+
# sample_rate should always be updated to ensure complete traces
168165
if sample_rate is not None:
169166
trace_state = trace_state.update(TRACESTATE_SAMPLE_RATE_KEY, str(sample_rate))
170167

168+
# only add sample_rand if there isn't one
171169
if sample_rand is not None:
172-
trace_state = trace_state.update(
170+
trace_state = trace_state.add(
173171
TRACESTATE_SAMPLE_RAND_KEY, f"{sample_rand:.6f}" # noqa: E231
174172
)
175173

176-
return SamplingResult(
177-
Decision.RECORD_AND_SAMPLE,
178-
attributes=attributes,
179-
trace_state=trace_state,
180-
)
174+
return trace_state
181175

182176

183177
class SentrySampler(Sampler):
@@ -274,17 +268,15 @@ def should_sample(
274268
logger.warning(
275269
f"[Tracing] Discarding {name} because of invalid sample rate."
276270
)
277-
return dropped_result(
278-
parent_span_context, attributes, sample_rand=sample_rand
279-
)
271+
return dropped_result(parent_span_context, attributes)
280272

281273
# Down-sample in case of back pressure monitor says so
282274
if is_root_span and client.monitor:
283275
sample_rate /= 2**client.monitor.downsample_factor
284276
if client.monitor.downsample_factor > 0:
285277
sample_rate_to_propagate = sample_rate
286278

287-
# Roll the dice on sample rate
279+
# Compare sample_rand to sample_rate to make the final sampling decision
288280
sample_rate = float(cast("Union[bool, float, int]", sample_rate))
289281
sampled = sample_rand < sample_rate
290282

@@ -293,14 +285,14 @@ def should_sample(
293285
parent_span_context,
294286
attributes,
295287
sample_rate=sample_rate_to_propagate,
296-
sample_rand=sample_rand,
288+
sample_rand=None if sample_rand == parent_sample_rand else sample_rand,
297289
)
298290
else:
299291
return dropped_result(
300292
parent_span_context,
301293
attributes,
302294
sample_rate=sample_rate_to_propagate,
303-
sample_rand=sample_rand,
295+
sample_rand=None if sample_rand == parent_sample_rand else sample_rand,
304296
)
305297

306298
def get_description(self) -> str:

sentry_sdk/tracing_utils.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,9 @@ def _generate_sample_rand(
783783
while sample_rand >= upper:
784784
sample_rand = rng.uniform(lower, upper)
785785

786-
return _round_sample_rand(sample_rand)
786+
return Decimal(sample_rand).quantize(
787+
Decimal("0.000001"), rounding=ROUND_DOWN, context=Context(prec=6)
788+
)
787789

788790

789791
def _sample_rand_range(parent_sampled, sample_rate):
@@ -801,21 +803,6 @@ def _sample_rand_range(parent_sampled, sample_rate):
801803
return sample_rate, 1.0
802804

803805

804-
def _round_sample_rand(sample_rand):
805-
# type: (Union[Decimal, float, str]) -> Optional[Decimal]
806-
# Round down to exactly six decimal-digit precision.
807-
# Setting the context is needed to avoid an InvalidOperation exception
808-
# in case the user has changed the default precision.
809-
try:
810-
return Decimal(sample_rand).quantize(
811-
Decimal("0.000001"), rounding=ROUND_DOWN, context=Context(prec=6)
812-
)
813-
except Exception as ex:
814-
logger.debug(f"Failed to round sample_rand {sample_rand}: {ex}")
815-
816-
return None
817-
818-
819806
# Circular imports
820807
from sentry_sdk.tracing import (
821808
BAGGAGE_HEADER_NAME,

tests/tracing/test_integration_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def test_trace_propagation_meta_head_sdk(sentry_init):
227227

228228
assert 'meta name="baggage"' in baggage
229229
baggage_content = re.findall('content="([^"]*)"', baggage)[0]
230-
assert baggage_content == root_span.get_baggage().serialize()
230+
assert SortedBaggage(baggage_content) == root_span.get_baggage().serialize()
231231

232232

233233
@pytest.mark.parametrize(

tests/tracing/test_sample_rand.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,18 @@ def test_decimal_context(sentry_init, capture_events):
8686
@pytest.mark.parametrize(
8787
"incoming_sample_rand,expected_sample_rand",
8888
(
89-
("0.0100015", "0.010001"),
90-
("0.1", "0.100000"),
89+
("0.0100015", "0.0100015"),
90+
("0.1", "0.1"),
9191
),
9292
)
93-
def test_sample_rand_precision(
93+
def test_unexpected_incoming_sample_rand_precision(
9494
sentry_init, capture_events, incoming_sample_rand, expected_sample_rand
9595
):
9696
"""
97-
Test that incoming sample_rand is correctly interpreted and optionally rounded.
97+
Test that incoming sample_rand is correctly interpreted even if it looks unexpected.
9898
9999
We shouldn't be getting arbitrary precision sample_rand in incoming headers,
100-
but if we do for some reason, check that we have this covered and we round
101-
it correctly.
100+
but if we do for some reason, check that we don't tamper with it.
102101
"""
103102
sentry_init(traces_sample_rate=1.0)
104103
events = capture_events()
@@ -118,8 +117,30 @@ def test_sample_rand_precision(
118117
assert len(events) == 1
119118

120119

120+
@pytest.mark.parametrize(
121+
"incoming_sample_rand",
122+
("abc", "null", "47"),
123+
)
124+
def test_invalid_incoming_sample_rand(sentry_init, incoming_sample_rand):
125+
"""Test that we handle malformed incoming sample_rand."""
126+
sentry_init(traces_sample_rate=1.0)
127+
128+
baggage = f"sentry-sample_rand={incoming_sample_rand},sentry-trace_id=771a43a4192642f0b136d5159a501700" # noqa: E231
129+
sentry_trace = "771a43a4192642f0b136d5159a501700-1234567890abcdef"
130+
131+
with sentry_sdk.continue_trace(
132+
{BAGGAGE_HEADER_NAME: baggage, SENTRY_TRACE_HEADER_NAME: sentry_trace}
133+
):
134+
with sentry_sdk.start_span():
135+
pass
136+
137+
# The behavior here is undefined since we got a broken incoming trace,
138+
# so as long as the SDK doesn't produce an error we consider this
139+
# testcase a success.
140+
141+
121142
@pytest.mark.parametrize("incoming", ((0.0, "true"), (1.0, "false")))
122-
def test_invalid_sampled_and_sample_rate(sentry_init, incoming):
143+
def test_invalid_incoming_sampled_and_sample_rate(sentry_init, incoming):
123144
"""
124145
Test that we don't error out in case we can't generate a sample_rand that
125146
would respect the incoming sampled and sample_rate.

0 commit comments

Comments
 (0)