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 client sdk info to profile chunk #3386

Merged
merged 8 commits into from
Aug 6, 2024
7 changes: 6 additions & 1 deletion sentry_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


if TYPE_CHECKING:
from collections.abc import Container, MutableMapping
from collections.abc import Container, MutableMapping, Sequence

from datetime import datetime

Expand All @@ -25,6 +25,11 @@
from typing import Union
from typing_extensions import Literal, TypedDict

class SDKInfo(TypedDict):
name: str
version: str
packages: Sequence[Mapping[str, str]]

# "critical" is an alias of "fatal" recognized by Relay
LogLevelStr = Literal["fatal", "critical", "error", "warning", "info", "debug"]

Expand Down
5 changes: 3 additions & 2 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
from typing import Type
from typing import Union

from sentry_sdk._types import Event, Hint
from sentry_sdk._types import Event, Hint, SDKInfo
from sentry_sdk.integrations import Integration
from sentry_sdk.metrics import MetricsAggregator
from sentry_sdk.scope import Scope
Expand All @@ -69,7 +69,7 @@
"name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations()
"version": VERSION,
"packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
}
} # type: SDKInfo


def _get_options(*args, **kwargs):
Expand Down Expand Up @@ -391,6 +391,7 @@ def _capture_envelope(envelope):
try:
setup_continuous_profiler(
self.options,
sdk_info=SDK_INFO,
capture_func=_capture_envelope,
)
except Exception as e:
Expand Down
49 changes: 30 additions & 19 deletions sentry_sdk/profiler/continuous_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import uuid
from datetime import datetime, timezone

from sentry_sdk.consts import VERSION
from sentry_sdk.envelope import Envelope
from sentry_sdk._lru_cache import LRUCache
from sentry_sdk._types import TYPE_CHECKING
Expand All @@ -31,7 +32,7 @@
from typing import Type
from typing import Union
from typing_extensions import TypedDict
from sentry_sdk._types import ContinuousProfilerMode
from sentry_sdk._types import ContinuousProfilerMode, SDKInfo
from sentry_sdk.profiler.utils import (
ExtractedSample,
FrameId,
Expand Down Expand Up @@ -65,8 +66,8 @@
_scheduler = None # type: Optional[ContinuousScheduler]


def setup_continuous_profiler(options, capture_func):
# type: (Dict[str, Any], Callable[[Envelope], None]) -> bool
def setup_continuous_profiler(options, sdk_info, capture_func):
# type: (Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> bool
global _scheduler

if _scheduler is not None:
Expand All @@ -91,9 +92,13 @@ def setup_continuous_profiler(options, capture_func):
frequency = DEFAULT_SAMPLING_FREQUENCY

if profiler_mode == ThreadContinuousScheduler.mode:
_scheduler = ThreadContinuousScheduler(frequency, options, capture_func)
_scheduler = ThreadContinuousScheduler(
frequency, options, sdk_info, capture_func
)
elif profiler_mode == GeventContinuousScheduler.mode:
_scheduler = GeventContinuousScheduler(frequency, options, capture_func)
_scheduler = GeventContinuousScheduler(
frequency, options, sdk_info, capture_func
)
else:
raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode))

Expand Down Expand Up @@ -162,10 +167,11 @@ def get_profiler_id():
class ContinuousScheduler(object):
mode = "unknown" # type: ContinuousProfilerMode

def __init__(self, frequency, options, capture_func):
# type: (int, Dict[str, Any], Callable[[Envelope], None]) -> None
def __init__(self, frequency, options, sdk_info, capture_func):
# type: (int, Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> None
self.interval = 1.0 / frequency
self.options = options
self.sdk_info = sdk_info
self.capture_func = capture_func
self.sampler = self.make_sampler()
self.buffer = None # type: Optional[ProfileBuffer]
Expand Down Expand Up @@ -194,7 +200,7 @@ def pause(self):
def reset_buffer(self):
# type: () -> None
self.buffer = ProfileBuffer(
self.options, PROFILE_BUFFER_SECONDS, self.capture_func
self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func
)

@property
Expand Down Expand Up @@ -266,9 +272,9 @@ class ThreadContinuousScheduler(ContinuousScheduler):
mode = "thread" # type: ContinuousProfilerMode
name = "sentry.profiler.ThreadContinuousScheduler"

def __init__(self, frequency, options, capture_func):
# type: (int, Dict[str, Any], Callable[[Envelope], None]) -> None
super().__init__(frequency, options, capture_func)
def __init__(self, frequency, options, sdk_info, capture_func):
# type: (int, Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> None
super().__init__(frequency, options, sdk_info, capture_func)

self.thread = None # type: Optional[threading.Thread]
self.pid = None # type: Optional[int]
Expand Down Expand Up @@ -341,13 +347,13 @@ class GeventContinuousScheduler(ContinuousScheduler):

mode = "gevent" # type: ContinuousProfilerMode

