Skip to content

Commit 5d7deb6

Browse files
authored
feat(asyncio): Add on-demand way to enable AsyncioIntegration (#5288)
Initializing the SDK in an async environment can be a challenge, since the requirement to initialize as early as possible might clash with the requirement to initialize when there's an event loop running, if one wants to use the AsyncioIntegration. These sort of scenarios would benefit from a dedicated way to activate the integration after `init()` has been called. Supersedes the needlessly more general #5285
1 parent a350506 commit 5d7deb6

File tree

3 files changed

+169
-1
lines changed

3 files changed

+169
-1
lines changed

sentry_sdk/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None:
188188
self.monitor: "Optional[Monitor]" = None
189189
self.log_batcher: "Optional[LogBatcher]" = None
190190
self.metrics_batcher: "Optional[MetricsBatcher]" = None
191+
self.integrations: "dict[str, Integration]" = {}
191192

192193
def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any":
193194
return {"options": {}}

sentry_sdk/integrations/asyncio.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def patch_asyncio() -> None:
4747
loop = asyncio.get_running_loop()
4848
orig_task_factory = loop.get_task_factory()
4949

50+
# Check if already patched
51+
if getattr(orig_task_factory, "_is_sentry_task_factory", False):
52+
return
53+
5054
def _sentry_task_factory(
5155
loop: "asyncio.AbstractEventLoop",
5256
coro: "Coroutine[Any, Any, Any]",
@@ -102,6 +106,7 @@ async def _task_with_sentry_span_creation() -> "Any":
102106

103107
return task
104108

109+
_sentry_task_factory._is_sentry_task_factory = True # type: ignore
105110
loop.set_task_factory(_sentry_task_factory) # type: ignore
106111

107112
except RuntimeError:
@@ -138,3 +143,48 @@ class AsyncioIntegration(Integration):
138143
@staticmethod
139144
def setup_once() -> None:
140145
patch_asyncio()
146+
147+
148+
def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None:
149+
"""
150+
Enable AsyncioIntegration with the provided options.
151+
152+
This is useful in scenarios where Sentry needs to be initialized before
153+
an event loop is set up, but you still want to instrument asyncio once there
154+
is an event loop. In that case, you can sentry_sdk.init() early on without
155+
the AsyncioIntegration and then, once the event loop has been set up,
156+
execute:
157+
158+
```python
159+
from sentry_sdk.integrations.asyncio import enable_asyncio_integration
160+
161+
async def async_entrypoint():
162+
enable_asyncio_integration()
163+
```
164+
165+
Any arguments provided will be passed to AsyncioIntegration() as is.
166+
167+
If AsyncioIntegration has already patched the current event loop, this
168+
function won't have any effect.
169+
170+
If AsyncioIntegration was provided in
171+
sentry_sdk.init(disabled_integrations=[...]), this function will ignore that
172+
and the integration will be enabled.
173+
"""
174+
client = sentry_sdk.get_client()
175+
if not client.is_active():
176+
return
177+
178+
# This function purposefully bypasses the integration machinery in
179+
# integrations/__init__.py. _installed_integrations/_processed_integrations
180+
# is used to prevent double patching the same module, but in the case of
181+
# the AsyncioIntegration, we don't monkeypatch the standard library directly,
182+
# we patch the currently running event loop, and we keep the record of doing
183+
# that on the loop itself.
184+
logger.debug("Setting up integration asyncio")
185+
186+
integration = AsyncioIntegration(*args, **kwargs)
187+
integration.setup_once()
188+
189+
if "asyncio" not in client.integrations:
190+
client.integrations["asyncio"] = integration

tests/integrations/asyncio/test_asyncio.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77

88
import sentry_sdk
99
from sentry_sdk.consts import OP
10-
from sentry_sdk.integrations.asyncio import AsyncioIntegration, patch_asyncio
10+
from sentry_sdk.integrations.asyncio import (
11+
AsyncioIntegration,
12+
patch_asyncio,
13+
enable_asyncio_integration,
14+
)
1115

1216
try:
1317
from contextvars import Context, ContextVar
@@ -229,6 +233,7 @@ def test_patch_asyncio(mock_get_running_loop):
229233
Test that the patch_asyncio function will patch the task factory.
230234
"""
231235
mock_loop = mock_get_running_loop.return_value
236+
mock_loop.get_task_factory.return_value._is_sentry_task_factory = False
232237

233238
patch_asyncio()
234239

@@ -278,6 +283,7 @@ def test_sentry_task_factory_with_factory(mock_get_running_loop):
278283

279284
# The original task factory will be mocked out here, let's retrieve the value for later
280285
orig_task_factory = mock_loop.get_task_factory.return_value
286+
orig_task_factory._is_sentry_task_factory = False
281287

282288
# Retieve sentry task factory (since it is an inner function within patch_asyncio)
283289
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)
@@ -340,6 +346,7 @@ def test_sentry_task_factory_context_with_factory(mock_get_running_loop):
340346

341347
# The original task factory will be mocked out here, let's retrieve the value for later
342348
orig_task_factory = mock_loop.get_task_factory.return_value
349+
orig_task_factory._is_sentry_task_factory = False
343350

344351
# Retieve sentry task factory (since it is an inner function within patch_asyncio)
345352
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)
@@ -386,3 +393,113 @@ async def test_span_origin(
386393

387394
assert event["contexts"]["trace"]["origin"] == "manual"
388395
assert event["spans"][0]["origin"] == "auto.function.asyncio"
396+
397+
398+
@minimum_python_38
399+
@pytest.mark.asyncio
400+
async def test_delayed_enable_integration(sentry_init, capture_events):
401+
sentry_init(traces_sample_rate=1.0)
402+
403+
assert "asyncio" not in sentry_sdk.get_client().integrations
404+
405+
events = capture_events()
406+
407+
with sentry_sdk.start_transaction(name="test"):
408+
await asyncio.create_task(foo())
409+
410+
assert len(events) == 1
411+
(transaction,) = events
412+
assert not transaction["spans"]
413+
414+
enable_asyncio_integration()
415+
416+
events = capture_events()
417+
418+
assert "asyncio" in sentry_sdk.get_client().integrations
419+
420+
with sentry_sdk.start_transaction(name="test"):
421+
await asyncio.create_task(foo())
422+
423+
assert len(events) == 1
424+
(transaction,) = events
425+
assert transaction["spans"]
426+
assert transaction["spans"][0]["origin"] == "auto.function.asyncio"
427+
428+
429+
@minimum_python_38
430+
@pytest.mark.asyncio
431+
async def test_delayed_enable_integration_with_options(sentry_init, capture_events):
432+
sentry_init(traces_sample_rate=1.0)
433+
434+
assert "asyncio" not in sentry_sdk.get_client().integrations
435+
436+
mock_init = MagicMock(return_value=None)
437+
mock_setup_once = MagicMock()
438+
with patch(
439+
"sentry_sdk.integrations.asyncio.AsyncioIntegration.__init__", mock_init
440+
):
441+
with patch(
442+
"sentry_sdk.integrations.asyncio.AsyncioIntegration.setup_once",
443+
mock_setup_once,
444+
):
445+
enable_asyncio_integration("arg", kwarg="kwarg")
446+
447+
assert "asyncio" in sentry_sdk.get_client().integrations
448+
mock_init.assert_called_once_with("arg", kwarg="kwarg")
449+
mock_setup_once.assert_called_once()
450+
451+
452+
@minimum_python_38
453+
@pytest.mark.asyncio
454+
async def test_delayed_enable_enabled_integration(sentry_init, uninstall_integration):
455+
# Ensure asyncio integration is not already installed from previous tests
456+
uninstall_integration("asyncio")
457+
458+
integration = AsyncioIntegration()
459+
sentry_init(integrations=[integration], traces_sample_rate=1.0)
460+
461+
assert "asyncio" in sentry_sdk.get_client().integrations
462+
463+
# Get the task factory after initial setup - it should be Sentry's
464+
loop = asyncio.get_running_loop()
465+
task_factory_before = loop.get_task_factory()
466+
assert getattr(task_factory_before, "_is_sentry_task_factory", False) is True
467+
468+
enable_asyncio_integration()
469+
470+
assert "asyncio" in sentry_sdk.get_client().integrations
471+
472+
# The task factory should be the same (loop not re-patched)
473+
task_factory_after = loop.get_task_factory()
474+
assert task_factory_before is task_factory_after
475+
476+
477+
@minimum_python_38
478+
@pytest.mark.asyncio
479+
async def test_delayed_enable_integration_after_disabling(sentry_init, capture_events):
480+
sentry_init(disabled_integrations=[AsyncioIntegration()], traces_sample_rate=1.0)
481+
482+
assert "asyncio" not in sentry_sdk.get_client().integrations
483+
484+
events = capture_events()
485+
486+
with sentry_sdk.start_transaction(name="test"):
487+
await asyncio.create_task(foo())
488+
489+
assert len(events) == 1
490+
(transaction,) = events
491+
assert not transaction["spans"]
492+
493+
enable_asyncio_integration()
494+
495+
events = capture_events()
496+
497+
assert "asyncio" in sentry_sdk.get_client().integrations
498+
499+
with sentry_sdk.start_transaction(name="test"):
500+
await asyncio.create_task(foo())
501+
502+
assert len(events) == 1
503+
(transaction,) = events
504+
assert transaction["spans"]
505+
assert transaction["spans"][0]["origin"] == "auto.function.asyncio"

0 commit comments

Comments
 (0)