Skip to content

Commit e906443

Browse files
committed
Move sampling to before the span is created
1 parent 817c655 commit e906443

4 files changed

Lines changed: 115 additions & 141 deletions

File tree

sentry_sdk/scope.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
has_tracing_enabled,
3232
has_span_streaming_enabled,
3333
is_ignored_span,
34+
make_sampling_decision,
3435
PropagationContext,
3536
)
3637
from sentry_sdk.traces import StreamedSpan, NoOpStreamedSpan
@@ -1255,7 +1256,19 @@ def start_streamed_span(
12551256
propagation_context = self.get_active_propagation_context()
12561257

12571258
if is_ignored_span(name, attributes):
1258-
return NoOpStreamedSpan(scope=self)
1259+
return NoOpStreamedSpan(
1260+
trace_id=propagation_context.trace_id, scope=self
1261+
)
1262+
1263+
sampled, sample_rate, sample_rand = make_sampling_decision(
1264+
name,
1265+
attributes,
1266+
self,
1267+
)
1268+
if sampled is False:
1269+
return NoOpStreamedSpan(
1270+
trace_id=propagation_context.trace_id, scope=self
1271+
)
12591272

12601273
return StreamedSpan(
12611274
name=name,
@@ -1267,14 +1280,16 @@ def start_streamed_span(
12671280
parent_span_id=propagation_context.parent_span_id,
12681281
parent_sampled=propagation_context.parent_sampled,
12691282
baggage=propagation_context.baggage,
1283+
sample_rand=sample_rand,
1284+
sample_rate=sample_rate,
12701285
)
12711286

12721287
# This is a child span; take propagation context from the parent span
12731288
with new_scope():
12741289
if is_ignored_span(name, attributes) or isinstance(
12751290
parent_span, NoOpStreamedSpan
12761291
):
1277-
return NoOpStreamedSpan()
1292+
return NoOpStreamedSpan(trace_id=parent_span.trace_id)
12781293

12791294
return StreamedSpan(
12801295
name=name,
@@ -1307,8 +1322,8 @@ def _update_sample_rate_from_segment(self, span: "StreamedSpan") -> None:
13071322
propagation_context = self.get_active_propagation_context()
13081323
baggage = propagation_context.baggage
13091324

1310-
if baggage is not None and span.sample_rate is not None:
1311-
baggage.sentry_items["sample_rate"] = str(span.sample_rate)
1325+
if baggage is not None and span._sample_rate is not None:
1326+
baggage.sentry_items["sample_rate"] = str(span._sample_rate)
13121327

13131328
def continue_trace(
13141329
self,

sentry_sdk/traces.py

Lines changed: 11 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@
1616
from sentry_sdk.profiler.continuous_profiler import get_profiler_id
1717
from sentry_sdk.tracing_utils import (
1818
Baggage,
19-
_generate_sample_rand,
2019
has_tracing_enabled,
2120
)
2221
from sentry_sdk.utils import (
2322
capture_internal_exceptions,
2423
format_attribute,
2524
get_current_thread_meta,
26-
is_valid_sample_rate,
2725
logger,
2826
nanosecond_time,
2927
should_be_treated_as_error,
@@ -226,10 +224,10 @@ class StreamedSpan:
226224
"_context_manager_state",
227225
"_continuous_profile",
228226
"_baggage",
229-
"sample_rate",
230-
"_sample_rand",
231227
"_finished",
232228
"_last_valid_parent_id",
229+
"_sample_rand",
230+
"_sample_rate",
233231
)
234232

235233
def __init__(
@@ -250,6 +248,8 @@ def __init__(
250248
baggage: "Optional[Baggage]" = None,
251249
segment: "Optional[StreamedSpan]" = None,
252250
sampled: "Optional[bool]" = None,
251+
sample_rate: "Optional[float]" = None,
252+
sample_rand: "Optional[float]" = None,
253253
last_valid_parent_id: "Optional[str]" = None,
254254
) -> None:
255255
self._scope = scope
@@ -282,19 +282,11 @@ def __init__(
282282
self.set_status(SpanStatus.OK)
283283
self.set_attribute("sentry.span.source", SegmentSource.CUSTOM.value)
284284

285-
self._sampled: "Optional[bool]" = sampled
286-
self.sample_rate: "Optional[float]" = None
287285
self._last_valid_parent_id: "Optional[str]" = last_valid_parent_id
288286

289-
# XXX[span-first]: just do this for segments?
290287
self._baggage = baggage
291-
baggage_sample_rand = (
292-
None if self._baggage is None else self._baggage._sample_rand()
293-
)
294-
if baggage_sample_rand is not None:
295-
self._sample_rand = baggage_sample_rand
296-
else:
297-
self._sample_rand = _generate_sample_rand(self.trace_id)
288+
self._sample_rand = sample_rand
289+
self._sample_rate = sample_rate
298290

299291
self._continuous_profile: "Optional[ContinuousProfile]" = None
300292

@@ -320,24 +312,6 @@ def __enter__(self) -> "StreamedSpan":
320312
self._context_manager_state = (scope, old_span)
321313

322314
if self.is_segment():
323-
sampling_context = {
324-
"name": self._name,
325-
"trace_id": self.trace_id,
326-
"span_id": self.span_id,
327-
"parent_span_id": self.parent_span_id,
328-
"parent_sampled": self.parent_sampled,
329-
"attributes": self._attributes,
330-
}
331-
custom_sampling_context = (
332-
scope.get_active_propagation_context()._custom_sampling_context
333-
)
334-
if custom_sampling_context:
335-
sampling_context.update(custom_sampling_context)
336-
337-
# Use traces_sample_rate, traces_sampler, and/or inheritance to make a
338-
# sampling decision
339-
self._set_sampling_decision(sampling_context=sampling_context)
340-
341315
scope._update_sample_rate_from_segment(self)
342316
scope._start_profile_on_segment(self)
343317

@@ -517,13 +491,7 @@ def trace_id(self) -> str:
517491

518492
@property
519493
def sampled(self) -> "Optional[bool]":
520-
if self._sampled is not None:
521-
return self._sampled
522-
523-
if not self.is_segment():
524-
self._sampled = self.segment.sampled
525-
526-
return self._sampled
494+
return True
527495

528496
def dynamic_sampling_context(self) -> "dict[str, str]":
529497
return self.segment.get_baggage().dynamic_sampling_context()
@@ -586,70 +554,6 @@ def get_baggage(self) -> "Baggage":
586554

587555
return self._baggage
588556

589-
def _set_sampling_decision(self, sampling_context: "SamplingContext") -> None:
590-
"""Set a segment's sampling decision."""
591-
client = sentry_sdk.get_client()
592-
593-
if not has_tracing_enabled(client.options):
594-
self._sampled = False
595-
return
596-
597-
if not self.is_segment():
598-
return
599-
600-
if self._sampled is not None:
601-
return
602-
603-
traces_sampler_defined = callable(client.options.get("traces_sampler"))
604-
605-
# We would have bailed already if neither `traces_sampler` nor
606-
# `traces_sample_rate` were defined, so one of these should work; prefer
607-
# the hook if so
608-
if traces_sampler_defined:
609-
sample_rate = client.options["traces_sampler"](sampling_context)
610-
else:
611-
if sampling_context["parent_sampled"] is not None:
612-
sample_rate = sampling_context["parent_sampled"]
613-
else:
614-
sample_rate = client.options["traces_sample_rate"]
615-
616-
# Since this is coming from the user (or from a function provided by the
617-
# user), who knows what we might get. (The only valid values are
618-
# booleans or numbers between 0 and 1.)
619-
if not is_valid_sample_rate(sample_rate, source="Tracing"):
620-
logger.warning(
621-
f"[Tracing] Discarding {self._name} because of invalid sample rate."
622-
)
623-
self._sampled = False
624-
return
625-
626-
self.sample_rate = float(sample_rate)
627-
628-
if client.monitor:
629-
self.sample_rate /= 2**client.monitor.downsample_factor
630-
631-
# if the function returned 0 (or false), or if `traces_sample_rate` is
632-
# 0, it's a sign the transaction should be dropped
633-
if not self.sample_rate:
634-
if traces_sampler_defined:
635-
reason = "traces_sampler returned 0 or False"
636-
else:
637-
reason = "traces_sample_rate is set to 0"
638-
639-
logger.debug(f"[Tracing] Discarding {self._name} because {reason}")
640-
self._sampled = False
641-
return
642-
643-
# Now we roll the dice.
644-
self._sampled = self._sample_rand < self.sample_rate
645-
646-
if self.sampled:
647-
logger.debug(f"[Tracing] Starting {self._name}")
648-
else:
649-
logger.debug(
650-
f"[Tracing] Discarding {self._name} because it's not included in the random sample (sampling rate = {self.sample_rate})"
651-
)
652-
653557
# TODO: Populate other fields as necessary
654558
def get_trace_context(self) -> "dict[str, Any]":
655559
warnings.warn(
@@ -674,15 +578,18 @@ def _set_segment_attributes(self) -> None:
674578
class NoOpStreamedSpan(StreamedSpan):
675579
__slots__ = (
676580
"_name",
581+
"_span_id",
582+
"_trace_id",
677583
"segment",
678584
"_scope",
679585
"_context_manager_state",
680586
)
681587

682588
def __init__(
683-
self, scope: "Optional[sentry_sdk.Scope]" = None, **kwargs: "Any"
589+
self, trace_id: str, scope: "Optional[sentry_sdk.Scope]" = None, **kwargs: "Any"
684590
) -> None:
685591
self.segment = None # type: ignore[assignment]
592+
self._trace_id = trace_id
686593
self._scope = scope # type: ignore[assignment]
687594

688595
def __repr__(self) -> str:
@@ -759,14 +666,6 @@ def to_traceparent(self) -> str:
759666

760667
return f"{propagation_context.trace_id}-{propagation_context.span_id}-0"
761668

762-
@property
763-
def span_id(self) -> str:
764-
return "000000"
765-
766-
@property
767-
def trace_id(self) -> str:
768-
return "000000"
769-
770669
@property
771670
def sampled(self) -> "Optional[bool]":
772671
return False

sentry_sdk/tracing_utils.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
to_string,
2525
try_convert,
2626
is_sentry_url,
27+
is_valid_sample_rate,
2728
_is_external_source,
2829
_is_in_project_root,
2930
_module_in_list,
@@ -441,7 +442,7 @@ class PropagationContext:
441442
"parent_span_id",
442443
"parent_sampled",
443444
"baggage",
444-
"_custom_sampling_context",
445+
"custom_sampling_context",
445446
)
446447

447448
def __init__(
@@ -475,7 +476,7 @@ def __init__(
475476
if baggage is None and dynamic_sampling_context is not None:
476477
self.baggage = Baggage(dynamic_sampling_context)
477478

478-
self._custom_sampling_context: "Optional[dict[str, Any]]" = None
479+
self.custom_sampling_context: "Optional[dict[str, Any]]" = None
479480

480481
@classmethod
481482
def from_incoming_data(
@@ -567,7 +568,7 @@ def update(self, other_dict: "Dict[str, Any]") -> None:
567568
def _set_custom_sampling_context(
568569
self, custom_sampling_context: "dict[str, Any]"
569570
) -> None:
570-
self._custom_sampling_context = custom_sampling_context
571+
self.custom_sampling_context = custom_sampling_context
571572

572573
def __repr__(self) -> str:
573574
return "<PropagationContext _trace_id={} _span_id={} parent_span_id={} parent_sampled={} baggage={}>".format(
@@ -819,8 +820,8 @@ def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage":
819820
) and segment._name:
820821
sentry_items["transaction"] = segment._name
821822

822-
if segment.sample_rate is not None:
823-
sentry_items["sample_rate"] = str(segment.sample_rate)
823+
if segment._sample_rate is not None:
824+
sentry_items["sample_rate"] = str(segment._sample_rate)
824825

825826
if segment.sampled is not None:
826827
sentry_items["sampled"] = "true" if segment.sampled else "false"
@@ -1474,6 +1475,75 @@ def add_sentry_baggage_to_headers(
14741475
)
14751476

14761477

1478+
def make_sampling_decision(
1479+
name: str,
1480+
attributes: "Optional[Attributes]",
1481+
scope: "sentry_sdk.Scope",
1482+
) -> "tuple[bool, Optional[float], Optional[float]]":
1483+
client = sentry_sdk.get_client()
1484+
1485+
if not has_tracing_enabled(client.options):
1486+
return False, None, None
1487+
1488+
propagation_context = scope.get_active_propagation_context()
1489+
1490+
if propagation_context.baggage is not None:
1491+
sample_rand = propagation_context.baggage._sample_rand()
1492+
else:
1493+
sample_rand = _generate_sample_rand(propagation_context.trace_id)
1494+
1495+
sampling_context = {
1496+
"name": name,
1497+
"trace_id": propagation_context.trace_id,
1498+
"parent_span_id": propagation_context.parent_span_id,
1499+
"parent_sampled": propagation_context.parent_sampled,
1500+
"attributes": attributes or {},
1501+
}
1502+
if propagation_context.custom_sampling_context:
1503+
sampling_context.update(propagation_context.custom_sampling_context)
1504+
1505+
# If there's a traces_sampler, use that; otherwise use traces_sample_rate
1506+
traces_sampler_defined = callable(client.options.get("traces_sampler"))
1507+
if traces_sampler_defined:
1508+
sample_rate = client.options["traces_sampler"](sampling_context)
1509+
else:
1510+
if sampling_context["parent_sampled"] is not None:
1511+
sample_rate = sampling_context["parent_sampled"]
1512+
else:
1513+
sample_rate = client.options["traces_sample_rate"]
1514+
1515+
# Validate whether the sample_rate we got is actually valid. Since
1516+
# traces_sampler is user provided, it could return anything.
1517+
if not is_valid_sample_rate(sample_rate, source="Tracing"):
1518+
logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.")
1519+
return False, None, None
1520+
1521+
sample_rate = float(sample_rate)
1522+
1523+
if client.monitor:
1524+
sample_rate /= 2**client.monitor.downsample_factor
1525+
1526+
if not sample_rate:
1527+
if traces_sampler_defined:
1528+
reason = "traces_sampler returned 0 or False"
1529+
else:
1530+
reason = "traces_sample_rate is set to 0"
1531+
1532+
logger.debug(f"[Tracing] Discarding {name} because {reason}")
1533+
return False, 0.0, None
1534+
1535+
sampled = sample_rand < sample_rate
1536+
1537+
if sampled:
1538+
logger.debug(f"[Tracing] Starting {name}")
1539+
else:
1540+
logger.debug(
1541+
f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})"
1542+
)
1543+
1544+
return sampled, sample_rate, sample_rand
1545+
1546+
14771547
def is_ignored_span(name: str, attributes: "Optional[Attributes]") -> bool:
14781548
client = sentry_sdk.get_client()
14791549
ignore_spans = (client.options.get("_experiments") or {}).get("ignore_spans")

0 commit comments

Comments
 (0)