Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions integrations/langchain-py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
23 changes: 23 additions & 0 deletions py/src/braintrust/otel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions py/src/braintrust/test_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand All @@ -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")

Expand Down
Loading