Skip to content

Commit 5283055

Browse files
authored
Add traces_sampler option (getsentry#863)
1 parent dd4ff15 commit 5283055

File tree

8 files changed

+341
-52
lines changed

8 files changed

+341
-52
lines changed

sentry_sdk/consts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def __init__(
6969
attach_stacktrace=False, # type: bool
7070
ca_certs=None, # type: Optional[str]
7171
propagate_traces=True, # type: bool
72-
traces_sample_rate=0.0, # type: float
72+
traces_sample_rate=None, # type: Optional[float]
7373
traces_sampler=None, # type: Optional[TracesSampler]
7474
auto_enabling_integrations=True, # type: bool
7575
_experiments={}, # type: Experiments # noqa: B006

sentry_sdk/hub.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import copy
2-
import random
32
import sys
43

54
from datetime import datetime
@@ -505,20 +504,28 @@ def start_transaction(
505504
When the transaction is finished, it will be sent to Sentry with all its
506505
finished child spans.
507506
"""
507+
custom_sampling_context = kwargs.pop("custom_sampling_context", {})
508+
509+
# if we haven't been given a transaction, make one
508510
if transaction is None:
509511
kwargs.setdefault("hub", self)
510512
transaction = Transaction(**kwargs)
511513

512-
client, scope = self._stack[-1]
513-
514-
if transaction.sampled is None:
515-
sample_rate = client and client.options["traces_sample_rate"] or 0
516-
transaction.sampled = random.random() < sample_rate
517-
514+
# use traces_sample_rate, traces_sampler, and/or inheritance to make a
515+
# sampling decision
516+
sampling_context = {
517+
"transaction_context": transaction.to_json(),
518+
"parent_sampled": transaction.parent_sampled,
519+
}
520+
sampling_context.update(custom_sampling_context)
521+
transaction._set_initial_sampling_decision(sampling_context=sampling_context)
522+
523+
# we don't bother to keep spans if we already know we're not going to
524+
# send the transaction
518525
if transaction.sampled:
519526
max_spans = (
520-
client and client.options["_experiments"].get("max_spans") or 1000
521-
)
527+
self.client and self.client.options["_experiments"].get("max_spans")
528+
) or 1000
522529
transaction.init_span_recorder(maxlen=max_spans)
523530

524531
return transaction

sentry_sdk/tracing.py

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
import uuid
33
import contextlib
44
import math
5+
import random
56
import time
67

78
from datetime import datetime, timedelta
89
from numbers import Real
910

1011
import sentry_sdk
1112

12-
from sentry_sdk.utils import capture_internal_exceptions, logger, to_string
13+
from sentry_sdk.utils import (
14+
capture_internal_exceptions,
15+
logger,
16+
to_string,
17+
)
1318
from sentry_sdk._compat import PY2
1419
from sentry_sdk._types import MYPY
1520

@@ -28,6 +33,8 @@
2833
from typing import List
2934
from typing import Tuple
3035

36+
from sentry_sdk._types import SamplingContext
37+
3138
_traceparent_header_format_re = re.compile(
3239
"^[ \t]*" # whitespace
3340
"([0-9a-f]{32})?" # trace_id
@@ -337,7 +344,7 @@ def from_traceparent(
337344
return Transaction(
338345
trace_id=trace_id,
339346
parent_span_id=parent_span_id,
340-
sampled=parent_sampled,
347+
parent_sampled=parent_sampled,
341348
**kwargs
342349
)
343350

@@ -555,6 +562,116 @@ def to_json(self):
555562

556563
return rv
557564

565+
def _set_initial_sampling_decision(self, sampling_context):
566+
# type: (SamplingContext) -> None
567+
"""
568+
Sets the transaction's sampling decision, according to the following
569+
precedence rules:
570+
571+
1. If a sampling decision is passed to `start_transaction`
572+
(`start_transaction(name: "my transaction", sampled: True)`), that
573+
decision will be used, regardlesss of anything else
574+
575+
2. If `traces_sampler` is defined, its decision will be used. It can
576+
choose to keep or ignore any parent sampling decision, or use the
577+
sampling context data to make its own decision or to choose a sample
578+
rate for the transaction.
579+
580+
3. If `traces_sampler` is not defined, but there's a parent sampling
581+
decision, the parent sampling decision will be used.
582+
583+
4. If `traces_sampler` is not defined and there's no parent sampling
584+
decision, `traces_sample_rate` will be used.
585+
"""
586+
587+
hub = self.hub or sentry_sdk.Hub.current
588+
client = hub.client
589+
options = (client and client.options) or {}
590+
transaction_description = "{op}transaction <{name}>".format(
591+
op=("<" + self.op + "> " if self.op else ""), name=self.name
592+
)
593+
594+
# nothing to do if there's no client or if tracing is disabled
595+
if not client or not has_tracing_enabled(options):
596+
self.sampled = False
597+
return
598+
599+
# if the user has forced a sampling decision by passing a `sampled`
600+
# value when starting the transaction, go with that
601+
if self.sampled is not None:
602+
return
603+
604+
# we would have bailed already if neither `traces_sampler` nor
605+
# `traces_sample_rate` were defined, so one of these should work; prefer
606+
# the hook if so
607+
sample_rate = (
608+
options["traces_sampler"](sampling_context)
609+
if callable(options.get("traces_sampler"))
610+
else (
611+
# default inheritance behavior
612+
sampling_context["parent_sampled"]
613+
if sampling_context["parent_sampled"] is not None
614+
else options["traces_sample_rate"]
615+
)
616+
)
617+
618+
# Since this is coming from the user (or from a function provided by the
619+
# user), who knows what we might get. (The only valid values are
620+
# booleans or numbers between 0 and 1.)
621+
if not _is_valid_sample_rate(sample_rate):
622+
logger.warning(
623+
"[Tracing] Discarding {transaction_description} because of invalid sample rate.".format(
624+
transaction_description=transaction_description,
625+
)
626+
)
627+
self.sampled = False
628+
return
629+
630+
# if the function returned 0 (or false), or if `traces_sample_rate` is
631+
# 0, it's a sign the transaction should be dropped
632+
if not sample_rate:
633+
logger.debug(
634+
"[Tracing] Discarding {transaction_description} because {reason}".format(
635+
transaction_description=transaction_description,
636+
reason=(
637+
"traces_sampler returned 0 or False"
638+
if callable(options.get("traces_sampler"))
639+
else "traces_sample_rate is set to 0"
640+
),
641+
)
642+
)
643+
self.sampled = False
644+
return
645+
646+
# Now we roll the dice. random.random is inclusive of 0, but not of 1,
647+
# so strict < is safe here. In case sample_rate is a boolean, cast it
648+
# to a float (True becomes 1.0 and False becomes 0.0)
649+
self.sampled = random.random() < float(sample_rate)
650+
651+
if self.sampled:
652+
logger.debug(
653+
"[Tracing] Starting {transaction_description}".format(
654+
transaction_description=transaction_description,
655+
)
656+
)
657+
else:
658+
logger.debug(
659+
"[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format(
660+
transaction_description=transaction_description,
661+
sample_rate=float(sample_rate),
662+
)
663+
)
664+
665+
666+
def has_tracing_enabled(options):
667+
# type: (Dict[str, Any]) -> bool
668+
"""
669+
Returns True if either traces_sample_rate or traces_sampler is
670+
non-zero/defined, False otherwise.
671+
"""
672+
673+
return bool(options.get("traces_sample_rate") or options.get("traces_sampler"))
674+
558675

559676
def _is_valid_sample_rate(rate):
560677
# type: (Any) -> bool

sentry_sdk/utils.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -968,13 +968,3 @@ def run(self):
968968
integer_configured_timeout
969969
)
970970
)
971-
972-
973-
def has_tracing_enabled(options):
974-
# type: (Dict[str, Any]) -> bool
975-
"""
976-
Returns True if either traces_sample_rate or traces_sampler is
977-
non-zero/defined, False otherwise.
978-
"""
979-
980-
return bool(options.get("traces_sample_rate") or options.get("traces_sampler"))

tests/conftest.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
import json
3-
from types import FunctionType
43

54
import pytest
65
import jsonschema
@@ -37,11 +36,6 @@ def benchmark():
3736
else:
3837
del pytest_benchmark
3938

40-
try:
41-
from unittest import mock # python 3.3 and above
42-
except ImportError:
43-
import mock # python < 3.3
44-
4539

4640
@pytest.fixture(autouse=True)
4741
def internal_exceptions(request, monkeypatch):
@@ -400,18 +394,3 @@ def __eq__(self, test_dict):
400394
return all(test_dict.get(key) == self.subdict[key] for key in self.subdict)
401395

402396
return DictionaryContaining
403-
404-
405-
@pytest.fixture(name="FunctionMock")
406-
def function_mock():
407-
"""
408-
Just like a mock.Mock object, but one which always passes an isfunction
409-
test.
410-
"""
411-
412-
class FunctionMock(mock.Mock):
413-
def __init__(self, *args, **kwargs):
414-
super(FunctionMock, self).__init__(*args, **kwargs)
415-
self.__class__ = FunctionType
416-
417-
return FunctionMock

tests/integrations/sqlalchemy/test_sqlalchemy.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ class Address(Base):
7676
def test_transactions(sentry_init, capture_events, render_span_tree):
7777

7878
sentry_init(
79-
integrations=[SqlalchemyIntegration()], _experiments={"record_sql_params": True}
79+
integrations=[SqlalchemyIntegration()],
80+
_experiments={"record_sql_params": True},
81+
traces_sample_rate=1.0,
8082
)
8183
events = capture_events()
8284

tests/tracing/test_integration_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def test_continue_from_headers(sentry_init, capture_events, sampled):
7070
# correctly
7171
transaction = Transaction.continue_from_headers(headers, name="WRONG")
7272
assert transaction is not None
73-
assert transaction.sampled == sampled
73+
assert transaction.parent_sampled == sampled
7474
assert transaction.trace_id == old_span.trace_id
7575
assert transaction.same_process_as_parent is False
7676
assert transaction.parent_span_id == old_span.span_id

0 commit comments

Comments
 (0)