Skip to content

Commit c45d834

Browse files
authored
Merge branch 'main' into deps/weekly-update-2026-09-28
2 parents f8ad04b + 2369c10 commit c45d834

5 files changed

Lines changed: 219 additions & 40 deletions

File tree

‎py/noxfile.py‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -822,12 +822,20 @@ def test_cli(session):
822822
_run_tests(session, DEVSERVER_DIR)
823823

824824

825+
OTEL_VERSIONS = _get_matrix_versions("opentelemetry-sdk")
826+
827+
825828
@nox.session()
826-
def test_otel(session):
829+
@nox.parametrize("version", OTEL_VERSIONS, ids=OTEL_VERSIONS)
830+
def test_otel(session, version):
827831
"""Test OtelExporter with OpenTelemetry installed."""
832+
if version != LATEST and Version(version) < Version("1.28.0") and sys.version_info >= (3, 14):
833+
session.skip("OpenTelemetry <1.28 requires protobuf<5, which does not support Python 3.14")
828834
_install_test_deps(session)
829-
session.install(".[otel]")
830-
_run_tests(session, "braintrust/test_otel.py")
835+
_install_matrix_dep(session, "opentelemetry-api", version)
836+
_install_matrix_dep(session, "opentelemetry-sdk", version)
837+
_install_matrix_dep(session, "opentelemetry-exporter-otlp-proto-http", version)
838+
_run_tests(session, "braintrust/test_otel.py", version=version)
831839

832840

833841
@nox.session()

‎py/pyproject.toml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,25 @@ latest = "boto3==1.43.103"
554554
latest = "botocore==1.43.103"
555555
"1.34.116" = "botocore==1.34.116"
556556

557+
# OpenTelemetry api/sdk/exporter release in lockstep; keep their keys in sync.
558+
# 1.45.0 moved OTLP HTTP exporter headers onto an internal client, so keep a
559+
# pre-1.45 version covered alongside latest. 1.16.0 is the oldest release that
560+
# imports without pkg_resources; test_otel skips it on Python 3.14 (protobuf<5).
561+
[tool.braintrust.matrix.opentelemetry-api]
562+
latest = "opentelemetry-api==1.45.0"
563+
"1.44.0" = "opentelemetry-api==1.44.0"
564+
"1.16.0" = "opentelemetry-api==1.16.0"
565+
566+
[tool.braintrust.matrix.opentelemetry-sdk]
567+
latest = "opentelemetry-sdk==1.45.0"
568+
"1.44.0" = "opentelemetry-sdk==1.44.0"
569+
"1.16.0" = "opentelemetry-sdk==1.16.0"
570+
571+
[tool.braintrust.matrix.opentelemetry-exporter-otlp-proto-http]
572+
latest = "opentelemetry-exporter-otlp-proto-http==1.45.0"
573+
"1.44.0" = "opentelemetry-exporter-otlp-proto-http==1.44.0"
574+
"1.16.0" = "opentelemetry-exporter-otlp-proto-http==1.16.0"
575+
557576
# ---------------------------------------------------------------------------
558577
# Vendor packages — optional third-party packages the SDK can work without.
559578
# Keys are matrix keys; values are Python import names. The noxfile uses this

‎py/scripts/session-weights.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@
7171
"test_openai_http2_streaming(latest)": 12,
7272
"test_openrouter(0.6.0)": 8,
7373
"test_openrouter(latest)": 12,
74-
"test_otel": 9,
74+
"test_otel(1.16.0)": 9,
75+
"test_otel(1.44.0)": 9,
76+
"test_otel(latest)": 9,
7577
"test_otel_not_installed": 5,
7678
"test_pydantic_ai_integration(1.10.0)": 34,
7779
"test_pydantic_ai_integration(latest)": 27,

‎py/src/braintrust/otel/__init__.py‎

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import logging
33
import os
4+
import threading
45
import warnings
56
from urllib.parse import urljoin
67

@@ -39,6 +40,9 @@ def export(self, *args, **kwargs):
3940
def force_flush(self, *args, **kwargs):
4041
raise ImportError(INSTALL_ERR_MSG)
4142

43+
def shutdown(self, *args, **kwargs):
44+
raise ImportError(INSTALL_ERR_MSG)
45+
4246
class BatchSpanProcessor:
4347
def __init__(self, *args, **kwargs):
4448
raise ImportError(INSTALL_ERR_MSG)
@@ -248,7 +252,6 @@ def __init__(
248252
)
249253

250254
self._braintrust_api_key_arg = api_key_arg
251-
self._braintrust_headers_override_authorization = "Authorization" in headers
252255
self._braintrust_has_api_key = bool(api_key and api_key.strip())
253256

254257
exporter_headers = {}
@@ -261,21 +264,23 @@ def __init__(
261264

262265
self.parent = parent
263266

264-
super().__init__(endpoint=endpoint, headers=exporter_headers, **kwargs)
267+
self._braintrust_exporter_kwargs = {"endpoint": endpoint, "headers": exporter_headers, **kwargs}
268+
self._braintrust_shutdown = False
269+
# Guards lazy re-init against a concurrent shutdown(), which re-init would undo.
270+
self._braintrust_lifecycle_lock = threading.Lock()
271+
super().__init__(**self._braintrust_exporter_kwargs)
265272

266273
def _set_api_key_header(self, api_key: str) -> None:
267-
if not self._braintrust_headers_override_authorization:
268-
authorization = {"Authorization": f"Bearer {api_key}"}
269-
exporter_headers = getattr(self, "_headers", None)
270-
if isinstance(exporter_headers, dict):
271-
exporter_headers.update(authorization)
272-
else:
273-
self._headers = {**dict(exporter_headers or {}), **authorization}
274-
275-
session = getattr(self, "_session", None)
276-
if session is not None:
277-
session.headers.update(authorization)
278-
self._braintrust_has_api_key = True
274+
with self._braintrust_lifecycle_lock:
275+
if self._braintrust_has_api_key:
276+
return
277+
exporter_kwargs = self._braintrust_exporter_kwargs
278+
if "Authorization" not in exporter_kwargs["headers"] and not self._braintrust_shutdown:
279+
# Re-run the upstream constructor instead of patching its private header
280+
# storage, which moved in opentelemetry-exporter-otlp-proto-http 1.45.
281+
headers = {"Authorization": f"Bearer {api_key}", **exporter_kwargs["headers"]}
282+
super().__init__(**{**exporter_kwargs, "headers": headers})
283+
self._braintrust_has_api_key = True
279284

280285
def _ensure_api_key(self) -> None:
281286
if self._braintrust_has_api_key:
@@ -298,6 +303,11 @@ def force_flush(self, timeout_millis=30000):
298303
self._ensure_api_key()
299304
return super().force_flush(timeout_millis)
300305

306+
def shutdown(self):
307+
with self._braintrust_lifecycle_lock:
308+
self._braintrust_shutdown = True
309+
return super().shutdown()
310+
301311

302312
def add_braintrust_span_processor(
303313
tracer_provider,

0 commit comments

Comments
 (0)