Skip to content

fix(instrumentation): preserve context attributes in async coroutine decorators (#3583) - #3662

Merged
caroger merged 6 commits into
Arize-ai:mainfrom
dlowzzxx:fix/context-attributes-async-coroutine-decorator
Sep 4, 2026
Merged

caroger merged 6 commits into
Arize-ai:mainfrom
dlowzzxx:fix/context-attributes-async-coroutine-decorator

Conversation

@dlowzzxx

@dlowzzxx dlowzzxx commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Why

contextlib.ContextDecorator runs its __exit__ as soon as the decorated call returns. For an async def function or a generator function, that call only builds the coroutine or generator object, so OpenInference context attributes detached before the body ran. Issue #3583 reports the coroutine case.

_UsingAttributesContextManager now owns the decorator protocol and handles each kind of callable. A coroutine attaches when the body starts. A generator body runs each step under a rolling OpenTelemetry context.

Scope

  • Drop contextlib.ContextDecorator as the base class. __call__ routes async generator functions, generator functions, coroutine functions, and plain functions.
  • Add _RollingOtelContext, _drive_sync_generator_in_rolling_otel, and _AwaitRollingOtelStep. Each generator invocation gets its own rolling context. After every successful body step, the saved context becomes get_current(), so a span the body holds across yield stays current when the body resumes.
  • Isolate only OpenTelemetry state. An earlier revision used one contextvars.copy_context() snapshot per generator, which froze every application ContextVar for the life of the generator. That snapshot is gone.
  • Keep one attach token per decorated invocation in a wrapper local. One decorator instance serves every call of the function it wraps, so a token on self would break concurrent and re-entrant calls. Only with and async with still store self._token.
  • Restore _context_with_attributes, the expanded using_* docstrings, and the corrected version / variables keywords in the using_prompt_template example.
  • Add the README decorator section and examples/async_context_attribute_decorators.py, with the matching mypy exclude in pyproject.toml.

The PR does not include the unrelated Anthropic token-count change from the original branch.

Tradeoffs

The generator drivers forward send, throw, close, asend, athrow, and aclose by hand, which is more code than yield from. The gain is that a generator body keeps its attributes and its open span across yield while application ContextVar values stay on the caller's live map.

Blast Radius

The change affects Python callers that use a using_* helper as a decorator. Context-manager behavior is unchanged. Sync decorators keep their existing semantics and additionally survive re-entrant calls. Generator decorators gain attribute and span scoping that the stdlib decorator never provided.

Verification

  • The issue reproduction returned {'session.id': 'async-session'}. Main returned {}.
  • py310-ci-instrumentation on tests/test_context_managers.py passed Ruff formatting, Ruff lint, strict mypy, and 39 tests.
  • New tests cover live ContextVar updates across yield for sync and async generators, a body-owned ContextVar surviving resume, a consumer span between yields not becoming the parent of body spans, and an abandoned generator running its finally with attributes attached.
  • Ported tests cover attributes in the body but not the consumer, span parentage across yield, bidirectional send / throw / asend / athrow / aclose, re-entrant sync decorators, concurrent coroutine calls, exception detach, and using_session stacked above tracer.agent.

@github-actions

github-actions Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@dlowzzxx

dlowzzxx commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Sep 2, 2026
@mikeldking mikeldking self-assigned this Sep 2, 2026
@github-project-automation github-project-automation Bot moved this to 📘 Todo in phoenix Sep 2, 2026
@mikeldking

Copy link
Copy Markdown
Contributor

@dlowzzxx thanks for the PR. I'll take a look soon

@mikeldking mikeldking assigned caroger and unassigned mikeldking Sep 3, 2026
@mikeldking

Copy link
Copy Markdown
Contributor

@caroger - I did a full audit. Could use a python review here.

dlowzzxx and others added 5 commits September 4, 2026 18:40
…decorators (Arize-ai#3583)

- Wrap async coroutines with reconstructed context manager to preserve span attributes across awaits

- Add NoneType guards for streaming token usage calculations (Arize-ai#3499)

- Add comprehensive unit tests covering async decorator execution, concurrent isolation, and exception handling
…ntext attribute decorators

- Own __call__ on _UsingAttributesContextManager instead of overriding the
  private ContextDecorator._recreate_cm hook; each decorated call gets its
  own context token so concurrent and re-entrant calls never clobber it.
- Cover coroutine functions, async generator functions and sync generator
  functions; generator bodies run in a copied contextvars context so their
  own spans survive across yield and nothing leaks into the consumer.
- Restore first-party import grouping flagged by ruff.
- Add a runnable Phoenix example and README section on decorator usage.
- Drop the unrelated Anthropic input_tokens guard; it is covered by Arize-ai#3504
  and Arize-ai#3556 for Arize-ai#3499.

Fixes Arize-ai#3583
…ribute wrappers and document decorator behavior

Extract _drive_in_contextvars_context so the sync generator wrapper and the
awaitable used by the async generator wrapper run steps through one
implementation. Expand docstrings on the using_* helpers, fix the swapped
version/variables keywords in the using_prompt_template example, parametrize
the per-attribute async decorator tests, and pass order ids through in the
example.
Restore ContextDecorator for synchronous callables and remove generator-specific context isolation. Keep one local OpenTelemetry context token per coroutine invocation.
Replace contextvars.copy_context isolation with per-invocation rolling
OpenTelemetry context so generator bodies see live caller ContextVars
while attributes and spans still do not leak to consumers.
@dlowzzxx
dlowzzxx force-pushed the fix/context-attributes-async-coroutine-decorator branch from 4d2f94c to 6570db0 Compare September 4, 2026 16:40
…rration

Keep the public using_* contract notes; the wrappers and tests already show the behavior.
@github-project-automation github-project-automation Bot moved this from 📘 Todo to 👍 Approved in phoenix Sep 4, 2026
@caroger
caroger enabled auto-merge (squash) September 4, 2026 17:20
@caroger
caroger disabled auto-merge September 4, 2026 17:21
@caroger
caroger enabled auto-merge (squash) September 4, 2026 17:22
@caroger
caroger merged commit d818097 into Arize-ai:main Sep 4, 2026
161 checks passed
@github-project-automation github-project-automation Bot moved this from 👍 Approved to ✅ Done in phoenix Sep 4, 2026
@mikeldking mikeldking mentioned this pull request Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done
Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

3 participants