Skip to content

[FEATURE] Close three observability gaps: GenAI token metrics, controller reconcile Events, and a controller log→OTLP bridge (all gated default-OFF) #2148

Closed as not planned

Description

@braghettos

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:

  1. GenAI token-usage metrics in the ADK (go/adk) — the first MeterProvider in kagent, emitting the OpenTelemetry GenAI-semconv instrument gen_ai.client.token.usage.
  2. Kubernetes Events on reconcile for the Agent / ModelConfig / MCPServerTool / RemoteMCPServer controllers (go/core), mirroring the pattern the harness/substrate controller already uses.
  3. Controller log → OTLP bridge (go/core) — an otelzap tee 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 MeterProvider anywhere in the repo (go/adk/pkg/telemetry/ contains only attributes.go + tracing.go). Every model adapter already computes token usage — go/adk/pkg/models/{openai,anthropic,bedrock,ollama,sapaicore}_adk.go all populate genai.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 /metrics endpoint.
  • Reconcile outcomes are invisible via kubectl. The Agent, ModelConfig, MCPServerTool, and RemoteMCPServer controllers emit no k8s Events (none reference EventRecorder/.Event(). Users debugging a bad Agent/ModelConfig get nothing from kubectl describe, even though the agentharness-substrate-controller already demonstrates the Event pattern in this codebase.
  • Controller logs can't be shipped over OTLP. go/core/internal/telemetry/ has only tracing.go. The ADK side already honors OTEL_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: a MeterProvider + OTLP metric exporter that reuses the existing resolveEndpoint(...) / resolveOTLPProtocol(...) helpers already in tracing.go, so protocol/endpoint resolution is identical to traces.
  • Emit gen_ai.client.token.usage — Int64 histogram, unit {token} — with attributes gen_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 standard OTEL_EXPORTER_OTLP[_METRICS]_ENDPOINT/_PROTOCOL), matching the existing OTEL_TRACING_ENABLED / OTEL_LOGGING_ENABLED gates. Unit test included.

2. Kubernetes Events on reconcile (go/core)

  • Add an EventRecorder to the Agent / ModelConfig / MCPServerTool / RemoteMCPServer controllers: Normal Accepted / ToolsDiscovered, Warning ReconcileFailed / ValidationFailed — mirroring the existing agentharness-substrate-controller pattern. 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: an otelzap tee installed on the controller zap core via RawZapOpts(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 /metrics endpoint (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 describe timeline 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

🙌 Contribution

  • I am willing to submit a PR for this feature (as up to 3 independent PRs, or one, per maintainer preference).

Activity

  1. braghettos commented on Jul 10, 2026

    @braghettos
    Author

    Hi @krisztianfekete — thanks again for the thorough reviews 🙏 Quick status on the three PRs under this issue, and a question on next steps.

    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() error refactor on #2151 so telemetry flushes on fatal-startup paths.

    Thanks!

  2. 9 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleThis issue or PR has become stale

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions