1- import random
1+ from decimal import Decimal
22from typing import cast
33
44from opentelemetry import trace
55from opentelemetry .sdk .trace .sampling import Sampler , SamplingResult , Decision
66from opentelemetry .trace .span import TraceState
77
88import sentry_sdk
9- from sentry_sdk .tracing_utils import has_tracing_enabled
9+ from sentry_sdk .tracing_utils import (
10+ _generate_sample_rand ,
11+ has_tracing_enabled ,
12+ )
1013from sentry_sdk .utils import is_valid_sample_rate , logger
1114from sentry_sdk .integrations .opentelemetry .consts import (
1215 TRACESTATE_SAMPLED_KEY ,
16+ TRACESTATE_SAMPLE_RAND_KEY ,
1317 TRACESTATE_SAMPLE_RATE_KEY ,
1418 SentrySpanAttribute ,
1519)
@@ -70,23 +74,40 @@ def get_parent_sample_rate(parent_context, trace_id):
7074 return None
7175
7276
73- def dropped_result (parent_span_context , attributes , sample_rate = None ):
74- # type: (SpanContext, Attributes, Optional[float]) -> SamplingResult
75- # these will only be added the first time in a root span sampling decision
76- # if sample_rate is provided, it'll be updated in trace state
77- trace_state = parent_span_context .trace_state
77+ def get_parent_sample_rand (parent_context , trace_id ):
78+ # type: (Optional[SpanContext], int) -> Optional[Decimal]
79+ if parent_context is None :
80+ return None
7881
79- if TRACESTATE_SAMPLED_KEY not in trace_state :
80- trace_state = trace_state .add (TRACESTATE_SAMPLED_KEY , "false" )
81- elif trace_state .get (TRACESTATE_SAMPLED_KEY ) == "deferred" :
82- trace_state = trace_state .update (TRACESTATE_SAMPLED_KEY , "false" )
82+ is_span_context_valid = parent_context is not None and parent_context .is_valid
8383
84- if sample_rate is not None :
85- trace_state = trace_state .update (TRACESTATE_SAMPLE_RATE_KEY , str (sample_rate ))
84+ if is_span_context_valid and parent_context .trace_id == trace_id :
85+ parent_sample_rand = parent_context .trace_state .get (TRACESTATE_SAMPLE_RAND_KEY )
86+ if parent_sample_rand is None :
87+ return None
8688
87- is_root_span = not (
88- parent_span_context .is_valid and not parent_span_context .is_remote
89+ return Decimal (parent_sample_rand )
90+
91+ return None
92+
93+
94+ def dropped_result (span_context , attributes , sample_rate = None , sample_rand = None ):
95+ # type: (SpanContext, Attributes, Optional[float], Optional[Decimal]) -> SamplingResult
96+ """
97+ React to a span getting unsampled and return a DROP SamplingResult.
98+
99+ Update the trace_state with the effective sampled, sample_rate and sample_rand,
100+ record that we dropped the event for client report purposes, and return
101+ an OTel SamplingResult with Decision.DROP.
102+
103+ See for more info about OTel sampling:
104+ https://opentelemetry-python.readthedocs.io/en/latest/sdk/trace.sampling.html
105+ """
106+ trace_state = _update_trace_state (
107+ span_context , sampled = False , sample_rate = sample_rate , sample_rand = sample_rand
89108 )
109+
110+ is_root_span = not (span_context .is_valid and not span_context .is_remote )
90111 if is_root_span :
91112 # Tell Sentry why we dropped the transaction/root-span
92113 client = sentry_sdk .get_client ()
@@ -108,19 +129,20 @@ def dropped_result(parent_span_context, attributes, sample_rate=None):
108129 )
109130
110131
111- def sampled_result (span_context , attributes , sample_rate ):
112- # type: (SpanContext, Attributes, Optional[float]) -> SamplingResult
113- # these will only be added the first time in a root span sampling decision
114- # if sample_rate is provided, it'll be updated in trace state
115- trace_state = span_context .trace_state
132+ def sampled_result (span_context , attributes , sample_rate = None , sample_rand = None ):
133+ # type: (SpanContext, Attributes, Optional[float], Optional[Decimal]) -> SamplingResult
134+ """
135+ React to a span being sampled and return a sampled SamplingResult.
116136
117- if TRACESTATE_SAMPLED_KEY not in trace_state :
118- trace_state = trace_state .add (TRACESTATE_SAMPLED_KEY , "true" )
119- elif trace_state .get (TRACESTATE_SAMPLED_KEY ) == "deferred" :
120- trace_state = trace_state .update (TRACESTATE_SAMPLED_KEY , "true" )
137+ Update the trace_state with the effective sampled, sample_rate and sample_rand,
138+ and return an OTel SamplingResult with Decision.RECORD_AND_SAMPLE.
121139
122- if sample_rate is not None :
123- trace_state = trace_state .update (TRACESTATE_SAMPLE_RATE_KEY , str (sample_rate ))
140+ See for more info about OTel sampling:
141+ https://opentelemetry-python.readthedocs.io/en/latest/sdk/trace.sampling.html
142+ """
143+ trace_state = _update_trace_state (
144+ span_context , sampled = True , sample_rate = sample_rate , sample_rand = sample_rand
145+ )
124146
125147 return SamplingResult (
126148 Decision .RECORD_AND_SAMPLE ,
@@ -129,6 +151,27 @@ def sampled_result(span_context, attributes, sample_rate):
129151 )
130152
131153
154+ def _update_trace_state (span_context , sampled , sample_rate = None , sample_rand = None ):
155+ # type: (SpanContext, bool, Optional[float], Optional[Decimal]) -> TraceState
156+ trace_state = span_context .trace_state
157+
158+ sampled = "true" if sampled else "false"
159+ if TRACESTATE_SAMPLED_KEY not in trace_state :
160+ trace_state = trace_state .add (TRACESTATE_SAMPLED_KEY , sampled )
161+ elif trace_state .get (TRACESTATE_SAMPLED_KEY ) == "deferred" :
162+ trace_state = trace_state .update (TRACESTATE_SAMPLED_KEY , sampled )
163+
164+ if sample_rate is not None :
165+ trace_state = trace_state .update (TRACESTATE_SAMPLE_RATE_KEY , str (sample_rate ))
166+
167+ if sample_rand is not None :
168+ trace_state = trace_state .update (
169+ TRACESTATE_SAMPLE_RAND_KEY , f"{ sample_rand :.6f} " # noqa: E231
170+ )
171+
172+ return trace_state
173+
174+
132175class SentrySampler (Sampler ):
133176 def should_sample (
134177 self ,
@@ -156,6 +199,18 @@ def should_sample(
156199
157200 sample_rate = None
158201
202+ parent_sampled = get_parent_sampled (parent_span_context , trace_id )
203+ parent_sample_rate = get_parent_sample_rate (parent_span_context , trace_id )
204+ parent_sample_rand = get_parent_sample_rand (parent_span_context , trace_id )
205+
206+ if parent_sample_rand is not None :
207+ # We have a sample_rand on the incoming trace or we already backfilled
208+ # it in PropagationContext
209+ sample_rand = parent_sample_rand
210+ else :
211+ # We are the head SDK and we need to generate a new sample_rand
212+ sample_rand = cast (Decimal , _generate_sample_rand (str (trace_id ), (0 , 1 )))
213+
159214 # Explicit sampled value provided at start_span
160215 custom_sampled = cast (
161216 "Optional[bool]" , attributes .get (SentrySpanAttribute .CUSTOM_SAMPLED )
@@ -165,11 +220,17 @@ def should_sample(
165220 sample_rate = float (custom_sampled )
166221 if sample_rate > 0 :
167222 return sampled_result (
168- parent_span_context , attributes , sample_rate = sample_rate
223+ parent_span_context ,
224+ attributes ,
225+ sample_rate = sample_rate ,
226+ sample_rand = sample_rand ,
169227 )
170228 else :
171229 return dropped_result (
172- parent_span_context , attributes , sample_rate = sample_rate
230+ parent_span_context ,
231+ attributes ,
232+ sample_rate = sample_rate ,
233+ sample_rand = sample_rand ,
173234 )
174235 else :
175236 logger .debug (
@@ -190,8 +251,6 @@ def should_sample(
190251 sample_rate_to_propagate = sample_rate
191252 else :
192253 # Check if there is a parent with a sampling decision
193- parent_sampled = get_parent_sampled (parent_span_context , trace_id )
194- parent_sample_rate = get_parent_sample_rate (parent_span_context , trace_id )
195254 if parent_sampled is not None :
196255 sample_rate = bool (parent_sampled )
197256 sample_rate_to_propagate = (
@@ -215,17 +274,23 @@ def should_sample(
215274 if client .monitor .downsample_factor > 0 :
216275 sample_rate_to_propagate = sample_rate
217276
218- # Roll the dice on sample rate
277+ # Compare sample_rand to sample_rate to make the final sampling decision
219278 sample_rate = float (cast ("Union[bool, float, int]" , sample_rate ))
220- sampled = random . random () < sample_rate
279+ sampled = sample_rand < sample_rate
221280
222281 if sampled :
223282 return sampled_result (
224- parent_span_context , attributes , sample_rate = sample_rate_to_propagate
283+ parent_span_context ,
284+ attributes ,
285+ sample_rate = sample_rate_to_propagate ,
286+ sample_rand = None if sample_rand == parent_sample_rand else sample_rand ,
225287 )
226288 else :
227289 return dropped_result (
228- parent_span_context , attributes , sample_rate = sample_rate_to_propagate
290+ parent_span_context ,
291+ attributes ,
292+ sample_rate = sample_rate_to_propagate ,
293+ sample_rand = None if sample_rand == parent_sample_rand else sample_rand ,
229294 )
230295
231296 def get_description (self ) -> str :
0 commit comments