def __init__(self, frequency, options, capture_func):
# type: (int, Dict[str, Any], Callable[[Envelope], None]) -> None
def __init__(self, frequency, options, sdk_info, capture_func):
# type: (int, Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> None

if ThreadPool is None:
raise ValueError("Profiler mode: {} is not available".format(self.mode))

super().__init__(frequency, options, capture_func)
super().__init__(frequency, options, sdk_info, capture_func)

self.thread = None # type: Optional[_ThreadPool]
self.pid = None # type: Optional[int]
Expand Down Expand Up @@ -405,9 +411,10 @@ def teardown(self):


class ProfileBuffer(object):
def __init__(self, options, buffer_size, capture_func):
# type: (Dict[str, Any], int, Callable[[Envelope], None]) -> None
def __init__(self, options, sdk_info, buffer_size, capture_func):
# type: (Dict[str, Any], SDKInfo, int, Callable[[Envelope], None]) -> None
self.options = options
self.sdk_info = sdk_info
self.buffer_size = buffer_size
self.capture_func = capture_func

Expand Down Expand Up @@ -445,7 +452,7 @@ def should_flush(self, monotonic_time):

def flush(self):
# type: () -> None
chunk = self.chunk.to_json(self.profiler_id, self.options)
chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info)
envelope = Envelope()
envelope.add_profile_chunk(chunk)
self.capture_func(envelope)
Expand Down Expand Up @@ -491,8 +498,8 @@ def write(self, ts, sample):
# When this happens, we abandon the current sample as it's bad.
capture_internal_exception(sys.exc_info())

def to_json(self, profiler_id, options):
# type: (str, Dict[str, Any]) -> Dict[str, Any]
def to_json(self, profiler_id, options, sdk_info):
# type: (str, Dict[str, Any], SDKInfo) -> Dict[str, Any]
profile = {
"frames": self.frames,
"stacks": self.stacks,
Expand All @@ -514,6 +521,10 @@ def to_json(self, profiler_id, options):

payload = {
"chunk_id": self.chunk_id,
"client_sdk": {
"name": sdk_info["name"],
"version": VERSION,
},
"platform": "python",
"profile": profile,
"profiler_id": profiler_id,
Expand Down
42 changes: 37 additions & 5 deletions tests/profiler/test_continuous_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

import sentry_sdk
from sentry_sdk.consts import VERSION
from sentry_sdk.profiler.continuous_profiler import (
setup_continuous_profiler,
start_profiler,
Expand All @@ -31,14 +32,25 @@ def experimental_options(mode=None, auto_start=None):
}


mock_sdk_info = {
"name": "sentry.python",
"version": VERSION,
"packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
}


@pytest.mark.parametrize("mode", [pytest.param("foo")])
@pytest.mark.parametrize(
"make_options",
[pytest.param(experimental_options, id="experiment")],
)
def test_continuous_profiler_invalid_mode(mode, make_options, teardown_profiling):
with pytest.raises(ValueError):
setup_continuous_profiler(make_options(mode=mode), lambda envelope: None)
setup_continuous_profiler(
make_options(mode=mode),
mock_sdk_info,
lambda envelope: None,
)


@pytest.mark.parametrize(
Expand All @@ -54,7 +66,11 @@ def test_continuous_profiler_invalid_mode(mode, make_options, teardown_profiling
)
def test_continuous_profiler_valid_mode(mode, make_options, teardown_profiling):
options = make_options(mode=mode)
setup_continuous_profiler(options, lambda envelope: None)
setup_continuous_profiler(
options,
mock_sdk_info,
lambda envelope: None,
)


@pytest.mark.parametrize(
Expand All @@ -71,9 +87,17 @@ def test_continuous_profiler_valid_mode(mode, make_options, teardown_profiling):
def test_continuous_profiler_setup_twice(mode, make_options, teardown_profiling):
options = make_options(mode=mode)
# setting up the first time should return True to indicate success
assert setup_continuous_profiler(options, lambda envelope: None)
assert setup_continuous_profiler(
options,
mock_sdk_info,
lambda envelope: None,
)
# setting up the second time should return False to indicate no-op
assert not setup_continuous_profiler(options, lambda envelope: None)
assert not setup_continuous_profiler(
options,
mock_sdk_info,
lambda envelope: None,
)


def assert_single_transaction_with_profile_chunks(envelopes, thread):
Expand Down Expand Up @@ -119,7 +143,15 @@ def assert_single_transaction_with_profile_chunks(envelopes, thread):
for profile_chunk_item in items["profile_chunk"]:
profile_chunk = profile_chunk_item.payload.json
assert profile_chunk == ApproxDict(
{"platform": "python", "profiler_id": profiler_id, "version": "2"}
{
"client_sdk": {
"name": mock.ANY,
"version": VERSION,
},
"platform": "python",
"profiler_id": profiler_id,
"version": "2",
}
)


Expand Down
Loading