From ecb53ca4ba4486ca038ab85cabf5616fa68faf1e Mon Sep 17 00:00:00 2001 From: Colin B Date: Wed, 4 Mar 2026 16:28:37 -0800 Subject: [PATCH] enhance BraintrustSpanProcessor to support custom exporters --- py/src/braintrust/otel/__init__.py | 31 ++++++++++++++++++++---------- py/src/braintrust/test_otel.py | 14 ++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/py/src/braintrust/otel/__init__.py b/py/src/braintrust/otel/__init__.py index fec3a4cd..fd4e1307 100644 --- a/py/src/braintrust/otel/__init__.py +++ b/py/src/braintrust/otel/__init__.py @@ -210,6 +210,7 @@ def add_braintrust_span_processor( filter_ai_spans: bool = False, custom_filter=None, headers: dict[str, str] | None = None, + exporter=None, ): processor = BraintrustSpanProcessor( api_key=api_key, @@ -218,6 +219,7 @@ def add_braintrust_span_processor( filter_ai_spans=filter_ai_spans, custom_filter=custom_filter, headers=headers, + exporter=exporter, ) tracer_provider.add_span_processor(processor) @@ -235,6 +237,10 @@ class BraintrustSpanProcessor: > processor = BraintrustSpanProcessor(filter_ai_spans=True) > provider.add_span_processor(processor) + + > custom_exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces") + > processor = BraintrustSpanProcessor(exporter=custom_exporter) + > provider.add_span_processor(processor) """ def __init__( @@ -245,6 +251,7 @@ def __init__( filter_ai_spans: bool = False, custom_filter=None, headers: dict[str, str] | None = None, + exporter=None, SpanProcessor: type | None = None, ): """ @@ -257,23 +264,27 @@ def __init__( filter_ai_spans: Whether to enable AI span filtering. Defaults to False. custom_filter: Optional custom filter function for filtering. headers: Additional headers to include in requests. + exporter: Optional pre-configured OpenTelemetry exporter instance. + When provided, api_key/parent/api_url/headers are ignored. SpanProcessor: Optional span processor class (BatchSpanProcessor or SimpleSpanProcessor). Defaults to BatchSpanProcessor. """ - # Create the exporter - # Convert api_url to the full endpoint URL that OtelExporter expects - exporter_url = None - if api_url: - exporter_url = f"{api_url.rstrip('/')}/otel/v1/traces" - - self._exporter = OtelExporter(url=exporter_url, api_key=api_key, parent=parent, headers=headers) - - # Create the processor chain if not OTEL_AVAILABLE: raise ImportError( "OpenTelemetry packages are not installed. " "Install optional OpenTelemetry dependencies with: pip install braintrust[otel]" ) + if exporter is not None: + self._exporter = exporter + else: + # Create the default Braintrust exporter. + # Convert api_url to the full endpoint URL that OtelExporter expects. + exporter_url = None + if api_url: + exporter_url = f"{api_url.rstrip('/')}/otel/v1/traces" + + self._exporter = OtelExporter(url=exporter_url, api_key=api_key, parent=parent, headers=headers) + if SpanProcessor is None: SpanProcessor = BatchSpanProcessor @@ -355,7 +366,7 @@ def force_flush(self, timeout_millis=30000): @property def exporter(self): - """Access to the underlying OtelExporter.""" + """Access to the underlying span exporter.""" return self._exporter @property diff --git a/py/src/braintrust/test_otel.py b/py/src/braintrust/test_otel.py index 2706cf62..5ad72629 100644 --- a/py/src/braintrust/test_otel.py +++ b/py/src/braintrust/test_otel.py @@ -209,6 +209,8 @@ def test_braintrust_span_processor_class(): pytest.skip("OpenTelemetry SDK not fully installed, skipping test") from braintrust.otel import BraintrustSpanProcessor + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter # Test basic processor without filtering with pytest.MonkeyPatch.context() as m: @@ -243,6 +245,18 @@ def test_braintrust_span_processor_class(): assert hasattr(processor_with_filtering, "shutdown") assert hasattr(processor_with_filtering, "force_flush") + # Test processor with custom exporter injection + custom_exporter = InMemorySpanExporter() + with pytest.MonkeyPatch.context() as m: + m.delenv("BRAINTRUST_API_KEY", raising=False) + m.delenv("BRAINTRUST_PARENT", raising=False) + processor_with_custom_exporter = BraintrustSpanProcessor( + exporter=custom_exporter, + SpanProcessor=SimpleSpanProcessor, + ) + + assert processor_with_custom_exporter.exporter is custom_exporter + # Test processor with custom parameters with pytest.MonkeyPatch.context() as m: m.setenv("BRAINTRUST_API_KEY", "test-api-key")