Skip to content

Metrics & health endpoints

The operator-facing monitoring surface: health probes, the authoritative Prometheus metric reference, the scrape config, and the Grafana dashboard. For tracing, histogram exemplars, and the audit/mesh telemetry internals, see Observability internals.

Health endpoints

Zion exposes built-in endpoints that bypass routing and upstream forwarding:

EndpointResponseAccessPurpose
GET /healthz200 okpublicLiveness probe (is the process alive?)
GET /readyz200 readypublicReadiness probe (is the process ready to serve?)
GET /metricsPrometheus text formatinternal IPs only (403 otherwise)Metrics scraping
GET /_zion/snapshot.jsonJSON (metrics + quantiles + platform)internal IPs onlyzion top / dashboards
POST /_zion/cache/purge{"purged":N,"scope":...}internal IPs only, POST-only (405 on GET)Flush the RAM cache on deploy; ?prefix=/path for scoped purge

/healthz and /readyz are handled before rate limiting and routing, so they always respond even under load. /metrics, /_zion/snapshot.json, and /_zion/cache/purge are restricted to internal source IPs (external clients get 403).

Prometheus metrics

GET /metrics returns counters in Prometheus text exposition format (text/plain; version=0.0.4).

Counter reference

This table is the authoritative list — every counter /metrics emits appears here. All are lock-free atomic u64 (a fetch_add with Relaxed ordering, ~2 ns). Counters marked (always emitted) render even when their feature is off (as a flat 0), so one dashboard works across builds; the single genuinely feature-gated series (absent, not zero, without the feature) is called out.

Traffic & responses

MetricTypeDescription
zion_requests_totalcounterTotal HTTP requests processed
zion_requests_by_status{class="2xx"/"4xx"/"5xx"}counterResponses by status class
zion_config_generationcounterConfig hot-reload generation (increments on every atomic swap)
zion_websocket_upgradescounterWebSocket upgrades completed

Cache

MetricTypeDescription
zion_cache_hitscounterResponses served from RAM cache
zion_cache_missescounterCache misses (fetched from upstream)

Security — WAF, rate limit, enforcement, tarpit

MetricTypeDescription
zion_waf_deniedcounterRequests blocked by the WAF
zion_waf_shadow_would_blockcounterRequests a shadow-mode WAF would have blocked (logged, not blocked)
zion_rate_limitedcounterRequests denied by the per-IP rate limiter
zion_connections_rejected_per_ipcounterConnections rejected by the per-IP concurrent-connection cap (at accept)
zion_enforcement_denied_total{reason="class"/"mesh_score"}counterSovereign-enforcement 403s, by trigger
zion_tarpit_activegaugeConnections currently held in the L7 tarpit
zion_tarpit_totalcounterTotal requests held by the tarpit before rejection
zion_tarpit_shed_totalcounterTarpit holds shed to a plain 403 when the global ceiling was hit
zion_tarpit_held_ms_totalcounterCumulative ms connections were held by the tarpit (mean hold = rate(held_ms)/rate(total))

Connections & TLS — histograms below.

MetricTypeDescription
zion_connections_totalcounterTotal TLS connections accepted
zion_tls_handshake_errorscounterFailed TLS handshakes

ACME (always emitted; 0 without --features acme)

MetricTypeDescription
zion_acme_renewals_totalcounterCertificates renewed
zion_acme_renewal_failures_totalcounterRenewal attempts that failed (sustained non-zero = certs will expire)

Mesh — AIMP (always emitted; 0 without --features sovereign-aimp)

MetricTypeDescription
zion_mesh_claims_emitted_totalcounterMesh claims published from this node
zion_mesh_claims_received_totalcounterClaims received and merged into local state
zion_mesh_claims_dropped_total{reason="signature"/"replay"/"rate"/"other"}counterInbound envelopes rejected, by reason
zion_mesh_score_lookups_totalcounterDispatcher hits that found a mesh score for the client IP
zion_mesh_gossip_bytes_in_totalcounterBytes received on the gossip socket
zion_mesh_gossip_bytes_out_totalcounterBytes sent on the gossip socket

Reliability & internals

MetricTypeDescription
zion_panics_totalcounterWorker panics caught by the panic hook (must stay 0)
zion_audit_events_totalcounterAudit-log events emitted (signed + HMAC-chained)
zion_audit_events_dropped_totalcounterAudit events dropped because the writer queue was full (alert on any drop)
zion_traces_emitted_totalcounterRequest spans observed (one per request)
zion_traces_invalid_totalcounterInbound traceparent headers rejected as malformed
zion_admin_rejects_totalcounterAdmin-API requests rejected (auth or rate-limit)

Sovereign classification (feature-gated — absent without --features geo-ita/geo-eu)

MetricTypeDescription
zion_sovereign_classifications_total{class="…"}counterRequests classified by origin (IT/EU/unknown), by class

Histograms

Latency is exposed as Prometheus histograms — each emits _bucket{le="…"}, _sum, and _count series; query with histogram_quantile(). In OpenMetrics output they also carry trace-ID exemplars.

