Skip to content

Commit 301028b

Browse files
committed
Merge branch 'master' into sentry-sdk-2.0
2 parents 88007c2 + b742c45 commit 301028b

31 files changed

+828
-424
lines changed

checkouts/data-schemas

sentry_sdk/consts.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,18 @@ class SPANDATA:
201201
Example: "http.handler"
202202
"""
203203

204+
THREAD_ID = "thread.id"
205+
"""
206+
Identifier of a thread from where the span originated. This should be a string.
207+
Example: "7972576320"
208+
"""
209+
210+
THREAD_NAME = "thread.name"
211+
"""
212+
Label identifying a thread from where the span originated. This should be a string.
213+
Example: "MainThread"
214+
"""
215+
204216

205217
class OP:
206218
CACHE_GET_ITEM = "cache.get_item"

sentry_sdk/crons/decorator.py

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
1-
import sys
2-
from contextlib import contextmanager
1+
from functools import wraps
2+
from inspect import iscoroutinefunction
33

44
from sentry_sdk._types import TYPE_CHECKING
55
from sentry_sdk.crons import capture_checkin
66
from sentry_sdk.crons.consts import MonitorStatus
7-
from sentry_sdk.utils import now, reraise
7+
from sentry_sdk.utils import now
88

99
if TYPE_CHECKING:
10-
from typing import Generator, Optional
10+
from types import TracebackType
11+
from typing import (
12+
Awaitable,
13+
Callable,
14+
Optional,
15+
ParamSpec,
16+
Type,
17+
TypeVar,
18+
Union,
19+
)
20+
21+
P = ParamSpec("P")
22+
R = TypeVar("R")
1123

1224

13-
@contextmanager
14-
def monitor(monitor_slug=None):
15-
# type: (Optional[str]) -> Generator[None, None, None]
25+
class monitor: # noqa: N801
1626
"""
1727
Decorator/context manager to capture checkin events for a monitor.
1828
@@ -39,32 +49,51 @@ def test(arg):
3949
with sentry_sdk.monitor(monitor_slug='my-fancy-slug'):
4050
print(arg)
4151
```
52+
"""
4253

54+
def __init__(self, monitor_slug=None):
55+
# type: (Optional[str]) -> None
56+
self.monitor_slug = monitor_slug
4357

44-
"""
58+
def __enter__(self):
59+
# type: () -> None
60+
self.start_timestamp = now()
61+
self.check_in_id = capture_checkin(
62+
monitor_slug=self.monitor_slug, status=MonitorStatus.IN_PROGRESS
63+
)
4564

46-
start_timestamp = now()
47-
check_in_id = capture_checkin(
48-
monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS
49-
)
65+
def __exit__(self, exc_type, exc_value, traceback):
66+
# type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None
67+
duration_s = now() - self.start_timestamp
68+
69+
if exc_type is None and exc_value is None and traceback is None:
70+
status = MonitorStatus.OK
71+
else:
72+
status = MonitorStatus.ERROR
5073

51-
try:
52-
yield
53-
except Exception:
54-
duration_s = now() - start_timestamp
5574
capture_checkin(
56-
monitor_slug=monitor_slug,
57-
check_in_id=check_in_id,
58-
status=MonitorStatus.ERROR,
75+
monitor_slug=self.monitor_slug,
76+
check_in_id=self.check_in_id,
77+
status=status,
5978
duration=duration_s,
6079
)
61-
exc_info = sys.exc_info()
62-
reraise(*exc_info)
63-
64-
duration_s = now() - start_timestamp
65-
capture_checkin(
66-
monitor_slug=monitor_slug,
67-
check_in_id=check_in_id,
68-
status=MonitorStatus.OK,
69-
duration=duration_s,
70-
)
80+
81+
def __call__(self, fn):
82+
# type: (Callable[P, R]) -> Callable[P, Union[R, Awaitable[R]]]
83+
if iscoroutinefunction(fn):
84+
85+
@wraps(fn)
86+
async def inner(*args: "P.args", **kwargs: "P.kwargs"):
87+
# type: (...) -> R
88+
with self:
89+
return await fn(*args, **kwargs)
90+
91+
else:
92+
93+
@wraps(fn)
94+
def inner(*args: "P.args", **kwargs: "P.kwargs"):
95+
# type: (...) -> R
96+
with self:
97+
return fn(*args, **kwargs)
98+
99+
return inner

sentry_sdk/profiler.py

Lines changed: 5 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
from sentry_sdk.utils import (
4444
capture_internal_exception,
4545
filename_for_module,
46+
get_current_thread_meta,
47+
is_gevent,
4648
is_valid_sample_rate,
4749
logger,
4850
nanosecond_time,
@@ -127,32 +129,16 @@
127129

128130

129131
try:
130-
from gevent import get_hub as get_gevent_hub # type: ignore
131-
from gevent.monkey import get_original, is_module_patched # type: ignore
132+
from gevent.monkey import get_original # type: ignore
132133
from gevent.threadpool import ThreadPool # type: ignore
133134

134135
thread_sleep = get_original("time", "sleep")
135136
except ImportError:
136-
137-
def get_gevent_hub():
138-
# type: () -> Any
139-
return None
140-
141137
thread_sleep = time.sleep
142138

143-
def is_module_patched(*args, **kwargs):
144-
# type: (*Any, **Any) -> bool
145-
# unable to import from gevent means no modules have been patched
146-
return False
147-
148139
ThreadPool = None
149140

150141

151-
def is_gevent():
152-
# type: () -> bool
153-
return is_module_patched("threading") or is_module_patched("_thread")
154-
155-
156142
_scheduler = None # type: Optional[Scheduler]
157143

158144
# The default sampling frequency to use. This is set at 101 in order to
@@ -395,52 +381,6 @@ def get_frame_name(frame):
395381
MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds
396382

397383

398-
def get_current_thread_id(thread=None):
399-
# type: (Optional[threading.Thread]) -> Optional[int]
400-
"""
401-
Try to get the id of the current thread, with various fall backs.
402-
"""
403-
404-
# if a thread is specified, that takes priority
405-
if thread is not None:
406-
try:
407-
thread_id = thread.ident
408-
if thread_id is not None:
409-
return thread_id
410-
except AttributeError:
411-
pass
412-
413-
# if the app is using gevent, we should look at the gevent hub first
414-
# as the id there differs from what the threading module reports
415-
if is_gevent():
416-
gevent_hub = get_gevent_hub()
417-
if gevent_hub is not None:
418-
try:
419-
# this is undocumented, so wrap it in try except to be safe
420-
return gevent_hub.thread_ident
421-
except AttributeError:
422-
pass
423-
424-
# use the current thread's id if possible
425-
try:
426-
current_thread_id = threading.current_thread().ident
427-
if current_thread_id is not None:
428-
return current_thread_id
429-
except AttributeError:
430-
pass
431-
432-
# if we can't get the current thread id, fall back to the main thread id
433-
try:
434-
main_thread_id = threading.main_thread().ident
435-
if main_thread_id is not None:
436-
return main_thread_id
437-
except AttributeError:
438-
pass
439-
440-
# we've tried everything, time to give up
441-
return None
442-
443-
444384
class Profile:
445385
def __init__(
446386
self,
@@ -462,7 +402,7 @@ def __init__(
462402

463403
# Various framework integrations are capable of overwriting the active thread id.
464404
# If it is set to `None` at the end of the profile, we fall back to the default.
465-
self._default_active_thread_id = get_current_thread_id() or 0 # type: int
405+
self._default_active_thread_id = get_current_thread_meta()[0] or 0 # type: int
466406
self.active_thread_id = None # type: Optional[int]
467407

468408
try:
@@ -485,7 +425,7 @@ def __init__(
485425

486426
def update_active_thread_id(self):
487427
# type: () -> None
488-
self.active_thread_id = get_current_thread_id()
428+
self.active_thread_id = get_current_thread_meta()[0]
489429
logger.debug(
490430
"[Profiling] updating active thread id to {tid}".format(
491431
tid=self.active_thread_id

sentry_sdk/tracing.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
from datetime import datetime, timedelta, timezone
44

55
import sentry_sdk
6-
from sentry_sdk.consts import INSTRUMENTER
7-
from sentry_sdk.utils import is_valid_sample_rate, logger, nanosecond_time
8-
from sentry_sdk.consts import SPANDATA
6+
from sentry_sdk.consts import INSTRUMENTER, SPANDATA
7+
from sentry_sdk.utils import (
8+
get_current_thread_meta,
9+
is_valid_sample_rate,
10+
logger,
11+
nanosecond_time,
12+
)
913
from sentry_sdk._types import TYPE_CHECKING
1014

1115

@@ -200,6 +204,9 @@ def __init__(
200204
self._span_recorder = None # type: Optional[_SpanRecorder]
201205
self._local_aggregator = None # type: Optional[LocalAggregator]
202206

207+
thread_id, thread_name = get_current_thread_meta()
208+
self.set_thread(thread_id, thread_name)
209+
203210
# TODO this should really live on the Transaction class rather than the Span
204211
# class
205212
def init_span_recorder(self, maxlen):
@@ -435,6 +442,15 @@ def set_status(self, value):
435442
# type: (str) -> None
436443
self.status = value
437444

445+
def set_thread(self, thread_id, thread_name):
446+
# type: (Optional[int], Optional[str]) -> None
447+
448+
if thread_id is not None:
449+
self.set_data(SPANDATA.THREAD_ID, str(thread_id))
450+
451+
if thread_name is not None:
452+
self.set_data(SPANDATA.THREAD_NAME, thread_name)
453+
438454
def set_http_status(self, http_status):
439455
# type: (int) -> None
440456
self.set_tag(

sentry_sdk/types.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
from typing import TYPE_CHECKING
1212

1313
if TYPE_CHECKING:
14-
from sentry_sdk._types import Event, Hint # noqa: F401
14+
from sentry_sdk._types import Event, Hint
15+
else:
16+
# The lines below allow the types to be imported from outside `if TYPE_CHECKING`
17+
# guards. The types in this module are only intended to be used for type hints.
18+
Event = None
19+
Hint = None
1520

16-
__all__ = ["Event", "Hint"]
21+
__all__ = ("Event", "Hint")

sentry_sdk/utils.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,9 +1783,14 @@ def now():
17831783

17841784

17851785
try:
1786+
from gevent import get_hub as get_gevent_hub
17861787
from gevent.monkey import is_module_patched
17871788
except ImportError:
17881789

1790+
def get_gevent_hub():
1791+
# type: () -> Any
1792+
return None
1793+
17891794
def is_module_patched(*args, **kwargs):
17901795
# type: (*Any, **Any) -> bool
17911796
# unable to import from gevent means no modules have been patched
@@ -1795,3 +1800,54 @@ def is_module_patched(*args, **kwargs):
17951800
def is_gevent():
17961801
# type: () -> bool
17971802
return is_module_patched("threading") or is_module_patched("_thread")
1803+
1804+
1805+
def get_current_thread_meta(thread=None):
1806+
# type: (Optional[threading.Thread]) -> Tuple[Optional[int], Optional[str]]
1807+
"""
1808+
Try to get the id of the current thread, with various fall backs.
1809+
"""
1810+
1811+
# if a thread is specified, that takes priority
1812+
if thread is not None:
1813+
try:
1814+
thread_id = thread.ident
1815+
thread_name = thread.name
1816+
if thread_id is not None:
1817+
return thread_id, thread_name
1818+
except AttributeError:
1819+
pass
1820+
1821+
# if the app is using gevent, we should look at the gevent hub first
1822+
# as the id there differs from what the threading module reports
1823+
if is_gevent():
1824+
gevent_hub = get_gevent_hub()
1825+
if gevent_hub is not None:
1826+
try:
1827+
# this is undocumented, so wrap it in try except to be safe
1828+
return gevent_hub.thread_ident, None
1829+
except AttributeError:
1830+
pass
1831+
1832+
# use the current thread's id if possible
1833+
try:
1834+
thread = threading.current_thread()
1835+
thread_id = thread.ident
1836+
thread_name = thread.name
1837+
if thread_id is not None:
1838+
return thread_id, thread_name
1839+
except AttributeError:
1840+
pass
1841+
1842+
# if we can't get the current thread id, fall back to the main thread id
1843+
try:
1844+
thread = threading.main_thread()
1845+
thread_id = thread.ident
1846+
thread_name = thread.name
1847+
if thread_id is not None:
1848+
return thread_id, thread_name
1849+
except AttributeError:
1850+
pass
1851+
1852+
# we've tried everything, time to give up
1853+
return None, None

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,3 +602,15 @@ def patch_start_tracing_child(fake_transaction_is_none=False):
602602
"sentry_sdk.tracing_utils.get_current_span", return_value=fake_transaction
603603
):
604604
yield fake_start_child
605+
606+
607+
class ApproxDict(dict):
608+
def __eq__(self, other):
609+
# For an ApproxDict to equal another dict, the other dict just needs to contain
610+
# all the keys from the ApproxDict with the same values.
611+
#
612+
# The other dict may contain additional keys with any value.
613+
return all(key in other and other[key] == value for key, value in self.items())
614+
615+
def __ne__(self, other):
616+
return not self.__eq__(other)

0 commit comments

Comments
 (0)