Skip to content

Commit dec2182

Browse files
google-genai-botcopybara-github
authored andcommitted
feat(telemetry): add gen_ai.invoke_agent.{inference,tool}_calls metrics
Adds the two per-invocation call-count metrics from semconv #336 (open-telemetry/semantic-conventions-genai#336): * gen_ai.invoke_agent.inference_calls: model calls in one invoke_agent span. * gen_ai.invoke_agent.tool_calls: tool calls in one invoke_agent span. Both are counted on the per-span TelemetryContext at the call sites (record_inference_telemetry, record_tool_execution) and flushed in record_agent_invocation, keyed by gen_ai.agent.name. The active span's TelemetryContext is carried in the OpenTelemetry context. One point per invoke_agent span, so a multi-agent invocation does not double count. Failed calls still count; only client-side tool calls count. Always emitted (a no-op without a MeterProvider). PiperOrigin-RevId: 941818886
1 parent d4c422e commit dec2182

6 files changed

Lines changed: 148 additions & 5 deletions

File tree

src/google/adk/telemetry/_instrumentation.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,23 @@
2828

2929
from . import _metrics
3030
from . import tracing
31-
from ..events import event as event_lib
3231
from ._schema_version import resolve_schema_version
3332
from ._schema_version import SCHEMA_VERSION_SEMCONV_ALIGNED
3433

34+
# pylint: disable=g-import-not-at-top
3535
if TYPE_CHECKING:
3636
from ..agents.base_agent import BaseAgent
3737
from ..agents.invocation_context import InvocationContext
38+
from ..events import event as event_lib
3839
from ..models.llm_request import LlmRequest
3940
from ..models.llm_response import LlmResponse
4041
from ..tools.base_tool import BaseTool
4142
from ..workflow._base_node import BaseNode
4243

4344
logger = logging.getLogger("google_adk." + __name__)
4445

46+
_INVOKE_AGENT_TELEMETRY_KEY = context_api.create_key("invoke_agent_telemetry")
47+
4548

4649
@contextlib.contextmanager
4750
def record_invocation(
@@ -91,6 +94,22 @@ class TelemetryContext:
9194
error_type: str | None = None
9295
span: tracing.GenerateContentSpan | trace.Span | None = None
9396
_llm_responses: list[LlmResponse] = dataclasses.field(default_factory=list)
97+
_inference_call_count: int = 0
98+
_tool_call_count: int = 0
99+
100+
@property
101+
def inference_call_count(self) -> int:
102+
return self._inference_call_count
103+
104+
def increment_inference_calls(self) -> None:
105+
self._inference_call_count += 1
106+
107+
@property
108+
def tool_call_count(self) -> int:
109+
return self._tool_call_count
110+
111+
def increment_tool_calls(self) -> None:
112+
self._tool_call_count += 1
94113

95114
@property
96115
def llm_responses(self) -> list[LlmResponse]:
@@ -120,6 +139,36 @@ def _record_agent_metrics(
120139
logger.exception("Failed to record agent metrics for agent %s", agent_name)
121140

122141

142+
def _flush_invoke_agent_metrics(
143+
tel_ctx: TelemetryContext, agent_name: str
144+
) -> None:
145+
"""Flushes this span's accumulated inference/tool-call metrics."""
146+
_metrics.record_invoke_agent_inference_calls(
147+
agent_name, tel_ctx.inference_call_count
148+
)
149+
_metrics.record_invoke_agent_tool_calls(agent_name, tel_ctx.tool_call_count)
150+
151+
152+
def _active_invoke_agent_tel_ctx() -> TelemetryContext | None:
153+
"""Returns the TelemetryContext of the active invoke_agent span."""
154+
value = context_api.get_value(_INVOKE_AGENT_TELEMETRY_KEY)
155+
return value if isinstance(value, TelemetryContext) else None
156+
157+
158+
def _accumulate_invoke_agent_tool_call() -> None:
159+
"""Counts one tool call against the active invoke_agent span."""
160+
span_tel_ctx = _active_invoke_agent_tel_ctx()
161+
if span_tel_ctx is not None:
162+
span_tel_ctx.increment_tool_calls()
163+
164+
165+
def _accumulate_invoke_agent_inference_call() -> None:
166+
"""Counts one model call against the active invoke_agent span."""
167+
span_tel_ctx = _active_invoke_agent_tel_ctx()
168+
if span_tel_ctx is not None:
169+
span_tel_ctx.increment_inference_calls()
170+
171+
123172
@contextlib.asynccontextmanager
124173
async def record_agent_invocation(
125174
ctx: InvocationContext, agent: BaseAgent
@@ -129,30 +178,36 @@ async def record_agent_invocation(
129178
caught_error: Exception | None = None
130179
span: trace.Span | None = None
131180
span_name = f"invoke_agent {agent.name}"
181+
tel_ctx = TelemetryContext()
182+
token = context_api.attach(
183+
context_api.set_value(_INVOKE_AGENT_TELEMETRY_KEY, tel_ctx)
184+
)
132185
try:
133186
with tracing.tracer.start_as_current_span(span_name) as s:
134187
span = s
135188
tracing.trace_agent_invocation(span, agent, ctx)
136-
tel_ctx = TelemetryContext(otel_context=context_api.get_current())
189+
tel_ctx.otel_context = context_api.get_current()
137190
yield tel_ctx
138191
except Exception as e:
139192
caught_error = e
140193
raise
141194
finally:
195+
context_api.detach(token)
142196
_record_agent_metrics(
143197
agent.name,
144198
_metrics.get_elapsed_s(span, start_time),
145199
getattr(getattr(ctx, "session", None), "events", []),
146200
caught_error,
147201
)
202+
_flush_invoke_agent_metrics(tel_ctx, agent.name)
148203

149204

150205
@contextlib.asynccontextmanager
151206
async def record_tool_execution(
152207
tool: BaseTool,
153208
agent: BaseAgent,
154209
function_args: dict[str, object],
155-
invocation_context: InvocationContext | None = None,
210+
invocation_context: InvocationContext,
156211
) -> AsyncIterator[TelemetryContext]:
157212
"""Unified context manager for consolidated tool execution telemetry."""
158213
start_time = time.monotonic()
@@ -181,6 +236,7 @@ async def record_tool_execution(
181236
error_type=tel_ctx.error_type,
182237
)
183238
finally:
239+
_accumulate_invoke_agent_tool_call()
184240
try:
185241
_metrics.record_tool_execution_duration(
186242
tool_name=tool.name,
@@ -214,6 +270,7 @@ async def record_inference_telemetry(
214270
yield tel_ctx
215271
finally:
216272
inference_error = sys.exc_info()[1]
273+
_accumulate_invoke_agent_inference_call()
217274
agent = invocation_context.agent
218275
elapsed_s = _metrics.get_elapsed_s(tel_ctx.span, start_time)
219276
try:

src/google/adk/telemetry/_metrics.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,44 @@
113113
gen_ai_metrics.create_gen_ai_client_operation_duration(meter)
114114
)
115115
_client_token_usage = gen_ai_metrics.create_gen_ai_client_token_usage(meter)
116+
_invoke_agent_inference_calls = meter.create_histogram(
117+
"gen_ai.invoke_agent.inference_calls",
118+
unit="1",
119+
description="Number of inference (model) calls per agent invocation.",
120+
explicit_bucket_boundaries_advisory=[
121+
1,
122+
2,
123+
3,
124+
4,
125+
5,
126+
6,
127+
8,
128+
12,
129+
16,
130+
24,
131+
32,
132+
64,
133+
],
134+
)
135+
_invoke_agent_tool_calls = meter.create_histogram(
136+
"gen_ai.invoke_agent.tool_calls",
137+
unit="1",
138+
description="Number of tool calls per agent invocation.",
139+
explicit_bucket_boundaries_advisory=[
140+
1,
141+
2,
142+
3,
143+
4,
144+
5,
145+
6,
146+
8,
147+
12,
148+
16,
149+
24,
150+
32,
151+
64,
152+
],
153+
)
116154

117155

118156
def record_agent_invocation_duration(
@@ -148,6 +186,18 @@ def record_workflow_invocation_duration(
148186
_workflow_invocation_duration.record(elapsed_s, attributes=attrs)
149187

150188

189+
def record_invoke_agent_inference_calls(agent_name: str, count: int) -> None:
190+
"""Records the number of inference (model) calls in an agent invocation."""
191+
attrs = {gen_ai_attributes.GEN_AI_AGENT_NAME: agent_name}
192+
_invoke_agent_inference_calls.record(count, attributes=attrs)
193+
194+
195+
def record_invoke_agent_tool_calls(agent_name: str, count: int) -> None:
196+
"""Records the number of tool calls in an agent invocation."""
197+
attrs = {gen_ai_attributes.GEN_AI_AGENT_NAME: agent_name}
198+
_invoke_agent_tool_calls.record(count, attributes=attrs)
199+
200+
151201
def record_agent_workflow_steps(agent_name: str, events: list[Event]):
152202
"""Records the number of steps in the agent workflow by counting the number of events."""
153203
attrs = {gen_ai_attributes.GEN_AI_AGENT_NAME: agent_name}

src/google/adk/telemetry/node_tracing.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,12 @@ async def start_as_current_node_span(
8282
- `invoke_workflow {workflow.name}`
8383
- `invoke_node {node.name}`
8484
85-
invoke_agent spans align with OpenTelemetry Semantic Conventions (semconv) version 1.36 spans for backwards compatibility.
85+
invoke_agent spans align with OpenTelemetry Semantic Conventions (semconv)
86+
version 1.36 spans for backwards compatibility.
8687
https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/gen-ai/README.md
8788
88-
invoke_workflow spans align with semconv version 1.41, because these were not included in any prior releases.
89+
invoke_workflow spans align with semconv version 1.41, because these were not
90+
included in any prior releases.
8991
https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/README.md
9092
9193
invoke_node spans are not present in any semconv release.

tests/unittests/telemetry/functional_node_test_cases.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2621,6 +2621,12 @@
26212621
value=NON_DETERMINISTIC,
26222622
),
26232623
}),
2624+
"gen_ai.invoke_agent.inference_calls": frozenset({
2625+
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
2626+
}),
2627+
"gen_ai.invoke_agent.tool_calls": frozenset({
2628+
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
2629+
}),
26242630
}
26252631

26262632

@@ -2665,6 +2671,12 @@
26652671
value=NON_DETERMINISTIC,
26662672
),
26672673
}),
2674+
"gen_ai.invoke_agent.inference_calls": frozenset({
2675+
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
2676+
}),
2677+
"gen_ai.invoke_agent.tool_calls": frozenset({
2678+
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
2679+
}),
26682680
}
26692681

