Skip to content

Commit d27b8ff

Browse files
authored
fix(telemetry): use per-invocation usage in agent span attributes (#2017)
1 parent ca6f599 commit d27b8ff

2 files changed

Lines changed: 73 additions & 10 deletions

File tree

src/strands/telemetry/tracer.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ class Tracer:
8383
When the OTEL_EXPORTER_OTLP_ENDPOINT environment variable is set, traces
8484
are sent to the OTLP endpoint.
8585
86-
Both attributes are controlled by including "gen_ai_latest_experimental" or "gen_ai_tool_definitions",
87-
respectively, in the OTEL_SEMCONV_STABILITY_OPT_IN environment variable.
86+
Both attributes are controlled by including "gen_ai_latest_experimental", "gen_ai_tool_definitions",
87+
or "gen_ai_use_latest_invocation_tokens", respectively, in the OTEL_SEMCONV_STABILITY_OPT_IN environment variable.
8888
"""
8989

9090
def __init__(self) -> None:
@@ -100,6 +100,7 @@ def __init__(self) -> None:
100100
## To-do: should not set below attributes directly, use env var instead
101101
self.use_latest_genai_conventions = "gen_ai_latest_experimental" in opt_in_values
102102
self._include_tool_definitions = "gen_ai_tool_definitions" in opt_in_values
103+
self._use_latest_invocation_tokens = "gen_ai_use_latest_invocation_tokens" in opt_in_values
103104

104105
def _parse_semconv_opt_in(self) -> set[str]:
105106
"""Parse the OTEL_SEMCONV_STABILITY_OPT_IN environment variable.
@@ -690,16 +691,26 @@ def end_agent_span(
690691
if hasattr(response, "metrics") and hasattr(response.metrics, "accumulated_usage"):
691692
if self.is_langfuse:
692693
attributes.update({"langfuse.observation.type": "span"})
693-
accumulated_usage = response.metrics.accumulated_usage
694+
if self._use_latest_invocation_tokens:
695+
latest_invocation = response.metrics.latest_agent_invocation
696+
if latest_invocation is None:
697+
logger.warning(
698+
"latest_agent_invocation is None despite _use_latest_invocation_tokens being set"
699+
)
700+
usage: Usage = Usage(inputTokens=0, outputTokens=0, totalTokens=0)
701+
else:
702+
usage = latest_invocation.usage
703+
else:
704+
usage = response.metrics.accumulated_usage
694705
attributes.update(
695706
{
696-
"gen_ai.usage.prompt_tokens": accumulated_usage["inputTokens"],
697-
"gen_ai.usage.completion_tokens": accumulated_usage["outputTokens"],
698-
"gen_ai.usage.input_tokens": accumulated_usage["inputTokens"],
699-
"gen_ai.usage.output_tokens": accumulated_usage["outputTokens"],
700-
"gen_ai.usage.total_tokens": accumulated_usage["totalTokens"],
701-
"gen_ai.usage.cache_read_input_tokens": accumulated_usage.get("cacheReadInputTokens", 0),
702-
"gen_ai.usage.cache_write_input_tokens": accumulated_usage.get("cacheWriteInputTokens", 0),
707+
"gen_ai.usage.prompt_tokens": usage["inputTokens"],
708+
"gen_ai.usage.completion_tokens": usage["outputTokens"],
709+
"gen_ai.usage.input_tokens": usage["inputTokens"],
710+
"gen_ai.usage.output_tokens": usage["outputTokens"],
711+
"gen_ai.usage.total_tokens": usage["totalTokens"],
712+
"gen_ai.usage.cache_read_input_tokens": usage.get("cacheReadInputTokens", 0),
713+
"gen_ai.usage.cache_write_input_tokens": usage.get("cacheWriteInputTokens", 0),
703714
}
704715
)
705716

tests/strands/telemetry/test_tracer.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23
import os
34
from datetime import date, datetime, timezone
45
from unittest import mock
@@ -1053,6 +1054,57 @@ def test_end_agent_span_latest_conventions(mock_span, monkeypatch):
10531054
mock_span.end.assert_called_once()
10541055

10551056

1057+
def test_end_agent_span_uses_per_invocation_usage_when_opted_in(mock_span, monkeypatch):
1058+
"""Test that agent span reports per-invocation usage when gen_ai_use_latest_invocation_tokens is set."""
1059+
monkeypatch.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_use_latest_invocation_tokens")
1060+
tracer = Tracer()
1061+
1062+
mock_invocation = mock.MagicMock()
1063+
mock_invocation.usage = {"inputTokens": 100, "outputTokens": 50, "totalTokens": 150}
1064+
1065+
mock_metrics = mock.MagicMock()
1066+
mock_metrics.accumulated_usage = {"inputTokens": 1000, "outputTokens": 500, "totalTokens": 1500}
1067+
mock_metrics.latest_agent_invocation = mock_invocation
1068+
1069+
mock_response = mock.MagicMock()
1070+
mock_response.metrics = mock_metrics
1071+
mock_response.stop_reason = "end_turn"
1072+
mock_response.__str__ = mock.MagicMock(return_value="Agent response")
1073+
1074+
tracer.end_agent_span(mock_span, mock_response)
1075+
1076+
call_args = mock_span.set_attributes.call_args[0][0]
1077+
assert call_args["gen_ai.usage.input_tokens"] == 100
1078+
assert call_args["gen_ai.usage.output_tokens"] == 50
1079+
assert call_args["gen_ai.usage.total_tokens"] == 150
1080+
assert call_args["gen_ai.usage.prompt_tokens"] == 100
1081+
assert call_args["gen_ai.usage.completion_tokens"] == 50
1082+
1083+
1084+
def test_end_agent_span_warns_when_opted_in_but_no_invocations(mock_span, monkeypatch, caplog):
1085+
"""Test warning and zero usage when opted in but no agent invocations exist."""
1086+
monkeypatch.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_use_latest_invocation_tokens")
1087+
tracer = Tracer()
1088+
1089+
mock_metrics = mock.MagicMock()
1090+
mock_metrics.accumulated_usage = {"inputTokens": 200, "outputTokens": 100, "totalTokens": 300}
1091+
mock_metrics.latest_agent_invocation = None
1092+
1093+
mock_response = mock.MagicMock()
1094+
mock_response.metrics = mock_metrics
1095+
mock_response.stop_reason = "end_turn"
1096+
mock_response.__str__ = mock.MagicMock(return_value="Agent response")
1097+
1098+
with caplog.at_level(logging.WARNING):
1099+
tracer.end_agent_span(mock_span, mock_response)
1100+
1101+
assert "latest_agent_invocation is None" in caplog.text
1102+
call_args = mock_span.set_attributes.call_args[0][0]
1103+
assert call_args["gen_ai.usage.input_tokens"] == 0
1104+
assert call_args["gen_ai.usage.output_tokens"] == 0
1105+
assert call_args["gen_ai.usage.total_tokens"] == 0
1106+
1107+
10561108
def test_end_model_invoke_span_with_cache_metrics(mock_span):
10571109
"""Test ending a model invoke span with cache metrics."""
10581110
tracer = Tracer()

0 commit comments

Comments
 (0)