Skip to content

Commit 1551386

Browse files
authored
1 parent b817baa commit 1551386

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

py/src/braintrust/integrations/strands/test_strands.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,16 @@ async def test_strands_openai_agent_traces_native_otel_lifecycle(memory_logger):
3636
task_spans = find_spans_by_type(spans, SpanTypeAttribute.TASK)
3737
llm_spans = find_spans_by_type(spans, SpanTypeAttribute.LLM)
3838
names = [span["span_attributes"]["name"] for span in spans]
39-
assert any(span["span_attributes"]["name"] == "bt-test-agent.invoke" for span in task_spans), names
40-
assert any(span["span_attributes"]["name"] == "event_loop.cycle" for span in task_spans), names
39+
agent_spans = [span for span in task_spans if span["span_attributes"]["name"] == "bt-test-agent.invoke"]
40+
event_loop_spans = [span for span in task_spans if span["span_attributes"]["name"] == "event_loop.cycle"]
41+
assert len(agent_spans) == 1, names
42+
assert len(event_loop_spans) == 1, names
43+
agent_span = agent_spans[0]
44+
event_loop_span = event_loop_spans[0]
45+
assert agent_span["output"]["message"]["role"] == "assistant"
46+
assert "end" in agent_span["metrics"]
47+
assert event_loop_span["output"]["message"]["role"] == "assistant"
48+
assert "end" in event_loop_span["metrics"]
4149
assert len(llm_spans) == 1
4250
llm_span = llm_spans[0]
4351
assert llm_span["input"]["messages"][0]["role"] == "user"

py/src/braintrust/integrations/strands/tracing.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Strands Agents tracing helpers."""
22

3+
import uuid
34
import weakref
45
from typing import Any
56

@@ -9,6 +10,32 @@
910

1011

1112
_SPANS_BY_OTEL_SPAN: "weakref.WeakKeyDictionary[Any, Span]" = weakref.WeakKeyDictionary()
13+
_SPANS_BY_INVALID_OTEL_KEY: dict[uuid.UUID, Span] = {}
14+
15+
16+
class _InvalidOtelSpanKey:
17+
"""Per-call key for OTEL's shared INVALID_SPAN singleton.
18+
19+
Strands keeps using the returned span object as an OTEL span, so this proxy
20+
delegates span methods to INVALID_SPAN while giving Braintrust a unique key
21+
for matching each start/end lifecycle pair.
22+
"""
23+
24+
def __init__(self, otel_span: Any):
25+
self.key = uuid.uuid4()
26+
self._otel_span = otel_span
27+
28+
def __getattr__(self, name: str) -> Any:
29+
return getattr(self._otel_span, name)
30+
31+
def __eq__(self, other: Any) -> bool:
32+
return self._otel_span == other
33+
34+
def __hash__(self) -> int:
35+
return hash(self.key)
36+
37+
def __bool__(self) -> bool:
38+
return bool(self._otel_span)
1239

1340

1441
def _arg(args: Any, kwargs: dict[str, Any], index: int, name: str, default: Any = None) -> Any:
@@ -51,15 +78,23 @@ def _agent_metrics_and_metadata(result: Any) -> tuple[dict[str, Any], dict[str,
5178
return bt_metrics, metadata
5279

5380

81+
def _is_valid_otel_span(otel_span: Any) -> bool:
82+
span_context = getattr(otel_span, "get_span_context", lambda: None)()
83+
return bool(getattr(span_context, "is_valid", False))
84+
85+
5486
def _span_for_otel(otel_span: Any) -> Span | None:
5587
if otel_span is None:
5688
return None
89+
if isinstance(otel_span, _InvalidOtelSpanKey):
90+
return _SPANS_BY_INVALID_OTEL_KEY.get(otel_span.key)
5791
return _SPANS_BY_OTEL_SPAN.get(otel_span)
5892

5993

6094
def _start_span_for_otel(otel_span: Any, *, name: str, span_type: str, input: Any = None, metadata: Any = None) -> Any:
6195
if otel_span is None:
6296
return otel_span
97+
span_key = otel_span if _is_valid_otel_span(otel_span) else _InvalidOtelSpanKey(otel_span)
6398
parent = None
6499
# Strands passes parent OTEL spans into child start methods. If present, nest under the mirrored BT span.
65100
if isinstance(metadata, dict):
@@ -68,8 +103,11 @@ def _start_span_for_otel(otel_span: Any, *, name: str, span_type: str, input: An
68103
span = (parent.start_span if parent is not None else start_span)(
69104
name=name, type=span_type, input=input, metadata=metadata
70105
)
71-
_SPANS_BY_OTEL_SPAN[otel_span] = span
72-
return otel_span
106+
if isinstance(span_key, _InvalidOtelSpanKey):
107+
_SPANS_BY_INVALID_OTEL_KEY[span_key.key] = span
108+
else:
109+
_SPANS_BY_OTEL_SPAN[span_key] = span
110+
return span_key
73111

74112

75113
def _end_span_for_otel(
@@ -80,7 +118,11 @@ def _end_span_for_otel(
80118
metrics: Any = None,
81119
error: BaseException | None = None,
82120
) -> None:
83-
span = _SPANS_BY_OTEL_SPAN.pop(otel_span, None)
121+
span = (
122+
_SPANS_BY_INVALID_OTEL_KEY.pop(otel_span.key, None)
123+
if isinstance(otel_span, _InvalidOtelSpanKey)
124+
else _SPANS_BY_OTEL_SPAN.pop(otel_span, None)
125+
)
84126
if span is None:
85127
return
86128
if error is not None:

0 commit comments

Comments
 (0)