diff --git a/integrations/langchain-py/pyproject.toml b/integrations/langchain-py/pyproject.toml index 579802b7..6aa1e850 100644 --- a/integrations/langchain-py/pyproject.toml +++ b/integrations/langchain-py/pyproject.toml @@ -30,9 +30,11 @@ build-backend = "setuptools.build_meta" [tool.setuptools] package-dir = { "" = "src" } +include-package-data = false [tool.setuptools.packages.find] where = ["src"] +exclude = ["tests*"] [tool.uv.workspace] diff --git a/py/src/braintrust/otel/__init__.py b/py/src/braintrust/otel/__init__.py index 71648f01..fec3a4cd 100644 --- a/py/src/braintrust/otel/__init__.py +++ b/py/src/braintrust/otel/__init__.py @@ -43,6 +43,18 @@ def get_tracer_provider(): FILTER_PREFIXES = ("gen_ai.", "braintrust.", "llm.", "ai.", "traceloop.") +def _forward_on_ending(processor, span) -> None: + """ + Forward OpenTelemetry's optional _on_ending hook when available. + + Newer OpenTelemetry SDK versions call _on_ending before on_end. Older SDK + versions may not implement the hook on wrapped processors. + """ + on_ending = getattr(processor, "_on_ending", None) + if callable(on_ending): + on_ending(span) + + class AISpanProcessor: """ A span processor that filters spans to only export filtered telemetry. @@ -79,6 +91,13 @@ def on_end(self, span): if self._should_keep_filtered_span(span): self._processor.on_end(span) + def _on_ending(self, span): + """ + Forward pre-end hook for kept spans when the wrapped processor supports it. + """ + if self._should_keep_filtered_span(span): + _forward_on_ending(self._processor, span) + def shutdown(self): """Shutdown the inner processor.""" self._processor.shutdown() @@ -322,6 +341,10 @@ def on_end(self, span): """Forward span end events to the inner processor.""" self._processor.on_end(span) + def _on_ending(self, span): + """Forward pre-end hook when the wrapped processor supports it.""" + _forward_on_ending(self._processor, span) + def shutdown(self): """Shutdown the inner processor.""" self._processor.shutdown() diff --git a/py/src/braintrust/test_otel.py b/py/src/braintrust/test_otel.py index a0aa4fb1..2706cf62 100644 --- a/py/src/braintrust/test_otel.py +++ b/py/src/braintrust/test_otel.py @@ -191,7 +191,9 @@ def test_braintrust_otel_filter_ai_spans_environment_variable(): # Verify it has the expected attributes assert hasattr(filter_processor, "_processor") assert hasattr(filter_processor, "_custom_filter") + assert hasattr(filter_processor, "_on_ending") assert hasattr(filter_processor, "_should_keep_filtered_span") + assert callable(filter_processor._on_ending) assert callable(filter_processor._should_keep_filtered_span) finally: @@ -216,10 +218,12 @@ def test_braintrust_span_processor_class(): # Should have the span processor interface assert hasattr(processor, "on_start") assert hasattr(processor, "on_end") + assert hasattr(processor, "_on_ending") assert hasattr(processor, "shutdown") assert hasattr(processor, "force_flush") assert callable(processor.on_start) assert callable(processor.on_end) + assert callable(processor._on_ending) assert callable(processor.shutdown) assert callable(processor.force_flush) @@ -235,6 +239,7 @@ def test_braintrust_span_processor_class(): # Should have the same interface assert hasattr(processor_with_filtering, "on_start") assert hasattr(processor_with_filtering, "on_end") + assert hasattr(processor_with_filtering, "_on_ending") assert hasattr(processor_with_filtering, "shutdown") assert hasattr(processor_with_filtering, "force_flush")