Skip to content

Commit 470e45f

Browse files
committed
chore: added swarm and graph spans
1 parent 7e8243a commit 470e45f

File tree

2 files changed

+179
-6
lines changed

2 files changed

+179
-6
lines changed

src/strands/telemetry/tracer.py

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from opentelemetry.trace import Span, StatusCode
1515

1616
from ..agent.agent_result import AgentResult
17-
from ..types.content import Message, Messages
17+
from ..types.content import ContentBlock, Message, Messages
1818
from ..types.streaming import StopReason, Usage
1919
from ..types.tools import ToolResult, ToolUse
2020
from ..types.traces import AttributeValue
@@ -113,7 +113,13 @@ def _start_span(
113113
if self.tracer is None:
114114
return None
115115

116-
context = trace_api.set_span_in_context(parent_span) if parent_span else None
116+
if not parent_span:
117+
parent_span = trace_api.get_current_span()
118+
119+
context = None
120+
if parent_span and parent_span.is_recording() and parent_span != trace_api.INVALID_SPAN:
121+
context = trace_api.set_span_in_context(parent_span)
122+
117123
span = self.tracer.start_span(name=span_name, context=context, kind=span_kind)
118124

119125
# Set start time as a common attribute
@@ -235,7 +241,7 @@ def start_model_invoke_span(
235241
# Add additional kwargs as attributes
236242
attributes.update({k: v for k, v in kwargs.items() if isinstance(v, (str, int, float, bool))})
237243

238-
span = self._start_span("chat", parent_span, attributes, span_kind=trace_api.SpanKind.CLIENT)
244+
span = self._start_span("chat", parent_span, attributes=attributes, span_kind=trace_api.SpanKind.CLIENT)
239245
for message in messages:
240246
self._add_event(
241247
span,
@@ -293,8 +299,8 @@ def start_tool_call_span(self, tool: ToolUse, parent_span: Optional[Span] = None
293299
# Add additional kwargs as attributes
294300
attributes.update(kwargs)
295301

296-
span_name = f"Tool: {tool['name']}"
297-
span = self._start_span(span_name, parent_span, attributes, span_kind=trace_api.SpanKind.INTERNAL)
302+
span_name = f"execute_tool {tool['name']}"
303+
span = self._start_span(span_name, parent_span, attributes=attributes, span_kind=trace_api.SpanKind.INTERNAL)
298304

299305
self._add_event(
300306
span,
@@ -497,6 +503,63 @@ def end_agent_span(
497503

498504
self._end_span(span, attributes, error)
499505

506+
def start_swarm_span(
507+
self,
508+
task: str | list[ContentBlock],
509+
) -> Optional[Span]:
510+
"""Start a new span for swarm invocation."""
511+
attributes: Dict[str, AttributeValue] = {
512+
"gen_ai.system": "strands-agents",
513+
"gen_ai.agent.name": "swarm",
514+
"gen_ai.operation.name": "invoke_swarm",
515+
}
516+
517+
span = self._start_span("invoke_swarm", attributes=attributes, span_kind=trace_api.SpanKind.CLIENT)
518+
content = serialize(task) if isinstance(task, list) else task
519+
self._add_event(
520+
span,
521+
"gen_ai.user.message",
522+
event_attributes={"content": content},
523+
)
524+
525+
return span
526+
527+
def end_swarm_span(
528+
self,
529+
span: Span,
530+
result: Optional[str] = None,
531+
) -> None:
532+
"""End a swarm span with results."""
533+
if result:
534+
self._add_event(
535+
span,
536+
"gen_ai.choice",
537+
event_attributes={"message": result},
538+
)
539+
540+
def start_graph_span(
541+
self,
542+
task: str,
543+
) -> Optional[Span]:
544+
"""Start a new span for graph invocation."""
545+
attributes: Dict[str, AttributeValue] = {
546+
"gen_ai.system": "strands-agents",
547+
"gen_ai.agent.name": "graph",
548+
"gen_ai.operation.name": "invoke_graph",
549+
}
550+
551+
span = self._start_span("invoke_graph", attributes=attributes, span_kind=trace_api.SpanKind.CLIENT)
552+
553+
self._add_event(
554+
span,
555+
"gen_ai.user.message",
556+
event_attributes={
557+
"content": task,
558+
},
559+
)
560+
561+
return span
562+
500563

501564
# Singleton instance for global access
502565
_tracer_instance = None

tests/strands/telemetry/test_tracer.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
)
1111

1212
from strands.telemetry.tracer import JSONEncoder, Tracer, get_tracer, serialize
13+
from strands.types.content import ContentBlock
1314
from strands.types.streaming import StopReason, Usage
1415

1516

@@ -198,7 +199,116 @@ def test_start_tool_call_span(mock_tracer):
198199
span = tracer.start_tool_call_span(tool)
199200

200201
mock_tracer.start_span.assert_called_once()
201-
assert mock_tracer.start_span.call_args[1]["name"] == "Tool: test-tool"
202+
assert mock_tracer.start_span.call_args[1]["name"] == "execute_tool test-tool"
203+
mock_span.set_attribute.assert_any_call("gen_ai.tool.name", "test-tool")
204+
mock_span.set_attribute.assert_any_call("gen_ai.system", "strands-agents")
205+
mock_span.set_attribute.assert_any_call("gen_ai.operation.name", "execute_tool")
206+
mock_span.set_attribute.assert_any_call("gen_ai.tool.call.id", "123")
207+
mock_span.add_event.assert_any_call(
208+
"gen_ai.tool.message", attributes={"role": "tool", "content": json.dumps({"param": "value"}), "id": "123"}
209+
)
210+
assert span is not None
211+
212+
213+
"""
214+
def start_swarm_span(
215+
self,
216+
task: str | list[ContentBlock],
217+
) -> Optional[Span]:
218+
attributes: Dict[str, AttributeValue] = {
219+
"gen_ai.system": "strands-agents",
220+
"gen_ai.agent.name": "swarm",
221+
"gen_ai.operation.name": "invoke_swarm",
222+
}
223+
224+
span = self._start_span("invoke_swarm", attributes=attributes, span_kind=trace_api.SpanKind.CLIENT)
225+
226+
self._add_event(
227+
span,
228+
"gen_ai.user.message",
229+
event_attributes={
230+
"content": serialize(task),
231+
},
232+
)
233+
234+
return span
235+
"""
236+
237+
238+
def test_start_swarm_call_span_with_string_task(mock_tracer):
239+
"""Test starting a swarm call span with task as string."""
240+
with mock.patch("strands.telemetry.tracer.trace_api.get_tracer", return_value=mock_tracer):
241+
tracer = Tracer()
242+
tracer.tracer = mock_tracer
243+
244+
mock_span = mock.MagicMock()
245+
mock_tracer.start_span.return_value = mock_span
246+
247+
task = "Design foo bar"
248+
249+
span = tracer.start_swarm_span(task)
250+
251+
mock_tracer.start_span.assert_called_once()
252+
assert mock_tracer.start_span.call_args[1]["name"] == "invoke_swarm"
253+
mock_span.set_attribute.assert_any_call("gen_ai.system", "strands-agents")
254+
mock_span.set_attribute.assert_any_call("gen_ai.agent.name", "swarm")
255+
mock_span.set_attribute.assert_any_call("gen_ai.operation.name", "invoke_swarm")
256+
mock_span.add_event.assert_any_call("gen_ai.user.message", attributes={"content": "Design foo bar"})
257+
assert span is not None
258+
259+
260+
def test_start_swarm_span_with_contentblock_task(mock_tracer):
261+
"""Test starting a swarm call span with task as list of contentBlock."""
262+
with mock.patch("strands.telemetry.tracer.trace_api.get_tracer", return_value=mock_tracer):
263+
tracer = Tracer()
264+
tracer.tracer = mock_tracer
265+
266+
mock_span = mock.MagicMock()
267+
mock_tracer.start_span.return_value = mock_span
268+
269+
task = [ContentBlock(text="Original Task: foo bar")]
270+
271+
span = tracer.start_swarm_span(task)
272+
273+
mock_tracer.start_span.assert_called_once()
274+
assert mock_tracer.start_span.call_args[1]["name"] == "invoke_swarm"
275+
mock_span.set_attribute.assert_any_call("gen_ai.system", "strands-agents")
276+
mock_span.set_attribute.assert_any_call("gen_ai.agent.name", "swarm")
277+
mock_span.set_attribute.assert_any_call("gen_ai.operation.name", "invoke_swarm")
278+
mock_span.add_event.assert_any_call(
279+
"gen_ai.user.message", attributes={"content": '[{"text": "Original Task: foo bar"}]'}
280+
)
281+
assert span is not None
282+
283+
284+
def test_end_swarm_span(mock_span):
285+
"""Test ending a tool call span."""
286+
tracer = Tracer()
287+
swarm_final_reuslt = "foo bar bar"
288+
289+
tracer.end_swarm_span(mock_span, swarm_final_reuslt)
290+
291+
mock_span.add_event.assert_called_with(
292+
"gen_ai.choice",
293+
attributes={"message": "foo bar bar"},
294+
)
295+
296+
297+
def test_start_graph_call_span(mock_tracer):
298+
"""Test starting a graph call span."""
299+
with mock.patch("strands.telemetry.tracer.trace_api.get_tracer", return_value=mock_tracer):
300+
tracer = Tracer()
301+
tracer.tracer = mock_tracer
302+
303+
mock_span = mock.MagicMock()
304+
mock_tracer.start_span.return_value = mock_span
305+
306+
tool = {"name": "test-tool", "toolUseId": "123", "input": {"param": "value"}}
307+
308+
span = tracer.start_tool_call_span(tool)
309+
310+
mock_tracer.start_span.assert_called_once()
311+
assert mock_tracer.start_span.call_args[1]["name"] == "execute_tool test-tool"
202312
mock_span.set_attribute.assert_any_call("gen_ai.tool.name", "test-tool")
203313
mock_span.set_attribute.assert_any_call("gen_ai.system", "strands-agents")
204314
mock_span.set_attribute.assert_any_call("gen_ai.operation.name", "execute_tool")

0 commit comments

Comments
 (0)