Observability
vanzik v2.0updated view on github
Everything Vanzik exposes for monitoring: the /metrics endpoint, /api/stats, response-header timings, and example Prometheus / alerting rules.
Source of truth: pkg/metrics/metrics.go.
/metrics — Prometheus text format#
- Unauthenticated, scrape-friendly.
- Always emits
HELPandTYPElines for every metric, including those with no observations yet. - Zero external deps — lightweight hand-rolled exposition.
curl -s http://127.0.0.1:8080/metrics
Counters#
| Name | Labels | What it counts |
|---|---|---|
vanzik_checks_total |
— | All /v1/check requests processed (sum of all decisions including rate-limit denies). |
vanzik_allowed_total |
— | Decisions of ALLOW. |
vanzik_denied_total |
— | Decisions of DENY, including rate-limit denies. |
vanzik_approval_required_total |
— | Decisions of REQUIRE_APPROVAL. |
vanzik_rate_limited_total |
— | Subset of _denied_total where the denial came from the rate limiter. |
vanzik_request_rejected_total |
reason |
Requests rejected before policy eval. Current reasons: body_too_large. |
vanzik_notify_events_dropped_total |
notifier, reason |
Notification events never delivered. Notifier ∈ {webhook,slack,console,log,unknown}; reason currently queue_full. |
vanzik_approvals_evicted_total |
reason |
Approval queue evictions. Reason ∈ {lru_resolved, queue_full}. |
vanzik_ratelimit_bucket_evictions_total |
scope |
Rate-limit buckets evicted under capacity pressure (MaxBuckets=10000). |
vanzik_sse_events_dropped_total |
reason |
SSE broadcasts dropped before reaching a subscriber. Reason currently slow_consumer. |
vanzik_audit_replay_entries_total |
— | Audit entries re-read at startup to seed counters. |
vanzik_audit_rotations_total |
— | Live audit file rotations triggered by size threshold. |
vanzik_audit_buffered_dropped_to_overflow_total |
— | Audit entries spilled to the buffered logger's overflow file (durable, not lost). |
vanzik_audit_buffered_drained_from_overflow_total |
— | Spilled audit entries re-enqueued by the recovery loop. |
vanzik_audit_corrupt_lines_total |
— | Audit lines that failed JSON parse during Query() and were skipped. |
vanzik_audit_seal_tail_mismatch_total |
— | Startups where the sealed audit log ended before the chain head the replay checkpoint recorded: entries may have been removed. Any increase deserves a look (vanzik audit verify). |
vanzik_notify_spooled_to_disk_total |
— | Notification events spilled to the --notify-spool file on queue overflow (retried, not dropped). |
vanzik_notify_despooled_total |
— | Spooled notification events re-enqueued for dispatch. |
vanzik_deprecations_used_total |
feature |
Times a deprecated feature was exercised. Keys match docs/DEPRECATIONS.md. In 2.x neither audit.migration.v040_to_v041 nor audit.backup.v040 calls deprecation.Warn, so vanzik_deprecations_used_total stays at 0 for them whether or not they run. |
Gauges#
| Name | Labels | What it reports |
|---|---|---|
vanzik_pending_approvals |
— | Current approval-queue depth. |
vanzik_ratelimit_buckets |
— | Currently tracked rate-limit token buckets. |
vanzik_sse_subscribers |
— | Live /api/stream subscribers. |
vanzik_notify_queue_depth |
— | Length of the shared notify dispatch queue (sampled at last enqueue). |
vanzik_audit_buffered_queue_depth |
— | Entries waiting in the buffered audit queue. |
vanzik_audit_replay_duration_seconds |
— | Wall-clock duration of the most recent startup audit replay. |
vanzik_audit_seal_seq |
— | seq of the newest published audit anchor: the sealed chain's head at the last anchor (every --audit-anchor-interval, at rotation, at startup and at shutdown). 0 when --audit-seal is off. It should only go up; see OPERATIONS § Sealing and anchoring. |
vanzik_audit_migration_status |
from, to, status |
1 = current outcome of an audit-schema migration; 0 for non-current statuses of the same (from, to). Status ∈ {ran,skipped,failed}. |
Histograms#
| Name | Unit | What it measures |
|---|---|---|
vanzik_request_duration_ms |
ms | End-to-end latency of /v1/check. |
vanzik_policy_eval_duration_ms |
ms | Time inside Engine.Check only. |
vanzik_audit_write_duration_ms |
ms | Time inside Logger.Log only. |
vanzik_notify_dispatch_duration_seconds |
seconds (labeled by notifier) |
Time inside each Notifier.Notify. |
Duration bucket bounds (ms, shared across the three _ms histograms):
0.25, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, +Inf
Notifier histogram buckets (seconds):
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, +Inf
Bucket boundaries are treated as a stable contract. Re-bucketing invalidates historical Prometheus data.
MCP Gateway / LLM API Proxy metrics (v0.5+)#
Neither proxy binary exposes /metrics. The LLM API Proxy serves only its API routes plus GET /healthz; the MCP Gateway is a stdio bridge with no HTTP surface at all. Per-request gating metrics for both flow through the central server's /metrics, distinguished in the audit stream by Entry.Transport (llm_api_proxy / mcp_gateway). See PROXY_ARCHITECTURE.md §8.
The LLM proxy additionally records these series in its own process (pkg/metrics), which serves no /metrics. Because the registry is shared code, the central server's /metrics lists all six too, but they never move there: the two unlabelled ones read 0 and the four provider counters have no samples, because the counting happens in the proxy process, not the server. Don't alert on them — such an alert never fires. They matter only to code embedding the proxy in-process (they are listed here so the names stay reserved):
| Name | Type | Labels | Meaning |
|---|---|---|---|
vanzik_llmproxy_streams_active |
gauge | — | Open streaming responses (bounded by --max-concurrent-streams). |
vanzik_llmproxy_streams_rejected_total |
counter | — | Streaming requests refused with 503 at the concurrent-stream cap. |
vanzik_llmproxy_buffer_overflow_total |
counter | provider |
Streaming tool-call buffer exceeded --max-buffer-bytes; converted to a synthetic refusal. |
vanzik_llmproxy_non_streaming_overflow_total |
counter | provider |
Same, for non-streaming responses. |
vanzik_llmproxy_protocol_violation_total |
counter | provider |
Streams refused fail-closed because the upstream's tool-call structure could not be bound to a gateable call: interleaved or conflicting tool_use blocks, tool input arriving with no block open, or tool calls spread across several OpenAI choices. The client-visible rule string names which — see docs/LLM_API_PROXY.md §5.4. |
vanzik_llmproxy_undecodable_tool_call_total |
counter | provider |
Non-streaming responses refused fail-closed because the body failed strict decode yet still carried a tool call a lenient client SDK would have executed ungated. Non-zero usually means an OpenAI-compatible shim is emitting a wrong-typed field. |
The MCP gateway emits no metrics of its own; its decisions surface in the central server's audit stream and counters.
Approval-id replay defense (central server): vanzik_approval_replay_mismatch_total — non-zero is a security alert. Its companion vanzik_approval_replay_refused_total{reason="consumed"|"expired"} counts shape-matched replays refused by the approval state machine (approvals are one-shot and time-boxed by --approval-validity); a rising consumed count means something is trying to reuse one human approval for multiple executions.
Prometheus scrape config#
scrape_configs:
- job_name: vanzik
metrics_path: /metrics
static_configs:
- targets: ['vanzik.internal:8080']
# /metrics is unauthenticated; gate at the network layer if needed.
scrape_interval: 15s
scrape_timeout: 10s
Behind a reverse proxy with path rewriting, keep /metrics un-prefixed (do not add auth headers — the endpoint is intentionally open).
Example alert rules#
groups:
- name: vanzik
rules:
# Sudden spike of denies: either an attack or a policy that's too strict.
- alert: VanzikDeniedSpike
expr: rate(vanzik_denied_total[5m]) > 5 * rate(vanzik_denied_total[1h] offset 1h)
for: 10m
labels: { severity: warning }
annotations:
summary: "DENY rate 5× its 1h baseline"
# Human approvers falling behind.
- alert: VanzikPendingApprovalsGrowing
expr: vanzik_pending_approvals > 100
for: 15m
labels: { severity: warning }
annotations:
summary: "Approval queue > 100 for 15m; triage is not keeping up"
# Approval queue saturation — approaching 503s.
- alert: VanzikApprovalQueueCritical
expr: vanzik_pending_approvals > 9000
for: 2m
labels: { severity: critical }
annotations:
summary: "Approval queue near MaxPendingApprovals=10000"
# Notifier events are being dropped — Slack/webhook probably slow.
- alert: VanzikNotifierDrops
expr: rate(vanzik_notify_events_dropped_total[5m]) > 0
for: 10m
labels: { severity: warning }
annotations:
summary: "Notifier {{ $labels.notifier }} dropping events: {{ $labels.reason }}"
# p99 policy eval > 50 ms sustained — policy is heavy or a reload is contending.
- alert: VanzikSlowPolicyEval
expr: histogram_quantile(0.99, sum by (le) (rate(vanzik_policy_eval_duration_ms_bucket[5m]))) > 50
for: 10m
labels: { severity: warning }
annotations:
summary: "p99 Engine.Check > 50 ms"
# Audit replay on startup is becoming the slow path.
- alert: VanzikSlowAuditReplay
expr: vanzik_audit_replay_duration_seconds > 30
labels: { severity: warning }
annotations:
summary: "Startup audit replay > 30s; consider rotating the audit log"
# Agents bypassing per-instance rate limits via multi-replica fanout.
- alert: VanzikRateLimitBucketEviction
expr: rate(vanzik_ratelimit_bucket_evictions_total[5m]) > 1
for: 15m
labels: { severity: info }
annotations:
summary: "Rate-limit buckets evicting in scope {{ $labels.scope }}"
/api/stats vs /metrics#
| Endpoint | Auth | Use when |
|---|---|---|
/metrics |
open | Prometheus scraping, alerting, long-term trending. |
/api/stats |
Bearer or session | Dashboard fetches a single JSON blob once per page load and on SSE events. Cheap, O(1), atomic snapshot. |
Example /api/stats response:
{
"total": 12034,
"allowed": 10998,
"denied": 842,
"approvals": 194
}
For the pending-approval count, use the length of /api/pending.
Do not poll /api/stats in a tight loop; use /api/stream (SSE) for real-time updates and fall back to one /api/stats on reconnect.
Response-header timings#
Every /v1/check response carries three timing headers (values in ms, 3-decimal precision). Useful for ad-hoc debugging without scraping /metrics.
| Header | What it measures |
|---|---|
X-Vanzik-Policy-Ms |
Time in Engine.Check. |
X-Vanzik-Audit-Ms |
Time in Logger.Log. |
X-Vanzik-Total-Ms |
End-to-end server processing, including decode, rate-limit, policy, audit, notify enqueue. |
Total - Policy - Audit approximates the framework overhead (JSON decode/encode, rate limit lookup, notify dispatch enqueue).
SSE event types on /api/stream#
The dashboard and any external listener can subscribe. Events are JSON objects sent as:
data: {"type":"check","timestamp":"…","request":{…},"result":{…}}\n\n
| Type | Emitted when |
|---|---|
check |
Every response from /v1/check (any decision, including rate-limit denies) and every POST /v1/audit record. REQUIRE_APPROVAL and DENY decisions arrive as check events — read result.decision. |
resolved |
The first approve / deny of a pending approval. result.decision is ALLOW or DENY, result.reason is manually allow / manually deny. Repeating the same resolution does not send it again. |
Events are unnamed SSE messages (no event: field), so an EventSource receives them through onmessage. Each carries transport (sdk, mcp_gateway, llm_api_proxy, or a coding-agent hook's claude_code, cursor, gemini_cli, codex_cli) and, for tenants other than local, tenant, alongside request and result. approval_required and denied are notifier event types (webhook / Slack / console), not stream events — see APPROVAL_WORKFLOW.md.
Slow consumers drop events; see vanzik_sse_events_dropped_total{reason="slow_consumer"}.
Debug-quick-loop checklist#
curl -s localhost:8080/health— is the server up?curl -s localhost:8080/metrics | head -80— counters growing? pending gauge reasonable?curl -D- -X POST localhost:8080/v1/check -d '{"scope":"shell","command":"ls"}'— check theX-Vanzik-*-Msheaders.curl -s -H "Authorization: Bearer $KEY" localhost:8080/v1/audit?limit=20— see the last 20 decisions.vanzik status— health + pending approvals CLI-style.
Related docs#
docs/OPERATIONS.md— notifier queue sizing, audit rotation, capacity limits.docs/DEPLOYMENT.md— exposing/metricssafely behind a reverse proxy.docs/TROUBLESHOOTING.md— keyed by symptom; most entries reference a metric to confirm.docs/DEPRECATIONS.md— feature keys forvanzik_deprecations_used_total.