From 09cbddaac5ff239c8c4d7386d3010f37eb4a4a72 Mon Sep 17 00:00:00 2001 From: maplexu Date: Tue, 25 Aug 2026 15:29:34 -0400 Subject: [PATCH 1/4] AI-472 Add replay-safe Google ADK metrics sample --- google_adk_agents/metrics/README.md | 31 ++++++++++ google_adk_agents/metrics/__init__.py | 1 + google_adk_agents/metrics/models/__init__.py | 1 + .../metrics/models/local_metrics_model.py | 29 ++++++++++ google_adk_agents/metrics/pyproject.toml | 25 ++++++++ .../metrics/run_metrics_workflow.py | 21 +++++++ google_adk_agents/metrics/run_worker.py | 35 +++++++++++ google_adk_agents/metrics/telemetry.py | 12 ++++ google_adk_agents/metrics/test_metrics.py | 58 +++++++++++++++++++ .../metrics/workflows/__init__.py | 1 + .../metrics/workflows/metrics_workflow.py | 39 +++++++++++++ 11 files changed, 253 insertions(+) create mode 100644 google_adk_agents/metrics/README.md create mode 100644 google_adk_agents/metrics/__init__.py create mode 100644 google_adk_agents/metrics/models/__init__.py create mode 100644 google_adk_agents/metrics/models/local_metrics_model.py create mode 100644 google_adk_agents/metrics/pyproject.toml create mode 100644 google_adk_agents/metrics/run_metrics_workflow.py create mode 100644 google_adk_agents/metrics/run_worker.py create mode 100644 google_adk_agents/metrics/telemetry.py create mode 100644 google_adk_agents/metrics/test_metrics.py create mode 100644 google_adk_agents/metrics/workflows/__init__.py create mode 100644 google_adk_agents/metrics/workflows/metrics_workflow.py diff --git a/google_adk_agents/metrics/README.md b/google_adk_agents/metrics/README.md new file mode 100644 index 000000000..14d92825d --- /dev/null +++ b/google_adk_agents/metrics/README.md @@ -0,0 +1,31 @@ +# Google ADK replay-safe metrics + +This sample exports Google ADK's OpenTelemetry metrics to a local Prometheus endpoint while preventing Workflow replay from recording the same observations again. The default scripted model is deterministic and makes no network model calls, so no API key is needed. + +Start a local Temporal development server: + +```shell +temporal server start-dev +``` + +In another terminal, start the worker from the repository root: + +```shell +uv run --project google_adk_agents/metrics python -m google_adk_agents.metrics.run_worker +``` + +Then run the Workflow: + +```shell +uv run --project google_adk_agents/metrics python -m google_adk_agents.metrics.run_metrics_workflow +``` + +The starter prints `Replay-safe metrics are ready.` Inspect the metrics exposed by the worker: + +```shell +curl http://127.0.0.1:9464/metrics | rg 'gen_ai' +``` + +The output includes `gen_ai.invoke_agent` metrics, `gen_ai.client.operation.duration`, and `gen_ai.client.token.usage` from instrumentation scope `gcp.vertex.agent`. The worker sets `max_cached_workflows=0`, forcing replay between Workflow tasks. `ReplaySafeMeterProvider` drops observations made while replaying, so replay does not multiply the recorded counts. + +OpenTelemetry's global meter provider can be installed only once per process. `run_worker.py` installs the replay-safe provider before importing Google ADK or the Workflow. Applications embedding this setup must likewise make it the first and only global meter provider installation in that process. diff --git a/google_adk_agents/metrics/__init__.py b/google_adk_agents/metrics/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/google_adk_agents/metrics/__init__.py @@ -0,0 +1 @@ + diff --git a/google_adk_agents/metrics/models/__init__.py b/google_adk_agents/metrics/models/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/google_adk_agents/metrics/models/__init__.py @@ -0,0 +1 @@ + diff --git a/google_adk_agents/metrics/models/local_metrics_model.py b/google_adk_agents/metrics/models/local_metrics_model.py new file mode 100644 index 000000000..01e8d0100 --- /dev/null +++ b/google_adk_agents/metrics/models/local_metrics_model.py @@ -0,0 +1,29 @@ +from collections.abc import AsyncGenerator + +from google.adk.models import BaseLlm +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from google.genai import types + +MODEL_NAME = "local-metrics-model" + + +class LocalMetricsModel(BaseLlm): + @classmethod + def supported_models(cls) -> list[str]: + return [MODEL_NAME] + + async def generate_content_async( + self, llm_request: LlmRequest, stream: bool = False + ) -> AsyncGenerator[LlmResponse, None]: + yield LlmResponse( + content=types.Content( + role="model", + parts=[types.Part(text="Replay-safe metrics are ready.")], + ), + usage_metadata=types.GenerateContentResponseUsageMetadata( + prompt_token_count=8, + candidates_token_count=5, + total_token_count=13, + ), + ) diff --git a/google_adk_agents/metrics/pyproject.toml b/google_adk_agents/metrics/pyproject.toml new file mode 100644 index 000000000..2aa1dc0b5 --- /dev/null +++ b/google_adk_agents/metrics/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "google-adk-agents-metrics-sample" +version = "0.1.0" +requires-python = ">=3.10" +dependencies = [ + "google-adk>=2.2.0,<3", + "opentelemetry-exporter-prometheus>=0.48b0", + "temporalio[google-adk,opentelemetry] @ git+https://github.com/temporalio/sdk-python.git@78159d7735b2defc6493669f6c14a0fae6eab985", +] + +[dependency-groups] +dev = [ + "pytest>=7.1.2,<9", + "pytest-asyncio>=0.23,<2", + "ruff>=0.5.0,<0.6", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" + +[tool.ruff] +target-version = "py310" + +[tool.uv] +package = false diff --git a/google_adk_agents/metrics/run_metrics_workflow.py b/google_adk_agents/metrics/run_metrics_workflow.py new file mode 100644 index 000000000..63c8d8104 --- /dev/null +++ b/google_adk_agents/metrics/run_metrics_workflow.py @@ -0,0 +1,21 @@ +import asyncio + +from temporalio.client import Client +from temporalio.contrib.google_adk_agents import GoogleAdkPlugin + +from google_adk_agents.metrics.workflows.metrics_workflow import MetricsWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233", plugins=[GoogleAdkPlugin()]) + result = await client.execute_workflow( + MetricsWorkflow.run, + "Explain replay-safe metrics.", + id="google-adk-agents-metrics-workflow-id", + task_queue="google-adk-agents-metrics", + ) + print(f"Result: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/google_adk_agents/metrics/run_worker.py b/google_adk_agents/metrics/run_worker.py new file mode 100644 index 000000000..be1d19739 --- /dev/null +++ b/google_adk_agents/metrics/run_worker.py @@ -0,0 +1,35 @@ +import asyncio + +from opentelemetry.exporter.prometheus import PrometheusMetricReader + +from google_adk_agents.metrics.telemetry import install_meter_provider + + +async def main() -> None: + install_meter_provider(PrometheusMetricReader()) + + from google.adk.models import LLMRegistry + from prometheus_client import start_http_server + from temporalio.client import Client + from temporalio.contrib.google_adk_agents import GoogleAdkPlugin + from temporalio.worker import Worker + + from google_adk_agents.metrics.models.local_metrics_model import LocalMetricsModel + from google_adk_agents.metrics.workflows.metrics_workflow import MetricsWorkflow + + LLMRegistry.register(LocalMetricsModel) + start_http_server(port=9464, addr="127.0.0.1") + plugin = GoogleAdkPlugin() + client = await Client.connect("localhost:7233", plugins=[plugin]) + worker = Worker( + client, + task_queue="google-adk-agents-metrics", + workflows=[MetricsWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/google_adk_agents/metrics/telemetry.py b/google_adk_agents/metrics/telemetry.py new file mode 100644 index 000000000..e69f6f467 --- /dev/null +++ b/google_adk_agents/metrics/telemetry.py @@ -0,0 +1,12 @@ +import opentelemetry.metrics +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import MetricReader +from temporalio.contrib.opentelemetry import ReplaySafeMeterProvider + + +def install_meter_provider(reader: MetricReader) -> ReplaySafeMeterProvider: + provider = ReplaySafeMeterProvider(MeterProvider(metric_readers=[reader])) + opentelemetry.metrics.set_meter_provider(provider) + if opentelemetry.metrics.get_meter_provider() is not provider: + raise RuntimeError("The global OpenTelemetry meter provider is already set") + return provider diff --git a/google_adk_agents/metrics/test_metrics.py b/google_adk_agents/metrics/test_metrics.py new file mode 100644 index 000000000..eb1c4f3c1 --- /dev/null +++ b/google_adk_agents/metrics/test_metrics.py @@ -0,0 +1,58 @@ +import uuid + +from opentelemetry.sdk.metrics.export import InMemoryMetricReader + +from google_adk_agents.metrics.telemetry import install_meter_provider + +ADK_METER_SCOPE = "gcp.vertex.agent" + + +async def test_metrics_are_not_inflated_by_replay() -> None: + reader = InMemoryMetricReader() + install_meter_provider(reader) + + from google.adk.models import LLMRegistry + from temporalio.contrib.google_adk_agents import GoogleAdkPlugin + from temporalio.testing import WorkflowEnvironment + from temporalio.worker import Worker + + from google_adk_agents.metrics.models.local_metrics_model import LocalMetricsModel + from google_adk_agents.metrics.workflows.metrics_workflow import MetricsWorkflow + + LLMRegistry.register(LocalMetricsModel) + async with await WorkflowEnvironment.start_time_skipping() as environment: + plugin = GoogleAdkPlugin() + config = environment.client.config() + config["plugins"] = [*config["plugins"], plugin] + client = type(environment.client)(**config) + task_queue = f"google-adk-agents-metrics-{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[MetricsWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + result = await client.execute_workflow( + MetricsWorkflow.run, + "Explain replay-safe metrics.", + id=f"google-adk-agents-metrics-{uuid.uuid4()}", + task_queue=task_queue, + ) + + assert result == "Replay-safe metrics are ready." + counts: dict[str, int] = {} + data = reader.get_metrics_data() + if data is not None: + for resource_metrics in data.resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + if scope_metrics.scope.name != ADK_METER_SCOPE: + continue + for metric in scope_metrics.metrics: + counts[metric.name] = sum( + getattr(point, "count", 1) for point in metric.data.data_points + ) + assert counts["gen_ai.invoke_agent.duration"] == 1 + assert counts["gen_ai.invoke_agent.inference_calls"] == 1 + assert counts["gen_ai.client.operation.duration"] == 1 + assert counts["gen_ai.client.token.usage"] == 2 diff --git a/google_adk_agents/metrics/workflows/__init__.py b/google_adk_agents/metrics/workflows/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/google_adk_agents/metrics/workflows/__init__.py @@ -0,0 +1 @@ + diff --git a/google_adk_agents/metrics/workflows/metrics_workflow.py b/google_adk_agents/metrics/workflows/metrics_workflow.py new file mode 100644 index 000000000..1cd7eb5a6 --- /dev/null +++ b/google_adk_agents/metrics/workflows/metrics_workflow.py @@ -0,0 +1,39 @@ +from datetime import timedelta + +from google.adk import Agent +from google.adk.runners import InMemoryRunner +from google.adk.utils.context_utils import Aclosing +from google.genai import types +from temporalio import workflow +from temporalio.contrib.google_adk_agents import TemporalModel + + +@workflow.defn +class MetricsWorkflow: + @workflow.run + async def run(self, prompt: str) -> str: + agent = Agent( + name="metrics_agent", + model=TemporalModel("local-metrics-model"), + instruction="Answer the user briefly.", + ) + runner = InMemoryRunner(agent=agent, app_name="metrics_app") + session = await runner.session_service.create_session( + app_name="metrics_app", user_id="sample-user" + ) + + final_text = "" + async with Aclosing( + runner.run_async( + user_id="sample-user", + session_id=session.id, + new_message=types.Content(role="user", parts=[types.Part(text=prompt)]), + ) + ) as events: + async for event in events: + if event.content and event.content.parts: + for part in event.content.parts: + if part.text: + final_text = part.text + await workflow.sleep(timedelta(milliseconds=1)) + return final_text From ea0a9211c103ea2338bc3eba5c35207f1e0a69c1 Mon Sep 17 00:00:00 2001 From: maplexu Date: Wed, 26 Aug 2026 15:27:52 -0400 Subject: [PATCH 2/4] AI-472 Avoid duplicate Google ADK worker plugin registration --- google_adk_agents/metrics/run_worker.py | 1 - google_adk_agents/metrics/test_metrics.py | 1 - 2 files changed, 2 deletions(-) diff --git a/google_adk_agents/metrics/run_worker.py b/google_adk_agents/metrics/run_worker.py index be1d19739..7405ac14c 100644 --- a/google_adk_agents/metrics/run_worker.py +++ b/google_adk_agents/metrics/run_worker.py @@ -25,7 +25,6 @@ async def main() -> None: client, task_queue="google-adk-agents-metrics", workflows=[MetricsWorkflow], - plugins=[plugin], max_cached_workflows=0, ) await worker.run() diff --git a/google_adk_agents/metrics/test_metrics.py b/google_adk_agents/metrics/test_metrics.py index eb1c4f3c1..bac352bf4 100644 --- a/google_adk_agents/metrics/test_metrics.py +++ b/google_adk_agents/metrics/test_metrics.py @@ -30,7 +30,6 @@ async def test_metrics_are_not_inflated_by_replay() -> None: client, task_queue=task_queue, workflows=[MetricsWorkflow], - plugins=[plugin], max_cached_workflows=0, ): result = await client.execute_workflow( From e758ef74fb7e0468a4583a59ddae62f673652471 Mon Sep 17 00:00:00 2001 From: maplexu Date: Thu, 27 Aug 2026 15:06:57 -0400 Subject: [PATCH 3/4] AI-472 Make replay metrics test deterministic --- google_adk_agents/metrics/test_metrics.py | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/google_adk_agents/metrics/test_metrics.py b/google_adk_agents/metrics/test_metrics.py index bac352bf4..f1be6bd02 100644 --- a/google_adk_agents/metrics/test_metrics.py +++ b/google_adk_agents/metrics/test_metrics.py @@ -14,7 +14,7 @@ async def test_metrics_are_not_inflated_by_replay() -> None: from google.adk.models import LLMRegistry from temporalio.contrib.google_adk_agents import GoogleAdkPlugin from temporalio.testing import WorkflowEnvironment - from temporalio.worker import Worker + from temporalio.worker import Replayer, Worker from google_adk_agents.metrics.models.local_metrics_model import LocalMetricsModel from google_adk_agents.metrics.workflows.metrics_workflow import MetricsWorkflow @@ -30,16 +30,30 @@ async def test_metrics_are_not_inflated_by_replay() -> None: client, task_queue=task_queue, workflows=[MetricsWorkflow], - max_cached_workflows=0, ): - result = await client.execute_workflow( + handle = await client.start_workflow( MetricsWorkflow.run, "Explain replay-safe metrics.", id=f"google-adk-agents-metrics-{uuid.uuid4()}", task_queue=task_queue, ) + result = await handle.result() assert result == "Replay-safe metrics are ready." + counts_before_replay = metric_counts(reader) + assert counts_before_replay["gen_ai.invoke_agent.duration"] > 0 + assert counts_before_replay["gen_ai.invoke_agent.inference_calls"] > 0 + assert counts_before_replay["gen_ai.client.operation.duration"] > 0 + assert counts_before_replay["gen_ai.client.token.usage"] > 0 + + await Replayer(workflows=[MetricsWorkflow], plugins=[plugin]).replay_workflow( + await handle.fetch_history() + ) + + assert metric_counts(reader) == counts_before_replay + + +def metric_counts(reader: InMemoryMetricReader) -> dict[str, int]: counts: dict[str, int] = {} data = reader.get_metrics_data() if data is not None: @@ -51,7 +65,4 @@ async def test_metrics_are_not_inflated_by_replay() -> None: counts[metric.name] = sum( getattr(point, "count", 1) for point in metric.data.data_points ) - assert counts["gen_ai.invoke_agent.duration"] == 1 - assert counts["gen_ai.invoke_agent.inference_calls"] == 1 - assert counts["gen_ai.client.operation.duration"] == 1 - assert counts["gen_ai.client.token.usage"] == 2 + return counts From 799da5c04ca89d57ab98d3ae5e5caf1daffb415e Mon Sep 17 00:00:00 2001 From: maplexu Date: Thu, 3 Sep 2026 16:46:29 -0400 Subject: [PATCH 4/4] AI-472 Address Google ADK metrics review --- google_adk_agents/README.md | 1 + google_adk_agents/metrics/README.md | 10 ++- .../metrics/models/local_metrics_model.py | 4 + google_adk_agents/metrics/pyproject.toml | 25 ------ google_adk_agents/metrics/run_worker.py | 1 - google_adk_agents/metrics/test_metrics.py | 68 --------------- .../metrics/workflows/metrics_workflow.py | 7 +- pyproject.toml | 7 +- tests/google_adk_agents/metrics_test.py | 83 ++++++++++++++++++ uv.lock | 86 ++++++++++++------- 10 files changed, 159 insertions(+), 133 deletions(-) delete mode 100644 google_adk_agents/metrics/pyproject.toml delete mode 100644 google_adk_agents/metrics/test_metrics.py create mode 100644 tests/google_adk_agents/metrics_test.py diff --git a/google_adk_agents/README.md b/google_adk_agents/README.md index edc29d4b1..da21d39e6 100644 --- a/google_adk_agents/README.md +++ b/google_adk_agents/README.md @@ -47,6 +47,7 @@ Each directory contains a complete example with its own README: | [agent_patterns](./agent_patterns/README.md) | A coordinator `LlmAgent` with `sub_agents`, each a `TemporalModel` with a per-agent activity summary. | | [mcp](./mcp/README.md) | A local echo MCP toolset via `TemporalMcpToolSet` / `TemporalMcpToolSetProvider`, running MCP tools as activities. Self-contained, no Node required. | | [streaming](./streaming/README.md) | Token streaming via `TemporalModel(streaming_topic=...)` + `WorkflowStream`, consumed by a starter with `WorkflowStreamClient`. | +| [metrics](./metrics/README.md) | Google ADK OpenTelemetry metrics exported to a local Prometheus endpoint, with replay suppression through `ReplaySafeMeterProvider`. | To run any scenario, start its worker in one terminal and its workflow starter in another: diff --git a/google_adk_agents/metrics/README.md b/google_adk_agents/metrics/README.md index 14d92825d..965cf4952 100644 --- a/google_adk_agents/metrics/README.md +++ b/google_adk_agents/metrics/README.md @@ -11,21 +11,23 @@ temporal server start-dev In another terminal, start the worker from the repository root: ```shell -uv run --project google_adk_agents/metrics python -m google_adk_agents.metrics.run_worker +uv run python -m google_adk_agents.metrics.run_worker ``` Then run the Workflow: ```shell -uv run --project google_adk_agents/metrics python -m google_adk_agents.metrics.run_metrics_workflow +uv run python -m google_adk_agents.metrics.run_metrics_workflow ``` The starter prints `Replay-safe metrics are ready.` Inspect the metrics exposed by the worker: ```shell -curl http://127.0.0.1:9464/metrics | rg 'gen_ai' +curl -s http://127.0.0.1:9464/metrics | grep gen_ai ``` -The output includes `gen_ai.invoke_agent` metrics, `gen_ai.client.operation.duration`, and `gen_ai.client.token.usage` from instrumentation scope `gcp.vertex.agent`. The worker sets `max_cached_workflows=0`, forcing replay between Workflow tasks. `ReplaySafeMeterProvider` drops observations made while replaying, so replay does not multiply the recorded counts. +The output includes `gen_ai.invoke_agent`, `gen_ai.client.operation.duration`, and `gen_ai.client.token.usage` metrics. Prometheus replaces dots with underscores, so an exported line looks like `gen_ai_invoke_agent_duration_seconds_count{gen_ai_agent_name="metrics_agent"} 1.0`. `ReplaySafeMeterProvider` drops observations made while replaying, so replay does not multiply the recorded counts. + +Recordings are first-execution-only rather than exactly-once. Replay is suppressed, but a Workflow Task retry re-executes live and can record again, so treat these metrics as at-least-once usage signals. OpenTelemetry's global meter provider can be installed only once per process. `run_worker.py` installs the replay-safe provider before importing Google ADK or the Workflow. Applications embedding this setup must likewise make it the first and only global meter provider installation in that process. diff --git a/google_adk_agents/metrics/models/local_metrics_model.py b/google_adk_agents/metrics/models/local_metrics_model.py index 01e8d0100..8e45b84cd 100644 --- a/google_adk_agents/metrics/models/local_metrics_model.py +++ b/google_adk_agents/metrics/models/local_metrics_model.py @@ -16,6 +16,10 @@ def supported_models(cls) -> list[str]: async def generate_content_async( self, llm_request: LlmRequest, stream: bool = False ) -> AsyncGenerator[LlmResponse, None]: + if stream: + raise NotImplementedError( + "LocalMetricsModel does not implement streaming responses." + ) yield LlmResponse( content=types.Content( role="model", diff --git a/google_adk_agents/metrics/pyproject.toml b/google_adk_agents/metrics/pyproject.toml deleted file mode 100644 index 2aa1dc0b5..000000000 --- a/google_adk_agents/metrics/pyproject.toml +++ /dev/null @@ -1,25 +0,0 @@ -[project] -name = "google-adk-agents-metrics-sample" -version = "0.1.0" -requires-python = ">=3.10" -dependencies = [ - "google-adk>=2.2.0,<3", - "opentelemetry-exporter-prometheus>=0.48b0", - "temporalio[google-adk,opentelemetry] @ git+https://github.com/temporalio/sdk-python.git@78159d7735b2defc6493669f6c14a0fae6eab985", -] - -[dependency-groups] -dev = [ - "pytest>=7.1.2,<9", - "pytest-asyncio>=0.23,<2", - "ruff>=0.5.0,<0.6", -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" - -[tool.ruff] -target-version = "py310" - -[tool.uv] -package = false diff --git a/google_adk_agents/metrics/run_worker.py b/google_adk_agents/metrics/run_worker.py index 7405ac14c..6ec00f12d 100644 --- a/google_adk_agents/metrics/run_worker.py +++ b/google_adk_agents/metrics/run_worker.py @@ -25,7 +25,6 @@ async def main() -> None: client, task_queue="google-adk-agents-metrics", workflows=[MetricsWorkflow], - max_cached_workflows=0, ) await worker.run() diff --git a/google_adk_agents/metrics/test_metrics.py b/google_adk_agents/metrics/test_metrics.py deleted file mode 100644 index f1be6bd02..000000000 --- a/google_adk_agents/metrics/test_metrics.py +++ /dev/null @@ -1,68 +0,0 @@ -import uuid - -from opentelemetry.sdk.metrics.export import InMemoryMetricReader - -from google_adk_agents.metrics.telemetry import install_meter_provider - -ADK_METER_SCOPE = "gcp.vertex.agent" - - -async def test_metrics_are_not_inflated_by_replay() -> None: - reader = InMemoryMetricReader() - install_meter_provider(reader) - - from google.adk.models import LLMRegistry - from temporalio.contrib.google_adk_agents import GoogleAdkPlugin - from temporalio.testing import WorkflowEnvironment - from temporalio.worker import Replayer, Worker - - from google_adk_agents.metrics.models.local_metrics_model import LocalMetricsModel - from google_adk_agents.metrics.workflows.metrics_workflow import MetricsWorkflow - - LLMRegistry.register(LocalMetricsModel) - async with await WorkflowEnvironment.start_time_skipping() as environment: - plugin = GoogleAdkPlugin() - config = environment.client.config() - config["plugins"] = [*config["plugins"], plugin] - client = type(environment.client)(**config) - task_queue = f"google-adk-agents-metrics-{uuid.uuid4()}" - async with Worker( - client, - task_queue=task_queue, - workflows=[MetricsWorkflow], - ): - handle = await client.start_workflow( - MetricsWorkflow.run, - "Explain replay-safe metrics.", - id=f"google-adk-agents-metrics-{uuid.uuid4()}", - task_queue=task_queue, - ) - result = await handle.result() - - assert result == "Replay-safe metrics are ready." - counts_before_replay = metric_counts(reader) - assert counts_before_replay["gen_ai.invoke_agent.duration"] > 0 - assert counts_before_replay["gen_ai.invoke_agent.inference_calls"] > 0 - assert counts_before_replay["gen_ai.client.operation.duration"] > 0 - assert counts_before_replay["gen_ai.client.token.usage"] > 0 - - await Replayer(workflows=[MetricsWorkflow], plugins=[plugin]).replay_workflow( - await handle.fetch_history() - ) - - assert metric_counts(reader) == counts_before_replay - - -def metric_counts(reader: InMemoryMetricReader) -> dict[str, int]: - counts: dict[str, int] = {} - data = reader.get_metrics_data() - if data is not None: - for resource_metrics in data.resource_metrics: - for scope_metrics in resource_metrics.scope_metrics: - if scope_metrics.scope.name != ADK_METER_SCOPE: - continue - for metric in scope_metrics.metrics: - counts[metric.name] = sum( - getattr(point, "count", 1) for point in metric.data.data_points - ) - return counts diff --git a/google_adk_agents/metrics/workflows/metrics_workflow.py b/google_adk_agents/metrics/workflows/metrics_workflow.py index 1cd7eb5a6..7c9aa8fe6 100644 --- a/google_adk_agents/metrics/workflows/metrics_workflow.py +++ b/google_adk_agents/metrics/workflows/metrics_workflow.py @@ -1,5 +1,3 @@ -from datetime import timedelta - from google.adk import Agent from google.adk.runners import InMemoryRunner from google.adk.utils.context_utils import Aclosing @@ -7,6 +5,8 @@ from temporalio import workflow from temporalio.contrib.google_adk_agents import TemporalModel +from google_adk_agents.metrics.models.local_metrics_model import MODEL_NAME + @workflow.defn class MetricsWorkflow: @@ -14,7 +14,7 @@ class MetricsWorkflow: async def run(self, prompt: str) -> str: agent = Agent( name="metrics_agent", - model=TemporalModel("local-metrics-model"), + model=TemporalModel(MODEL_NAME), instruction="Answer the user briefly.", ) runner = InMemoryRunner(agent=agent, app_name="metrics_app") @@ -35,5 +35,4 @@ async def run(self, prompt: str) -> str: for part in event.content.parts: if part.text: final_text = part.text - await workflow.sleep(timedelta(milliseconds=1)) return final_text diff --git a/pyproject.toml b/pyproject.toml index 63bbed3d8..3bcb3ffd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,12 @@ external-storage = [ ] external-storage-redis = ["redis>=5.0.0,<8"] gevent = ["gevent>=25.4.2 ; python_version >= '3.8'"] -google-adk = ["temporalio[google-adk] >= 1.32.0", "google-adk>=2.2.0,<3"] +google-adk = [ + "temporalio[google-adk,opentelemetry]>=1.32.0,<2", + "google-adk>=2.2.0,<3", + "opentelemetry-exporter-prometheus>=0.48b0", + "prometheus-client>=0.21", +] google-genai = [ "mcp>=1.0.0", "temporalio[google-genai,pydantic]>=1.32.0", diff --git a/tests/google_adk_agents/metrics_test.py b/tests/google_adk_agents/metrics_test.py new file mode 100644 index 000000000..2cd7f4fc1 --- /dev/null +++ b/tests/google_adk_agents/metrics_test.py @@ -0,0 +1,83 @@ +import uuid + +import pytest +from google.adk.models import BaseLlm, LLMRegistry +from opentelemetry.sdk.metrics.export import HistogramDataPoint, InMemoryMetricReader +from temporalio.client import Client +from temporalio.contrib.google_adk_agents import GoogleAdkPlugin +from temporalio.worker import Replayer, Worker + +from google_adk_agents.metrics.models.local_metrics_model import ( + MODEL_NAME, + LocalMetricsModel, +) +from google_adk_agents.metrics.telemetry import install_meter_provider +from google_adk_agents.metrics.workflows.metrics_workflow import MetricsWorkflow + +ADK_METER_SCOPE = "gcp.vertex.agent" + + +async def test_metrics_are_not_inflated_by_replay( + client: Client, monkeypatch: pytest.MonkeyPatch +) -> None: + reader = InMemoryMetricReader() + install_meter_provider(reader) + + original_new_llm = LLMRegistry.new_llm + + def new_llm(model: str) -> BaseLlm: + if model == MODEL_NAME: + return LocalMetricsModel(model=model) + return original_new_llm(model) + + monkeypatch.setattr(LLMRegistry, "new_llm", staticmethod(new_llm)) + + plugin = GoogleAdkPlugin() + config = client.config() + config["plugins"] = [*config["plugins"], plugin] + client = Client(**config) + task_queue = f"google-adk-agents-metrics-{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[MetricsWorkflow], + ): + handle = await client.start_workflow( + MetricsWorkflow.run, + "Explain replay-safe metrics.", + id=f"google-adk-agents-metrics-{uuid.uuid4()}", + task_queue=task_queue, + ) + result = await handle.result() + history = await handle.fetch_history() + + assert result == "Replay-safe metrics are ready." + counts_before_replay = metric_counts(reader) + assert counts_before_replay["gen_ai.invoke_agent.duration"] > 0 + assert counts_before_replay["gen_ai.invoke_agent.inference_calls"] > 0 + assert counts_before_replay["gen_ai.client.operation.duration"] > 0 + assert counts_before_replay["gen_ai.client.token.usage"] > 0 + + await Replayer(workflows=[MetricsWorkflow], plugins=[plugin]).replay_workflow( + history + ) + + assert metric_counts(reader) == counts_before_replay + + +def metric_counts(reader: InMemoryMetricReader) -> dict[str, int]: + counts: dict[str, int] = {} + data = reader.get_metrics_data() + if data is not None: + for resource_metrics in data.resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + if scope_metrics.scope.name != ADK_METER_SCOPE: + continue + for metric in scope_metrics.metrics: + count = 0 + for point in metric.data.data_points: + if not isinstance(point, HistogramDataPoint): + raise TypeError(f"Unexpected metric point: {type(point)}") + count += point.count + counts[metric.name] = count + return counts diff --git a/uv.lock b/uv.lock index 537beabdd..8469affea 100644 --- a/uv.lock +++ b/uv.lock @@ -2767,32 +2767,31 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.41.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.41.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz", hash = "sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", size = 20409, upload-time = "2026-04-24T13:15:40.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl", hash = "sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", size = 18366, upload-time = "2026-04-24T13:15:18.917Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.41.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -2803,14 +2802,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/9b/e4503060b8695579dbaad187dc8cef4554188de68748c88060599b77489e/opentelemetry_exporter_otlp_proto_grpc-1.41.1.tar.gz", hash = "sha256:b05df8fa1333dc9a3fda36b676b96b5095ab6016d3f0c3296d430d629ba1443b", size = 25755, upload-time = "2026-04-24T13:15:41.93Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/f2/c54f33c92443d087703e57e52e55f22f111373a5c4c4aa349ea60efe512e/opentelemetry_exporter_otlp_proto_grpc-1.41.1-py3-none-any.whl", hash = "sha256:537926dcef951136992479af1d9cd88f25e33d56c530e9f020ed57774dca2f94", size = 20297, upload-time = "2026-04-24T13:15:20.212Z" }, + { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.41.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -2821,14 +2820,28 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/5b/9d3c7f70cca10136ba82a81e738dee626c8e7fc61c6887ea9a58bf34c606/opentelemetry_exporter_otlp_proto_http-1.41.1.tar.gz", hash = "sha256:4747a9604c8550ab38c6fd6180e2fcb80de3267060bef2c306bad3cb443302bc", size = 24139, upload-time = "2026-04-24T13:15:42.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, +] + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.63b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/2a/dfeddff262b12eff0c72f4ad9e258aab8889f48c4dc1417a0377a13bc427/opentelemetry_exporter_prometheus-0.63b1.tar.gz", hash = "sha256:31902e22c89431058a95b6dcdb644f9309f226aa4872cc755f0a780d2895e97f", size = 15234, upload-time = "2026-05-21T16:32:57.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/4d/ef07ff2fc630849f2080ae0ae73a61f67257905b7ac79066640bfa0c5739/opentelemetry_exporter_otlp_proto_http-1.41.1-py3-none-any.whl", hash = "sha256:1a21e8f49c7a946d935551e90947d6c3eb39236723c6624401da0f33d68edcb4", size = 22673, upload-time = "2026-04-24T13:15:21.313Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ec/d7c7435e9000fb69837cf7753b7cbbbdeb5d0585203daf1b6ebf8fa93e02/opentelemetry_exporter_prometheus-0.63b1-py3-none-any.whl", hash = "sha256:0efd00aa6b1939345ddcc6de141b83ebffa2b4401a37a68f880e54217602701d", size = 12466, upload-time = "2026-05-21T16:32:36.622Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.62b1" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -2836,9 +2849,9 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/cb/0523b92c112a6cc70be43724343dc45225d3af134419844d7879a07755d4/opentelemetry_instrumentation-0.62b1.tar.gz", hash = "sha256:90e92a905ba4f84db06ac3aec96701df6c079b2d66e9379f8739f0a1bdcc7f45", size = 34043, upload-time = "2026-04-24T13:22:31.997Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/0f/45adbaea1f81b847cffdcee4f4b5f89297e42facf7fac78c7aaac4c38e75/opentelemetry_instrumentation-0.62b1-py3-none-any.whl", hash = "sha256:976fc6e640f2006599e97429c949e622c108d0c17c2059347d1e6c93c707f257", size = 34163, upload-time = "2026-04-24T13:21:31.722Z" }, + { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" }, ] [[package]] @@ -2858,55 +2871,55 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-threading" -version = "0.62b1" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/2d/2537d5990fa341198cbc8ae70b2c3637037061b8ab1196af1d924a275f55/opentelemetry_instrumentation_threading-0.62b1.tar.gz", hash = "sha256:4b3c876907657e3b8b977bfe15d248f2c02db56302c51883724e7ac2f8ce26d2", size = 9180, upload-time = "2026-04-24T13:23:06.15Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/90/7b0279192fab614d57af3d57584ef7ac9e38fa3df0b1d412224f6f55a85b/opentelemetry_instrumentation_threading-0.63b1.tar.gz", hash = "sha256:afa8c2cada8ed136f07b04dc8739bc861a15e9a5edea1a65e4c5e1919c62946c", size = 9080, upload-time = "2026-05-21T16:36:49.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/37/a80fb13b76f85b4e433ff44b4ba177615823c36c28dca12e94d2c37de681/opentelemetry_instrumentation_threading-0.62b1-py3-none-any.whl", hash = "sha256:4596e79c47de122eb2e85877c1a8bfed1cd6ab06bd2c29d120ebcf8a708a433a", size = 9335, upload-time = "2026-04-24T13:22:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3d/3a991a4fcdf5ac82c04215e38cea4e73ad63713707014f9a70d1ab257f5f/opentelemetry_instrumentation_threading-0.63b1-py3-none-any.whl", hash = "sha256:33059298e68c94b13c38b562ad28799ec16a2fd06182ebfc762bb4e956e55d94", size = 8486, upload-time = "2026-05-21T16:35:58.084Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.41.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, + { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.41.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.62b1" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, ] [[package]] @@ -3290,6 +3303,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/01/a9d6ea30351919298d3dc237172ae6a60ce0224ecaaee289b671ab979c13/poethepoet-0.46.0-py3-none-any.whl", hash = "sha256:dc6d770a14792d124abac9066c5a707876027d1878ac9ca26cf57e9b2a96dc89", size = 150581, upload-time = "2026-05-15T15:52:01.118Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -4576,7 +4598,9 @@ gevent = [ ] google-adk = [ { name = "google-adk" }, - { name = "temporalio", extra = ["google-adk"] }, + { name = "opentelemetry-exporter-prometheus" }, + { name = "prometheus-client" }, + { name = "temporalio", extra = ["google-adk", "opentelemetry"] }, ] google-genai = [ { name = "mcp" }, @@ -4689,7 +4713,9 @@ external-storage-redis = [{ name = "redis", specifier = ">=5.0.0,<8" }] gevent = [{ name = "gevent", marker = "python_full_version >= '3.8'", specifier = ">=25.4.2" }] google-adk = [ { name = "google-adk", specifier = ">=2.2.0,<3" }, - { name = "temporalio", extras = ["google-adk"], specifier = ">=1.32.0" }, + { name = "opentelemetry-exporter-prometheus", specifier = ">=0.48b0" }, + { name = "prometheus-client", specifier = ">=0.21" }, + { name = "temporalio", extras = ["google-adk", "opentelemetry"], specifier = ">=1.32.0,<2" }, ] google-genai = [ { name = "mcp", specifier = ">=1.0.0" },