Skip to content

instrument treats an async method behind a sync decorator as sync #2933

Open

Description

@Abelo9996

What happens

If an async def sits under a plain functools.wraps decorator, @instrument wraps it as a sync function. The span ends as soon as the coroutine object is returned (about 0.1 ms), the record output is <coroutine object ...>, and instrumented calls made while it runs start their own records and lose their span type. OpenAI's AsyncCompletions.create is shaped like this.

Repro

import asyncio
import functools

from trulens.core.otel.instrument import instrument
from trulens.otel.semconv.trace import SpanAttributes

def passthrough(fn):
    @functools.wraps(fn)
    def wrapper(*a, **k):
        return fn(*a, **k)
    return wrapper

@instrument(span_type=SpanAttributes.SpanType.RETRIEVAL)
async def retrieve(q):
    return ["doc"]

class App:
    @instrument()
    @passthrough
    async def query(self, q):
        await asyncio.sleep(0.05)
        return str(await retrieve(q))

Run App().query("q") inside a recording: you get 2 records instead of 1.

Expected

One record. The span covers the awaited work, the output is "['doc']", and retrieve is a RETRIEVAL child span.

Cause

instrument.__call__ picks the wrapper with inspect.iscoroutinefunction(func), which does not follow __wrapped__.

Activity

  1. feiiiiii5 commented on Oct 8, 2026

    @feiiiiii5
    Collaborator

    Confirmed on main @ 5eaf76ba. With the functools.wraps passthrough decorator, instrument falls back to the sync wrapper: the run emits 2 record events instead of 1, the record output is <coroutine object _App.query ...>, and TruSession logs "Could not determine main output of query from coroutine value ...". The span also ends when the coroutine is returned rather than when the awaited work finishes.

    The cause is that the async detection at src/core/trulens/core/otel/instrument.py:~703 does not unwrap __wrapped__ before deciding between async_wrapper and sync_wrapper. Unwrapping first (or using inspect.iscoroutinefunction on the unwrapped function, e.g. via functools.unwrap) fixes it for the plain-wraps shape.

  2. feiiiiii5 commented on Oct 8, 2026

    @feiiiiii5
    Collaborator

    Follow-up: handled in open PR #2934.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions