Skip to content

fix(fi-instrumentation-otel): keep generator spans open through consumption - #344

Open
nik13 wants to merge 3 commits into
devfrom
fix/core-generator-span-lifecycle
Open

nik13 wants to merge 3 commits into
devfrom
fix/core-generator-span-lifecycle

Conversation

@nik13

@nik13 nik13 commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Keep fi-instrumentation-otel as the custom-application tracing core. Fix existing FITracer.chain, agent, and tool decorators rather than adding a package or replacing their public APIs.

  • Recognize synchronous and asynchronous generator functions. Start the span on first consumption, parented to the consuming context, and end it on exhaustion, an escaping error/cancellation, or explicit close.
  • Activate the span only while advancing or closing the generator. Restore the caller's current span between yields and support sequential consumption from different async tasks.
  • Forward send/throw/close and asend/athrow/aclose, including cleanup errors, without swallowing application exceptions.
  • Record errors and cancellation once through FiSpan, preserving existing error privacy on the OTLP wire. Explicit early close is not an error.
  • Do not accumulate yielded chunks or serialize generator object reprs as outputs. Capture an explicit synchronous return value unless the application already set output; application-set outputs continue to work.

Ordinary functions/coroutines, decorator signatures, bound methods, tool metadata, and manual span contexts remain unchanged. No version bump, changelog edit, new dependency, or additional LLM decorator.

Why

On the development base, decorating a generator exported a completed span before its body ran. Iteration had no recording parent, and errors raised after yielding left the exported span UNSET. Generator-aware dispatch follows the design used by Braintrust; implementation is original.

Verification

All current verification below ran in fresh E2B cloud sandboxes with local mock collectors / in-memory OpenTelemetry exporters and no provider credentials. Core selection matches CI: tests --ignore-glob='tests/test_framework_*.py'.

  • Before the round-2 fix, q1104-pr344-refix-r2-red-01 reproduced the double-close exception on all three decorators on both Python 3.13.18 / OTel 1.45.2 and Python 3.10.22 / OTel 1.29.0. The new regression suite failed 15 tests and passed 9 on each matrix entry; undecorated close-race controls passed.
  • After the fix, q1104-pr344-refix-r2-green-02 passed the complete core suite: 825 passed, 6 skipped on Python 3.13.18 / OTel 1.45.2; 822 passed, 9 skipped on Python 3.10.22 / OTel 1.29.0. No framework package tests are claimed by this core-only run.
  • q1104-pr344-refix-r2-reviewed-03 re-ran the original review's close-race and instrumented-suppression probes, its independent SDK tests, and the new shutdown tests: 45 passed on each matrix entry. Three undecorated suppression-context probes were not selected; undecorated close-race controls were included.

python/tests/test_async_generator_shutdown.py covers all three decorators under asyncio.run, explicit shutdown_asyncgens, and garbage collection. It asserts no loop exception or false ERROR on successful awaited cleanup, span lifetime through the last cleanup await, generator-owned context tokens and suppression, real cleanup error propagation, and restoration of async-generator hooks for nested/unrelated generators. Existing real-SDK generator/context-isolation suites also pass, including localhost HTTP/protobuf OTLP privacy tests.

Re-run with python -m pytest python/tests/test_async_generator_shutdown.py python/tests/test_generator_decorators.py python/tests/test_generator_context_isolation.py -q.

Stream ownership and rollout

Call close() / aclose() when stopping early; use contextlib.closing / aclosing around consumers that may break or be cancelled between yields. A still-referenced, partially consumed generator remains open by design. Event-loop shutdown closes the tracing wrapper, which is the sole finalization owner of the wrapped generator. It awaits underlying cleanup in the generator-owned execution context before ending the span; the underlying generator is not separately registered for concurrent loop finalization.

This fixes generator functions, not arbitrary functions/coroutines returning iterators. Use a manual span around consumption for those. No chunks are buffered, so no unbounded tracing memory growth.

Release the existing shared core through release-please after review. Customer docs describing this behavior are held until that package is on the registry. No packages should be deleted for this finding. No merge, tag, or registry publish was performed by this change.

E2E coverage

Not applicable (no product e2e/ harness in this SDK repository). Real-SDK and localhost OTLP tests cover the changed tracing contract. No database, API route, frontend, migration, or enterprise boundary changes.

🤖 Generated with Claude Code

…pletion

Keep chain, agent and tool spans alive through sync and async generator exhaustion, errors, cancellation and explicit early close. Preserve send/throw protocols without buffering stream contents or leaving a current span in the consumer. Add real SDK lifecycle and localhost OTLP privacy regressions.

Co-Authored-By: Claude Code <[email protected]>
nik13 and others added 2 commits November 4, 2026 11:54
Keep suppression, nested spans and context-variable tokens in one generator-owned context across sync and async advances and closes. Preserve Python 3.13 close return values and exercise exporter timeout behavior without private OTel fields.

Co-Authored-By: Claude Code <[email protected]>
Initialize the wrapped native generator without registering a competing loop finalizer, restoring thread-local hooks before application code or awaits. The tracing wrapper now awaits cleanup in the generator-owned context before ending its span.

Cover loop shutdown, explicit shutdown_asyncgens, garbage collection, awaited private cleanup, real cleanup failures, and unrelated generator hooks for chain, agent, and tool. E2B package matrix: 825 passed on Python 3.13 / OTel 1.45.2; 822 passed on Python 3.10 / OTel 1.29.0.

Co-Authored-By: Claude Code <[email protected]>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant