Repository navigation
Description
Affected Service(s): Multiple services / System-wide (App Service + Controller Service)
📋 Prerequisites
- I have searched the existing issues to avoid creating a duplicate
- By submitting this issue, you agree to follow the Code of Conduct
📝 Feature Summary
Add three small, independent, default-OFF observability improvements that build directly on the OTel pipeline landed in #1433 / #1292:
- GenAI token-usage metrics in the ADK (
go/adk) — the firstMeterProviderin kagent, emitting the OpenTelemetry GenAI-semconv instrumentgen_ai.client.token.usage. - Kubernetes Events on reconcile for the
Agent/ModelConfig/MCPServerTool/RemoteMCPServercontrollers (go/core), mirroring the pattern the harness/substrate controller already uses. - Controller log → OTLP bridge (
go/core) — anotelzaptee on the controller zap core so logs can be exported over OTLP additively, without changing stdout.
Each is additive and byte-identical to current behavior when its gate is unset. Happy to split this into three separate PRs (they're fully independent) — filing one issue first to align on approach per CONTRIBUTING.
❓ Problem Statement / Motivation
kagent already has solid tracing (controller distributed tracing + A2A propagation, #1433) and standardized OTel env-var naming (#1292), but three observability primitives are still missing on main:
- No metrics pipeline at all. There is no
MeterProvideranywhere in the repo (go/adk/pkg/telemetry/contains onlyattributes.go+tracing.go). Every model adapter already computes token usage —go/adk/pkg/models/{openai,anthropic,bedrock,ollama,sapaicore}_adk.goall populategenai.GenerateContentResponseUsageMetadata(PromptTokenCount/CandidatesTokenCount/ThoughtsTokenCount) — but that data is never recorded to a metric, so operators can't graph or alert on token spend / cost per model or provider without scraping traces. This is the still-unchecked "Metrics: LLM calls/token-count" item from the (now closed) observability EPIC [EPIC] Observability improvements #130, and the abandoned Prometheus attempt in Enable /metrics endpoint with prometheus metrics #1412 (closed unmerged) shows the demand is real; this proposes a standards-aligned OTLP/semconv take instead of a scraped/metricsendpoint. - Reconcile outcomes are invisible via
kubectl. TheAgent,ModelConfig,MCPServerTool, andRemoteMCPServercontrollers emit no k8s Events (none referenceEventRecorder/.Event(). Users debugging a bad Agent/ModelConfig get nothing fromkubectl describe, even though theagentharness-substrate-controlleralready demonstrates the Event pattern in this codebase. - Controller logs can't be shipped over OTLP.
go/core/internal/telemetry/has onlytracing.go. The ADK side already honorsOTEL_LOGGING_ENABLED, but the controller's zap logs remain stdout-only, so log/trace correlation isn't possible in the controller.
Who benefits: anyone running kagent in a cluster with an OTel collector / ClickHouse / Langfuse / Datadog backend who wants token-cost dashboards, kubectl-visible reconcile status, and correlated controller logs.
💡 Proposed Solution
Three independent changes, each gated and additive. All reuse existing conventions.
1. GenAI token metrics (go/adk)
- New
pkg/telemetry/metrics.go: aMeterProvider+ OTLP metric exporter that reuses the existingresolveEndpoint(...)/resolveOTLPProtocol(...)helpers already intracing.go, so protocol/endpoint resolution is identical to traces. - Emit
gen_ai.client.token.usage— Int64 histogram, unit{token}— with attributesgen_ai.token.type(input/output),gen_ai.request.model,gen_ai.provider.name, per GenAI semantic conventions. - Record from the A2A executor loop where per-event usage metadata is already surfaced (
pkg/a2a/executor.go):input = PromptTokenCount,output = CandidatesTokenCount + ThoughtsTokenCount. Model/provider labels resolved at startup (cmd/main.go). - Gate:
OTEL_METRICS_ENABLED=true(+ the standardOTEL_EXPORTER_OTLP[_METRICS]_ENDPOINT/_PROTOCOL), matching the existingOTEL_TRACING_ENABLED/OTEL_LOGGING_ENABLEDgates. Unit test included.
2. Kubernetes Events on reconcile (go/core)
- Add an
EventRecorderto theAgent/ModelConfig/MCPServerTool/RemoteMCPServercontrollers: NormalAccepted/ToolsDiscovered, WarningReconcileFailed/ValidationFailed— mirroring the existingagentharness-substrate-controllerpattern. Nil-guarded so it's a no-op if no recorder is wired. - RBAC is already covered by the leader-election role (
events: create,patch). - Gate: none needed — it's a purely additive status surface.
3. Controller log → OTLP bridge (go/core)
- New
internal/telemetry/logging.go: anotelzaptee installed on the controller zap core viaRawZapOpts(zap.WrapCore(NewTee(...))). Stdout logging is preserved; OTLP is additive. - Gate:
OTEL_LOGGING_ENABLED=true(same gate name already used ADK-side).
Acceptance bar: everything default-OFF, additive, unit-tested, make lint + make test green, DCO sign-off on every commit.
🔄 Alternatives Considered
- Prometheus
/metricsendpoint (Enable /metrics endpoint with prometheus metrics #1412). A prior attempt exposed agent LLM/token metrics via a scraped Prometheus endpoint; it was closed unmerged. OTLP histograms fit kagent's existing OTLP-first pipeline (same exporter/env-var machinery as traces), avoid a second telemetry transport, and align with GenAI semconv — so downstream backends (ClickHouse, Langfuse, Datadog, Honeycomb) get token metrics through the collector they already run. - Status conditions instead of Events (gap 2). Conditions and Events are complementary; Events give the
kubectl describetimeline that's missing today and are cheap/additive. Conditions could be a follow-up. - Structured stdout log scraping instead of an OTLP bridge (gap 3). Works, but loses trace/log correlation that the OTLP path (already used for traces via feat: instrument controller with distributed tracing and A2A trace propagation #1433) provides for free.
📚 Additional Context
- Builds on feat: instrument controller with distributed tracing and A2A trace propagation #1433 (controller tracing + A2A propagation, merged) and Standardize OpenTelemetry environment variable naming #1292 (OTel env-var standardization, merged); related to open Helm OTLP-config work in feat(helm): expose OTEL headers, service name, and resource attributes #2128 / discussion helm/kagent: declarative OTLP headers + service name + resource attributes (unblocks Langfuse Cloud / Honeycomb / Datadog / ...) #2127 (headers / service name / resource attributes) — these three changes slot alongside that config surface.
- Revisits the metrics items from the closed EPIC [EPIC] Observability improvements #130 and the abandoned Enable /metrics endpoint with prometheus metrics #1412.
🙌 Contribution
- I am willing to submit a PR for this feature (as up to 3 independent PRs, or one, per maintainer preference).
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsBacklog
Activity
feat(adk): expose gen_ai.client.token.usage Prometheus metric
feat(adk): address review — semconv provider enum, response.model, er…
feat(core): only emit MCP success Events on meaningful transitions
feat(core): address review — OTLP log export severity, shared resourc…
docs: add live gen_ai.client.token.usage output from a Vertex-backed …
docs: add live OTLP controller-log output from an otel collector debu…
docs: add gen_ai.client.token.usage /metrics screenshot (refs kagent-…
braghettos commentedon Jul 10, 2026
Hi @krisztianfekete — thanks again for the thorough reviews 🙏 Quick status on the three PRs under this issue, and a question on next steps.
gen_ai.response.model,error.type, semconv buckets, streaming double-count guard (+ test), and Prometheus scrape annotations on the Go-runtime agent objects. Added docs + a live screenshot: verified end-to-end against Vertex AIgemini-2.5-flash(/metricsshows the metric with the right labels,_count= number of LLM calls).minsevprocessor, a single shared resource, and tests for both. Added docs + a screenshot verifying it with an OTel collector +debugexporter.Is there anything else you need from my side to move these forward — or a preferred order / process (I couldn't re-request review as an external contributor)? I'm also happy to fold in the two optional follow-ups you mentioned if you'd like them in-scope now: the OTel Prometheus→OTLP push bridge on #2149, and a
run() errorrefactor on #2151 so telemetry flushes on fatal-startup paths.Thanks!
feat(core): add controller log to OTLP bridge (gated OTEL_LOGGING_ENA…
feat(core): address review — OTLP log export severity, shared resourc…
docs: add live OTLP controller-log output from an otel collector debu…
feat(adk): expose gen_ai.client.token.usage Prometheus metric
9 remaining items
feat(go): add gated OTLP metrics, reconcile events, and log bridge
feat(go): add gated OTLP metrics, reconcile events, and log bridge
feat(go): add gated OTLP metrics, reconcile events, and log bridge
feat(core): serve the controller's metrics, and its gRPC metrics with…
feat(go): add gated OTLP metrics, reconcile events, and log bridge
feat(core): serve the controller's metrics, and its gRPC metrics with…
feat(go): add reconcile events and controller log bridge
feat(go): add reconcile events and controller log bridge
feat(go): add reconcile events and controller log bridge
feat(go): add reconcile events and controller log bridge
feat(go): add reconcile events and controller log bridge
feat(go): add reconcile events and controller log bridge
feat(go): add reconcile events and controller log bridge