MetricDescription
zion_request_duration_secondsEnd-to-end request latency (client-facing)
zion_upstream_duration_secondsTime spent waiting on the upstream
zion_tls_handshake_duration_secondsTLS handshake duration

Runtime resource gauges

/metrics also exposes process self-introspection gauges, so you can watch the daemon's own footprint live — and catch a slow leak (for example ~1 MB per 1000 connections) by its RSS slope, without restarting under a profiler.

MetricTypeDescription
zion_active_connectionsgaugeCurrently active TLS connections
zion_process_resident_memory_bytesgaugeResident set size of the Zion process, in bytes (Linux /proc/self/status VmRSS; 0 on other platforms)
zion_process_open_fdsgaugeOpen file descriptors held by the process (Linux /proc/self/fd; 0 on other platforms)

The two process_* gauges are sampled from /proc/self once per scrape — the /metrics render is cached for one second, so the two small file reads never run on the hot connection path. The same values are surfaced in /_zion/snapshot.json and the zion top TUI ("rss" / "open fds" rows). They are Linux-only; on macOS/Windows they render as 0 so one dashboard works across hosts. Run zion doctor to confirm the host actually exposes /proc/self/status — a hardened container runtime that masks /proc will report 0 here, and the check warns you up front.

Prometheus scrape config

yaml
scrape_configs:
  - job_name: zion
    static_configs:
      - targets: ['zion-host:443']
    scheme: https
    tls_config:
      insecure_skip_verify: true  # if using self-signed certs

Grafana dashboard

An importable dashboard covering the whole fleet — golden signals, security, TLS/upstream, a leak-watch row (RSS slope + open FDs per instance), protocols & tarpit detail, the AIMP mesh, and a reliability & internals row (panics / dropped audit events / admin rejects). Every metric /metrics exposes has a panel. It is committed at deploy/grafana/zion-overview.json. Grafana → Dashboards → Import → upload the JSON → pick your Prometheus source. See deploy/grafana/README.md.

The raw PromQL, if you'd rather build your own panels or alerts:

text
# Request rate
rate(zion_requests_total[5m])

# Error rate
rate(zion_requests_by_status{class="5xx"}[5m])

# WAF deny rate
rate(zion_waf_denied[5m])

# Cache hit ratio
zion_cache_hits / (zion_cache_hits + zion_cache_misses)

# TLS handshake failure rate
rate(zion_tls_handshake_errors[5m])

# Memory-leak slope: RSS growth rate over 30m. A sustained positive
# slope under flat request traffic is the silent-leak signal.
deriv(zion_process_resident_memory_bytes[30m])

# File-descriptor leak: open fds climbing without bound (alert if it
# approaches the `zion doctor` fd soft limit).
zion_process_open_fds

# Panics — must stay flat 0; page immediately on any non-zero rate.
rate(zion_panics_total[5m])

# Audit-log gaps — alert on any dropped event (the HMAC chain has holes).
rate(zion_audit_events_dropped_total[5m])

# Tarpit mean hold time (ms) — how long a flooding source is stalled.
rate(zion_tarpit_held_ms_total[5m]) / clamp_min(rate(zion_tarpit_total[5m]), 1)

# Mesh: inbound envelopes dropped by reason (spike in signature/replay = a
# probing peer). Requires --features sovereign-aimp; flat 0 otherwise.
sum by (reason) (rate(zion_mesh_claims_dropped_total[5m]))

X-Request-ID

Every HTTPS response includes an X-Request-ID header for request tracing.

Behavior:

  • If the incoming request contains X-Request-ID, Zion preserves it and echoes it back on the response
  • If absent, Zion generates a unique ID in the format {timestamp_hex}-{counter_hex} (e.g., 191a2b3c4d5e-0042)
  • The ID is forwarded to the upstream in the request headers
  • The same ID is added to the response headers for client correlation

The counter is a global atomic u64, ensuring uniqueness across all concurrent requests.

Structured logging

Configure log format in [server]:

toml
[server]
log_format = "json"   # or "text" (default)

Text format (default, development)

text
config loaded from zion.toml
  route /api/{*rest} -> backend [waf=strict, cache=off]
ZION ONLINE.

JSON format (production)

json
{"ts":"1712000000","level":"info","event":"config","msg":"loaded from zion.toml"}
{"ts":"1712000000","level":"info","event":"shutdown","msg":"signal received, draining..."}

JSON logs are structured for ingestion by Loki, ELK, Datadog, or any log aggregator. Fields:

FieldDescription
tsUnix timestamp (seconds)
levelinfo, warn, or error
eventEvent category (e.g., config, health, shutdown, tls)
msgHuman-readable message

Upstream health monitoring

Zion runs a background health checker that pings all unique upstream URLs every 30 seconds:

  • Sends GET / to each upstream
  • Healthy = 2xx or 3xx response within 5 seconds
  • State transitions (UP -> DOWN, DOWN -> UP) are logged
  • Health state is stored as an atomic boolean per upstream

The health checker uses a separate HTTP client and does not affect the main proxy connection pool.

Released under the MIT License.