Skip to content

SDK not instrumenting Asyncio calls when sentry_sdk.init called at app lifecycle's beginning, outside an async function #2328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
szokeasaurusrex opened this issue Aug 29, 2023 · 3 comments
Labels
Better async support Integration: Asyncio Triaged Has been looked at recently during old issue triage

Comments

@szokeasaurusrex
Copy link
Member

szokeasaurusrex commented Aug 29, 2023

How do you use Sentry?

Sentry Saas (sentry.io)

Version

1.30.0

Steps to Reproduce

To observe the issue, we can run the following code snippet:

import asyncio
import time

import sentry_sdk
from sentry_sdk.integrations.asyncio import AsyncioIntegration


sentry_sdk.init(
    dsn="[your dsn here]",
    traces_sample_rate=1.0,
    integrations=(AsyncioIntegration(),),
)


async def fib(n):
    if n == 0:
        return 0

    if n == 1:
        return 1

    time.sleep(0.05)

    async with asyncio.TaskGroup() as tg:
        fib1 = tg.create_task(fib(n - 1))
        fib2 = tg.create_task(fib(n - 2))

    return fib1.result() + fib2.result()


async def main():
    with sentry_sdk.start_transaction(op="test", name="Fibonacci"):
        result = await fib(5)

    print(result)


if __name__ == "__main__":
    asyncio.run(main())

Expected Result

We would expect the SDK to record spans for all async calls. In particular, calls to the function fib should appear as spans in the performance transaction, so that we observe the following when viewing the performance transaction in Sentry:

image

Actual Result

However, if we actually run the above code snippet and then view the generated performance transaction in Sentry, we will observe that the fib spans are missing! Instead, our transaction looks like the following:

image

The missing spans indicate that the SDK is not instrumenting async task calls as we would expect it to.

Workaround

For now, we can work around this bug by initializing the SDK within our first async function call, like so:

import asyncio
import time

import sentry_sdk
from sentry_sdk.integrations.asyncio import AsyncioIntegration


async def fib(n):
    if n == 0:
        return 0

    if n == 1:
        return 1

    time.sleep(0.05)

    async with asyncio.TaskGroup() as tg:
        fib1 = tg.create_task(fib(n - 1))
        fib2 = tg.create_task(fib(n - 2))

    return fib1.result() + fib2.result()


async def main():
    # Note that the SDK init call has been moved here!
    sentry_sdk.init(
        dsn="[your dsn here]",
        traces_sample_rate=1.0,
        integrations=(AsyncioIntegration(),),
    )
    with sentry_sdk.start_transaction(op="test", name="Fibonacci"):
        result = await fib(5)

    print(result)


if __name__ == "__main__":
    asyncio.run(main())

If we run the workaround code, we observe the expected result. However, we should fix the bug so that we also observe the expected result when we initialize the SDK outside an async function call.

Possible fix

The issue appears to arise from within the patch_asyncio function, which is defined in sentry_sdk/integrations/asyncio.py, specifically by the line which obtains the loop like so:

loop = asyncio.get_running_loop()

asyncio.get_running_loop can only be called from within a coroutine or callback. When we initialize the SDK outside an async function, we violate this precondition and a RuntimeError is raised. The SDK handles this error, but the error prevents the SDK from auto-instrumenting calls to async functions.

Replacing the asyncio.get_running_loop() call with a call to asyncio.get_event_loop() would likely fix this bug, but we need to ensure that get_event_loop does not cause any unintended side effects before implementing this change.

Alternatively, this issue could likely be resolved by instrumenting asyncio through the use of a custom event loop policy which instruments the SDK when a new event loop is created in addition to instrumenting the current event loop. This solution may be preferable since it would likely also resolve #2333.

@szokeasaurusrex
Copy link
Member Author

See also issue #2333, which is closely related to this issue

@sentrivana
Copy link
Contributor

sentrivana commented Jul 1, 2024

Potential solution: In addition to patching the async task factory directly (backwards compat), patch something higher up so that any event loop that starts gets the patched async factory.

@szokeasaurusrex
Copy link
Member Author

Closing. We have decided not to change this behavior. Our docs direct users to initialize the SDK inside an async function

@szokeasaurusrex szokeasaurusrex closed this as not planned Won't fix, can't repro, duplicate, stale Feb 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Better async support Integration: Asyncio Triaged Has been looked at recently during old issue triage
Projects
None yet
Development

No branches or pull requests

4 participants