Observability
Both halves land in the platform’s Victoria stack and Grafana; nothing here runs a second telemetry pipeline. Serving is watched per model, agents per run.
flowchart LR
subgraph serving["Serving"]
V["vLLM pods<br/>vllm:* :8000"]
AIG["ai-gateway extproc<br/>gen_ai_* :1064"]
end
subgraph agents["Agents"]
RUN["Agent run<br/>OTLP spans, step log"]
AGW["agentgateway agent-router<br/>access log, gen_ai_* :15020"]
KSM["kube-state-metrics<br/>AgentRun state"]
end
VM[("VictoriaMetrics")]
VL[("VictoriaLogs")]
VT[("VictoriaTraces")]
COL["agent-traces-collector<br/>allowlisted metadata"]
KEDA["KEDA<br/>three triggers"]
G["Grafana"]
V --> VM
AIG --> VM
VM --> KEDA
RUN --> COL --> VT
RUN -->|step log| VL
AGW --> VL
AGW --> VM
KSM --> VM
VM --> G
VL --> G
VT --> G
Serving
| Signal | Source | Read it in |
|---|---|---|
| Engine metrics | Each vLLM pod’s /metrics on port 8000, vllm:* labelled by model_name, scraped by the VMServiceScrape each claim renders (observability.metrics.interval, 30s on all four) | LLM Platform — Self-hosted vLLM Fleet: replicas, queue depth, throughput, TTFT and decode latency, GPU KV-cache usage, prefix-cache hit rate, vLLM error logs |
| Gateway metrics | The AI Gateway extproc’s /metrics on port 1064, gen_ai_* (OpenTelemetry semantic conventions) | LLM Gateway — Envoy AI Gateway GenAI Telemetry: token spend per model, base-vs-canary attribution, TTFT and time per output token |
| Autoscaling | KEDA’s three triggers query the same vllm:* series every 15 s | Autoscaling & GPUs |
Both dashboards live in the Grafana folder llm (apps/base/ai/llm/grafana-folder.yaml), which
only exists once the opt-in umbrella is resumed.
Alerts are lagging signals, deliberately not scale triggers:
| File | Alerts |
|---|---|
apps/base/ai/llm/vmrule-llm-slo.yaml | Serving: sustained queue depth, time to first token breached, a replica scaled to zero. Gateway: gen_ai telemetry missing, scrape down, high error rate, canary error-rate and latency regressions |
apps/base/ai/llm/vmrule-ai-fleet.yaml | Fleet abort rate, Semantic Router down, Promptfoo regression and staleness |
Agents
One trace, one step log and one dashboard page per run, so a human can open any run and see where it stands, what it did, what it cost and where its time went.
Metric names. Maku’s metrics are
maku_factory_*andmaku_rooms_*, and the dashboards and alerts read those names.
| Component | Software | What it does |
|---|---|---|
| Logs | VictoriaLogs | Every run’s step log and every gateway call, attributed to the run |
| Metrics | VictoriaMetrics | Tokens, cost, latency and errors per run, and each AgentRun’s state through kube-state-metrics |
| Traces | VictoriaTraces, behind an OpenTelemetry Collector that keeps only allowlisted metadata | One trace per run: steps, model calls and tool calls. Metadata only: no prompts or outputs |
| Dashboards | Grafana | In observability/base/agent-platform/: agent-run, one page per run; agent-fleet; agents-overview; agent-room; agent-gateway; agent-identity, spend per charged principal; and agent-factory, the factory’s own page: PR outcomes, merge decisions, a Human path row (hand-off to merge, waiting on a human, Classes paused) and Cost by role over 7 days |
The full transcript (prompts and outputs) stays in the room, behind room access. Alerting on runs belongs to the factory’s rules, per the observability design.
What you can inspect
| Question | Where |
|---|---|
| Where is my run? | kubectl get agentrun -n agents, or the agent-run dashboard’s status panel: phase, reason, PR, tokens against budget |
| What did it do? | The step log, in kubectl logs or on the dashboard, with every gateway and MCP call of that run |
| What did it cost, how fast was it? | The dashboard: tokens in and out, cost, model latency, error rate. agent-fleet shows every run |
| Where did the time go? | A trace per run: steps, model calls and tool calls, with timings. Metadata only |
A run’s page is /d/agent-run/agent-run?var-run=<id>; its This run’s window link sets the time range
to the run. A resumed run counts each step once and has one trace per attempt. The room holds a run’s
transcript to its end: before the harness stops, even on a reclaimed node, it asks maku-bridge to read
its log to the last event.
The merge gate’s signals
The agent-factory dashboard’s Human path row reads the factory’s merge series; each series, its
labels and what each decision means are on
Run auto-merge safely.
Cost by role reads maku_factory_run_cost_usd{tier,role}.
Alerts in observability/base/agent-platform/vmrule-agent-factory.yaml:
| Alert | Fires when |
|---|---|
AgentFactoryAutoMergeReverted | A merged agent PR was reverted. Unless it carries factory/drill, the breaker pauses its class |
AgentFactoryMergeRegression | The post-merge monitor found a regression after a merge; the class is demoted at its next decision |
AgentFactoryClassPausedLong | A live class of cloud-native-ref has been paused for 7 days |
Spend per identity
The agent-identity dashboard (Spend per identity) shows one charged principal at a time, never
All: a person or a sponsored service account. It attributes cost to the work a principal caused. It
does not score the person: no panel groups, sorts or ranks by principal.
| Section | Reads |
|---|---|
| Spend | Today against the principal’s daily cap and against the fleet’s, by kind, per day |
| Tokens | By type, and the cache-read share |
| Work | Runs, rooms and tasks, each linked |
| Outcomes | Pull requests opened, merged and reverted, and cost per accepted pull request |
| Limits | Refusals and revocations |
| Waits | Tasks waiting for a human now, and how long its tasks waited |
| Sponsored | The sponsor, and ceilings against spend |
Every figure is in UTC, the ledger’s day. The sources are the factory’s per-payer series
(maku_factory_charged_usd_total, the maku_factory_budget_*_usd gauges) and
maku_factory_run_charge_info{run, principal, task, room}, which joins the gateway’s per-run
tokens to the payer. The panels are held by test-agent-identity-dashboard and
test-agent-identity-queries.
The agent gateway’s signals
agentgateway (gateways)
emits under its own names. The scrape (infrastructure/base/agent-gateway/vmpodscrape.yaml) renames
only the families the run meter, the budget alerts and the run page read, so they keep the gen_ai_*
contract:
| agentgateway emits | Stored as |
|---|---|
agentgateway_gen_ai_client_token_usage_{bucket,sum,count} | gen_ai_client_token_usage_{bucket,sum,count} |
gen_ai_token_type="input_cache_read" | gen_ai_token_type="cached_input" |
gen_ai_token_type="input_cache_write" | gen_ai_token_type="cache_creation_input" |
agentgateway_gen_ai_server_request_duration_* | gen_ai_server_request_duration_seconds_* |
gen_ai_system | gen_ai_provider_name |
Every other series keeps its agentgateway_ name, and all carry namespace="agent-gateway", which
keeps them apart from ai-gateway’s. Without the cache relabel the meter would price every cache read
at the input rate.
Access logs are JSON with no msg, so LogsQL selects by pod labels and log.* fields:
kubernetes.pod_namespace:"agent-gateway" AND kubernetes.pod_labels.gateway.networking.k8s.io/gateway-name:"agent-router"
| unpack_json | log.x_ar_agent:"system:serviceaccount:agents:<run>"Useful fields: log.http.status, log.http.path, log.reason, log.route, log.x_ar_agent,
log.mcp.method.name, log.gen_ai.tool.name.
Alerts in observability/base/agent-platform/vmrule.yaml, read on the Agent gateway dashboard
(uid: agent-gateway):
| Alert | Fires when |
|---|---|
AgentRouterMetricContractBroken | Model calls succeed but no gen_ai_client_token_usage series follows, or a token type outside the four the meter reads appears: an upgrade renamed something the relabel maps |
AgentGateway5xxRatioHigh | Over 5% of requests answer 5xx for 10 minutes |
AgentGatewayModelErrorRatioHigh | Over 10% of model calls fail for 10 minutes |
AgentGatewayUnauthorizedBurst | More than 20 refusals with reason JwtAuth or Authorization in 5 minutes |
AgentRateLimitDown | agent-ratelimit is down: budgets B1–B2 count nothing while the router fails open |
AgentBudgetShadowOverrun (info) | In shadow, a budget admitted calls over its limit: what enforcing would refuse |
AgentBudgetRefused | Enforcing, a budget refused a run’s calls with 429 |
AgentCacheReadZero (critical) | An internal run read nothing from the prompt cache after 100k input tokens: a dropped cache marker, every step at full price |
AgentCacheReadShareLow | An internal run read under 60% of its input from the cache over 15 minutes, with 200k input in the window |
AgentCacheMarkerMissing | A run’s harness logged prompt cache NOT applied (a LogsQL rule): the SDK changed under the harness’s patch |