Skip to content

Commit

Permalink
feat(profiling): Add profiler options to init (#1947)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Zylphrex committed Mar 13, 2023
1 parent 2c8d277 commit 3e67535
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 95 deletions.
2 changes: 2 additions & 0 deletions sentry_sdk/_types.py
Expand Up @@ -85,3 +85,5 @@

FractionUnit = Literal["ratio", "percent"]
MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str]

ProfilerMode = Literal["sleep", "thread", "gevent", "unknown"]
5 changes: 2 additions & 3 deletions sentry_sdk/client.py
Expand Up @@ -28,7 +28,7 @@
from sentry_sdk.utils import ContextVar
from sentry_sdk.sessions import SessionFlusher
from sentry_sdk.envelope import Envelope
from sentry_sdk.profiler import setup_profiler
from sentry_sdk.profiler import has_profiling_enabled, setup_profiler

from sentry_sdk._types import TYPE_CHECKING

Expand Down Expand Up @@ -174,8 +174,7 @@ def _capture_envelope(envelope):
finally:
_client_init_debug.set(old_debug)

profiles_sample_rate = self.options["_experiments"].get("profiles_sample_rate")
if profiles_sample_rate is not None and profiles_sample_rate > 0:
if has_profiling_enabled(self.options):
try:
setup_profiler(self.options)
except ValueError as e:
Expand Down
7 changes: 6 additions & 1 deletion sentry_sdk/consts.py
Expand Up @@ -19,6 +19,7 @@
BreadcrumbProcessor,
Event,
EventProcessor,
ProfilerMode,
TracesSampler,
TransactionProcessor,
)
Expand All @@ -33,8 +34,9 @@
"max_spans": Optional[int],
"record_sql_params": Optional[bool],
"smart_transaction_trimming": Optional[bool],
# TODO: Remvoe these 2 profiling related experiments
"profiles_sample_rate": Optional[float],
"profiler_mode": Optional[str],
"profiler_mode": Optional[ProfilerMode],
},
total=False,
)
Expand Down Expand Up @@ -115,6 +117,9 @@ def __init__(
propagate_traces=True, # type: bool
traces_sample_rate=None, # type: Optional[float]
traces_sampler=None, # type: Optional[TracesSampler]
profiles_sample_rate=None, # type: Optional[float]
profiles_sampler=None, # type: Optional[TracesSampler]
profiler_mode=None, # type: Optional[ProfilerMode]
auto_enabling_integrations=True, # type: bool
auto_session_tracking=True, # type: bool
send_client_reports=True, # type: bool
Expand Down
49 changes: 43 additions & 6 deletions sentry_sdk/profiler.py
Expand Up @@ -27,6 +27,7 @@
from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk.utils import (
filename_for_module,
is_valid_sample_rate,
logger,
nanosecond_time,
set_in_app_in_frames,
Expand All @@ -46,7 +47,7 @@
from typing_extensions import TypedDict

import sentry_sdk.tracing
from sentry_sdk._types import SamplingContext
from sentry_sdk._types import SamplingContext, ProfilerMode

ThreadId = str

Expand Down Expand Up @@ -148,6 +149,23 @@ def is_gevent():
PROFILE_MINIMUM_SAMPLES = 2


def has_profiling_enabled(options):
# type: (Dict[str, Any]) -> bool
profiles_sampler = options["profiles_sampler"]
if profiles_sampler is not None:
return True

profiles_sample_rate = options["profiles_sample_rate"]
if profiles_sample_rate is not None and profiles_sample_rate > 0:
return True

profiles_sample_rate = options["_experiments"].get("profiles_sample_rate")
if profiles_sample_rate is not None and profiles_sample_rate > 0:
return True

return False


def setup_profiler(options):
# type: (Dict[str, Any]) -> bool
global _scheduler
Expand All @@ -171,7 +189,13 @@ def setup_profiler(options):
else:
default_profiler_mode = ThreadScheduler.mode

profiler_mode = options["_experiments"].get("profiler_mode", default_profiler_mode)
if options.get("profiler_mode") is not None:
profiler_mode = options["profiler_mode"]
else:
profiler_mode = (
options.get("_experiments", {}).get("profiler_mode")
or default_profiler_mode
)

if (
profiler_mode == ThreadScheduler.mode
Expand Down Expand Up @@ -491,7 +515,13 @@ def _set_initial_sampling_decision(self, sampling_context):
return

options = client.options
sample_rate = options["_experiments"].get("profiles_sample_rate")

if callable(options.get("profiles_sampler")):
sample_rate = options["profiles_sampler"](sampling_context)
elif options["profiles_sample_rate"] is not None:
sample_rate = options["profiles_sample_rate"]
else:
sample_rate = options["_experiments"].get("profiles_sample_rate")

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

if not is_valid_sample_rate(sample_rate, source="Profiling"):
logger.warning(
"[Profiling] Discarding profile because of invalid sample rate."
)
self.sampled = False
return

# Now we roll the dice. random.random is inclusive of 0, but not of 1,
# so strict < is safe here. In case sample_rate is a boolean, cast it
# to a float (True becomes 1.0 and False becomes 0.0)
Expand Down Expand Up @@ -695,7 +732,7 @@ def valid(self):


class Scheduler(object):
mode = "unknown"
mode = "unknown" # type: ProfilerMode

def __init__(self, frequency):
# type: (int) -> None
Expand Down Expand Up @@ -824,7 +861,7 @@ class ThreadScheduler(Scheduler):
the sampler at a regular interval.
"""

mode = "thread"
mode = "thread" # type: ProfilerMode
name = "sentry.profiler.ThreadScheduler"

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

mode = "gevent"
mode = "gevent" # type: ProfilerMode
name = "sentry.profiler.GeventScheduler"

def __init__(self, frequency):
Expand Down
5 changes: 2 additions & 3 deletions sentry_sdk/tracing.py
Expand Up @@ -5,7 +5,7 @@

import sentry_sdk
from sentry_sdk.consts import INSTRUMENTER
from sentry_sdk.utils import logger, nanosecond_time
from sentry_sdk.utils import is_valid_sample_rate, logger, nanosecond_time
from sentry_sdk._types import TYPE_CHECKING


Expand Down Expand Up @@ -722,7 +722,7 @@ def _set_initial_sampling_decision(self, sampling_context):
# Since this is coming from the user (or from a function provided by the
# user), who knows what we might get. (The only valid values are
# booleans or numbers between 0 and 1.)
if not is_valid_sample_rate(sample_rate):
if not is_valid_sample_rate(sample_rate, source="Tracing"):
logger.warning(
"[Tracing] Discarding {transaction_description} because of invalid sample rate.".format(
transaction_description=transaction_description,
Expand Down Expand Up @@ -810,6 +810,5 @@ def finish(self, hub=None, end_timestamp=None):
EnvironHeaders,
extract_sentrytrace_data,
has_tracing_enabled,
is_valid_sample_rate,
maybe_create_breadcrumbs_from_span,
)
36 changes: 0 additions & 36 deletions sentry_sdk/tracing_utils.py
@@ -1,17 +1,12 @@
import re
import contextlib
import math

from numbers import Real
from decimal import Decimal

import sentry_sdk
from sentry_sdk.consts import OP

from sentry_sdk.utils import (
capture_internal_exceptions,
Dsn,
logger,
to_string,
)
from sentry_sdk._compat import PY2, iteritems
Expand Down Expand Up @@ -100,37 +95,6 @@ def has_tracing_enabled(options):
)


def is_valid_sample_rate(rate):
# type: (Any) -> bool
"""
Checks the given sample rate to make sure it is valid type and value (a
boolean or a number between 0 and 1, inclusive).
"""

# both booleans and NaN are instances of Real, so a) checking for Real
# checks for the possibility of a boolean also, and b) we have to check
# separately for NaN and Decimal does not derive from Real so need to check that too
if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
logger.warning(
"[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(
rate=rate, type=type(rate)
)
)
return False

# in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
rate = float(rate)
if rate < 0 or rate > 1:
logger.warning(
"[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
rate=rate
)
)
return False

return True


@contextlib.contextmanager
def record_sql_queries(
hub, # type: sentry_sdk.Hub
Expand Down
34 changes: 34 additions & 0 deletions sentry_sdk/utils.py
Expand Up @@ -2,13 +2,16 @@
import json
import linecache
import logging
import math
import os
import re
import subprocess
import sys
import threading
import time
from collections import namedtuple
from decimal import Decimal
from numbers import Real

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


def is_valid_sample_rate(rate, source):
# type: (Any, str) -> bool
"""
Checks the given sample rate to make sure it is valid type and value (a
boolean or a number between 0 and 1, inclusive).
"""

# both booleans and NaN are instances of Real, so a) checking for Real
# checks for the possibility of a boolean also, and b) we have to check
# separately for NaN and Decimal does not derive from Real so need to check that too
if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
logger.warning(
"{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(
source=source, rate=rate, type=type(rate)
)
)
return False

# in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
rate = float(rate)
if rate < 0 or rate > 1:
logger.warning(
"{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
source=source, rate=rate
)
)
return False

return True


if PY37:

def nanosecond_time():
Expand Down

0 comments on commit 3e67535

Please sign in to comment.