feat(adk): expose gen_ai.client.token.usage Prometheus metric#2149
feat(adk): expose gen_ai.client.token.usage Prometheus metric#2149braghettos wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an OTLP-backed OpenTelemetry metrics pipeline to the ADK and emits the GenAI semantic-convention metric gen_ai.client.token.usage (gated behind OTEL_METRICS_ENABLED) using token usage already surfaced by the A2A executor loop.
Changes:
- Introduces an OTLP
MeterProvider+ periodic reader/exporter and a package-levelRecordTokenUsage(...)helper forgen_ai.client.token.usage. - Wires metrics initialization/shutdown into ADK telemetry init (alongside existing tracing/logging setup).
- Plumbs model/provider labels from agent config into the executor so token usage points include
gen_ai.request.modelandgen_ai.provider.name.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| go/go.mod | Promotes OTLP metric exporter + otel metric SDK deps to direct requirements. |
| go/adk/pkg/telemetry/tracing.go | Initializes MeterProvider when metrics are enabled and composes shutdown. |
| go/adk/pkg/telemetry/metrics.go | Implements OTLP metrics pipeline and GenAI token-usage histogram + recorder. |
| go/adk/pkg/telemetry/metrics_test.go | Adds unit tests for recording/no-op behavior. |
| go/adk/pkg/config/config_usage.go | Exposes model-name helper for telemetry labeling. |
| go/adk/pkg/a2a/executor.go | Records token usage from UsageMetadata during the A2A event loop. |
| go/adk/cmd/main.go | Derives model/provider labels from agent config and passes them to the executor. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| meterProvider, mpErr := newMeterProvider(ctx, telemetryResource) | ||
| if mpErr != nil { | ||
| return nil, true, mpErr | ||
| } |
| if instErr := initGenAIMetrics(); instErr != nil { | ||
| return nil, true, instErr | ||
| } |
| // genAIMetrics holds the GenAI metric instruments. It is recorded by the A2A | ||
| // executor when the agent runtime reports token usage. When metrics are | ||
| // disabled (the default), instrumentInstance is nil and the public Record* | ||
| // helpers are cheap no-ops, keeping the runtime byte-identical when off. |
krisztianfekete
left a comment
There was a problem hiding this comment.
Thanks for opening this PR!
Can you please follow the recommendations in this issue (you're umbrella issue references this actually...) comment: #1412 (review), and use the official Prometheus client library for instrumentation?
Add GenAI token-usage instrumentation to the ADK agent using the native Prometheus client library (github.com/prometheus/client_golang), matching kagent's existing Go metrics approach on the controller side. - pkg/telemetry/metrics.go: a gen_ai_client_token_usage histogram (Prometheus name for the OTel semconv metric gen_ai.client.token.usage) with labels gen_ai_token_type (input/output), gen_ai_request_model, gen_ai_provider_name and gen_ai_operation_name, following GenAI semantic conventions (semconv 1.40.0). Exposed via MetricsHandler. - pkg/a2a/server/server.go: serve the metrics at /metrics on the agent's A2A mux (excluded from request tracing). - pkg/a2a/executor.go: record token counts from the A2A event loop where UsageMetadata is surfaced (input = PromptTokenCount, output = CandidatesTokenCount + ThoughtsTokenCount). - cmd/main.go / pkg/config: resolve the request model and provider labels from the agent config. Replaces the earlier OTLP MeterProvider approach per maintainer review, keeping OTel-semconv naming. prometheus/client_golang is already a dependency; no new direct requirements. Refs: kagent-dev#2148 Signed-off-by: Diego Braga <diego.braga86@gmail.com>
55e63ba to
fa6939a
Compare
|
Thanks for the review, @krisztianfekete! I've reworked the PR to use the native Prometheus client library ( Kept it OTel-semconv compliant (targeting semconv 1.40.0). Specifically, addressing the points from the #1412 review:
The metric is a Happy to adjust the metric buckets, the operation-name handling, or add the response model / other semconv attributes if you'd prefer. Thanks! |
krisztianfekete
left a comment
There was a problem hiding this comment.
Thanks for the changes, added some comments!
Couple of additional recommendations:
- Since you wanted to go with the OTel SDK, you might want to push metrics. You can add the OTel Go bridge to enable optionally pushing metrics to an otlp endpoint. This can be a follow-up.
- Please make sure to include steps (or even better, docs) in the PR on how did you test this (with screenshots) and how users should configure it to leverage the feature.
| if agentConfig == nil || agentConfig.Model == nil { | ||
| return "", "" | ||
| } | ||
| return config.ModelName(agentConfig.Model), agentConfig.Model.GetType() |
There was a problem hiding this comment.
While some of these are semconv compliant, not all. Let's use an enum for this to align with the well-known names.
| // Record GenAI token usage on the events that carry it. Output combines | ||
| // candidate + reasoning tokens. | ||
| if um := adkEvent.UsageMetadata; um != nil { | ||
| input := int64(um.PromptTokenCount) | ||
| output := int64(um.CandidatesTokenCount) + int64(um.ThoughtsTokenCount) | ||
| telemetry.RecordTokenUsage(e.modelName, e.providerName, input, output) | ||
| } |
There was a problem hiding this comment.
This seems to be correct today, but can you please make sure we don't double count partial events?
We should also add a test with multiple LLM streaming sessions and asserts the histogram observations equal the number of LLM calls (not the number of stream chunks).
| Name: metricGenAIClientTokenUsage, | ||
| Help: "Measures the number of input and output tokens used by GenAI requests.", | ||
| // Token-count buckets spanning short prompts to large-context requests. | ||
| Buckets: []float64{1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576}, |
There was a problem hiding this comment.
Please use the semconv recommended bucket layout.
| metricGenAIClientTokenUsage = "gen_ai_client_token_usage" | ||
|
|
||
| labelGenAITokenType = "gen_ai_token_type" | ||
| labelGenAIRequestModel = "gen_ai_request_model" |
There was a problem hiding this comment.
Please also add gen_ai.response.model
| labelGenAIRequestModel = "gen_ai_request_model" | ||
| labelGenAIProviderName = "gen_ai_provider_name" | ||
| labelGenAIOperationName = "gen_ai_operation_name" | ||
|
|
There was a problem hiding this comment.
Can you please add error.type for failures as per the spec?
| mux := http.NewServeMux() | ||
| RegisterHealthEndpoints(mux) | ||
| // Expose GenAI token-usage (and standard Go/process) metrics for scraping. | ||
| mux.Handle("/metrics", telemetry.MetricsHandler()) |
There was a problem hiding this comment.
I guess we'd need to expose metrics on the controller generated objects as well.
…ror.type, scrape annotations Addresses @krisztianfekete's review on kagent-dev#2149: - provider label mapped to semconv well-known gen_ai.provider.name values via SemconvProviderName (gemini_vertex_ai->gcp.vertex_ai, bedrock->aws.bedrock, azure_openai->azure.ai.openai, etc.); unknown types pass through. - add gen_ai.response.model (falls back to request model) and error.type labels. - use the semconv-recommended histogram bucket layout. - guard against double-counting streaming partial events (record once per LLM call, not per chunk) + test asserting observations == LLM calls. - controller: annotate Go-runtime agent pods for Prometheus discovery (prometheus.io/scrape|port|path) so the /metrics endpoint is scrapeable. - docs/genai-token-metrics.md: what is emitted, how to scrape, how to verify. The OTel Prometheus->OTLP push bridge is left as a follow-up per the review. Refs: kagent-dev#2148 Signed-off-by: Diego Braga <diego.braga86@gmail.com>
|
Thanks for the detailed review @krisztianfekete! Pushed changes addressing all points:
The three earlier Copilot comments referenced the removed OTLP |
Thank you, will re-review once you had a chance to test it. |
What
Adds GenAI token-usage instrumentation to the ADK agent, exposing the OTel-semconv metric
gen_ai.client.token.usagevia the native Prometheus client library (github.com/prometheus/client_golang) — matching kagent's existing Go metrics approach on the controller side. First of the three observability gaps in #2148.How
pkg/telemetry/metrics.go— agen_ai_client_token_usagehistogram (Prometheus name for the semconv metricgen_ai.client.token.usage) with labels, following GenAI semantic conventions (semconv 1.40.0), mapped to Prometheus naming (dots → underscores):gen_ai_token_type—input/outputgen_ai_request_model— e.g.gpt-4ogen_ai_provider_name— e.g.openai,anthropicgen_ai_operation_name—chat(the chat-completion operation that produces these tokens)pkg/a2a/server/server.go— serves the metrics at/metricson the agent's A2A mux (excluded from request tracing).pkg/a2a/executor.go— records token counts from the A2A event loop whereUsageMetadatais surfaced:input = PromptTokenCount,output = CandidatesTokenCount + ThoughtsTokenCount.cmd/main.go/pkg/config— resolve the request model + provider labels from the agent config.Semconv alignment
Addresses the naming points raised on #1412: uses
gen_ai.provider.name(notgen_ai.system), includesgen_ai.request.model, and setsgen_ai.operation.name(chat) rather than omitting it. Naming targets semconv 1.40.0.Testing
pkg/telemetry/metrics_test.go— asserts the histogram records one series per token type, skips zero counts, and that the/metricshandler serves the series with the expected labels (viaprometheus/client_golang/testutil+httptest).go build ./adk/...,go test -race -skip 'TestE2E.*' ./adk/..., andgolangci-lint runall pass.Notes
prometheus/client_golangis already a dependency; no new direct requirements (only a transitivegodebugfrom the test util).Refs: #2148