Skip to content

Commit 3e67535

Browse files
authored
feat(profiling): Add profiler options to init (#1947)
This adds the `profiles_sample_rate`, `profiles_sampler` and `profiler_mode` options to the top level of the init call. The `_experiment` options will still be available temporarily but is deprecated and will be removed in the future.
1 parent 2c8d277 commit 3e67535

File tree

10 files changed

+239
-95
lines changed

10 files changed

+239
-95
lines changed

sentry_sdk/_types.py

+2
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,5 @@
8585

8686
FractionUnit = Literal["ratio", "percent"]
8787
MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str]
88+
89+
ProfilerMode = Literal["sleep", "thread", "gevent", "unknown"]

sentry_sdk/client.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from sentry_sdk.utils import ContextVar
2929
from sentry_sdk.sessions import SessionFlusher
3030
from sentry_sdk.envelope import Envelope
31-
from sentry_sdk.profiler import setup_profiler
31+
from sentry_sdk.profiler import has_profiling_enabled, setup_profiler
3232

3333
from sentry_sdk._types import TYPE_CHECKING
3434

@@ -174,8 +174,7 @@ def _capture_envelope(envelope):
174174
finally:
175175
_client_init_debug.set(old_debug)
176176

177-
profiles_sample_rate = self.options["_experiments"].get("profiles_sample_rate")
178-
if profiles_sample_rate is not None and profiles_sample_rate > 0:
177+
if has_profiling_enabled(self.options):
179178
try:
180179
setup_profiler(self.options)
181180
except ValueError as e:

sentry_sdk/consts.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
BreadcrumbProcessor,
2020
Event,
2121
EventProcessor,
22+
ProfilerMode,
2223
TracesSampler,
2324
TransactionProcessor,
2425
)
@@ -33,8 +34,9 @@
3334
"max_spans": Optional[int],
3435
"record_sql_params": Optional[bool],
3536
"smart_transaction_trimming": Optional[bool],
37+
# TODO: Remvoe these 2 profiling related experiments
3638
"profiles_sample_rate": Optional[float],
37-
"profiler_mode": Optional[str],
39+
"profiler_mode": Optional[ProfilerMode],
3840
},
3941
total=False,
4042
)
@@ -115,6 +117,9 @@ def __init__(
115117
propagate_traces=True, # type: bool
116118
traces_sample_rate=None, # type: Optional[float]
117119
traces_sampler=None, # type: Optional[TracesSampler]
120+
profiles_sample_rate=None, # type: Optional[float]
121+
profiles_sampler=None, # type: Optional[TracesSampler]
122+
profiler_mode=None, # type: Optional[ProfilerMode]
118123
auto_enabling_integrations=True, # type: bool
119124
auto_session_tracking=True, # type: bool
120125
send_client_reports=True, # type: bool

sentry_sdk/profiler.py

+43-6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from sentry_sdk._types import TYPE_CHECKING
2828
from sentry_sdk.utils import (
2929
filename_for_module,
30+
is_valid_sample_rate,
3031
logger,
3132
nanosecond_time,
3233
set_in_app_in_frames,
@@ -46,7 +47,7 @@
4647
from typing_extensions import TypedDict
4748

4849
import sentry_sdk.tracing
49-
from sentry_sdk._types import SamplingContext
50+
from sentry_sdk._types import SamplingContext, ProfilerMode
5051

5152
ThreadId = str
5253

@@ -148,6 +149,23 @@ def is_gevent():
148149
PROFILE_MINIMUM_SAMPLES = 2
149150

150151

152+
def has_profiling_enabled(options):
153+
# type: (Dict[str, Any]) -> bool
154+
profiles_sampler = options["profiles_sampler"]
155+
if profiles_sampler is not None:
156+
return True
157+
158+
profiles_sample_rate = options["profiles_sample_rate"]
159+
if profiles_sample_rate is not None and profiles_sample_rate > 0:
160+
return True
161+
162+
profiles_sample_rate = options["_experiments"].get("profiles_sample_rate")
163+
if profiles_sample_rate is not None and profiles_sample_rate > 0:
164+
return True
165+
166+
return False
167+
168+
151169
def setup_profiler(options):
152170
# type: (Dict[str, Any]) -> bool
153171
global _scheduler
@@ -171,7 +189,13 @@ def setup_profiler(options):
171189
else:
172190
default_profiler_mode = ThreadScheduler.mode
173191

174-
profiler_mode = options["_experiments"].get("profiler_mode", default_profiler_mode)
192+
if options.get("profiler_mode") is not None:
193+
profiler_mode = options["profiler_mode"]
194+
else:
195+
profiler_mode = (
196+
options.get("_experiments", {}).get("profiler_mode")
197+
or default_profiler_mode
198+
)
175199

176200
if (
177201
profiler_mode == ThreadScheduler.mode
@@ -491,7 +515,13 @@ def _set_initial_sampling_decision(self, sampling_context):
491515
return
492516

493517
options = client.options
494-
sample_rate = options["_experiments"].get("profiles_sample_rate")
518+
519+
if callable(options.get("profiles_sampler")):
520+
sample_rate = options["profiles_sampler"](sampling_context)
521+
elif options["profiles_sample_rate"] is not None:
522+
sample_rate = options["profiles_sample_rate"]
523+
else:
524+
sample_rate = options["_experiments"].get("profiles_sample_rate")
495525

496526
# The profiles_sample_rate option was not set, so profiling
497527
# was never enabled.
@@ -502,6 +532,13 @@ def _set_initial_sampling_decision(self, sampling_context):
502532
self.sampled = False
503533
return
504534

535+
if not is_valid_sample_rate(sample_rate, source="Profiling"):
536+
logger.warning(
537+
"[Profiling] Discarding profile because of invalid sample rate."
538+
)
539+
self.sampled = False
540+
return
541+
505542
# Now we roll the dice. random.random is inclusive of 0, but not of 1,
506543
# so strict < is safe here. In case sample_rate is a boolean, cast it
507544
# to a float (True becomes 1.0 and False becomes 0.0)
@@ -695,7 +732,7 @@ def valid(self):
695732

696733

697734
class Scheduler(object):
698-
mode = "unknown"
735+
mode = "unknown" # type: ProfilerMode
699736

700737
def __init__(self, frequency):
701738
# type: (int) -> None
@@ -824,7 +861,7 @@ class ThreadScheduler(Scheduler):
824861
the sampler at a regular interval.
825862
"""
826863

827-
mode = "thread"
864+
mode = "thread" # type: ProfilerMode
828865
name = "sentry.profiler.ThreadScheduler"
829866

830867
def __init__(self, frequency):
@@ -905,7 +942,7 @@ class GeventScheduler(Scheduler):
905942
results in a sample containing only the sampler's code.
906943
"""
907944

908-
mode = "gevent"
945+
mode = "gevent" # type: ProfilerMode
909946
name = "sentry.profiler.GeventScheduler"
910947

911948
def __init__(self, frequency):

sentry_sdk/tracing.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import sentry_sdk
77
from sentry_sdk.consts import INSTRUMENTER
8-
from sentry_sdk.utils import logger, nanosecond_time
8+
from sentry_sdk.utils import is_valid_sample_rate, logger, nanosecond_time
99
from sentry_sdk._types import TYPE_CHECKING
1010

1111

@@ -722,7 +722,7 @@ def _set_initial_sampling_decision(self, sampling_context):
722722
# Since this is coming from the user (or from a function provided by the
723723
# user), who knows what we might get. (The only valid values are
724724
# booleans or numbers between 0 and 1.)
725-
if not is_valid_sample_rate(sample_rate):
725+
if not is_valid_sample_rate(sample_rate, source="Tracing"):
726726
logger.warning(
727727
"[Tracing] Discarding {transaction_description} because of invalid sample rate.".format(
728728
transaction_description=transaction_description,
@@ -810,6 +810,5 @@ def finish(self, hub=None, end_timestamp=None):
810810
EnvironHeaders,
811811
extract_sentrytrace_data,
812812
has_tracing_enabled,
813-
is_valid_sample_rate,
814813
maybe_create_breadcrumbs_from_span,
815814
)

sentry_sdk/tracing_utils.py

-36
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
import re
22
import contextlib
3-
import math
4-
5-
from numbers import Real
6-
from decimal import Decimal
73

84
import sentry_sdk
95
from sentry_sdk.consts import OP
106

117
from sentry_sdk.utils import (
128
capture_internal_exceptions,
139
Dsn,
14-
logger,
1510
to_string,
1611
)
1712
from sentry_sdk._compat import PY2, iteritems
@@ -100,37 +95,6 @@ def has_tracing_enabled(options):
10095
)
10196

10297

103-
def is_valid_sample_rate(rate):
104-
# type: (Any) -> bool
105-
"""
106-
Checks the given sample rate to make sure it is valid type and value (a
107-
boolean or a number between 0 and 1, inclusive).
108-
"""
109-
110-
# both booleans and NaN are instances of Real, so a) checking for Real
111-
# checks for the possibility of a boolean also, and b) we have to check
112-
# separately for NaN and Decimal does not derive from Real so need to check that too
113-
if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
114-
logger.warning(
115-
"[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
116-
rate=rate, type=type(rate)
117-
)
118-
)
119-
return False
120-
121-
# in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
122-
rate = float(rate)
123-
if rate < 0 or rate > 1:
124-
logger.warning(
125-
"[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
126-
rate=rate
127-
)
128-
)
129-
return False
130-
131-
return True
132-
133-
13498
@contextlib.contextmanager
13599
def record_sql_queries(
136100
hub, # type: sentry_sdk.Hub

sentry_sdk/utils.py

+34
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
import json
33
import linecache
44
import logging
5+
import math
56
import os
67
import re
78
import subprocess
89
import sys
910
import threading
1011
import time
1112
from collections import namedtuple
13+
from decimal import Decimal
14+
from numbers import Real
1215

1316
try:
1417
# Python 3
@@ -1260,6 +1263,37 @@ def parse_url(url, sanitize=True):
12601263
return ParsedUrl(url=base_url, query=parsed_url.query, fragment=parsed_url.fragment)
12611264

12621265

1266+
def is_valid_sample_rate(rate, source):
1267+
# type: (Any, str) -> bool
1268+
"""
1269+
Checks the given sample rate to make sure it is valid type and value (a
1270+
boolean or a number between 0 and 1, inclusive).
1271+
"""
1272+
1273+
# both booleans and NaN are instances of Real, so a) checking for Real
1274+
# checks for the possibility of a boolean also, and b) we have to check
1275+
# separately for NaN and Decimal does not derive from Real so need to check that too
1276+
if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
1277+
logger.warning(
1278+
"{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
1279+
source=source, rate=rate, type=type(rate)
1280+
)
1281+
)
1282+
return False
1283+
1284+
# in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
1285+
rate = float(rate)
1286+
if rate < 0 or rate > 1:
1287+
logger.warning(
1288+
"{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
1289+
source=source, rate=rate
1290+
)
1291+
)
1292+
return False
1293+
1294+
return True
1295+
1296+
12631297
if PY37:
12641298

12651299
def nanosecond_time():

0 commit comments

Comments
 (0)