Skip to content

Stream wrappers cannot finalize telemetry for generator-backed streams #386

Description

@lmolkova

SyncStreamWrapper / AsyncStreamWrapper are wrapt proxies that finalize telemetry from close() / __exit__ / __aexit__ and from iteration reaching its end. When the wrapped stream is a generator and the caller stops consuming it early, none of those run: the caller drops the proxy, and GC finalizes the underlying generator directly, so the span is never ended.

This is normal traffic for agent instrumentations that wrap generator-returning APIs. For example QwenPaw's ACP server breaks out of the async for when the client cancels a turn:

async for msg, _is_last in runner.query_handler(msgs, request=request):
    if cancel_event.is_set():
        break

Effects: the invoke_agent span is never exported and never recorded in gen_ai.client.operation.duration, and because the invocation attached its context at start and only detaches on finalize, later spans in the same task become children of the span that never ended.

Two related gaps:

  • AsyncStreamWrapper expects the stream to expose async close(), but async generators expose aclose(). Instrumentations have to override aclose / close / __aexit__ to bridge that.
  • No proxy-based wrapper can catch GC teardown at all. Catching it requires the wrapper to itself be an async generator, so GeneratorExit reaches a finally:
async def _driver():
    try:
        async for item in gen:
            yield item
    finally:
        ...finalize telemetry...
        await gen.aclose()

This shape does finalize on the break path above, and it keeps the returned object an async_generator, which is closer to the SDK's original type than a proxy is.

Suggest a generator-aware wrapper in opentelemetry.util.genai.stream so instrumentations wrapping generator APIs (qwen-agent, the proposed qwenpaw package in #311) get correct finalization instead of each working around it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    New issues

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions