Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(profiling): Add profiler options to init #1947

Merged
merged 5 commits into from Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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