Skip to content

[BUG]: AsyncGenerators are not wrapped correctly by tracer.wrap() #14399

@pfaion

Description

@pfaion

Tracer Version(s)

3.12.2

Python Version(s)

3.13.5

Pip Version(s)

Using "uv" as package manager, version 0.7.13

Bug Report

Thank you for maintaining datadog and it's Python SDK, I am generally very happy with the tooling!

However, I noticed the SDK does not correctly wrap async "generators" (!). It only handles iterators correctly. A generator is a generalization of an iterator that can also receive values, not only yield them.

See the following code:

import asyncio
from collections.abc import AsyncGenerator
from ddtrace.trace import tracer

# @tracer.wrap()
async def accumulator() -> AsyncGenerator[int, int]:
    total = 0
    while True:
        incoming = yield total
        total += incoming

async def main() -> None:
    acc = accumulator()
    await anext(acc)  # Start the generator

    for i in range(5):
        print(f"Current value: {await acc.asend(i)}")

asyncio.run(main())

If you run this as-is, it will sum up the values from range(5):

Current value: 0
Current value: 1
Current value: 3
Current value: 6
Current value: 10

However, if you uncomment the @tracer.wrap() decorator, it will crash with:

Traceback (most recent call last):
  File "/redacted/tmp.py", line 23, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "~/.local/share/uv/python/cpython-3.13.5-macos-aarch64-none/lib/python3.13/asyncio/runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "~/.local/share/uv/python/cpython-3.13.5-macos-aarch64-none/lib/python3.13/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "~/.local/share/uv/python/cpython-3.13.5-macos-aarch64-none/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "/redacted/tmp.py", line 20, in main
    print(f"Current value: {await acc.asend(i)}")
                            ^^^^^^^^^^^^^^^^^^
  File "/redacted/.venv/lib/python3.13/site-packages/ddtrace/_trace/tracer.py", line 793, in func_wrapper
    async for value in f(*args, **kwargs):
        yield value
  File "/redacted/tmp.py", line 12, in accumulator
    total += incoming
TypeError: unsupported operand type(s) for +=: 'int' and 'NoneType'

Looking at the implementation, I saw that you only process iterators correctly (that don't receive values from the outside), but not generic generators:

def _wrap_generator_async(
self,
f: AnyCallable,
span_name: str,
service: Optional[str] = None,
resource: Optional[str] = None,
span_type: Optional[str] = None,
) -> AnyCallable:
"""Wrap a generator function with tracing."""
@functools.wraps(f)
async def func_wrapper(*args, **kwargs):
with self.trace(span_name, service=service, resource=resource, span_type=span_type):
async for value in f(*args, **kwargs):
yield value
return cast(AnyCallable, func_wrapper)

Reproduction Code

No response

Error Logs

No response

Libraries in Use

No response

Operating System

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions