Skip to content

Commit 1e6ba4d

Browse files
author
Vamil Gandhi
committed
feat(telemetry): add cache usage metrics to OpenTelemetry spans
Adds cacheReadInputTokens and cacheWriteInputTokens to span attributes in both end_model_invoke_span and end_agent_span methods to enable monitoring of cache token usage for cost calculation. Closes #776
1 parent ab125f5 commit 1e6ba4d

File tree

2 files changed

+66
-0
lines changed

2 files changed

+66
-0
lines changed

src/strands/telemetry/tracer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ def end_model_invoke_span(
263263
"gen_ai.usage.completion_tokens": usage["outputTokens"],
264264
"gen_ai.usage.output_tokens": usage["outputTokens"],
265265
"gen_ai.usage.total_tokens": usage["totalTokens"],
266+
"gen_ai.usage.cache_read_input_tokens": usage.get("cacheReadInputTokens", 0),
267+
"gen_ai.usage.cache_write_input_tokens": usage.get("cacheWriteInputTokens", 0),
266268
}
267269

268270
self._add_event(
@@ -491,6 +493,8 @@ def end_agent_span(
491493
"gen_ai.usage.input_tokens": accumulated_usage["inputTokens"],
492494
"gen_ai.usage.output_tokens": accumulated_usage["outputTokens"],
493495
"gen_ai.usage.total_tokens": accumulated_usage["totalTokens"],
496+
"gen_ai.usage.cache_read_input_tokens": accumulated_usage.get("cacheReadInputTokens", 0),
497+
"gen_ai.usage.cache_write_input_tokens": accumulated_usage.get("cacheWriteInputTokens", 0),
494498
}
495499
)
496500

tests/strands/telemetry/test_tracer.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ def test_end_model_invoke_span(mock_span):
177177
mock_span.set_attribute.assert_any_call("gen_ai.usage.completion_tokens", 20)
178178
mock_span.set_attribute.assert_any_call("gen_ai.usage.output_tokens", 20)
179179
mock_span.set_attribute.assert_any_call("gen_ai.usage.total_tokens", 30)
180+
mock_span.set_attribute.assert_any_call("gen_ai.usage.cache_read_input_tokens", 0)
181+
mock_span.set_attribute.assert_any_call("gen_ai.usage.cache_write_input_tokens", 0)
180182
mock_span.add_event.assert_called_with(
181183
"gen_ai.choice",
182184
attributes={"message": json.dumps(message["content"]), "finish_reason": "end_turn"},
@@ -404,6 +406,8 @@ def test_end_agent_span(mock_span):
404406
mock_span.set_attribute.assert_any_call("gen_ai.usage.completion_tokens", 100)
405407
mock_span.set_attribute.assert_any_call("gen_ai.usage.output_tokens", 100)
406408
mock_span.set_attribute.assert_any_call("gen_ai.usage.total_tokens", 150)
409+
mock_span.set_attribute.assert_any_call("gen_ai.usage.cache_read_input_tokens", 0)
410+
mock_span.set_attribute.assert_any_call("gen_ai.usage.cache_write_input_tokens", 0)
407411
mock_span.add_event.assert_any_call(
408412
"gen_ai.choice",
409413
attributes={"message": "Agent response", "finish_reason": "end_turn"},
@@ -412,6 +416,64 @@ def test_end_agent_span(mock_span):
412416
mock_span.end.assert_called_once()
413417

414418

419+
def test_end_model_invoke_span_with_cache_metrics(mock_span):
420+
"""Test ending a model invoke span with cache metrics."""
421+
tracer = Tracer()
422+
message = {"role": "assistant", "content": [{"text": "Response"}]}
423+
usage = Usage(
424+
inputTokens=10,
425+
outputTokens=20,
426+
totalTokens=30,
427+
cacheReadInputTokens=5,
428+
cacheWriteInputTokens=3,
429+
)
430+
stop_reason: StopReason = "end_turn"
431+
432+
tracer.end_model_invoke_span(mock_span, message, usage, stop_reason)
433+
434+
mock_span.set_attribute.assert_any_call("gen_ai.usage.prompt_tokens", 10)
435+
mock_span.set_attribute.assert_any_call("gen_ai.usage.input_tokens", 10)
436+
mock_span.set_attribute.assert_any_call("gen_ai.usage.completion_tokens", 20)
437+
mock_span.set_attribute.assert_any_call("gen_ai.usage.output_tokens", 20)
438+
mock_span.set_attribute.assert_any_call("gen_ai.usage.total_tokens", 30)
439+
mock_span.set_attribute.assert_any_call("gen_ai.usage.cache_read_input_tokens", 5)
440+
mock_span.set_attribute.assert_any_call("gen_ai.usage.cache_write_input_tokens", 3)
441+
mock_span.set_status.assert_called_once_with(StatusCode.OK)
442+
mock_span.end.assert_called_once()
443+
444+
445+
def test_end_agent_span_with_cache_metrics(mock_span):
446+
"""Test ending an agent span with cache metrics."""
447+
tracer = Tracer()
448+
449+
# Mock AgentResult with metrics including cache tokens
450+
mock_metrics = mock.MagicMock()
451+
mock_metrics.accumulated_usage = {
452+
"inputTokens": 50,
453+
"outputTokens": 100,
454+
"totalTokens": 150,
455+
"cacheReadInputTokens": 25,
456+
"cacheWriteInputTokens": 10,
457+
}
458+
459+
mock_response = mock.MagicMock()
460+
mock_response.metrics = mock_metrics
461+
mock_response.stop_reason = "end_turn"
462+
mock_response.__str__ = mock.MagicMock(return_value="Agent response")
463+
464+
tracer.end_agent_span(mock_span, mock_response)
465+
466+
mock_span.set_attribute.assert_any_call("gen_ai.usage.prompt_tokens", 50)
467+
mock_span.set_attribute.assert_any_call("gen_ai.usage.input_tokens", 50)
468+
mock_span.set_attribute.assert_any_call("gen_ai.usage.completion_tokens", 100)
469+
mock_span.set_attribute.assert_any_call("gen_ai.usage.output_tokens", 100)
470+
mock_span.set_attribute.assert_any_call("gen_ai.usage.total_tokens", 150)
471+
mock_span.set_attribute.assert_any_call("gen_ai.usage.cache_read_input_tokens", 25)
472+
mock_span.set_attribute.assert_any_call("gen_ai.usage.cache_write_input_tokens", 10)
473+
mock_span.set_status.assert_called_once_with(StatusCode.OK)
474+
mock_span.end.assert_called_once()
475+
476+
415477
def test_get_tracer_singleton():
416478
"""Test that get_tracer returns a singleton instance."""
417479
# Reset the singleton first

0 commit comments

Comments
 (0)