Skip to content

fix(otel): send lazily resolved API key with opentelemetry-exporter-otlp-proto-http 1.45 - #811

Merged
Abhijeet Prasad (AbhiPrasad) merged 2 commits into
mainfrom
fix/otel-1.45-headers
Sep 28, 2026
Merged

Abhijeet Prasad (AbhiPrasad) merged 2 commits into
mainfrom
fix/otel-1.45-headers

Conversation

@AbhiPrasad

@AbhiPrasad Abhijeet Prasad (AbhiPrasad) commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Summary

opentelemetry-exporter-otlp-proto-http 1.45.0 (released 2026-09-25) moved the exporter's request headers off _headers / _session and onto an internal HTTP client. When OtelExporter resolved the Braintrust API key after construction, for example from .env.braintrust or an env var set later, it wrote the Authorization header to attributes the exporter no longer reads. Spans were exported without authentication. The key-at-construction path still worked.

test_otel installs otel unpinned, so it started failing in CI on every PR (e.g. #809, shard 2 on all Pythons and OSes).

1. Fix

  • What changed: OtelExporter stores the kwargs it passes to OTLPSpanExporter.__init__. When the key is resolved lazily, it re-runs that public constructor with the Authorization header added, instead of writing into private upstream state. The constructor is the only public way to set headers across 1.16–1.45.
  • Shutdown guard: OtelExporter.shutdown() now records the shutdown, so resolving a key can't revive an exporter that was already shut down. Re-running the constructor resets upstream's shutdown flag.
  • Unchanged: a user-supplied Authorization header still takes precedence, and .env.braintrust is still only read on first export.

2. Tests

  • Wire-level assertions: header tests that asserted on exporter._headers now export a span to a local OTLP collector (the shared braintrust.api._test_server.scripted_server) and assert on the headers it receives. The old .env.braintrust test passed on 1.45 while no header was actually sent.
  • New tests: a key resolved after construction is sent on every export; a lazy key doesn't override an explicit Authorization header; resolving a key doesn't revive a shut-down exporter.

3. OpenTelemetry nox matrix

  • Versions: test_otel is now parametrized over latest (1.45.0), 1.44.0 (the last version before the change) and 1.16.0, pinned in [tool.braintrust.matrix] as separate opentelemetry-api / -sdk / -exporter-otlp-proto-http tables. They are kept separate because older exporters pin the SDK only loosely, e.g. 1.12.0 accepts opentelemetry-sdk~=1.11.
  • Why 1.16.0: it's the oldest release that imports without pkg_resources.
  • Python 3.14: versions below 1.28 require protobuf<5, which crashes on 3.14, so test_otel skips them there. 1.44.0 still covers the pre-1.45 layout on 3.14.
  • Other files: session-weights.json gets the parametrized session names.
  • Dependency updates: the weekly update-matrix-latest.py will bump these pins. It classifies them as provider packages, so otel bumps are labeled needs-cassette-rerecord for human review and are not auto-merged.

Test plan

  • Red: before the fix, on 1.45.0 the two lazy-key tests fail and all other tests pass. On 1.44.0 everything passes.
  • Red: the shutdown test fails with the re-init change until the shutdown guard is added.
  • Green: nox -s "test_otel(latest)", "test_otel(1.44.0)" and "test_otel(1.16.0)" pass on Python 3.10 and 3.13. On 3.14, latest and 1.44.0 pass and 1.16.0 is skipped.
  • test_otel_not_installed passes. The src/braintrust/otel/ tests (which CI only runs without otel installed) pass on every matrix version.
  • End to end: an exporter created without a key, with the key set before the first export, sends Authorization: Bearer … to a local HTTP server on 1.16.0, 1.44.0 and 1.45.0. Before the fix, 1.45.0 sent no Authorization header.
  • pre-commit and nox -s pylint
  • Windows: covered by CI only

Notes

🤖 Generated with Claude Code

Co-authored by StarfolkAI (@starfolkai)[bot]

…tlp-proto-http 1.45

opentelemetry-exporter-otlp-proto-http 1.45.0 moved request headers off the
exporter (`_headers` / `_session`) and onto an internal `_client`. When the
Braintrust API key was resolved after the exporter was constructed (e.g. from
`.env.braintrust` or an env var set later), `OtelExporter._set_api_key_header`
updated attributes the exporter no longer reads, so spans were exported without
an `Authorization` header.

Instead of patching upstream's private header storage, re-run the public
`OTLPSpanExporter.__init__` with the resolved `Authorization` header. Track
`shutdown()` so resolving a key cannot revive an exporter that was already shut
down.

Replace the tests that asserted on private exporter state with tests that
export to a local OTLP collector (the shared `scripted_server` helper) and
check the headers it receives.

Add an OpenTelemetry version matrix for `test_otel` (latest=1.45.0, 1.44.0,
1.16.0) so both the pre- and post-1.45 exporter layouts stay covered. 1.16.0 is
the oldest release that imports without pkg_resources; it is skipped on Python
3.14 because OpenTelemetry <1.28 requires protobuf<5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 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-09-28T18:02:57.030666Z e5490c1 New commits
ℹ️ 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65f23c1079

ℹ️ About Codex in GitHub

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

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/otel/__init__.py Outdated
Comment on lines +272 to +276
if "Authorization" not in exporter_kwargs["headers"] and not self._braintrust_shutdown:
# Re-run the upstream constructor instead of patching its private header
# storage, which moved in opentelemetry-exporter-otlp-proto-http 1.45.
headers = {"Authorization": f"Bearer {api_key}", **exporter_kwargs["headers"]}
super().__init__(**{**exporter_kwargs, "headers": headers})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Synchronize lazy reinitialization with shutdown

When the first lazy-key export races with shutdown()—for example, a SimpleSpanProcessor ending a span on one thread while its provider shuts down on another—the shutdown flag can change after this check but before super().__init__(). The constructor then resets the upstream shutdown state and recreates its HTTP client after shutdown, so the current and subsequent exports can continue using an exporter that was supposed to remain closed. Protect the check/reinitialization and shutdown transition with the same lock.

Useful? React with 👍 / 👎.

When the first lazy-key export raced with shutdown() (e.g. a
SimpleSpanProcessor ending a span on one thread while the provider shuts
down on another), shutdown could land between the shutdown check and the
upstream re-init. Re-running OTLPSpanExporter.__init__ then reset the
upstream shutdown state and recreated the HTTP client, so the exporter kept
sending after it had been shut down.

Guard the check/re-init and the shutdown transition with one lock, and
re-check the resolved-key latch under it so concurrent first exports only
re-init once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) merged commit 2369c10 into main Sep 28, 2026
83 checks passed
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) deleted the fix/otel-1.45-headers branch September 28, 2026 18:49
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