26702682

tests/unittests/telemetry/functional_test_cases.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2306,6 +2306,12 @@
23062306
value=NON_DETERMINISTIC,
23072307
),
23082308
}),
2309+
"gen_ai.invoke_agent.inference_calls": frozenset({
2310+
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
2311+
}),
2312+
"gen_ai.invoke_agent.tool_calls": frozenset({
2313+
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
2314+
}),
23092315
}
23102316

23112317

@@ -2350,6 +2356,12 @@
23502356
value=NON_DETERMINISTIC,
23512357
),
23522358
}),
2359+
"gen_ai.invoke_agent.inference_calls": frozenset({
2360+
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
2361+
}),
2362+
"gen_ai.invoke_agent.tool_calls": frozenset({
2363+
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
2364+
}),
23532365
}
23542366

23552367

tests/unittests/telemetry/functional_test_helpers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,16 @@ class HistogramSpec(NamedTuple):
334334
attr="_workflow_invocation_duration",
335335
metric_name="gen_ai.invoke_workflow.duration",
336336
),
337+
HistogramSpec(
338+
module=_metrics,
339+
attr="_invoke_agent_inference_calls",
340+
metric_name="gen_ai.invoke_agent.inference_calls",
341+
),
342+
HistogramSpec(
343+
module=_metrics,
344+
attr="_invoke_agent_tool_calls",
345+
metric_name="gen_ai.invoke_agent.tool_calls",
346+
),
337347
)
338348

339349

0 commit comments

Comments
 (0)