Skip to content

Commit f44dec5

Browse files
committed
feat(profiling): Add thread data to spans
As per getsentry/rfc#75, this adds the thread data to the spans. This will be needed for the continuous profiling mode in #2830.
1 parent 16d25e2 commit f44dec5

File tree

6 files changed

+163
-89
lines changed

6 files changed

+163
-89
lines changed

sentry_sdk/consts.py

+12
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,18 @@ class SPANDATA:
191191
Example: "http.handler"
192192
"""
193193

194+
THREAD_ID = "thread.id"
195+
"""
196+
The thread id within which the span was started. This should be a string.
197+
Example: "7972576320"
198+
"""
199+
200+
THREAD_NAME = "thread.name"
201+
"""
202+
The thread name within which the span was started. This should be a string.
203+
Example: "MainThread"
204+
"""
205+
194206

195207
class OP:
196208
CACHE_GET_ITEM = "cache.get_item"

sentry_sdk/profiler.py

+5-65
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
from sentry_sdk.utils import (
4343
capture_internal_exception,
4444
filename_for_module,
45+
get_current_thread_meta,
46+
is_gevent,
4547
is_valid_sample_rate,
4648
logger,
4749
nanosecond_time,
@@ -126,32 +128,16 @@
126128

127129

128130
try:
129-
from gevent import get_hub as get_gevent_hub # type: ignore
130-
from gevent.monkey import get_original, is_module_patched # type: ignore
131+
from gevent.monkey import get_original # type: ignore
131132
from gevent.threadpool import ThreadPool # type: ignore
132133

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

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

149140

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

157143
# The default sampling frequency to use. This is set at 101 in order to
@@ -389,52 +375,6 @@ def get_frame_name(frame):
389375
MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds
390376

391377

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

457397
# Various framework integrations are capable of overwriting the active thread id.
458398
# If it is set to `None` at the end of the profile, we fall back to the default.
459-
self._default_active_thread_id = get_current_thread_id() or 0 # type: int
399+
self._default_active_thread_id = get_current_thread_meta()[0] or 0 # type: int
460400
self.active_thread_id = None # type: Optional[int]
461401

462402
try:
@@ -479,7 +419,7 @@ def __init__(
479419

480420
def update_active_thread_id(self):
481421
# type: () -> None
482-
self.active_thread_id = get_current_thread_id()
422+
self.active_thread_id = get_current_thread_meta()[0]
483423
logger.debug(
484424
"[Profiling] updating active thread id to {tid}".format(
485425
tid=self.active_thread_id

sentry_sdk/tracing.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55

66
import sentry_sdk
77
from sentry_sdk.consts import INSTRUMENTER
8-
from sentry_sdk.utils import is_valid_sample_rate, logger, nanosecond_time
8+
from sentry_sdk.utils import (
9+
get_current_thread_meta,
10+
is_valid_sample_rate,
11+
logger,
12+
nanosecond_time,
13+
)
914
from sentry_sdk._compat import datetime_utcnow, utc_from_timestamp, PY2
1015
from sentry_sdk.consts import SPANDATA
1116
from sentry_sdk._types import TYPE_CHECKING
@@ -172,6 +177,9 @@ def __init__(
172177
self._span_recorder = None # type: Optional[_SpanRecorder]
173178
self._local_aggregator = None # type: Optional[LocalAggregator]
174179

180+
thread_id, thread_name = get_current_thread_meta()
181+
self.set_thread(thread_id, thread_name)
182+
175183
# TODO this should really live on the Transaction class rather than the Span
176184
# class
177185
def init_span_recorder(self, maxlen):
@@ -418,6 +426,15 @@ def set_status(self, value):
418426
# type: (str) -> None
419427
self.status = value
420428

429+
def set_thread(self, thread_id, thread_name):
430+
# type: (Optional[int], Optional[str]) -> None
431+
432+
if thread_id is not None:
433+
self.set_data(SPANDATA.THREAD_ID, thread_id)
434+
435+
if thread_name is not None:
436+
self.set_data(SPANDATA.THREAD_NAME, thread_name)
437+
421438
def set_http_status(self, http_status):
422439
# type: (int) -> None
423440
self.set_tag(

sentry_sdk/utils.py

+55
Original file line numberDiff line numberDiff line change
@@ -1746,8 +1746,12 @@ def now():
17461746

17471747

17481748
try:
1749+
from gevent import get_hub as get_gevent_hub
17491750
from gevent.monkey import is_module_patched
17501751
except ImportError:
1752+
def get_gevent_hub():
1753+
# type: () -> Any
1754+
return None
17511755

17521756
def is_module_patched(*args, **kwargs):
17531757
# type: (*Any, **Any) -> bool
@@ -1758,3 +1762,54 @@ def is_module_patched(*args, **kwargs):
17581762
def is_gevent():
17591763
# type: () -> bool
17601764
return is_module_patched("threading") or is_module_patched("_thread")
1765+
1766+
1767+
def get_current_thread_meta(thread=None):
1768+
# type: (Optional[threading.Thread]) -> Tuple[Optional[int], Optional[str]]
1769+
"""
1770+
Try to get the id of the current thread, with various fall backs.
1771+
"""
1772+
1773+
# if a thread is specified, that takes priority
1774+
if thread is not None:
1775+
try:
1776+
thread_id = thread.ident
1777+
thread_name = thread.name
1778+
if thread_id is not None:
1779+
return thread_id, thread_name
1780+
except AttributeError:
1781+
pass
1782+
1783+
# if the app is using gevent, we should look at the gevent hub first
1784+
# as the id there differs from what the threading module reports
1785+
if is_gevent():
1786+
gevent_hub = get_gevent_hub()
1787+
if gevent_hub is not None:
1788+
try:
1789+
# this is undocumented, so wrap it in try except to be safe
1790+
return gevent_hub.thread_ident, gevent_hub.name
1791+
except AttributeError:
1792+
pass
1793+
1794+
# use the current thread's id if possible
1795+
try:
1796+
thread = threading.current_thread()
1797+
thread_id = thread.ident
1798+
thread_name = thread.name
1799+
if thread_id is not None:
1800+
return thread_id, thread_name
1801+
except AttributeError:
1802+
pass
1803+
1804+
# if we can't get the current thread id, fall back to the main thread id
1805+
try:
1806+
thread = threading.main_thread()
1807+
thread_id = thread.ident
1808+
thread_name = thread.name
1809+
if thread_id is not None:
1810+
return thread_id, thread_name
1811+
except AttributeError:
1812+
pass
1813+
1814+
# we've tried everything, time to give up
1815+
return None, None

tests/test_profiler.py

-23
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
extract_frame,
1717
extract_stack,
1818
frame_id,
19-
get_current_thread_id,
2019
get_frame_name,
2120
setup_profiler,
2221
)
@@ -556,28 +555,6 @@ def test_extract_stack_with_cache(frame, depth):
556555
assert frame1 is frame2, i
557556

558557

559-
@requires_python_version(3, 3)
560-
def test_get_current_thread_id_explicit_thread():
561-
results = Queue(maxsize=1)
562-
563-
def target1():
564-
pass
565-
566-
def target2():
567-
results.put(get_current_thread_id(thread1))
568-
569-
thread1 = threading.Thread(target=target1)
570-
thread1.start()
571-
572-
thread2 = threading.Thread(target=target2)
573-
thread2.start()
574-
575-
thread2.join()
576-
thread1.join()
577-
578-
assert thread1.ident == results.get(timeout=1)
579-
580-
581558
@requires_python_version(3, 3)
582559
@requires_gevent
583560
def test_get_current_thread_id_gevent_in_thread():

tests/test_utils.py

+73
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import pytest
22
import re
33
import sys
4+
import threading
45
from datetime import timedelta
56

67
from sentry_sdk._compat import duration_in_milliseconds
8+
from sentry_sdk._queue import Queue
79
from sentry_sdk.utils import (
810
Components,
911
Dsn,
12+
get_current_thread_meta,
1013
get_default_release,
1114
get_error_message,
1215
get_git_revision,
@@ -29,6 +32,11 @@
2932
except ImportError:
3033
import mock # python < 3.3
3134

35+
try:
36+
import gevent
37+
except ImportError:
38+
gevent = None
39+
3240
try:
3341
# Python 3
3442
FileNotFoundError
@@ -607,3 +615,68 @@ def test_default_release_empty_string():
607615
)
608616
def test_duration_in_milliseconds(timedelta, expected_milliseconds):
609617
assert duration_in_milliseconds(timedelta) == expected_milliseconds
618+
619+
620+
def test_get_current_thread_id_explicit_thread():
621+
results = Queue(maxsize=1)
622+
623+
def target1():
624+
pass
625+
626+
def target2():
627+
results.put(get_current_thread_meta(thread1))
628+
629+
thread1 = threading.Thread(target=target1)
630+
thread1.start()
631+
632+
thread2 = threading.Thread(target=target2)
633+
thread2.start()
634+
635+
thread2.join()
636+
thread1.join()
637+
638+
assert (thread1.ident, thread1.name) == results.get(timeout=1)
639+
640+
641+
@pytest.mark.skipif(gevent is None, reason="gevent not enabled")
642+
def test_get_current_thread_id_gevent_in_thread():
643+
results = Queue(maxsize=1)
644+
645+
def target():
646+
job = gevent.spawn(get_current_thread_meta)
647+
job.join()
648+
results.put(job.value)
649+
650+
thread = threading.Thread(target=target)
651+
thread.start()
652+
thread.join()
653+
assert (thread.ident, thread.name) == results.get(timeout=1)
654+
655+
656+
def test_get_current_thread_id_running_thread():
657+
results = Queue(maxsize=1)
658+
659+
def target():
660+
results.put(get_current_thread_meta())
661+
662+
thread = threading.Thread(target=target)
663+
thread.start()
664+
thread.join()
665+
assert (thread.ident, thread.name) == results.get(timeout=1)
666+
667+
668+
@pytest.mark.skipif(sys.version_info < (3, 4), reason="threading.main_thread() Not available")
669+
def test_get_current_thread_id_main_thread():
670+
results = Queue(maxsize=1)
671+
672+
def target():
673+
# mock that somehow the current thread doesn't exist
674+
with mock.patch("threading.current_thread", side_effect=[None]):
675+
results.put(get_current_thread_meta())
676+
677+
main_thread = threading.main_thread()
678+
679+
thread = threading.Thread(target=target)
680+
thread.start()
681+
thread.join()
682+
assert (main_thread.ident, main_thread.name) == results.get(timeout=1)

0 commit comments

Comments
 (0)