Skip to content

fix(otel): don't log an error when a span ends in another context - #1071

Merged
Abhijeet Prasad (AbhiPrasad) merged 1 commit into
mainfrom
abhi/fix-otel-context-detach-197c036d
Nov 5, 2026
Merged

Abhijeet Prasad (AbhiPrasad) merged 1 commit into
mainfrom
abhi/fix-otel-context-detach-197c036d

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Member

🤖 This text was AI-generated.

With BRAINTRUST_OTEL_COMPAT=true, async frameworks such as LangGraph can start a span in one asyncio task and end it in another. Each task runs in its own contextvars.Context, so the token saved when the span became current belongs to a different Context than the one ending it:

task A (Context 1)             task B (Context 2)
on_chain_start                 on_chain_end
  span.set_current()             span.unset_current()
    token = attach(ctx)            context.detach(token)
                                     -> ValueError (other Context)
                                     -> OTel logs it at ERROR

OpenTelemetry's context.detach() catches that ValueError itself and logs "Failed to detach context" with a traceback. Nothing reaches Braintrust, so the except ValueError guard in the LangChain callback handler never ran. Every such run printed an ERROR, even though span parenting was correct (#1062).

The OTel ContextManager now resets OTel's default contextvars token directly instead of calling context.detach():

try:
    token.var.reset(token)
except ValueError:
    # token came from another Context; leave both contexts as-is
    log.debug(...)
except Exception:
    log.exception("Failed to detach context")
  • A token from another Context is logged at DEBUG. Neither context is changed, so an unrelated span that is active where the run ends stays current.
  • Other failures, such as reusing a token, are still logged at ERROR and not raised, same as before.
  • Tokens from other OTel runtime implementations still go through context.detach().

The dead try/except ValueError around span.unset_current() in the LangChain handler is removed, since unset_current() never raises.

Limitation: the Context where the span started still treats the ended span as current. BraintrustContextManager skips ended spans when it looks up the current one; the OTel manager does not yet.

Fixes #1062

> 🤖 **This text was AI-generated.**

With `BRAINTRUST_OTEL_COMPAT=true`, async frameworks such as LangGraph can
start a span in one asyncio task and end it in another. Each task runs in
its own `contextvars.Context`, so the token saved when the span became
current belongs to a different Context than the one ending it:

```
task A (Context 1)             task B (Context 2)
on_chain_start                 on_chain_end
  span.set_current()             span.unset_current()
    token = attach(ctx)            context.detach(token)
                                     -> ValueError (other Context)
                                     -> OTel logs it at ERROR
```

OpenTelemetry's `context.detach()` catches that `ValueError` itself and
logs "Failed to detach context" with a traceback. Nothing reaches
Braintrust, so the `except ValueError` guard in the LangChain callback
handler never ran. Every such run printed an ERROR, even though span
parenting was correct (#1062).

The OTel `ContextManager` now resets OTel's default contextvars token
directly instead of calling `context.detach()`:

```python
try:
    token.var.reset(token)
except ValueError:
    # token came from another Context; leave both contexts as-is
    log.debug(...)
except Exception:
    log.exception("Failed to detach context")
```

- A token from another Context is logged at DEBUG. Neither context is
  changed, so an unrelated span that is active where the run ends stays
  current.
- Other failures, such as reusing a token, are still logged at ERROR and
  not raised, same as before.
- Tokens from other OTel runtime implementations still go through
  `context.detach()`.

The dead `try/except ValueError` around `span.unset_current()` in the
LangChain handler is removed, since `unset_current()` never raises.

Limitation: the Context where the span started still treats the ended span
as current. `BraintrustContextManager` skips ended spans when it looks
up the current one; the OTel manager does not yet.

Fixes #1062
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T04:05:09.314615Z c816ac5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) merged commit 8f85b89 into main Nov 5, 2026
98 checks passed
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) deleted the abhi/fix-otel-context-detach-197c036d branch November 5, 2026 15:00
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.

LangChain callback handler: "benign" cross-context detach is logged at ERROR by OpenTelemetry (the except ValueError guard never fires)

1 participant