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. 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: 940760757
This commit is contained in:
committed by
Copybara-Service
parent
196f7708f8
commit
c6dec00a6e
@@ -18,7 +18,6 @@ import asyncio
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from google.adk.platform import uuid as platform_uuid
|
||||
from google.genai import types
|
||||
@@ -48,9 +47,6 @@ from .live_request_queue import LiveRequestQueue
|
||||
from .run_config import RunConfig
|
||||
from .transcription_entry import TranscriptionEntry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from google.adk.telemetry._instrumentation import TelemetryContext
|
||||
|
||||
|
||||
class LlmCallsLimitExceededError(Exception):
|
||||
"""Error thrown when the number of LLM calls exceed the limit."""
|
||||
@@ -274,12 +270,6 @@ class InvocationContext(BaseModel):
|
||||
of this invocation.
|
||||
"""
|
||||
|
||||
_invoke_agent_telemetry_context: Optional[TelemetryContext] = PrivateAttr(
|
||||
default=None
|
||||
)
|
||||
"""TelemetryContext of the active ``invoke_agent`` span, if any.
|
||||
"""
|
||||
|
||||
@property
|
||||
def is_resumable(self) -> bool:
|
||||
"""Returns whether the current invocation is resumable."""
|
||||
|
||||
@@ -27,11 +27,11 @@ import opentelemetry.context as context_api
|
||||
|
||||
from . import _metrics
|
||||
from . import tracing
|
||||
from ..events import event as event_lib
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..agents.base_agent import BaseAgent
|
||||
from ..agents.invocation_context import InvocationContext
|
||||
from ..events import event as event_lib
|
||||
from ..models.llm_request import LlmRequest
|
||||
from ..models.llm_response import LlmResponse
|
||||
from ..tools.base_tool import BaseTool
|
||||
@@ -78,31 +78,11 @@ class TelemetryContext:
|
||||
error_type: str | None = None
|
||||
span: tracing.GenerateContentSpan | trace.Span | None = None
|
||||
_llm_responses: list[LlmResponse] = dataclasses.field(default_factory=list)
|
||||
_inference_call_count: int = 0
|
||||
_tool_call_count: int = 0
|
||||
|
||||
@property
|
||||
def llm_responses(self) -> list[LlmResponse]:
|
||||
return self._llm_responses
|
||||
|
||||
@property
|
||||
def inference_call_count(self) -> int:
|
||||
"""Number of model calls counted against this invoke_agent span."""
|
||||
return self._inference_call_count
|
||||
|
||||
def increment_inference_calls(self) -> None:
|
||||
"""Counts one model call against this span (including calls that raised)."""
|
||||
self._inference_call_count += 1
|
||||
|
||||
def increment_tool_calls(self) -> None:
|
||||
"""Counts one tool call against this invoke_agent span."""
|
||||
self._tool_call_count += 1
|
||||
|
||||
@property
|
||||
def tool_call_count(self) -> int:
|
||||
"""Number of tool calls counted against this invoke_agent span."""
|
||||
return self._tool_call_count
|
||||
|
||||
def record_llm_response(
|
||||
self, invocation_context: InvocationContext, response: LlmResponse
|
||||
) -> None:
|
||||
@@ -130,30 +110,6 @@ def _record_agent_metrics(
|
||||
logger.exception("Failed to record agent metrics for agent %s", agent_name)
|
||||
|
||||
|
||||
def _flush_invoke_agent_metrics(
|
||||
tel_ctx: TelemetryContext, agent_name: str
|
||||
) -> None:
|
||||
"""Flushes this span's accumulated inference/tool-call metrics."""
|
||||
_metrics.record_invoke_agent_inference_calls(
|
||||
agent_name, tel_ctx.inference_call_count
|
||||
)
|
||||
_metrics.record_invoke_agent_tool_calls(agent_name, tel_ctx.tool_call_count)
|
||||
|
||||
|
||||
def _accumulate_invoke_agent_tool_call(ctx: InvocationContext) -> None:
|
||||
"""Counts one tool call against the active invoke_agent span."""
|
||||
span_tel_ctx = ctx._invoke_agent_telemetry_context # pylint: disable=protected-access
|
||||
if span_tel_ctx is not None:
|
||||
span_tel_ctx.increment_tool_calls()
|
||||
|
||||
|
||||
def _accumulate_invoke_agent_inference_call(ctx: InvocationContext) -> None:
|
||||
"""Counts one model call against the active invoke_agent span."""
|
||||
span_tel_ctx = ctx._invoke_agent_telemetry_context # pylint: disable=protected-access
|
||||
if span_tel_ctx is not None:
|
||||
span_tel_ctx.increment_inference_calls()
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def record_agent_invocation(
|
||||
ctx: InvocationContext, agent: BaseAgent
|
||||
@@ -163,13 +119,11 @@ async def record_agent_invocation(
|
||||
caught_error: Exception | None = None
|
||||
span: trace.Span | None = None
|
||||
span_name = f"invoke_agent {agent.name}"
|
||||
tel_ctx = TelemetryContext()
|
||||
try:
|
||||
with tracing.tracer.start_as_current_span(span_name) as s:
|
||||
span = s
|
||||
tracing.trace_agent_invocation(span, agent, ctx)
|
||||
tel_ctx.otel_context = context_api.get_current()
|
||||
ctx._invoke_agent_telemetry_context = tel_ctx # pylint: disable=protected-access
|
||||
tel_ctx = TelemetryContext(otel_context=context_api.get_current())
|
||||
yield tel_ctx
|
||||
except Exception as e:
|
||||
caught_error = e
|
||||
@@ -182,7 +136,6 @@ async def record_agent_invocation(
|
||||
getattr(getattr(ctx, "session", None), "events", []),
|
||||
caught_error,
|
||||
)
|
||||
_flush_invoke_agent_metrics(tel_ctx, agent.name)
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
@@ -190,7 +143,7 @@ async def record_tool_execution(
|
||||
tool: BaseTool,
|
||||
agent: BaseAgent,
|
||||
function_args: dict[str, object],
|
||||
invocation_context: InvocationContext,
|
||||
invocation_context: InvocationContext | None = None,
|
||||
) -> AsyncIterator[TelemetryContext]:
|
||||
"""Unified context manager for consolidated tool execution telemetry."""
|
||||
start_time = time.monotonic()
|
||||
@@ -218,7 +171,6 @@ async def record_tool_execution(
|
||||
invocation_context=invocation_context,
|
||||
error_type=tel_ctx.error_type,
|
||||
)
|
||||
_accumulate_invoke_agent_tool_call(invocation_context)
|
||||
finally:
|
||||
try:
|
||||
_metrics.record_tool_execution_duration(
|
||||
@@ -253,7 +205,6 @@ async def record_inference_telemetry(
|
||||
yield tel_ctx
|
||||
finally:
|
||||
inference_error = sys.exc_info()[1]
|
||||
_accumulate_invoke_agent_inference_call(invocation_context)
|
||||
agent = invocation_context.agent
|
||||
elapsed_s = _get_elapsed_s(tel_ctx.span, start_time)
|
||||
try:
|
||||
|
||||
@@ -146,44 +146,6 @@ _client_operation_duration = (
|
||||
gen_ai_metrics.create_gen_ai_client_operation_duration(meter)
|
||||
)
|
||||
_client_token_usage = gen_ai_metrics.create_gen_ai_client_token_usage(meter)
|
||||
_invoke_agent_inference_calls = meter.create_histogram(
|
||||
"gen_ai.invoke_agent.inference_calls",
|
||||
unit="1",
|
||||
description="Number of inference (model) calls per agent invocation.",
|
||||
explicit_bucket_boundaries_advisory=[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
8,
|
||||
12,
|
||||
16,
|
||||
24,
|
||||
32,
|
||||
64,
|
||||
],
|
||||
)
|
||||
_invoke_agent_tool_calls = meter.create_histogram(
|
||||
"gen_ai.invoke_agent.tool_calls",
|
||||
unit="1",
|
||||
description="Number of tool calls per agent invocation.",
|
||||
explicit_bucket_boundaries_advisory=[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
8,
|
||||
12,
|
||||
16,
|
||||
24,
|
||||
32,
|
||||
64,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def record_agent_invocation_duration(
|
||||
@@ -198,18 +160,6 @@ def record_agent_invocation_duration(
|
||||
_agent_invocation_duration.record(elapsed_s, attributes=attrs)
|
||||
|
||||
|
||||
def record_invoke_agent_inference_calls(agent_name: str, count: int):
|
||||
"""Records the number of inference (model) calls in an agent invocation."""
|
||||
attrs = {gen_ai_attributes.GEN_AI_AGENT_NAME: agent_name}
|
||||
_invoke_agent_inference_calls.record(count, attributes=attrs)
|
||||
|
||||
|
||||
def record_invoke_agent_tool_calls(agent_name: str, count: int):
|
||||
"""Records the number of tool calls in an agent invocation."""
|
||||
attrs = {gen_ai_attributes.GEN_AI_AGENT_NAME: agent_name}
|
||||
_invoke_agent_tool_calls.record(count, attributes=attrs)
|
||||
|
||||
|
||||
def record_agent_request_size(
|
||||
agent_name: str, user_content: types.Content | None
|
||||
):
|
||||
|
||||
@@ -2900,12 +2900,6 @@ EXPECTED_NODE_METRICS_V1: dict[str, frozenset[MetricPoint]] = {
|
||||
value=NON_DETERMINISTIC,
|
||||
),
|
||||
}),
|
||||
"gen_ai.invoke_agent.inference_calls": frozenset({
|
||||
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
|
||||
}),
|
||||
"gen_ai.invoke_agent.tool_calls": frozenset({
|
||||
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -2953,12 +2947,6 @@ EXPECTED_NODE_METRICS_V2: dict[str, frozenset[MetricPoint]] = {
|
||||
value=NON_DETERMINISTIC,
|
||||
),
|
||||
}),
|
||||
"gen_ai.invoke_agent.inference_calls": frozenset({
|
||||
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
|
||||
}),
|
||||
"gen_ai.invoke_agent.tool_calls": frozenset({
|
||||
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2294,12 +2294,6 @@ EXPECTED_METRICS_V1: dict[str, frozenset[MetricPoint]] = {
|
||||
value=NON_DETERMINISTIC,
|
||||
),
|
||||
}),
|
||||
"gen_ai.invoke_agent.inference_calls": frozenset({
|
||||
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
|
||||
}),
|
||||
"gen_ai.invoke_agent.tool_calls": frozenset({
|
||||
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -2347,12 +2341,6 @@ EXPECTED_METRICS_V2: dict[str, frozenset[MetricPoint]] = {
|
||||
value=NON_DETERMINISTIC,
|
||||
),
|
||||
}),
|
||||
"gen_ai.invoke_agent.inference_calls": frozenset({
|
||||
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
|
||||
}),
|
||||
"gen_ai.invoke_agent.tool_calls": frozenset({
|
||||
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -339,16 +339,6 @@ _PATCHED_HISTOGRAMS: tuple[HistogramSpec, ...] = (
|
||||
attr="_client_token_usage",
|
||||
metric_name="gen_ai.client.token.usage",
|
||||
),
|
||||
HistogramSpec(
|
||||
module=_metrics,
|
||||
attr="_invoke_agent_inference_calls",
|
||||
metric_name="gen_ai.invoke_agent.inference_calls",
|
||||
),
|
||||
HistogramSpec(
|
||||
module=_metrics,
|
||||
attr="_invoke_agent_tool_calls",
|
||||
metric_name="gen_ai.invoke_agent.tool_calls",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
# pylint: disable=protected-access
|
||||
|
||||
import time
|
||||
import types
|
||||
from unittest import mock
|
||||
|
||||
from google.adk.telemetry import _instrumentation
|
||||
@@ -95,8 +94,8 @@ async def test_record_agent_invocation_tolerates_minimal_context():
|
||||
"""
|
||||
agent = mock.MagicMock()
|
||||
agent.name = "test_agent"
|
||||
# Context-like without `user_content` and without `session`.
|
||||
bare_ctx = types.SimpleNamespace()
|
||||
# Bare object without `user_content` and without `session`.
|
||||
bare_ctx = object()
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
|
||||
Reference in New Issue
Block a user