Skip to content

Commit dc730ed

Browse files
authored
Added new functions_to_trace option for celtral way of performance instrumentation (#1960)
Have a list of functions that can be passed to "sentry_sdk.init()". When the SDK starts it goes through the list and instruments all the functions in the list. functions_to_trace = [ {"qualified_name": "tests.test_basics._hello_world_counter"}, {"qualified_name": "time.sleep"}, {"qualified_name": "collections.Counter.most_common"}, ] sentry_sdk.init( dsn="...", traces_sample_rate=1.0, functions_to_trace=functions_to_trace, )
1 parent 8642de0 commit dc730ed

File tree

4 files changed

+129
-0
lines changed

4 files changed

+129
-0
lines changed

sentry_sdk/client.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from importlib import import_module
12
import os
23
import uuid
34
import random
@@ -17,6 +18,7 @@
1718
logger,
1819
)
1920
from sentry_sdk.serializer import serialize
21+
from sentry_sdk.tracing import trace
2022
from sentry_sdk.transport import make_transport
2123
from sentry_sdk.consts import (
2224
DEFAULT_OPTIONS,
@@ -38,6 +40,7 @@
3840
from typing import Callable
3941
from typing import Dict
4042
from typing import Optional
43+
from typing import Sequence
4144

4245
from sentry_sdk.scope import Scope
4346
from sentry_sdk._types import Event, Hint
@@ -118,6 +121,14 @@ def _get_options(*args, **kwargs):
118121
return rv
119122

120123

124+
try:
125+
# Python 3.6+
126+
module_not_found_error = ModuleNotFoundError
127+
except Exception:
128+
# Older Python versions
129+
module_not_found_error = ImportError # type: ignore
130+
131+
121132
class _Client(object):
122133
"""The client is internally responsible for capturing the events and
123134
forwarding them to sentry through the configured transport. It takes
@@ -140,6 +151,52 @@ def __setstate__(self, state):
140151
self.options = state["options"]
141152
self._init_impl()
142153

154+
def _setup_instrumentation(self, functions_to_trace):
155+
# type: (Sequence[Dict[str, str]]) -> None
156+
"""
157+
Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator.
158+
"""
159+
for function in functions_to_trace:
160+
class_name = None
161+
function_qualname = function["qualified_name"]
162+
module_name, function_name = function_qualname.rsplit(".", 1)
163+
164+
try:
165+
# Try to import module and function
166+
# ex: "mymodule.submodule.funcname"
167+
168+
module_obj = import_module(module_name)
169+
function_obj = getattr(module_obj, function_name)
170+
setattr(module_obj, function_name, trace(function_obj))
171+
logger.debug("Enabled tracing for %s", function_qualname)
172+
173+
except module_not_found_error:
174+
try:
175+
# Try to import a class
176+
# ex: "mymodule.submodule.MyClassName.member_function"
177+
178+
module_name, class_name = module_name.rsplit(".", 1)
179+
module_obj = import_module(module_name)
180+
class_obj = getattr(module_obj, class_name)
181+
function_obj = getattr(class_obj, function_name)
182+
setattr(class_obj, function_name, trace(function_obj))
183+
setattr(module_obj, class_name, class_obj)
184+
logger.debug("Enabled tracing for %s", function_qualname)
185+
186+
except Exception as e:
187+
logger.warning(
188+
"Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
189+
function_qualname,
190+
e,
191+
)
192+
193+
except Exception as e:
194+
logger.warning(
195+
"Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
196+
function_qualname,
197+
e,
198+
)
199+
143200
def _init_impl(self):
144201
# type: () -> None
145202
old_debug = _client_init_debug.get(False)
@@ -184,6 +241,8 @@ def _capture_envelope(envelope):
184241
except ValueError as e:
185242
logger.debug(str(e))
186243

244+
self._setup_instrumentation(self.options.get("functions_to_trace", []))
245+
187246
@property
188247
def dsn(self):
189248
# type: () -> Optional[str]

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def __init__(
133133
trace_propagation_targets=[ # noqa: B006
134134
MATCH_ALL
135135
], # type: Optional[Sequence[str]]
136+
functions_to_trace=[], # type: Sequence[str] # noqa: B006
136137
event_scrubber=None, # type: Optional[sentry_sdk.scrubber.EventScrubber]
137138
):
138139
# type: (...) -> None

tests/test_basics.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import os
33
import sys
4+
import time
45

56
import pytest
67

@@ -618,3 +619,70 @@ def foo(event, hint):
618619
)
619620
def test_get_sdk_name(installed_integrations, expected_name):
620621
assert get_sdk_name(installed_integrations) == expected_name
622+
623+
624+
def _hello_world(word):
625+
return "Hello, {}".format(word)
626+
627+
628+
def test_functions_to_trace(sentry_init, capture_events):
629+
functions_to_trace = [
630+
{"qualified_name": "tests.test_basics._hello_world"},
631+
{"qualified_name": "time.sleep"},
632+
]
633+
634+
sentry_init(
635+
traces_sample_rate=1.0,
636+
functions_to_trace=functions_to_trace,
637+
)
638+
639+
events = capture_events()
640+
641+
with start_transaction(name="something"):
642+
time.sleep(0)
643+
644+
for word in ["World", "You"]:
645+
_hello_world(word)
646+
647+
assert len(events) == 1
648+
649+
(event,) = events
650+
651+
assert len(event["spans"]) == 3
652+
assert event["spans"][0]["description"] == "time.sleep"
653+
assert event["spans"][1]["description"] == "tests.test_basics._hello_world"
654+
assert event["spans"][2]["description"] == "tests.test_basics._hello_world"
655+
656+
657+
class WorldGreeter:
658+
def __init__(self, word):
659+
self.word = word
660+
661+
def greet(self, new_word=None):
662+
return "Hello, {}".format(new_word if new_word else self.word)
663+
664+
665+
def test_functions_to_trace_with_class(sentry_init, capture_events):
666+
functions_to_trace = [
667+
{"qualified_name": "tests.test_basics.WorldGreeter.greet"},
668+
]
669+
670+
sentry_init(
671+
traces_sample_rate=1.0,
672+
functions_to_trace=functions_to_trace,
673+
)
674+
675+
events = capture_events()
676+
677+
with start_transaction(name="something"):
678+
wg = WorldGreeter("World")
679+
wg.greet()
680+
wg.greet("You")
681+
682+
assert len(events) == 1
683+
684+
(event,) = events
685+
686+
assert len(event["spans"]) == 2
687+
assert event["spans"][0]["description"] == "tests.test_basics.WorldGreeter.greet"
688+
assert event["spans"][1]["description"] == "tests.test_basics.WorldGreeter.greet"

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ deps =
177177
arq: arq>=0.23.0
178178
arq: fakeredis>=2.2.0,<2.8
179179
arq: pytest-asyncio
180+
arq: async-timeout
180181

181182
# Asgi
182183
asgi: pytest-asyncio

0 commit comments

Comments
 (0)