chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from google.adk.telemetry import _metrics
|
||||
from google.genai import types
|
||||
from opentelemetry import metrics
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_meter_setup")
|
||||
def _mock_meter_setup(monkeypatch):
|
||||
"""Sets up mock meter and histograms for testing."""
|
||||
mock_meter = mock.MagicMock()
|
||||
agent_duration_hist = mock.MagicMock(spec=metrics.Histogram)
|
||||
workflow_duration_hist = mock.MagicMock(spec=metrics.Histogram)
|
||||
tool_duration_hist = mock.MagicMock(spec=metrics.Histogram)
|
||||
client_duration_hist = mock.MagicMock(spec=metrics.Histogram)
|
||||
client_token_usage_hist = mock.MagicMock(spec=metrics.Histogram)
|
||||
|
||||
agent_duration_hist.name = "agent_invocation_duration"
|
||||
workflow_duration_hist.name = "workflow_invocation_duration"
|
||||
tool_duration_hist.name = "tool_execution_duration"
|
||||
client_duration_hist.name = "client_operation_duration"
|
||||
client_token_usage_hist.name = "client_token_usage"
|
||||
|
||||
def create_histogram_side_effect(name, **_kwargs):
|
||||
if name == "gen_ai.invoke_agent.duration":
|
||||
return agent_duration_hist
|
||||
elif name == "gen_ai.invoke_workflow.duration":
|
||||
return workflow_duration_hist
|
||||
elif name == "gen_ai.execute_tool.duration":
|
||||
return tool_duration_hist
|
||||
elif name == "gen_ai.client.operation.duration":
|
||||
return client_duration_hist
|
||||
elif name == "gen_ai.client.token.usage":
|
||||
return client_token_usage_hist
|
||||
raise ValueError(f"Unknown metric name: {name}")
|
||||
|
||||
mock_meter.create_histogram.side_effect = create_histogram_side_effect
|
||||
|
||||
# Re-initialize the module-level variables in _metrics with mocked histograms
|
||||
monkeypatch.setattr(_metrics, "meter", mock_meter)
|
||||
monkeypatch.setattr(
|
||||
_metrics, "_agent_invocation_duration", agent_duration_hist
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
_metrics, "_workflow_invocation_duration", workflow_duration_hist
|
||||
)
|
||||
monkeypatch.setattr(_metrics, "_tool_execution_duration", tool_duration_hist)
|
||||
monkeypatch.setattr(
|
||||
_metrics, "_client_operation_duration", client_duration_hist
|
||||
)
|
||||
monkeypatch.setattr(_metrics, "_client_token_usage", client_token_usage_hist)
|
||||
|
||||
return {
|
||||
"meter": mock_meter,
|
||||
"agent_duration": agent_duration_hist,
|
||||
"workflow_duration": workflow_duration_hist,
|
||||
"tool_duration": tool_duration_hist,
|
||||
"client_duration": client_duration_hist,
|
||||
"client_token_usage": client_token_usage_hist,
|
||||
}
|
||||
|
||||
|
||||
def test_record_agent_invocation_duration(mock_meter_setup):
|
||||
"""Tests record_agent_invocation_duration records correctly."""
|
||||
_metrics.record_agent_invocation_duration(
|
||||
"test_agent",
|
||||
1.0,
|
||||
)
|
||||
agent_duration_hist = mock_meter_setup["agent_duration"]
|
||||
agent_duration_hist.record.assert_called_once()
|
||||
args, kwargs = agent_duration_hist.record.call_args
|
||||
assert args[0] == 1.0
|
||||
want_attributes = {"gen_ai.agent.name": "test_agent"}
|
||||
assert kwargs["attributes"] == want_attributes
|
||||
|
||||
|
||||
def test_record_agent_invocation_duration_with_error(mock_meter_setup):
|
||||
"""Tests record_agent_invocation_duration records error correctly."""
|
||||
test_error = ValueError("agent failed")
|
||||
_metrics.record_agent_invocation_duration(
|
||||
"test_agent",
|
||||
1.0,
|
||||
error=test_error,
|
||||
)
|
||||
agent_duration_hist = mock_meter_setup["agent_duration"]
|
||||
agent_duration_hist.record.assert_called_once()
|
||||
_, kwargs = agent_duration_hist.record.call_args
|
||||
assert kwargs["attributes"]["error.type"] == "ValueError"
|
||||
|
||||
|
||||
def test_record_workflow_invocation_duration_root(mock_meter_setup):
|
||||
"""Tests record_workflow_invocation_duration omits nested for the root."""
|
||||
_metrics.record_workflow_invocation_duration(
|
||||
workflow_name="my_workflow",
|
||||
elapsed_s=1.0,
|
||||
nested=False,
|
||||
)
|
||||
hist = mock_meter_setup["workflow_duration"]
|
||||
hist.record.assert_called_once()
|
||||
args, kwargs = hist.record.call_args
|
||||
assert args[0] == 1.0
|
||||
assert kwargs["attributes"] == {
|
||||
"gen_ai.operation.name": "invoke_workflow",
|
||||
"gen_ai.workflow.name": "my_workflow",
|
||||
}
|
||||
|
||||
|
||||
def test_record_workflow_invocation_duration_nested_with_error(
|
||||
mock_meter_setup,
|
||||
):
|
||||
"""Tests record_workflow_invocation_duration records nested + error."""
|
||||
_metrics.record_workflow_invocation_duration(
|
||||
workflow_name="nested_workflow",
|
||||
elapsed_s=2.0,
|
||||
nested=True,
|
||||
error=ValueError("boom"),
|
||||
)
|
||||
hist = mock_meter_setup["workflow_duration"]
|
||||
hist.record.assert_called_once()
|
||||
_, kwargs = hist.record.call_args
|
||||
assert kwargs["attributes"]["gen_ai.workflow.nested"] is True
|
||||
assert kwargs["attributes"]["error.type"] == "ValueError"
|
||||
|
||||
|
||||
def test_record_tool_execution_duration(mock_meter_setup):
|
||||
"""Tests record_tool_execution_duration records correctly."""
|
||||
_metrics.record_tool_execution_duration(
|
||||
"test_tool",
|
||||
"test_tool_type",
|
||||
"test_agent",
|
||||
0.5,
|
||||
)
|
||||
tool_duration_hist = mock_meter_setup["tool_duration"]
|
||||
tool_duration_hist.record.assert_called_once()
|
||||
args, kwargs = tool_duration_hist.record.call_args
|
||||
assert args[0] == 0.5
|
||||
want_attributes = {
|
||||
"gen_ai.agent.name": "test_agent",
|
||||
"gen_ai.tool.name": "test_tool",
|
||||
"gen_ai.tool.type": "test_tool_type",
|
||||
}
|
||||
assert kwargs["attributes"] == want_attributes
|
||||
|
||||
|
||||
def test_record_tool_execution_duration_with_error(mock_meter_setup):
|
||||
"""Tests record_tool_execution_duration records error correctly."""
|
||||
test_error = ValueError("tool failed")
|
||||
_metrics.record_tool_execution_duration(
|
||||
"test_tool",
|
||||
"test_tool_type",
|
||||
"test_agent",
|
||||
0.5,
|
||||
error=test_error,
|
||||
)
|
||||
tool_duration_hist = mock_meter_setup["tool_duration"]
|
||||
tool_duration_hist.record.assert_called_once()
|
||||
_, kwargs = tool_duration_hist.record.call_args
|
||||
assert kwargs["attributes"]["error.type"] == "ValueError"
|
||||
|
||||
|
||||
def test_record_client_operation_duration(mock_meter_setup):
|
||||
"""Tests record_client_operation_duration records correctly."""
|
||||
llm_request = mock.MagicMock(
|
||||
contents=[types.Content(parts=[types.Part(text="hello")])]
|
||||
)
|
||||
response = mock.MagicMock(
|
||||
content=types.Content(parts=[types.Part(text="hello response")])
|
||||
)
|
||||
_metrics.record_client_operation_duration(
|
||||
agent_name="test_agent",
|
||||
elapsed_s=0.1,
|
||||
llm_request=llm_request,
|
||||
responses=[response],
|
||||
)
|
||||
client_duration_hist = mock_meter_setup["client_duration"]
|
||||
client_duration_hist.record.assert_called_once()
|
||||
args, kwargs = client_duration_hist.record.call_args
|
||||
assert args[0] == 0.1
|
||||
want_attributes = {
|
||||
"gen_ai.agent.name": "test_agent",
|
||||
"gen_ai.operation.name": "generate_content",
|
||||
"gen_ai.provider.name": "gemini",
|
||||
"gen_ai.request.model": llm_request.model,
|
||||
"gen_ai.response.model": response.model_version,
|
||||
}
|
||||
assert kwargs["attributes"] == want_attributes
|
||||
|
||||
|
||||
def test_record_client_token_usage(mock_meter_setup):
|
||||
"""Tests record_client_token_usage records correctly under different usage conditions."""
|
||||
llm_request = mock.MagicMock(
|
||||
contents=[types.Content(parts=[types.Part(text="hello")])],
|
||||
model="test-model",
|
||||
)
|
||||
response = mock.MagicMock(
|
||||
content=types.Content(parts=[types.Part(text="hello response")]),
|
||||
model_version="test-model-v1",
|
||||
usage_metadata=types.GenerateContentResponseUsageMetadata(
|
||||
prompt_token_count=20,
|
||||
candidates_token_count=30,
|
||||
tool_use_prompt_token_count=5,
|
||||
thoughts_token_count=10,
|
||||
),
|
||||
)
|
||||
_metrics.record_client_token_usage(
|
||||
agent_name="test_agent",
|
||||
llm_request=llm_request,
|
||||
responses=[response],
|
||||
)
|
||||
client_token_usage_hist = mock_meter_setup["client_token_usage"]
|
||||
assert client_token_usage_hist.record.call_count == 2
|
||||
|
||||
base_attributes = {
|
||||
"gen_ai.agent.name": "test_agent",
|
||||
"gen_ai.operation.name": "generate_content",
|
||||
"gen_ai.provider.name": "gemini",
|
||||
"gen_ai.request.model": "test-model",
|
||||
"gen_ai.response.model": "test-model-v1",
|
||||
}
|
||||
|
||||
input_call = None
|
||||
output_call = None
|
||||
|
||||
for args, kwargs in client_token_usage_hist.record.call_args_list:
|
||||
token_type = kwargs.get("attributes", {}).get("gen_ai.token.type")
|
||||
if token_type == "input":
|
||||
input_call = (args, kwargs)
|
||||
elif token_type == "output":
|
||||
output_call = (args, kwargs)
|
||||
|
||||
assert input_call is not None, "Missing 'input' token usage record"
|
||||
assert output_call is not None, "Missing 'output' token usage record"
|
||||
|
||||
# Verify input tokens (prompt_token_count + tool_use_prompt_token_count)
|
||||
assert input_call[0][0] == 25
|
||||
assert input_call[1]["attributes"] == base_attributes | {
|
||||
"gen_ai.token.type": "input"
|
||||
}
|
||||
|
||||
# Verify output tokens (candidates_token_count + thoughts_token_count)
|
||||
assert output_call[0][0] == 40
|
||||
assert output_call[1]["attributes"] == base_attributes | {
|
||||
"gen_ai.token.type": "output"
|
||||
}
|
||||
Reference in New Issue
Block a user