From 67a5572abed2abf7f47e07dac5453159d8642c9e Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Mon, 27 Jun 2022 15:01:30 -0700 Subject: [PATCH 01/33] Work-in-progress integration of profiler into WSGI --- sentry_sdk/integrations/wsgi.py | 56 +++++++++--------- sentry_sdk/profiler.py | 100 ++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 sentry_sdk/profiler.py diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 803406fb6d..9b429a4ead 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -1,4 +1,5 @@ import sys +import sentry_sdk.profiler as profiler from sentry_sdk._functools import partial from sentry_sdk.hub import Hub, _should_send_default_pii @@ -109,34 +110,35 @@ def __call__(self, environ, start_response): _wsgi_middleware_applied.set(True) try: hub = Hub(Hub.current) - with auto_session_tracking(hub, session_mode="request"): - with hub: - with capture_internal_exceptions(): - with hub.configure_scope() as scope: - scope.clear_breadcrumbs() - scope._name = "wsgi" - scope.add_event_processor( - _make_wsgi_event_processor( - environ, self.use_x_forwarded_for + with profiler.Sampler(): # TODO: Check if profiling flag is set to True + with auto_session_tracking(hub, session_mode="request"): + with hub: + with capture_internal_exceptions(): + with hub.configure_scope() as scope: + scope.clear_breadcrumbs() + scope._name = "wsgi" + scope.add_event_processor( + _make_wsgi_event_processor( + environ, self.use_x_forwarded_for + ) ) - ) - - transaction = Transaction.continue_from_environ( - environ, op="http.server", name="generic WSGI request" - ) - - with hub.start_transaction( - transaction, custom_sampling_context={"wsgi_environ": environ} - ): - try: - rv = self.app( - environ, - partial( - _sentry_start_response, start_response, transaction - ), - ) - except BaseException: - reraise(*_capture_exception(hub)) + + transaction = Transaction.continue_from_environ( + environ, op="http.server", name="generic WSGI request" + ) + + with hub.start_transaction( + transaction, custom_sampling_context={"wsgi_environ": environ} + ): + try: + rv = self.app( + environ, + partial( + _sentry_start_response, start_response, transaction + ), + ) + except BaseException: + reraise(*_capture_exception(hub)) finally: _wsgi_middleware_applied.set(False) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py new file mode 100644 index 0000000000..0b84efb95a --- /dev/null +++ b/sentry_sdk/profiler.py @@ -0,0 +1,100 @@ +""" +This file contains code from https://github.com/nylas/nylas-perftools, which is published under the following license: + +The MIT License (MIT) + +Copyright (c) 2014 Nylas + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +import atexit +import os +import signal +import time +from sentry_sdk.utils import logger + +def nanosecond_time(): + return int(time.perf_counter() * 1e9) + +class FrameData: + def __init__(self, frame): + self._function_name = frame.f_code.co_name + self._module = frame.f_globals['__name__'] + + # Depending on Python version, frame.f_code.co_filename either stores just the file name or the entire absolute path. + self._file_name = os.path.basename(frame.f_code.co_filename) + self._abs_path = os.path.abspath(frame.f_code.co_filename) # TODO: Must verify this will give us correct absolute paths in all cases! + self._line_number = frame.f_lineno + + def __str__(self): + return f'{self._function_name}({self._module}) in {self._file_name}:{self._line_number}' + +class StackSample: + def __init__(self, top_frame, profiler_start_time): + self._sample_time = nanosecond_time() - profiler_start_time + self._stack = [] + self._add_all_frames(top_frame) + + def _add_all_frames(self, top_frame): + frame = top_frame + while frame is not None: + self._stack.append(FrameData(frame)) + frame = frame.f_back + + def __str__(self): + return f'Time: {self._sample_time}; Stack: {[str(frame) for frame in reversed(self._stack)]}' + +class Sampler(object): + """ + A simple stack sampler for low-overhead CPU profiling: samples the call + stack every `interval` seconds and keeps track of counts by frame. Because + this uses signals, it only works on the main thread. + """ + def __init__(self, interval=0.01): + self.interval = interval + self._stack_samples = [] + + def __enter__(self): + self.start() + + def __exit__(self, *_): + self.stop() + print(self) + + def start(self): + self._start_time = nanosecond_time() + self._stack_samples = [] + try: + signal.signal(signal.SIGVTALRM, self._sample) + except ValueError: + logger.warn('Profiler failed to run because it was started from a non-main thread') # TODO: Does not print anything + return + + signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) + atexit.register(self.stop) + + def _sample(self, signum, frame): + self._stack_samples.append(StackSample(frame, self._start_time)) + #print('j') + signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) + + def _format_frame(self, frame): + return '{}({})'.format(frame.f_code.co_name, + frame.f_globals.get('__name__')) + + def samples(self): + return len(self._stack_samples) + + def __str__(self): + return '\n'.join([str(sample) for sample in self._stack_samples]) + + def stop(self): + signal.setitimer(signal.ITIMER_VIRTUAL, 0) + + def __del__(self): + self.stop() From e591c9049fcb4300c08d8f3ad21472a065e1f465 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Tue, 28 Jun 2022 15:21:01 -0700 Subject: [PATCH 02/33] Deleted dead code --- sentry_sdk/profiler.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 0b84efb95a..15f2bbd631 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -83,10 +83,6 @@ def _sample(self, signum, frame): #print('j') signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) - def _format_frame(self, frame): - return '{}({})'.format(frame.f_code.co_name, - frame.f_globals.get('__name__')) - def samples(self): return len(self._stack_samples) From 1da4c764c9f2fc5c0bd074b4ccaac389ae1cb1ab Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Wed, 29 Jun 2022 16:11:17 -0700 Subject: [PATCH 03/33] Sentry-compatible JSON representation of profile --- sentry_sdk/profiler.py | 138 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 15f2bbd631..b1ad7a7eb9 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -16,6 +16,8 @@ import os import signal import time +import threading +import json from sentry_sdk.utils import logger def nanosecond_time(): @@ -23,20 +25,35 @@ def nanosecond_time(): class FrameData: def __init__(self, frame): - self._function_name = frame.f_code.co_name - self._module = frame.f_globals['__name__'] + self.function_name = frame.f_code.co_name + self.module = frame.f_globals['__name__'] # Depending on Python version, frame.f_code.co_filename either stores just the file name or the entire absolute path. - self._file_name = os.path.basename(frame.f_code.co_filename) - self._abs_path = os.path.abspath(frame.f_code.co_filename) # TODO: Must verify this will give us correct absolute paths in all cases! - self._line_number = frame.f_lineno + self.file_name = os.path.basename(frame.f_code.co_filename) + self.abs_path = os.path.abspath(frame.f_code.co_filename) # TODO: Must verify this will give us correct absolute paths in all cases! + self.line_number = frame.f_code.co_firstlineno + + @property + def _attribute_tuple(self): + """Returns a tuple of the attributes used in comparison""" + + # Do not need to include self.file_name because it depends on self.abs_path + return (self.function_name, self.module, self.abs_path, self.line_number) + + def __eq__(self, other): + if isinstance(other, FrameData): + return self._attribute_tuple == other._attribute_tuple + return False + + def __hash__(self): + return hash(self._attribute_tuple) def __str__(self): - return f'{self._function_name}({self._module}) in {self._file_name}:{self._line_number}' + return f'{self.function_name}({self.module}) in {self.file_name}:{self.line_number}' class StackSample: def __init__(self, top_frame, profiler_start_time): - self._sample_time = nanosecond_time() - profiler_start_time + self.sample_time = nanosecond_time() - profiler_start_time self._stack = [] self._add_all_frames(top_frame) @@ -45,9 +62,24 @@ def _add_all_frames(self, top_frame): while frame is not None: self._stack.append(FrameData(frame)) frame = frame.f_back + self._stack = list(reversed(self._stack)) + + def numeric_representation(self, frame_indices): + """ + Returns a list of numbers representing this stack. The numbers correspond to the value stored in frame_indices + corresponding to the frame. If a given frame does not exist in frame_indices, it will be added with a value equal + to the current size of the frame_indices dictionary. + """ + representation = [] + for frame in self._stack: + if frame not in frame_indices: + frame_indices[frame] = len(frame_indices) + representation.append(frame_indices[frame]) + + return representation def __str__(self): - return f'Time: {self._sample_time}; Stack: {[str(frame) for frame in reversed(self._stack)]}' + return f'Time: {self.sample_time}; Stack: {[str(frame) for frame in reversed(self._stack)]}' class Sampler(object): """ @@ -57,7 +89,7 @@ class Sampler(object): """ def __init__(self, interval=0.01): self.interval = interval - self._stack_samples = [] + self.stack_samples = [] def __enter__(self): self.start() @@ -65,10 +97,13 @@ def __enter__(self): def __exit__(self, *_): self.stop() print(self) + if len(self.stack_samples) > 0: + with open('test_profile.json', 'w') as f: + f.write(self.to_json()) def start(self): self._start_time = nanosecond_time() - self._stack_samples = [] + self.stack_samples = [] try: signal.signal(signal.SIGVTALRM, self._sample) except ValueError: @@ -77,20 +112,95 @@ def start(self): signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) atexit.register(self.stop) + + def sample_weights(self): + """ + Return the weights of each sample (difference between the sample's and previous sample's timestamp). + """ + if self.stack_samples == []: + return [] - def _sample(self, signum, frame): - self._stack_samples.append(StackSample(frame, self._start_time)) + return [self.stack_samples[0].sample_time, *(sample.sample_time - prev_sample.sample_time for sample, prev_sample in zip(self.stack_samples[1:], self.stack_samples))] + + def _sample(self, _, frame): + self.stack_samples.append(StackSample(frame, self._start_time)) #print('j') signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) + def to_json(self): + """ + Exports this object to a JSON format compatible with Sentry's profiling visualizer + """ + return json.dumps(self, cls=self.JSONEncoder) + + def samples_numeric_representation(self): + """ + Returns the samples with a numeric representation. There are two return values. + The first return value actually contains the samples. It is a list of lists of numbers. + Each list of numbers in the list represents a stack sample. The numbers correspond + to a frame; specifically, the numbers are indices of the list returned as the second + return value. This second return value contains a list of each unique frame that was + observed. The samples array (first return value) is interpreted by looking up the + indexes in the frames array (second return value). + """ + frame_indices = dict() # Stores eventual index of each frame in the frame array + numeric_representation = [sample.numeric_representation(frame_indices) for sample in self.stack_samples] + + # Build frame array from the frame indices + frames = [None] * len(frame_indices) + for frame, index in frame_indices.items(): + frames[index] = frame + + return numeric_representation, frames + + def samples(self): - return len(self._stack_samples) + return len(self.stack_samples) def __str__(self): - return '\n'.join([str(sample) for sample in self._stack_samples]) + return '\n'.join([str(sample) for sample in self.stack_samples]) def stop(self): signal.setitimer(signal.ITIMER_VIRTUAL, 0) def __del__(self): self.stop() + + class JSONEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, Sampler): + samples, frames = o.samples_numeric_representation() + return { + 'transactionName': 'test', # TODO: What should this be? + 'profiles': [{ + 'weights': o.sample_weights(), + 'samples': samples, + 'type': 'sampled', + 'endValue': o.stack_samples[-1].sample_time, # end ts + 'startValue': 0, # start ts + 'name': 'Python profile', # TODO: What should the name be? + 'unit': 'nanoseconds', + 'threadID': threading.get_ident() + }], + 'shared': { + 'frames': [{ + 'name': frame.function_name, + 'file': frame.abs_path, + 'line': frame.line_number + } for frame in frames] # TODO: Add all elements + # 'frames': [{ + # 'key': string | number, + # 'name': string, + # 'file': string, + # 'line': number, + # 'column': number, + # 'is_application': boolean, + # 'image': string, + # 'resource': string, + # 'threadId': number + # }] # TODO: Add all elements + } + } + + else: + return json.JSONEncoder.default(self, o) From cfa6fa9519537167a0508038f32220c590c8db47 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Thu, 30 Jun 2022 14:08:16 -0700 Subject: [PATCH 04/33] Changed profile name to "main" --- sentry_sdk/profiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index b1ad7a7eb9..9c9978a0bd 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -178,7 +178,7 @@ def default(self, o): 'type': 'sampled', 'endValue': o.stack_samples[-1].sample_time, # end ts 'startValue': 0, # start ts - 'name': 'Python profile', # TODO: What should the name be? + 'name': 'main', 'unit': 'nanoseconds', 'threadID': threading.get_ident() }], From be558ec72e0afb9dcd6baee08298c29c06c70696 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Thu, 30 Jun 2022 14:09:10 -0700 Subject: [PATCH 05/33] Added option for conditionally enabling profiling --- sentry_sdk/consts.py | 1 + sentry_sdk/integrations/wsgi.py | 66 +++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 34faec3c12..527a1d5e6d 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -78,6 +78,7 @@ def __init__( auto_enabling_integrations=True, # type: bool auto_session_tracking=True, # type: bool send_client_reports=True, # type: bool + enable_profiling=False, # type: bool _experiments={}, # type: Experiments # noqa: B006 ): # type: (...) -> None diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 9b429a4ead..119f640a4c 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -1,6 +1,7 @@ import sys import sentry_sdk.profiler as profiler +from contextlib import contextmanager from sentry_sdk._functools import partial from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import ( @@ -93,6 +94,16 @@ def get_request_url(environ, use_x_forwarded_for=False): wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"), ) +@contextmanager +def conditional_context_manager(manager, condition): + """ + Conditionally calls a given context manager if condition met; otherwise, acts as a no-op context manager. + """ + if condition: + with manager: + yield + else: + yield class SentryWsgiMiddleware(object): __slots__ = ("app", "use_x_forwarded_for") @@ -110,35 +121,34 @@ def __call__(self, environ, start_response): _wsgi_middleware_applied.set(True) try: hub = Hub(Hub.current) - with profiler.Sampler(): # TODO: Check if profiling flag is set to True - with auto_session_tracking(hub, session_mode="request"): - with hub: - with capture_internal_exceptions(): - with hub.configure_scope() as scope: - scope.clear_breadcrumbs() - scope._name = "wsgi" - scope.add_event_processor( - _make_wsgi_event_processor( - environ, self.use_x_forwarded_for - ) - ) - - transaction = Transaction.continue_from_environ( - environ, op="http.server", name="generic WSGI request" - ) - - with hub.start_transaction( - transaction, custom_sampling_context={"wsgi_environ": environ} - ): - try: - rv = self.app( - environ, - partial( - _sentry_start_response, start_response, transaction - ), + with auto_session_tracking(hub, session_mode="request"): + with hub: + with capture_internal_exceptions(): + with hub.configure_scope() as scope: + scope.clear_breadcrumbs() + scope._name = "wsgi" + scope.add_event_processor( + _make_wsgi_event_processor( + environ, self.use_x_forwarded_for ) - except BaseException: - reraise(*_capture_exception(hub)) + ) + + transaction = Transaction.continue_from_environ( + environ, op="http.server", name="generic WSGI request" + ) + + with hub.start_transaction( + transaction, custom_sampling_context={"wsgi_environ": environ} + ), conditional_context_manager(profiler.Sampler(), hub.client.options['enable_profiling']): + try: + rv = self.app( + environ, + partial( + _sentry_start_response, start_response, transaction + ), + ) + except BaseException: + reraise(*_capture_exception(hub)) finally: _wsgi_middleware_applied.set(False) From 7e8e9cc175112a66398bbccbbbfd801ee777c3e4 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Thu, 30 Jun 2022 15:12:30 -0700 Subject: [PATCH 06/33] Adding transaction name to profile --- sentry_sdk/integrations/wsgi.py | 2 +- sentry_sdk/profiler.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 119f640a4c..4623d34f15 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -139,7 +139,7 @@ def __call__(self, environ, start_response): with hub.start_transaction( transaction, custom_sampling_context={"wsgi_environ": environ} - ), conditional_context_manager(profiler.Sampler(), hub.client.options['enable_profiling']): + ), conditional_context_manager(profiler.Sampler(transaction), hub.client.options['enable_profiling']): try: rv = self.app( environ, diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 9c9978a0bd..703f3a9260 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -87,9 +87,10 @@ class Sampler(object): stack every `interval` seconds and keeps track of counts by frame. Because this uses signals, it only works on the main thread. """ - def __init__(self, interval=0.01): + def __init__(self, transaction, interval=0.01): self.interval = interval self.stack_samples = [] + self._transaction = transaction def __enter__(self): self.start() @@ -166,12 +167,16 @@ def stop(self): def __del__(self): self.stop() + @property + def transaction_name(self): + return self._transaction.name + class JSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, Sampler): samples, frames = o.samples_numeric_representation() return { - 'transactionName': 'test', # TODO: What should this be? + 'transactionName': o.transaction_name, 'profiles': [{ 'weights': o.sample_weights(), 'samples': samples, From 596f474841be061ab431d185a985dc0c70725da2 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Thu, 30 Jun 2022 15:18:26 -0700 Subject: [PATCH 07/33] Save only the raw file name --- sentry_sdk/profiler.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 703f3a9260..3762e7aec5 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -29,16 +29,13 @@ def __init__(self, frame): self.module = frame.f_globals['__name__'] # Depending on Python version, frame.f_code.co_filename either stores just the file name or the entire absolute path. - self.file_name = os.path.basename(frame.f_code.co_filename) - self.abs_path = os.path.abspath(frame.f_code.co_filename) # TODO: Must verify this will give us correct absolute paths in all cases! + self.file_name = frame.f_code.co_filename self.line_number = frame.f_code.co_firstlineno @property def _attribute_tuple(self): """Returns a tuple of the attributes used in comparison""" - - # Do not need to include self.file_name because it depends on self.abs_path - return (self.function_name, self.module, self.abs_path, self.line_number) + return (self.function_name, self.module, self.file_name, self.line_number) def __eq__(self, other): if isinstance(other, FrameData): @@ -190,7 +187,7 @@ def default(self, o): 'shared': { 'frames': [{ 'name': frame.function_name, - 'file': frame.abs_path, + 'file': frame.file_name, 'line': frame.line_number } for frame in frames] # TODO: Add all elements # 'frames': [{ From d1755a1e9dd93d8ecd4c2484d4e72a12754003e2 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Wed, 6 Jul 2022 11:03:17 -0700 Subject: [PATCH 08/33] Memory optimized, no overwriting profiles --- sentry_sdk/profiler.py | 74 +++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 48 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 3762e7aec5..320e97e770 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -13,13 +13,16 @@ """ import atexit -import os +from datetime import datetime import signal import time import threading import json from sentry_sdk.utils import logger +# TODO: temp import to output file +import os + def nanosecond_time(): return int(time.perf_counter() * 1e9) @@ -49,34 +52,23 @@ def __str__(self): return f'{self.function_name}({self.module}) in {self.file_name}:{self.line_number}' class StackSample: - def __init__(self, top_frame, profiler_start_time): + def __init__(self, top_frame, profiler_start_time, frame_indices): self.sample_time = nanosecond_time() - profiler_start_time - self._stack = [] - self._add_all_frames(top_frame) + self.stack = [] + self._add_all_frames(top_frame, frame_indices) - def _add_all_frames(self, top_frame): + def _add_all_frames(self, top_frame, frame_indices): frame = top_frame while frame is not None: - self._stack.append(FrameData(frame)) + frame_data = FrameData(frame) + if frame_data not in frame_indices: + frame_indices[frame_data] = len(frame_indices) + self.stack.append(frame_indices[frame_data]) frame = frame.f_back - self._stack = list(reversed(self._stack)) - - def numeric_representation(self, frame_indices): - """ - Returns a list of numbers representing this stack. The numbers correspond to the value stored in frame_indices - corresponding to the frame. If a given frame does not exist in frame_indices, it will be added with a value equal - to the current size of the frame_indices dictionary. - """ - representation = [] - for frame in self._stack: - if frame not in frame_indices: - frame_indices[frame] = len(frame_indices) - representation.append(frame_indices[frame]) - - return representation + self.stack = list(reversed(self.stack)) def __str__(self): - return f'Time: {self.sample_time}; Stack: {[str(frame) for frame in reversed(self._stack)]}' + return f'Time: {self.sample_time}; Stack: {[str(frame) for frame in reversed(self.stack)]}' class Sampler(object): """ @@ -94,14 +86,16 @@ def __enter__(self): def __exit__(self, *_): self.stop() - print(self) if len(self.stack_samples) > 0: - with open('test_profile.json', 'w') as f: + if not os.path.exists('./profiles'): + os.makedirs('./profiles') + with open(f'./profiles/{datetime.utcnow().isoformat()}Z.json', 'w') as f: f.write(self.to_json()) def start(self): self._start_time = nanosecond_time() self.stack_samples = [] + self._frame_indices = dict() try: signal.signal(signal.SIGVTALRM, self._sample) except ValueError: @@ -121,8 +115,7 @@ def sample_weights(self): return [self.stack_samples[0].sample_time, *(sample.sample_time - prev_sample.sample_time for sample, prev_sample in zip(self.stack_samples[1:], self.stack_samples))] def _sample(self, _, frame): - self.stack_samples.append(StackSample(frame, self._start_time)) - #print('j') + self.stack_samples.append(StackSample(frame, self._start_time, self._frame_indices)) signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) def to_json(self): @@ -130,27 +123,13 @@ def to_json(self): Exports this object to a JSON format compatible with Sentry's profiling visualizer """ return json.dumps(self, cls=self.JSONEncoder) - - def samples_numeric_representation(self): - """ - Returns the samples with a numeric representation. There are two return values. - The first return value actually contains the samples. It is a list of lists of numbers. - Each list of numbers in the list represents a stack sample. The numbers correspond - to a frame; specifically, the numbers are indices of the list returned as the second - return value. This second return value contains a list of each unique frame that was - observed. The samples array (first return value) is interpreted by looking up the - indexes in the frames array (second return value). - """ - frame_indices = dict() # Stores eventual index of each frame in the frame array - numeric_representation = [sample.numeric_representation(frame_indices) for sample in self.stack_samples] - # Build frame array from the frame indices - frames = [None] * len(frame_indices) - for frame, index in frame_indices.items(): + def frame_list(self): + # Build frame array from the frame indices + frames = [None] * len(self._frame_indices) + for frame, index in self._frame_indices.items(): frames[index] = frame - - return numeric_representation, frames - + return frames def samples(self): return len(self.stack_samples) @@ -171,12 +150,11 @@ def transaction_name(self): class JSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, Sampler): - samples, frames = o.samples_numeric_representation() return { 'transactionName': o.transaction_name, 'profiles': [{ 'weights': o.sample_weights(), - 'samples': samples, + 'samples': [sample.stack for sample in o.stack_samples], 'type': 'sampled', 'endValue': o.stack_samples[-1].sample_time, # end ts 'startValue': 0, # start ts @@ -189,7 +167,7 @@ def default(self, o): 'name': frame.function_name, 'file': frame.file_name, 'line': frame.line_number - } for frame in frames] # TODO: Add all elements + } for frame in o.frame_list()] # TODO: Add all elements # 'frames': [{ # 'key': string | number, # 'name': string, From cff0127c9248d2f0c818a01adfd4ad1871f8b7b0 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Fri, 8 Jul 2022 15:03:39 -0700 Subject: [PATCH 09/33] Changed JSON output format --- sentry_sdk/profiler.py | 40 +++++++++++----------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 320e97e770..0f1779d018 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -150,36 +150,18 @@ def transaction_name(self): class JSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, Sampler): + thread_id = threading.get_ident() return { - 'transactionName': o.transaction_name, - 'profiles': [{ - 'weights': o.sample_weights(), - 'samples': [sample.stack for sample in o.stack_samples], - 'type': 'sampled', - 'endValue': o.stack_samples[-1].sample_time, # end ts - 'startValue': 0, # start ts - 'name': 'main', - 'unit': 'nanoseconds', - 'threadID': threading.get_ident() - }], - 'shared': { - 'frames': [{ - 'name': frame.function_name, - 'file': frame.file_name, - 'line': frame.line_number - } for frame in o.frame_list()] # TODO: Add all elements - # 'frames': [{ - # 'key': string | number, - # 'name': string, - # 'file': string, - # 'line': number, - # 'column': number, - # 'is_application': boolean, - # 'image': string, - # 'resource': string, - # 'threadId': number - # }] # TODO: Add all elements - } + 'samples': [{ + 'frames': sample.stack, + 'relative_timestamp_ns': sample.sample_time, + 'thread_id': thread_id + } for sample in o.stack_samples], + 'frames': [{ + 'name': frame.function_name, + 'file': frame.file_name, + 'line': frame.line_number + } for frame in o.frame_list()] } else: From 7b97f698f46d11d708a5111c33af19835d062fac Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Fri, 8 Jul 2022 15:46:50 -0700 Subject: [PATCH 10/33] Upload profile data to Sentry --- sentry_sdk/profiler.py | 1 + sentry_sdk/tracing.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 0f1779d018..4e037b9078 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -80,6 +80,7 @@ def __init__(self, transaction, interval=0.01): self.interval = interval self.stack_samples = [] self._transaction = transaction + transaction._profile = self def __enter__(self): self.start() diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index f6f625acc8..cbcde75c71 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -488,6 +488,7 @@ class Transaction(Span): # tracestate data from other vendors, of the form `dogs=yes,cats=maybe` "_third_party_tracestate", "_measurements", + "_profile", ) def __init__( @@ -604,6 +605,7 @@ def finish(self, hub=None): "timestamp": self.timestamp, "start_timestamp": self.start_timestamp, "spans": finished_spans, + "profile": self._profile.to_json() } if has_custom_measurements_enabled(): From 3b5f032e08f922ea40924aa12d2c9ffca8b051a0 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Fri, 8 Jul 2022 15:48:36 -0700 Subject: [PATCH 11/33] Stop printing profiles to JSON file locally --- sentry_sdk/profiler.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 4e037b9078..10f098a98d 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -87,11 +87,6 @@ def __enter__(self): def __exit__(self, *_): self.stop() - if len(self.stack_samples) > 0: - if not os.path.exists('./profiles'): - os.makedirs('./profiles') - with open(f'./profiles/{datetime.utcnow().isoformat()}Z.json', 'w') as f: - f.write(self.to_json()) def start(self): self._start_time = nanosecond_time() From 8da6d1049b0fe7954114ecd7c1e3357007932987 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Mon, 11 Jul 2022 12:10:03 -0700 Subject: [PATCH 12/33] Only save profile to event if profiling enabled --- sentry_sdk/tracing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index cbcde75c71..87fba94d51 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -605,9 +605,11 @@ def finish(self, hub=None): "timestamp": self.timestamp, "start_timestamp": self.start_timestamp, "spans": finished_spans, - "profile": self._profile.to_json() } + if hub.client.options["enable_profiling"]: + event["profile"] = self._profile.to_json() + if has_custom_measurements_enabled(): event["measurements"] = self._measurements From 78e6c1dcd7385294aa713da77ef0cffeafac18dc Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Mon, 11 Jul 2022 12:15:18 -0700 Subject: [PATCH 13/33] Fix bug: error when JSON generated no sample --- sentry_sdk/profiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 10f098a98d..7cc05a79dd 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -13,7 +13,6 @@ """ import atexit -from datetime import datetime import signal import time import threading @@ -79,6 +78,7 @@ class Sampler(object): def __init__(self, transaction, interval=0.01): self.interval = interval self.stack_samples = [] + self._frame_indices = dict() self._transaction = transaction transaction._profile = self From ccd2e452c8cccf179e6850985562c196cf74c37d Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Mon, 11 Jul 2022 16:38:42 -0700 Subject: [PATCH 14/33] Profile sent as separate envelope item --- sentry_sdk/client.py | 3 +++ sentry_sdk/envelope.py | 6 ++++++ sentry_sdk/profiler.py | 37 +++++++++++++++---------------------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 63a1205f57..e1d16245ec 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -391,6 +391,9 @@ def capture_event( envelope = Envelope(headers=headers) if is_transaction: + if "profile" in event_opt: + envelope.add_profile(event_opt["profile"]) + del event_opt["profile"] envelope.add_transaction(event_opt) else: envelope.add_event(event_opt) diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index 928c691cdd..917b04969b 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -61,6 +61,12 @@ def add_transaction( ): # type: (...) -> None self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) + + def add_profile( + self, profile # type: dict + ): + # type: (...) -> None + self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) def add_session( self, session # type: Union[Session, Any] diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 7cc05a79dd..df2767a5b7 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -116,9 +116,22 @@ def _sample(self, _, frame): def to_json(self): """ - Exports this object to a JSON format compatible with Sentry's profiling visualizer + Exports this object to a JSON format compatible with Sentry's profiling visualizer. + Returns dictionary which can be serialized to JSON. """ - return json.dumps(self, cls=self.JSONEncoder) + thread_id = threading.get_ident() + return { + 'samples': [{ + 'frames': sample.stack, + 'relative_timestamp_ns': sample.sample_time, + 'thread_id': thread_id + } for sample in self.stack_samples], + 'frames': [{ + 'name': frame.function_name, + 'file': frame.file_name, + 'line': frame.line_number + } for frame in self.frame_list()] + } def frame_list(self): # Build frame array from the frame indices @@ -142,23 +155,3 @@ def __del__(self): @property def transaction_name(self): return self._transaction.name - - class JSONEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, Sampler): - thread_id = threading.get_ident() - return { - 'samples': [{ - 'frames': sample.stack, - 'relative_timestamp_ns': sample.sample_time, - 'thread_id': thread_id - } for sample in o.stack_samples], - 'frames': [{ - 'name': frame.function_name, - 'file': frame.file_name, - 'line': frame.line_number - } for frame in o.frame_list()] - } - - else: - return json.JSONEncoder.default(self, o) From 68d77e3b6649f60f465d580f53307a129685a871 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Tue, 12 Jul 2022 14:07:23 -0700 Subject: [PATCH 15/33] Fix bug crashing program when no profile collected --- sentry_sdk/tracing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 87fba94d51..d980eb7ee6 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -608,7 +608,10 @@ def finish(self, hub=None): } if hub.client.options["enable_profiling"]: - event["profile"] = self._profile.to_json() + try: + event["profile"] = self._profile.to_json() + except AttributeError: + pass # self._profile will not exist if a profile is not collected (e.g. on non-WSGI) if has_custom_measurements_enabled(): event["measurements"] = self._measurements From cb8ea79a18908aa9118f8fd54a2076fa251126d5 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Wed, 13 Jul 2022 10:45:06 -0700 Subject: [PATCH 16/33] Changed licensing notice in profiler.py --- sentry_sdk/profiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 320e97e770..e3c29243a5 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -1,5 +1,5 @@ """ -This file contains code from https://github.com/nylas/nylas-perftools, which is published under the following license: +This file is originally based on code from https://github.com/nylas/nylas-perftools, which is published under the following license: The MIT License (MIT) From 5ba799fe72f809f047d98878b8af4794c6594830 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Fri, 15 Jul 2022 12:21:20 -0700 Subject: [PATCH 17/33] Changes from Neel's code review feedback --- sentry_sdk/integrations/wsgi.py | 17 ++++++++++------- sentry_sdk/profiler.py | 13 ++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 4623d34f15..a517698d6f 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -25,6 +25,7 @@ from typing import Optional from typing import TypeVar from typing import Protocol + from typing import Generator from sentry_sdk.utils import ExcInfo from sentry_sdk._types import EventProcessor @@ -95,12 +96,12 @@ def get_request_url(environ, use_x_forwarded_for=False): ) @contextmanager -def conditional_context_manager(manager, condition): - """ - Conditionally calls a given context manager if condition met; otherwise, acts as a no-op context manager. - """ - if condition: - with manager: +def profiling(transaction, hub=None): + # type: (Transaction, Optional[Hub]) -> Generator[None, None, None] + if hub is None: + hub = Hub.current + if hub.client.options["enable_profiling"]: + with profiler.Sampler(transaction): yield else: yield @@ -139,7 +140,9 @@ def __call__(self, environ, start_response): with hub.start_transaction( transaction, custom_sampling_context={"wsgi_environ": environ} - ), conditional_context_manager(profiler.Sampler(transaction), hub.client.options['enable_profiling']): + ), profiling( + transaction, hub + ): try: rv = self.app( environ, diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index e3c29243a5..76443d5afc 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -48,9 +48,6 @@ def __eq__(self, other): def __hash__(self): return hash(self._attribute_tuple) - def __str__(self): - return f'{self.function_name}({self.module}) in {self.file_name}:{self.line_number}' - class StackSample: def __init__(self, top_frame, profiler_start_time, frame_indices): self.sample_time = nanosecond_time() - profiler_start_time @@ -67,9 +64,6 @@ def _add_all_frames(self, top_frame, frame_indices): frame = frame.f_back self.stack = list(reversed(self.stack)) - def __str__(self): - return f'Time: {self.sample_time}; Stack: {[str(frame) for frame in reversed(self.stack)]}' - class Sampler(object): """ A simple stack sampler for low-overhead CPU profiling: samples the call @@ -89,7 +83,7 @@ def __exit__(self, *_): if len(self.stack_samples) > 0: if not os.path.exists('./profiles'): os.makedirs('./profiles') - with open(f'./profiles/{datetime.utcnow().isoformat()}Z.json', 'w') as f: + with open('./profiles/{0}Z.json'.format(datetime.utcnow().isoformat()), 'w') as f: f.write(self.to_json()) def start(self): @@ -99,7 +93,7 @@ def start(self): try: signal.signal(signal.SIGVTALRM, self._sample) except ValueError: - logger.warn('Profiler failed to run because it was started from a non-main thread') # TODO: Does not print anything + logger.error('Profiler failed to run because it was started from a non-main thread') return signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) @@ -134,9 +128,6 @@ def frame_list(self): def samples(self): return len(self.stack_samples) - def __str__(self): - return '\n'.join([str(sample) for sample in self.stack_samples]) - def stop(self): signal.setitimer(signal.ITIMER_VIRTUAL, 0) From b418ac0d2dcda32797fa3080f98bd0fc40989c4f Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Fri, 15 Jul 2022 12:42:10 -0700 Subject: [PATCH 18/33] Code review changes for Neel --- sentry_sdk/client.py | 3 +-- sentry_sdk/tracing.py | 9 ++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 3f30adbff0..d2df844587 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -402,8 +402,7 @@ def capture_event( if is_transaction: if "profile" in event_opt: - envelope.add_profile(event_opt["profile"]) - del event_opt["profile"] + envelope.add_profile(event_opt.pop("profile")) envelope.add_transaction(event_opt) else: envelope.add_event(event_opt) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index c6f52654c7..c9ae976a30 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -19,6 +19,7 @@ from typing import List from typing import Tuple from typing import Iterator + from sentry_sdk.profiler import Sampler from sentry_sdk._types import SamplingContext, MeasurementUnit @@ -565,6 +566,7 @@ def __init__( self._sentry_tracestate = sentry_tracestate self._third_party_tracestate = third_party_tracestate self._measurements = {} # type: Dict[str, Any] + self._profile = None # type: Optional[Sampler] self._baggage = baggage def __repr__(self): @@ -657,11 +659,8 @@ def finish(self, hub=None): "spans": finished_spans, } - if hub.client.options["enable_profiling"]: - try: - event["profile"] = self._profile.to_json() - except AttributeError: - pass # self._profile will not exist if a profile is not collected (e.g. on non-WSGI) + if hub.client.options["enable_profiling"] and self._profile is not None: + event["profile"] = self._profile.to_json() if has_custom_measurements_enabled(): event["measurements"] = self._measurements From 2ace5fce64e27053a99cf0f115f0f92580c8c2ca Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Wed, 20 Jul 2022 13:32:28 -0700 Subject: [PATCH 19/33] Deleted dead sample_weights method --- sentry_sdk/profiler.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 8ffa748e25..7c6a20744f 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -94,15 +94,6 @@ def start(self): signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) atexit.register(self.stop) - - def sample_weights(self): - """ - Return the weights of each sample (difference between the sample's and previous sample's timestamp). - """ - if self.stack_samples == []: - return [] - - return [self.stack_samples[0].sample_time, *(sample.sample_time - prev_sample.sample_time for sample, prev_sample in zip(self.stack_samples[1:], self.stack_samples))] def _sample(self, _, frame): self.stack_samples.append(StackSample(frame, self._start_time, self._frame_indices)) From c4752734c9bb5a65afbaf6667bfe0b511834660d Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Wed, 20 Jul 2022 13:54:55 -0700 Subject: [PATCH 20/33] Fixed linter errors --- sentry_sdk/client.py | 2 +- sentry_sdk/envelope.py | 4 ++-- sentry_sdk/integrations/wsgi.py | 2 ++ sentry_sdk/profiler.py | 20 ++++++++++---------- sentry_sdk/tracing.py | 2 +- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index d2df844587..32c62d5f57 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -402,7 +402,7 @@ def capture_event( if is_transaction: if "profile" in event_opt: - envelope.add_profile(event_opt.pop("profile")) + envelope.add_profile(event_opt.pop("profile")) envelope.add_transaction(event_opt) else: envelope.add_event(event_opt) diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index 917b04969b..33ffcbaf42 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -61,9 +61,9 @@ def add_transaction( ): # type: (...) -> None self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) - + def add_profile( - self, profile # type: dict + self, profile # type: dict ): # type: (...) -> None self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index a517698d6f..3547783455 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -95,6 +95,7 @@ def get_request_url(environ, use_x_forwarded_for=False): wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"), ) + @contextmanager def profiling(transaction, hub=None): # type: (Transaction, Optional[Hub]) -> Generator[None, None, None] @@ -106,6 +107,7 @@ def profiling(transaction, hub=None): else: yield + class SentryWsgiMiddleware(object): __slots__ = ("app", "use_x_forwarded_for") diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 7c6a20744f..748c7da291 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -16,15 +16,13 @@ import signal import time import threading -import json from sentry_sdk.utils import logger -# TODO: temp import to output file -import os def nanosecond_time(): return int(time.perf_counter() * 1e9) + class FrameData: def __init__(self, frame): self.function_name = frame.f_code.co_name @@ -33,7 +31,7 @@ def __init__(self, frame): # Depending on Python version, frame.f_code.co_filename either stores just the file name or the entire absolute path. self.file_name = frame.f_code.co_filename self.line_number = frame.f_code.co_firstlineno - + @property def _attribute_tuple(self): """Returns a tuple of the attributes used in comparison""" @@ -43,10 +41,11 @@ def __eq__(self, other): if isinstance(other, FrameData): return self._attribute_tuple == other._attribute_tuple return False - + def __hash__(self): return hash(self._attribute_tuple) + class StackSample: def __init__(self, top_frame, profiler_start_time, frame_indices): self.sample_time = nanosecond_time() - profiler_start_time @@ -63,6 +62,7 @@ def _add_all_frames(self, top_frame, frame_indices): frame = frame.f_back self.stack = list(reversed(self.stack)) + class Sampler(object): """ A simple stack sampler for low-overhead CPU profiling: samples the call @@ -75,10 +75,10 @@ def __init__(self, transaction, interval=0.01): self._frame_indices = dict() self._transaction = transaction transaction._profile = self - + def __enter__(self): self.start() - + def __exit__(self, *_): self.stop() @@ -90,7 +90,7 @@ def start(self): signal.signal(signal.SIGVTALRM, self._sample) except ValueError: logger.error('Profiler failed to run because it was started from a non-main thread') - return + return signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) atexit.register(self.stop) @@ -119,7 +119,7 @@ def to_json(self): } def frame_list(self): - # Build frame array from the frame indices + # Build frame array from the frame indices frames = [None] * len(self._frame_indices) for frame, index in self._frame_indices.items(): frames[index] = frame @@ -133,7 +133,7 @@ def stop(self): def __del__(self): self.stop() - + @property def transaction_name(self): return self._transaction.name diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index c9ae976a30..b6db4be383 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -566,7 +566,7 @@ def __init__( self._sentry_tracestate = sentry_tracestate self._third_party_tracestate = third_party_tracestate self._measurements = {} # type: Dict[str, Any] - self._profile = None # type: Optional[Sampler] + self._profile = None # type: Optional[Sampler] self._baggage = baggage def __repr__(self): From fb8989d447f36b0d2895ea6403fb508cf8955d00 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Wed, 20 Jul 2022 14:03:53 -0700 Subject: [PATCH 21/33] Fixed linter error that was missed in previous commit --- sentry_sdk/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 4eedd824c2..3b37eed0f2 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -78,7 +78,7 @@ def __init__( auto_enabling_integrations=True, # type: bool auto_session_tracking=True, # type: bool send_client_reports=True, # type: bool - enable_profiling=False, # type: bool + enable_profiling=False, # type: bool _experiments={}, # type: Experiments # noqa: B006 ): # type: (...) -> None From 3797c390b8dfb98f5d778cfc9c41be719799f5aa Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Wed, 20 Jul 2022 14:29:48 -0700 Subject: [PATCH 22/33] Ran pre-commit hooks against the code --- sentry_sdk/integrations/wsgi.py | 4 +--- sentry_sdk/profiler.py | 37 +++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 3547783455..6ed6e022dc 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -142,9 +142,7 @@ def __call__(self, environ, start_response): with hub.start_transaction( transaction, custom_sampling_context={"wsgi_environ": environ} - ), profiling( - transaction, hub - ): + ), profiling(transaction, hub): try: rv = self.app( environ, diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 748c7da291..6be1bd17d7 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -26,7 +26,7 @@ def nanosecond_time(): class FrameData: def __init__(self, frame): self.function_name = frame.f_code.co_name - self.module = frame.f_globals['__name__'] + self.module = frame.f_globals["__name__"] # Depending on Python version, frame.f_code.co_filename either stores just the file name or the entire absolute path. self.file_name = frame.f_code.co_filename @@ -69,6 +69,7 @@ class Sampler(object): stack every `interval` seconds and keeps track of counts by frame. Because this uses signals, it only works on the main thread. """ + def __init__(self, transaction, interval=0.01): self.interval = interval self.stack_samples = [] @@ -89,14 +90,18 @@ def start(self): try: signal.signal(signal.SIGVTALRM, self._sample) except ValueError: - logger.error('Profiler failed to run because it was started from a non-main thread') + logger.error( + "Profiler failed to run because it was started from a non-main thread" + ) return signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) atexit.register(self.stop) def _sample(self, _, frame): - self.stack_samples.append(StackSample(frame, self._start_time, self._frame_indices)) + self.stack_samples.append( + StackSample(frame, self._start_time, self._frame_indices) + ) signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) def to_json(self): @@ -106,16 +111,22 @@ def to_json(self): """ thread_id = threading.get_ident() return { - 'samples': [{ - 'frames': sample.stack, - 'relative_timestamp_ns': sample.sample_time, - 'thread_id': thread_id - } for sample in self.stack_samples], - 'frames': [{ - 'name': frame.function_name, - 'file': frame.file_name, - 'line': frame.line_number - } for frame in self.frame_list()] + "samples": [ + { + "frames": sample.stack, + "relative_timestamp_ns": sample.sample_time, + "thread_id": thread_id, + } + for sample in self.stack_samples + ], + "frames": [ + { + "name": frame.function_name, + "file": frame.file_name, + "line": frame.line_number, + } + for frame in self.frame_list() + ], } def frame_list(self): From 9c84589bd1d47efed868babffe7802c33ee3422a Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Wed, 20 Jul 2022 15:18:52 -0700 Subject: [PATCH 23/33] Fix MYPY errors --- sentry_sdk/envelope.py | 2 +- sentry_sdk/integrations/wsgi.py | 2 +- sentry_sdk/profiler.py | 36 ++++++++++++++++++++++++--------- sentry_sdk/tracing.py | 6 +++++- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index 33ffcbaf42..f8d895d0bf 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -63,7 +63,7 @@ def add_transaction( self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) def add_profile( - self, profile # type: dict + self, profile # type: Any ): # type: (...) -> None self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 6ed6e022dc..e1b02dc636 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -101,7 +101,7 @@ def profiling(transaction, hub=None): # type: (Transaction, Optional[Hub]) -> Generator[None, None, None] if hub is None: hub = Hub.current - if hub.client.options["enable_profiling"]: + if hub.client is not None and hub.client.options["enable_profiling"]: with profiler.Sampler(transaction): yield else: diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 6be1bd17d7..58869a4fb8 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -18,13 +18,21 @@ import threading from sentry_sdk.utils import logger +from sentry_sdk._types import MYPY + +if MYPY: + import typing + import sentry_sdk.tracing + def nanosecond_time(): + # type: () -> int return int(time.perf_counter() * 1e9) class FrameData: def __init__(self, frame): + # type: (typing.Any) -> None self.function_name = frame.f_code.co_name self.module = frame.f_globals["__name__"] @@ -34,25 +42,30 @@ def __init__(self, frame): @property def _attribute_tuple(self): + # type: () -> typing.Tuple[str, str, str, int] """Returns a tuple of the attributes used in comparison""" return (self.function_name, self.module, self.file_name, self.line_number) def __eq__(self, other): + # type: (typing.Any) -> bool if isinstance(other, FrameData): return self._attribute_tuple == other._attribute_tuple return False def __hash__(self): + # type: () -> int return hash(self._attribute_tuple) class StackSample: def __init__(self, top_frame, profiler_start_time, frame_indices): + # type: (typing.Any, int, typing.Dict[FrameData, int]) -> None self.sample_time = nanosecond_time() - profiler_start_time - self.stack = [] + self.stack = [] # type: typing.List[int] self._add_all_frames(top_frame, frame_indices) def _add_all_frames(self, top_frame, frame_indices): + # type: (typing.Any, typing.Dict[FrameData, int]) -> None frame = top_frame while frame is not None: frame_data = FrameData(frame) @@ -71,19 +84,23 @@ class Sampler(object): """ def __init__(self, transaction, interval=0.01): + # type: (sentry_sdk.tracing.Transaction, float) -> None self.interval = interval - self.stack_samples = [] - self._frame_indices = dict() + self.stack_samples = [] # type: typing.List[StackSample] + self._frame_indices = dict() # type: typing.Dict[FrameData, int] self._transaction = transaction transaction._profile = self def __enter__(self): + # type: () -> None self.start() def __exit__(self, *_): + # type: (*typing.List[typing.Any]) -> None self.stop() def start(self): + # type: () -> None self._start_time = nanosecond_time() self.stack_samples = [] self._frame_indices = dict() @@ -99,12 +116,14 @@ def start(self): atexit.register(self.stop) def _sample(self, _, frame): + # type: (typing.Any, typing.Any) -> None self.stack_samples.append( StackSample(frame, self._start_time, self._frame_indices) ) signal.setitimer(signal.ITIMER_VIRTUAL, self.interval) def to_json(self): + # type: () -> typing.Any """ Exports this object to a JSON format compatible with Sentry's profiling visualizer. Returns dictionary which can be serialized to JSON. @@ -130,21 +149,18 @@ def to_json(self): } def frame_list(self): + # type: () -> typing.List[FrameData] # Build frame array from the frame indices - frames = [None] * len(self._frame_indices) + frames = [None] * len(self._frame_indices) # type: typing.List[typing.Any] for frame, index in self._frame_indices.items(): frames[index] = frame return frames - def samples(self): - return len(self.stack_samples) - def stop(self): + # type: () -> None signal.setitimer(signal.ITIMER_VIRTUAL, 0) - def __del__(self): - self.stop() - @property def transaction_name(self): + # type: () -> str return self._transaction.name diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index b6db4be383..daba8999f7 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -659,7 +659,11 @@ def finish(self, hub=None): "spans": finished_spans, } - if hub.client.options["enable_profiling"] and self._profile is not None: + if ( + hub.client is not None + and hub.client.options["enable_profiling"] + and self._profile is not None + ): event["profile"] = self._profile.to_json() if has_custom_measurements_enabled(): From fb58394029667ed48d7a182db0d066d425e13296 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Thu, 21 Jul 2022 15:06:17 -0700 Subject: [PATCH 24/33] Added metadata to profile --- sentry_sdk/client.py | 2 ++ sentry_sdk/profiler.py | 2 ++ sentry_sdk/tracing.py | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 32c62d5f57..449cf5624e 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -402,6 +402,8 @@ def capture_event( if is_transaction: if "profile" in event_opt: + event_opt["profile"]["transaction_id"] = event_opt["event_id"] + event_opt["profile"]["version_name"] = event_opt["release"] envelope.add_profile(event_opt.pop("profile")) envelope.add_transaction(event_opt) else: diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 58869a4fb8..45676b70bc 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -89,6 +89,7 @@ def __init__(self, transaction, interval=0.01): self.stack_samples = [] # type: typing.List[StackSample] self._frame_indices = dict() # type: typing.Dict[FrameData, int] self._transaction = transaction + self.duration = 0 # This value will only be correct after the profiler has been started and stopped transaction._profile = self def __enter__(self): @@ -158,6 +159,7 @@ def frame_list(self): def stop(self): # type: () -> None + self.duration = nanosecond_time() - self._start_time signal.setitimer(signal.ITIMER_VIRTUAL, 0) @property diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index daba8999f7..d0145059ef 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,6 +1,7 @@ import uuid import random import time +import platform from datetime import datetime, timedelta @@ -664,7 +665,20 @@ def finish(self, hub=None): and hub.client.options["enable_profiling"] and self._profile is not None ): - event["profile"] = self._profile.to_json() + event["profile"] = { + "device_os_name": platform.system(), + "device_os_version": platform.release(), + "duration_ns": self._profile.duration, + "environment": hub.client.options["environment"], + "platform": "python", + "platform_version": platform.python_version(), + "profile_id": uuid.uuid4().hex, + "profile": self._profile.to_json(), + "trace_id": self.trace_id, + "transaction_id": None, # Gets added in client.py + "transaction_name": self.name, + "version_name": None, # Gets added in client.py + } if has_custom_measurements_enabled(): event["measurements"] = self._measurements From b95f46cdfaf8a3dde79ce8610b4c54a8647c7095 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Fri, 22 Jul 2022 14:41:34 -0700 Subject: [PATCH 25/33] enable_profiling flag moved to experiment --- sentry_sdk/consts.py | 2 +- sentry_sdk/integrations/wsgi.py | 4 +++- sentry_sdk/tracing.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 3b37eed0f2..9132317327 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -34,6 +34,7 @@ "smart_transaction_trimming": Optional[bool], "propagate_tracestate": Optional[bool], "custom_measurements": Optional[bool], + "enable_profiling": Optional[bool], }, total=False, ) @@ -78,7 +79,6 @@ def __init__( auto_enabling_integrations=True, # type: bool auto_session_tracking=True, # type: bool send_client_reports=True, # type: bool - enable_profiling=False, # type: bool _experiments={}, # type: Experiments # noqa: B006 ): # type: (...) -> None diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index e1b02dc636..4eb6ee00e5 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -101,7 +101,9 @@ def profiling(transaction, hub=None): # type: (Transaction, Optional[Hub]) -> Generator[None, None, None] if hub is None: hub = Hub.current - if hub.client is not None and hub.client.options["enable_profiling"]: + if hub.client is not None and hub.client.options["_experiments"].get( + "enable_profiling", False + ): with profiler.Sampler(transaction): yield else: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 9f9b08b061..320d264c54 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -664,7 +664,7 @@ def finish(self, hub=None): if ( hub.client is not None - and hub.client.options["enable_profiling"] + and hub.client.options["_experiments"].get("enable_profiling", False) and self._profile is not None ): event["profile"] = { From 9d6a49489e6640706462a4387082ab369545d56a Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Fri, 22 Jul 2022 16:41:55 -0700 Subject: [PATCH 26/33] Added integration tests --- tests/integrations/wsgi/test_wsgi.py | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 66cc1a1de7..a45b6fa154 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -279,3 +279,43 @@ def sample_app(environ, start_response): assert session_aggregates[0]["exited"] == 2 assert session_aggregates[0]["crashed"] == 1 assert len(session_aggregates) == 1 + + +def test_profile_sent_when_profiling_enabled(capture_envelopes, sentry_init): + def test_app(environ, start_response): + start_response("200 OK", []) + return ["Go get the ball! Good dog!"] + + sentry_init(traces_sample_rate=1.0, _experiments={"enable_profiling": True}) + app = SentryWsgiMiddleware(test_app) + envelopes = capture_envelopes() + + client = Client(app) + client.get("/") + + profile_sent = False + for item in envelopes[0].items: + if item.headers["type"] == "profile": + profile_sent = True + break + assert profile_sent + + +def test_profile_not_sent_when_profiling_disabled(capture_envelopes, sentry_init): + def test_app(environ, start_response): + start_response("200 OK", []) + return ["Go get the ball! Good dog!"] + + sentry_init(traces_sample_rate=1.0) + app = SentryWsgiMiddleware(test_app) + envelopes = capture_envelopes() + + client = Client(app) + client.get("/") + + profile_sent = False + for item in envelopes[0].items: + if item.headers["type"] == "profile": + profile_sent = True + break + assert not profile_sent From 6c8fc385dc29410cdcb10b0a0fcc62c8e1194d02 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Mon, 25 Jul 2022 12:30:00 -0700 Subject: [PATCH 27/33] Fixed thread ID call for Python 2.7 --- sentry_sdk/profiler.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 45676b70bc..9a09d62b27 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -30,6 +30,21 @@ def nanosecond_time(): return int(time.perf_counter() * 1e9) +def thread_id(): + """ + Returns the thread ID of the current thread. This function is written to be compatible with Python 3 or Python 2.7. + """ + # type: () -> int + try: + thread_id = threading.get_ident() + except AttributeError: # Python 2.7 + import thread + + threading.get_ident = thread.get_ident + thread_id = threading.get_ident() + return thread_id + + class FrameData: def __init__(self, frame): # type: (typing.Any) -> None @@ -129,13 +144,12 @@ def to_json(self): Exports this object to a JSON format compatible with Sentry's profiling visualizer. Returns dictionary which can be serialized to JSON. """ - thread_id = threading.get_ident() return { "samples": [ { "frames": sample.stack, "relative_timestamp_ns": sample.sample_time, - "thread_id": thread_id, + "thread_id": thread_id(), } for sample in self.stack_samples ], From 18595258c45c1eeae8501bb5894653305b16c0b4 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Mon, 25 Jul 2022 12:37:24 -0700 Subject: [PATCH 28/33] Fixed MYPY error --- sentry_sdk/profiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 9a09d62b27..6ef827cd95 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -31,10 +31,10 @@ def nanosecond_time(): def thread_id(): + # type: () -> int """ Returns the thread ID of the current thread. This function is written to be compatible with Python 3 or Python 2.7. """ - # type: () -> int try: thread_id = threading.get_ident() except AttributeError: # Python 2.7 From a93f10c5640a6c64b42dcea7ecf5dc6b3a5a5725 Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Mon, 25 Jul 2022 12:44:31 -0700 Subject: [PATCH 29/33] Add some comments, suppress missing import --- sentry_sdk/profiler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 6ef827cd95..00e6221d5a 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -36,9 +36,11 @@ def thread_id(): Returns the thread ID of the current thread. This function is written to be compatible with Python 3 or Python 2.7. """ try: + # threading.get_ident is only available in Python 3.3+, so this will fail for Python 2.7. thread_id = threading.get_ident() except AttributeError: # Python 2.7 - import thread + # The thread module is only available in Python 2.7, need to suppress MYPY missing import error + import thread # type: ignore threading.get_ident = thread.get_ident thread_id = threading.get_ident() From 07e1f4b4b546251d6b9c32e1de7d3c1a83b51bdf Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Mon, 25 Jul 2022 15:10:52 -0700 Subject: [PATCH 30/33] Fixed Python2 compatibility issues --- sentry_sdk/profiler.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 00e6221d5a..81eada52f2 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -15,9 +15,14 @@ import atexit import signal import time -import threading +from sentry_sdk._compat import PY2 from sentry_sdk.utils import logger +if PY2: + import thread # noqa +else: + import threading + from sentry_sdk._types import MYPY if MYPY: @@ -25,26 +30,25 @@ import sentry_sdk.tracing -def nanosecond_time(): - # type: () -> int - return int(time.perf_counter() * 1e9) +if PY2: + def thread_id(): + # type: () -> int + return thread.get_ident() -def thread_id(): - # type: () -> int - """ - Returns the thread ID of the current thread. This function is written to be compatible with Python 3 or Python 2.7. - """ - try: - # threading.get_ident is only available in Python 3.3+, so this will fail for Python 2.7. - thread_id = threading.get_ident() - except AttributeError: # Python 2.7 - # The thread module is only available in Python 2.7, need to suppress MYPY missing import error - import thread # type: ignore - - threading.get_ident = thread.get_ident - thread_id = threading.get_ident() - return thread_id + def nanosecond_time(): + # type: () -> int + return int(time.clock() * 1e9) + +else: + + def thread_id(): + # type: () -> int + return threading.get_ident() + + def nanosecond_time(): + # type: () -> int + return int(time.perf_counter() * 1e9) class FrameData: From d1ca7d990d81e35a1981e8a425747a57a4cad1aa Mon Sep 17 00:00:00 2001 From: szokeasaurusrex Date: Mon, 25 Jul 2022 16:05:09 -0700 Subject: [PATCH 31/33] Set version_code field to empty string --- sentry_sdk/tracing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 320d264c54..c442b4880a 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -679,6 +679,7 @@ def finish(self, hub=None): "trace_id": self.trace_id, "transaction_id": None, # Gets added in client.py "transaction_name": self.name, + "version_code": "", # TODO: Determine appropriate value. Currently set to empty string so profile will not get rejected. "version_name": None, # Gets added in client.py } From c1f0b64149f62f91a7db76e54cdb9a10d0f18392 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Thu, 28 Jul 2022 11:55:19 +0200 Subject: [PATCH 32/33] Move context manager to profiler --- sentry_sdk/integrations/wsgi.py | 18 +----------------- sentry_sdk/profiler.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 4eb6ee00e5..32bba51cd2 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -1,7 +1,5 @@ import sys -import sentry_sdk.profiler as profiler -from contextlib import contextmanager from sentry_sdk._functools import partial from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import ( @@ -13,6 +11,7 @@ from sentry_sdk.tracing import Transaction from sentry_sdk.sessions import auto_session_tracking from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.profiler import profiling from sentry_sdk._types import MYPY @@ -25,7 +24,6 @@ from typing import Optional from typing import TypeVar from typing import Protocol - from typing import Generator from sentry_sdk.utils import ExcInfo from sentry_sdk._types import EventProcessor @@ -96,20 +94,6 @@ def get_request_url(environ, use_x_forwarded_for=False): ) -@contextmanager -def profiling(transaction, hub=None): - # type: (Transaction, Optional[Hub]) -> Generator[None, None, None] - if hub is None: - hub = Hub.current - if hub.client is not None and hub.client.options["_experiments"].get( - "enable_profiling", False - ): - with profiler.Sampler(transaction): - yield - else: - yield - - class SentryWsgiMiddleware(object): __slots__ = ("app", "use_x_forwarded_for") diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 81eada52f2..22f9b100cf 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -15,7 +15,10 @@ import atexit import signal import time +from contextlib import contextmanager + from sentry_sdk._compat import PY2 +from sentry_sdk.hub import Hub from sentry_sdk.utils import logger if PY2: @@ -27,6 +30,8 @@ if MYPY: import typing + from typing import Generator + from typing import Optional import sentry_sdk.tracing @@ -186,3 +191,20 @@ def stop(self): def transaction_name(self): # type: () -> str return self._transaction.name + + +@contextmanager +def profiling(transaction, hub=None): + # type: (sentry_sdk.tracing.Transaction, Optional[Hub]) -> Generator[None, None, None] + if hub is None: + hub = Hub.current + + if ( + hub.client + and hub.client.options + and hub.client.options["_experiments"].get("enable_profiling", False) + ): + with Sampler(transaction): + yield + else: + yield From 837c4096e2d0791c0ff521fad34a8efb523af124 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Thu, 28 Jul 2022 12:02:36 +0200 Subject: [PATCH 33/33] Factor out profiling hceck --- sentry_sdk/profiler.py | 22 ++++++++++++---------- sentry_sdk/tracing.py | 5 +++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 22f9b100cf..f499a5eac2 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -17,8 +17,8 @@ import time from contextlib import contextmanager +import sentry_sdk from sentry_sdk._compat import PY2 -from sentry_sdk.hub import Hub from sentry_sdk.utils import logger if PY2: @@ -193,17 +193,19 @@ def transaction_name(self): return self._transaction.name -@contextmanager -def profiling(transaction, hub=None): - # type: (sentry_sdk.tracing.Transaction, Optional[Hub]) -> Generator[None, None, None] +def has_profiling_enabled(hub=None): + # type: (Optional[sentry_sdk.Hub]) -> bool if hub is None: - hub = Hub.current + hub = sentry_sdk.Hub.current + + options = hub.client and hub.client.options + return bool(options and options["_experiments"].get("enable_profiling")) - if ( - hub.client - and hub.client.options - and hub.client.options["_experiments"].get("enable_profiling", False) - ): + +@contextmanager +def profiling(transaction, hub=None): + # type: (sentry_sdk.tracing.Transaction, Optional[sentry_sdk.Hub]) -> Generator[None, None, None] + if has_profiling_enabled(hub): with Sampler(transaction): yield else: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index c442b4880a..fa95b6ec6f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -7,6 +7,7 @@ import sentry_sdk +from sentry_sdk.profiler import has_profiling_enabled from sentry_sdk.utils import logger from sentry_sdk._types import MYPY @@ -663,8 +664,8 @@ def finish(self, hub=None): } if ( - hub.client is not None - and hub.client.options["_experiments"].get("enable_profiling", False) + has_profiling_enabled(hub) + and hub.client is not None and self._profile is not None ): event["profile"] = {