Skip to content

Commit 973b2f6

Browse files
authored
asyncio integration (#1671)
* Make sure each asyncio task that is run has its own Hub and also creates a span. * Make sure to not break custom task factory if there is one set.
1 parent 7d004f0 commit 973b2f6

File tree

4 files changed

+183
-0
lines changed

4 files changed

+183
-0
lines changed

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class OP:
111111
DB = "db"
112112
DB_REDIS = "db.redis"
113113
EVENT_DJANGO = "event.django"
114+
FUNCTION = "function"
114115
FUNCTION_AWS = "function.aws"
115116
FUNCTION_GCP = "function.gcp"
116117
HTTP_CLIENT = "http.client"

sentry_sdk/integrations/asyncio.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import absolute_import
2+
3+
from sentry_sdk.consts import OP
4+
from sentry_sdk.hub import Hub
5+
from sentry_sdk.integrations import Integration, DidNotEnable
6+
from sentry_sdk._types import MYPY
7+
8+
try:
9+
import asyncio
10+
from asyncio.tasks import Task
11+
except ImportError:
12+
raise DidNotEnable("asyncio not available")
13+
14+
15+
if MYPY:
16+
from typing import Any
17+
18+
19+
def _sentry_task_factory(loop, coro):
20+
# type: (Any, Any) -> Task[None]
21+
22+
async def _coro_creating_hub_and_span():
23+
# type: () -> None
24+
hub = Hub(Hub.current)
25+
with hub:
26+
with hub.start_span(op=OP.FUNCTION, description=coro.__qualname__):
27+
await coro
28+
29+
# Trying to use user set task factory (if there is one)
30+
orig_factory = loop.get_task_factory()
31+
if orig_factory:
32+
return orig_factory(loop, _coro_creating_hub_and_span)
33+
34+
# The default task factory in `asyncio` does not have its own function
35+
# but is just a couple of lines in `asyncio.base_events.create_task()`
36+
# Those lines are copied here.
37+
38+
# WARNING:
39+
# If the default behavior of the task creation in asyncio changes,
40+
# this will break!
41+
task = Task(_coro_creating_hub_and_span, loop=loop) # type: ignore
42+
if task._source_traceback: # type: ignore
43+
del task._source_traceback[-1] # type: ignore
44+
45+
return task
46+
47+
48+
def patch_asyncio():
49+
# type: () -> None
50+
try:
51+
loop = asyncio.get_running_loop()
52+
loop.set_task_factory(_sentry_task_factory)
53+
except RuntimeError:
54+
# When there is no running loop, we have nothing to patch.
55+
pass
56+
57+
58+
class AsyncioIntegration(Integration):
59+
identifier = "asyncio"
60+
61+
@staticmethod
62+
def setup_once():
63+
# type: () -> None
64+
patch_asyncio()

tests/integrations/asyncio/__init__.py

Whitespace-only changes.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import asyncio
2+
import sys
3+
4+
import pytest
5+
import pytest_asyncio
6+
7+
import sentry_sdk
8+
from sentry_sdk.consts import OP
9+
from sentry_sdk.integrations.asyncio import AsyncioIntegration
10+
11+
12+
minimum_python_36 = pytest.mark.skipif(
13+
sys.version_info < (3, 6), reason="ASGI is only supported in Python >= 3.6"
14+
)
15+
16+
17+
async def foo():
18+
await asyncio.sleep(0.01)
19+
20+
21+
async def bar():
22+
await asyncio.sleep(0.01)
23+
24+
25+
@pytest_asyncio.fixture(scope="session")
26+
def event_loop(request):
27+
"""Create an instance of the default event loop for each test case."""
28+
loop = asyncio.get_event_loop_policy().new_event_loop()
29+
yield loop
30+
loop.close()
31+
32+
33+
@minimum_python_36
34+
@pytest.mark.asyncio
35+
async def test_create_task(
36+
sentry_init,
37+
capture_events,
38+
event_loop,
39+
):
40+
sentry_init(
41+
traces_sample_rate=1.0,
42+
send_default_pii=True,
43+
debug=True,
44+
integrations=[
45+
AsyncioIntegration(),
46+
],
47+
)
48+
49+
events = capture_events()
50+
51+
with sentry_sdk.start_transaction(name="test_transaction_for_create_task"):
52+
with sentry_sdk.start_span(op="root", description="not so important"):
53+
tasks = [event_loop.create_task(foo()), event_loop.create_task(bar())]
54+
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
55+
56+
sentry_sdk.flush()
57+
58+
(transaction_event,) = events
59+
60+
assert transaction_event["spans"][0]["op"] == "root"
61+
assert transaction_event["spans"][0]["description"] == "not so important"
62+
63+
assert transaction_event["spans"][1]["op"] == OP.FUNCTION
64+
assert transaction_event["spans"][1]["description"] == "foo"
65+
assert (
66+
transaction_event["spans"][1]["parent_span_id"]
67+
== transaction_event["spans"][0]["span_id"]
68+
)
69+
70+
assert transaction_event["spans"][2]["op"] == OP.FUNCTION
71+
assert transaction_event["spans"][2]["description"] == "bar"
72+
assert (
73+
transaction_event["spans"][2]["parent_span_id"]
74+
== transaction_event["spans"][0]["span_id"]
75+
)
76+
77+
78+
@minimum_python_36
79+
@pytest.mark.asyncio
80+
async def test_gather(
81+
sentry_init,
82+
capture_events,
83+
):
84+
sentry_init(
85+
traces_sample_rate=1.0,
86+
send_default_pii=True,
87+
debug=True,
88+
integrations=[
89+
AsyncioIntegration(),
90+
],
91+
)
92+
93+
events = capture_events()
94+
95+
with sentry_sdk.start_transaction(name="test_transaction_for_gather"):
96+
with sentry_sdk.start_span(op="root", description="not so important"):
97+
await asyncio.gather(foo(), bar(), return_exceptions=True)
98+
99+
sentry_sdk.flush()
100+
101+
(transaction_event,) = events
102+
103+
assert transaction_event["spans"][0]["op"] == "root"
104+
assert transaction_event["spans"][0]["description"] == "not so important"
105+
106+
assert transaction_event["spans"][1]["op"] == OP.FUNCTION
107+
assert transaction_event["spans"][1]["description"] == "foo"
108+
assert (
109+
transaction_event["spans"][1]["parent_span_id"]
110+
== transaction_event["spans"][0]["span_id"]
111+
)
112+
113+
assert transaction_event["spans"][2]["op"] == OP.FUNCTION
114+
assert transaction_event["spans"][2]["description"] == "bar"
115+
assert (
116+
transaction_event["spans"][2]["parent_span_id"]
117+
== transaction_event["spans"][0]["span_id"]
118+
)

0 commit comments

Comments
 (0)