Skip to content

Commit

Permalink
feat(profiling): Add client sdk info to profile chunk (#3386)
Browse files Browse the repository at this point in the history
* feat(profiling): Add client sdk info to profile chunk

We want to attach the client sdk info for debugging purposes.

* address PR comments

* use class syntax for typed dict

* import Sequence from collections.abc

* fix typing

---------

Co-authored-by: Anton Pirker <anton.pirker@sentry.io>
  • Loading branch information
2 people authored and sentrivana committed Aug 12, 2024
1 parent dd3111d commit 8e65998
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 27 deletions.
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 @@ -53,7 +53,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 @@ -68,7 +68,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 @@ -386,6 +386,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

0 comments on commit 8e65998

Please sign in to comment.