agentgateway full telemetry → ClickStack (distinct MCP/LLM usage)
agentgateway full telemetry → ClickStack
Section titled “agentgateway full telemetry → ClickStack”Bead: hl-rpm9.11 (design) — child of epic hl-rpm9 (openrouter-gw lane). Closes acceptance for: hl-rpm9.10 (confirm Detect-lane LLM telemetry lands in observability).
Problem
Section titled “Problem”agentgateway is the cluster’s dual-purpose AI gateway — llm-gw.fzymgc.house
(LLM proxy for engram, octopus, karakeep, mealie, claude-code),
mcp-gw.fzymgc.house (MCP proxy for firewalla, clickhouse, engram), and the new
openrouter-gw.fzymgc.house Detect lane. Despite carrying all in-cluster AI
traffic, it emits zero telemetry to ClickStack.
Verified 2026-06-20 against live ClickHouse:
otel_traces(last 7d) ServiceNames:traefik,hdx-oss-api,engram,fovea— noagentgateway.otel_metrics_sum(last 1d) ServiceNames: 34 services incl. apiserver, coredns, vault, dolt — noagentgateway.
This is not a broken transport (the classic “pull-metrics work, push-signals dark” fingerprint). Telemetry was simply never enabled:
argocd/app-configs/agentgateway/parameters.yaml(AgentgatewayParameters) configures onlylogging,TZ, and the:15000admin listener — notracing/OTLP block.argocd/app-configs/agentgateway-controller/values-controller.yamlsetsmonitoring.enabled: false— no ServiceMonitor/PodMonitor.
Without telemetry, the cluster cannot answer “how much is each client spending on which model” (LLM) or “which MCP servers/tools are being called” (MCP) — and the Detect-over-Passthrough choice (ADR hl-l875), whose entire justification is preserved OTel telemetry, is unvalidated.
- Land agentgateway metrics in ClickStack, separating LLM usage
(
gen_ai_*) from MCP usage (mcp_requests). - Land agentgateway traces in ClickStack (per-request spans: model, latency, status).
- Provide a HyperDX dashboard reporting LLM and MCP usage distinctly.
- Satisfy hl-rpm9.10’s acceptance: provider/model/token telemetry from the Detect lane is observable.
Non-goals (YAGNI)
Section titled “Non-goals (YAGNI)”- No alerts in this iteration (dashboard-only; alerting is a follow-up using
the
clickstack-alertsbootstrap pattern if desired). - No trace-sampling cap — run full fidelity (homelab volume is low).
- No kube-prometheus-stack ServiceMonitor for agentgateway — see Decision 1.
- No NetworkPolicy changes — see Decision 3.
Grounding (Rule 7)
Section titled “Grounding (Rule 7)”- deepwiki
agentgateway/agentgateway— telemetry model: traces via OTLP push (rawConfig.config.tracing.otlpEndpoint/otlpProtocol, orOTEL_EXPORTER_OTLP_*env); metrics via Prometheus/metrics(pull) only. Metric families: LLMgen_ai_token_usage/gen_ai_request_duration/gen_ai_time_to_first_token; MCPmcp_requests. - deepwiki — agentgateway’s Rust data plane has no OTLP metrics exporter,
no
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, no metricsrawConfig. Metrics areprometheus_client-crate, pull-only. (The OTel Go metric modules ingo.sumbelong to the Go controller, not the Rust data plane.) This forces the scrape lane for metrics. - deepwiki —
AgentgatewayParameters.spec.enviscorev1.EnvVar, so it supportsvalueFrom.secretKeyRef. The data plane readsOTLP_HEADERS(comma-separatedkey=valueor JSON) viaparse_otlp_headers.rawConfigheader values do not do env-substitution — auth must rideOTLP_HEADERS. - Live ClickHouse — confirmed agentgateway absent from traces + metrics.
- Live k8s —
agentgateway-adminClusterIP:15000exists (metrics target). - Repo —
monitoring-otel-scraper/scrape-collector-values.yamluses static scrape targets (20 jobs) → OTLP push tocs-otel-collector:4317. Theotel-ingest-tokenExternalSecret reads Vaultfzymgc-house/cluster/clickstack#otel_ingest_api_key. - Repo —
clickstackCiliumNetworkPolicynow opens:4317/:4318tofromEntities: [all](hl-pwwf); auth is the ingest token (raw, noBearer). Per-namespace whitelists are documentation-only. - Repo — HyperDX dashboards are MongoDB-stored, session-cookie API only; no
ArgoCD/Vault headless path. Source of truth =
tools/hyperdx/dashboards/*.jsonapplied via the agent-browser/devtools session workflow. Metrics source id6a0381ba6d27c492599151a2; histogram tiles useaggFn: quantile+level+alias.
Architecture
Section titled “Architecture” ┌──────────── agentgateway pod (ns: agentgateway) ────────────┐ │ data plane (Rust) │ LLM + MCP ───▶│ • Prometheus /metrics on admin :15000 (gen_ai_*, mcp_*) │ clients │ • OTLP traces (push) ───────────────────────┐ │ └────────────────────────────────────────────────┼────────────┘ │ metrics: PULL traces: PUSH │ raw-token auth │ ▼ ▼ cs-otel-collector.clickstack:4317 monitoring-otel-scraper (otelcol-k8s) │ • static job "agentgateway" → :15000/metrics ▼ • PUSH OTLP ─────────────────────────────────────▶ ClickHouse otel_metrics_* / otel_traces │ HyperDX dashboard ◀───────┘ (LLM vs MCP tiles)Two intake paths because agentgateway offers two surfaces and only one (traces) supports OTLP push. Both converge on ClickStack as OTLP — the Prometheus hop is the only metrics intake agentgateway exposes, transcoded to OTLP by the scrape collector (identical to how vault/coredns/apiserver reach ClickHouse).
Decisions
Section titled “Decisions”-
Metrics via the static-scrape collector, not the chart’s
monitoring.enabled. Enabling the chart ServiceMonitor would surface metrics only in kube-prometheus-stack/Grafana; kps does not forward app metrics to ClickStack, so the data would never reach ClickHouse. Themonitoring-otel-scraperis the cluster’s only Prometheus→OTLP→ClickStack bridge and already lands 20 targets this way. -
Metrics are scraped, traces are pushed — asymmetry forced by upstream. agentgateway has no OTLP metrics exporter (grounded). If/when it gains one, collapse both lanes into the single telemetry config block and retire the scrape job (tracked as a follow-up bead).
-
No NetworkPolicy change for traces.
clickstack-defaultalready opens:4317to all senders; the ingest token gates access. (Metrics: verify the agentgateway namespace has no ingress CNP blockingotel-scraper → :15000; if one exists, add an allow rule — expected to be a no-op.) -
Auth header via
OTLP_HEADERSenv, raw token (noBearer).rawConfigheaders can’t reference env vars, andcs-otel-collectorrejects theBearerscheme. The token is injected from a new ExternalSecret.
Components
Section titled “Components”A. Metrics lane — monitoring-otel-scraper/scrape-collector-values.yaml
Section titled “A. Metrics lane — monitoring-otel-scraper/scrape-collector-values.yaml”Append one scrape job under receivers.prometheus/scrape.config.scrape_configs:
# agentgateway — LLM (gen_ai_*) + MCP (mcp_requests) usage metrics.# Pull-only emitter (no OTLP metrics exporter upstream); the collector# transcodes to OTLP → ClickStack. Lands as ServiceName='agentgateway'.- job_name: agentgateway scrape_interval: 30s static_configs: - targets: ["agentgateway-admin.agentgateway.svc.cluster.local:15000"]Lands under ServiceName='agentgateway':
- LLM:
gen_ai_token_usage{gen_ai_system, gen_ai_request_model, gen_ai_response_model, gen_ai_token_type}(sum →otel_metrics_sum),gen_ai_request_duration(histogram →otel_metrics_histogram),gen_ai_time_to_first_token. - MCP:
mcp_requests{method, server, resource, route}(sum).
B. Traces lane — agentgateway/parameters.yaml
Section titled “B. Traces lane — agentgateway/parameters.yaml”Add OTLP tracing config + token env to the existing AgentgatewayParameters:
spec: env: - { name: TZ, value: "America/Los_Angeles" } - { name: ADMIN_ADDR, value: "0.0.0.0:15000" } # OTLP ingest token for cs-otel-collector (raw token, NOT "Bearer"). - name: OTLP_AUTH_TOKEN valueFrom: secretKeyRef: name: agentgateway-otel-ingest key: INGEST_TOKEN - { name: OTLP_HEADERS, value: "authorization=$(OTLP_AUTH_TOKEN)" } rawConfig: # NOTE: `$(OTLP_AUTH_TOKEN)` is Kubernetes downward env-var interpolation # (the referenced var must be declared earlier in the same list — it is). # This works but is mildly discouraged upstream. Alternative if preferred: # have the ExternalSecret template the full header string # (`authorization=<token>`) into one key and point `OTLP_HEADERS` at it via # `valueFrom.secretKeyRef` directly, dropping the two-step interpolation. config: tracing: otlpEndpoint: cs-otel-collector.clickstack.svc.cluster.local:4317 otlpProtocol: grpc randomSampling: true # full fidelity; low homelab volumeC. ExternalSecret — agentgateway/otel-ingest-token-externalsecret.yaml (new)
Section titled “C. ExternalSecret — agentgateway/otel-ingest-token-externalsecret.yaml (new)”Mirror the otel-scraper’s secret into the agentgateway namespace:
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: agentgateway-otel-ingest namespace: agentgatewayspec: refreshInterval: 1h secretStoreRef: { name: vault, kind: ClusterSecretStore } target: { name: agentgateway-otel-ingest, creationPolicy: Owner } data: - secretKey: INGEST_TOKEN remoteRef: key: fzymgc-house/cluster/clickstack property: otel_ingest_api_keyAdd to agentgateway/kustomization.yaml. No new Vault path or policy: the
cluster’s ClusterSecretStore (vault) uses a single ESO Vault role with a
broad secret/data/* policy that already covers all fzymgc-house/cluster/*
paths — there is no per-namespace role to extend. Verification reduces to
confirming the ExternalSecret reaches SecretSynced=True.
D. Dashboard — tools/hyperdx/dashboards/agentgateway-llm-mcp-usage.json (new)
Section titled “D. Dashboard — tools/hyperdx/dashboards/agentgateway-llm-mcp-usage.json (new)”Version-controlled definition {name, tiles}, applied via the documented
agent-browser/devtools import workflow against the logged-in
hyperdx.fzymgc.house session (Metrics source 6a0381ba6d27c492599151a2).
Tiles:
- LLM — tokens by provider+model (stacked sum of
gen_ai_token_usagegrouped bygen_ai_system/gen_ai_request_model/gen_ai_token_type); request rate by model;gen_ai_request_durationp95 (quantile,level: 0.95); TTFT p95. - MCP — calls by server+method (sum of
mcp_requestsgrouped byserver/method); tool-call rate by server.
Data flow
Section titled “Data flow”- A client calls
llm-gw/mcp-gw/openrouter-gw. The data plane records Prometheus metrics (gen_ai_*ormcp_requests) and, if tracing is enabled, emits an OTLP span. - Metrics:
monitoring-otel-scraperscrapes:15000/metricsevery 30s, enriches with k8s attributes, batches, and pushes OTLP/gRPC tocs-otel-collector:4317→otel_metrics_{sum,histogram}. - Traces: the data plane pushes spans directly to
cs-otel-collector:4317with the raw ingest token →otel_traces. - HyperDX reads both from ClickHouse; the dashboard renders LLM/MCP tiles.
Error handling & risks
Section titled “Error handling & risks”- Wrong
/metricspath or port. Mitigation: verify:15000/metricsreturns Prometheus text at implementation (live curl/port-forward) before merge. rawConfig.tracingclobbers generated config. Mitigation: agentgateway server dry-run + confirm the merged config still renders all listeners/routes.Bearervs raw token. Encoded as raw inOTLP_HEADERS; aBearerprefix would 401 silently and drop all spans.- ExternalSecret fails to sync. Low risk — the shared
ClusterSecretStorerole already reads allfzymgc-house/cluster/*paths. Mitigation: confirm theExternalSecretreachesSecretSynced=Trueafter sync. - Admin-port exposure.
:15000is ClusterIP-only and already used for the UI; scraping it adds no external surface. - Dashboard drift. HyperDX dashboards aren’t GitOps; the JSON file is the restore source of truth and must be re-exported after UI edits.
Verification (acceptance)
Section titled “Verification (acceptance)”After ArgoCD sync:
-- metrics: both families present under agentgatewaySELECT MetricName, count() FROM default.otel_metrics_sumWHERE ServiceName = 'agentgateway' AND TimeUnix > now() - INTERVAL 1 HOUR AND (MetricName LIKE 'gen_ai%' OR MetricName LIKE 'mcp%')GROUP BY MetricName;
-- metrics: histogram families (gen_ai_request_duration, _time_to_first_token)SELECT MetricName, count() FROM default.otel_metrics_histogramWHERE ServiceName = 'agentgateway' AND TimeUnix > now() - INTERVAL 1 HOUR AND MetricName LIKE 'gen_ai%'GROUP BY MetricName;
-- traces: spans landingSELECT count(), max(Timestamp) FROM default.otel_tracesWHERE ServiceName = 'agentgateway' AND Timestamp > now() - INTERVAL 1 HOUR;Acceptance = non-zero gen_ai_token_usage and mcp_requests rows + spans in
otel_traces. This is exactly the confirmation hl-rpm9.10 requires; closing
this validates ADR hl-l875.
Follow-ups
Section titled “Follow-ups”- Collapse metrics+traces into one telemetry block + retire the scrape job if agentgateway adds an OTLP metrics exporter upstream.
- Optional Pushover alerts (error-rate spike, token-spend threshold) via
clickstack-alerts. - Document the agentgateway telemetry surfaces in
docs/operations/clickstack.mdanddocs/reference/services.md.