Operations
Metrics, dashboards, and logs
The metrics listener, the metric families that matter, the shipped Grafana dashboards, what to alert on, and how novad logs
Every novad exposes one ops listener carrying Prometheus metrics, the health probes, pprof, and the admin dashboard. This page names the metrics worth a panel, the dashboards that already have them, the one alerting principle, and what the log says at each level.
The metrics listener
--metrics-listen <addr> (NOVAD_METRICS_LISTEN) serves /metrics; empty disables it and with it /healthz, /readyz, /debug/pprof/, and /admin/. The compose stack publishes it as localhost:9191 through 9193; the Fly and Kubernetes deployments scrape port 9090. novagateway has the same flag, defaulting to :9090.
Instruments are declared in OpenTelemetry dotted form (nova.append.bytes) and exported to Prometheus with unit suffixes: . becomes _, an s-unit histogram becomes _seconds (_bucket, _sum, _count), a By unit becomes _bytes, and counters gain _total. So nova.auditor.due.age is nova_auditor_due_age_seconds and nova.write.bytes is nova_write_bytes_total. The per-node dimension is Prometheus's own instance label. The gRPC RED metrics (rpc_server_call_duration_seconds) come from the otelgrpc stats handler.
The metric families
Names below are the OpenTelemetry form; apply the export rule.
Append and write path
| Metric | Meaning |
|---|---|
nova.write.latency | dispatch admission to durable ack, per class: the uniform write round-trip |
nova.write.bytes | durably acked payload bytes, per class |
nova.write.errors | failed writes: dispatch refusals and failed durability waits |
nova.append.bytes | cumulative committed payload bytes; rate() is append throughput |
nova.direct.pool.wait | per-batch wait from admission to its flight's cut: the pool queue ahead of the publisher |
nova.direct.publish.duration | one flight's publish as the flusher sees it |
nova.direct.flush.duration, nova.direct.flush.bytes | per-flush publish latency (build plus slot PUT) and bundle size |
nova.direct.commit.duration | per-flight commit: slot PUT through ordered ack release |
nova.direct.pending.bytes, nova.direct.pending.limit.bytes | bytes held against the admission budget, and the cap admission blocks at |
nova.dataplane.ack.send | one ack's wire write; a spike here with flat write.latency is a wire stall on this star |
nova.dataplane.shed | requests shed under backpressure, by resource (session, subscription, capacity) |
nova.dataplane.sessions | concurrent producer sessions and subscriptions, by resource |
nova.dataplane.orphaned.lease | orphaned-lease heals; flat zero is the only healthy reading |
Chain, backlog, and absorb
| Metric | Meaning |
|---|---|
nova.chain.backlog.age | age of the oldest flight in the leader's flight index: the write-side age signal |
nova.chain.backlog.flights, nova.chain.backlog.bytes | backlog fill |
nova.chain.backlog.stall | publishes that blocked on backlog tokens, by diagnosed cause (absorb_lag, metastore_slow, settling) |
nova.direct.backlog.wait | per-flight wait for a backlog token; nonzero means the absorb's checkpoint is pacing producers |
nova.chain.slot.duration, nova.chain.boarding.duration | slot commit latency, and the pre-PUT wait between token and slot acceptance |
nova.chain.roll.count, nova.chain.roll.waiting | epoch turnovers by outcome; roll.waiting at 1 means appends are failing fast on the roll pacer |
nova.commit.gate.holds | opens refused while a prior chain epoch is unsettled |
nova.absorb.pass.duration, nova.absorb.pass.rounds | one absorb pass's wall clock and batches; sustained multi-round passes mean the drain is chasing ingest |
nova.absorb.batch.slots, nova.absorb.batch.streams, nova.absorb.phase.duration | per-batch amortization and phase timing |
nova.absorb.wake, nova.absorb.feed, nova.absorb.memo.miss | wake-ups by reason, feed outcomes, memo misses |
nova.writer.limbo.streams, nova.writer.limbo.age | streams departed uncertified with their lease retained; a growing age means coverage cannot land |
nova.writer.lease.release.failed | releases parked for the retry sweep: a metastore-storm signal |
Read and subscribe
| Metric | Meaning |
|---|---|
nova.read.latency | read latency by source: the tail window (hot) versus object storage |
nova.tailwindowcache.hits, .served.bytes, .fallbacks, .evictions, .skipped, .resident.bytes | the leader's in-memory tail window: what it served, what fell to ranged GETs, budget pressure |
nova.subscribe.parked, nova.subscribe.wakes | subscriptions parked on the leader's wake, and wakes delivered |
nova.subscribe.redirects | subscriptions redirected off this star, by cause (departure, frontier) |
nova.dataplane.trim.redirects | trims answered NOT_OWNER off the holder; tracks lease churn, not error |
Object storage and caches
| Metric | Meaning |
|---|---|
nova.objectstore.duration | operation latency by operation (PUT/GET/LIST/DELETE) and store |
nova.objectstore.bytes | bytes transferred by operation (put, put_if_absent, get) |
nova.objectstore.attempt.errors | failed attempts inside the SDK retry loop, by operation and cause; a transient the retry recovers appears only here |
nova.objectcache.requests, .fallbacks, .spills, .ejections, .ring.size | the shared object cache: cache-routed requests, falls to direct by cause, hot-key spills, ejected instances, eligible instances |
nova.fetchcache.hits, .downloads, .download.bytes, .fallbacks, .evictions, .prefetch.dropped, .resident.bytes | the auditor's whole-object fetch cache, when enabled |
Auditor
| Metric | Meaning |
|---|---|
nova.auditor.due.age | age of the oldest ready item per due queue: the backlog-age alert signal |
nova.audit.jobs.armed, nova.audit.jobs.resolved | markers armed and resolved, by queue; read for flow balance, never alert on their rates |
nova.auditor.step.duration | one leased job's handle latency, by step |
nova.auditor.lease.skips | claims lost to a peer, by job type; climbing against a standing marker means the holder is wedged |
nova.auditor.settle.spans, nova.auditor.settle.bytes | span settlements by outcome; bytes relocated into per-stream outputs (the rewrite budget's demand) |
nova.auditor.group.members, .member.latency, .member.phase | group settle width and per-member timing |
nova.auditor.probe.verdicts | reclamation probe judgments: stand, observe, confirm-reap, gc, gone |
nova.auditor.garbage.reaped, nova.auditor.slots.trimmed, nova.auditor.teardown.gated | zombie slot copies deleted above the cut; slots trimmed; teardown walks re-armed behind the settle frontier |
Fleet and node
| Metric | Meaning |
|---|---|
nova.leader.count | streams this star leads |
nova.node.utilization, nova.node.utilization.axis | bottleneck utilization (saturated above 0.85, relieved below 0.70) and the per-axis breakdown |
nova.node.saturated, nova.node.stranded | 1 while at a ceiling and refusing leadership; 1 while no live peer can absorb a led stream |
nova.node.handoffs | cumulative load-balance handoffs |
nova.kv.duration | metastore point-operation latency |
The Gateway exports nova.gateway.vended.bytes and .proxied.bytes (the offload ratio), .fetch.wait, .fetch.inflight.bytes, .replans, .seam.handoffs, and the footer cache's hits and evictions.
The Grafana dashboards
dashboards/ holds the JSON; the compose stack provisions it, the Kubernetes controller ships it as sidecar ConfigMaps when observability.dashboards is on, and it imports into any Grafana by hand.
Nova — Streaming Log Store (nova-overview) is the single pane: Overview (streams led, stars saturated, append p99, the oldest reclamation backlog, traffic distribution, utilization by star, sheds), Write path, Read path (latency and rate by source), Tiering — object storage (op latency and rate, bytes, average bytes per PUT), Chain — backlog, absorb & turnover (backlog age marked alert on this, resident flights and bytes, stalls by cause, admission wait, slot commit latency, epoch turnovers), Metadata — oxia, Auditor — background reclamation (backlog age by queue marked alert on this, step latency and rate, jobs armed and resolved, garbage reaped, probe verdicts, slots trimmed), Auditor — settle fetch cache, RPC — gRPC, Reliability — recovery & sheds, and Runtime.
Nova — Oxia Metastore (nova-oxia) follows a ref commit through the metastore — client latency as Nova sees it, leader and follower write latency, batch commit, WAL sync, pebble compaction — and graphs each server resource against its provisioned ceiling, so "which layer is the bottleneck" is a glance.
Nova — Object Cache (nova-objectcache) shows the cache tier's effectiveness (hit ratio, S3 traffic saved), residency, and health.
The one alert to wire first
nova_auditor_due_age_seconds is the age of the oldest past-due reclamation item. A throughput graph looks healthy while a queue silently grows; a rising age does not. Alert when it climbs past your grace windows (minutes to warn, hours to page).
The principle generalizes: alert on backlog age, never rate. A full backlog at any fill turns over within seconds and is the normal full-rate condition; minutes of standing age mean the chain is wedged or chronically overdriven. The same rule applies to nova_chain_backlog_age_seconds, nova_writer_limbo_age_seconds, and nova_gateway_fetch_wait_seconds. The Kubernetes controller ships rules for exactly these (fleet and node saturation, stranded, per-class write SLOs, writer-limbo age, rewrite-backlog age).
Beside age, three fleet signals deserve rules: nova_node_stranded at 1 (capacity, now), a sustained rate of nova_dataplane_shed_total{resource="capacity"} (producers being refused), and a sustained rate of nova_node_handoffs_total (the fleet rebalancing constantly instead of settling).
Logging
novad writes structured logs through slog. Three levels, one rule each:
| Level | Carries |
|---|---|
| Info | Lifecycle edges, one line per state transition of a resource (stream, bucket, credential, leadership, chain epoch, settle walk, the star's own drain, saturation, and registration state, each close phase), and the star summary. An edge logs the change, never the steady state. |
| Debug | Per-connection and per-job lines: producer sessions and subscriptions (caller, peer, plane, bytes, duration), auditor jobs. Debug because they scale with client churn, not with the fleet. |
| Warn / Error | Failures and sheds. Every limit that sheds or truncates logs a line; silent shedding reads as healthy. |
Keys are snake_case, the error key is err, durations render as strings, and every message opens with its package prefix (novad:, writer:, auditor:). --log-level (NOVAD_LOG_LEVEL) takes debug, info, warn, or error; default info.
The star summary
One Info line per --log-summary-interval (default 1m0s; 0 disables), the log's proof of life and load between edges, written even when the star is idle:
novad: star summary leaders=… sessions=… subscriptions=… append_mibps=… offered_mibps=… read_mibps=…
backlog_standard_age=… backlog_express_age=… retained_mib=… utilization=… axis=…
draining=… drained=… saturated=… stranded=… auditor_jobs=… auditor_failed=…Rates are MiB/s over the interval (offered is what producers submitted; append is what the chain acked; read is hot-tail bytes served); backlog_<class>_age is per configured class; retained_mib sums the admission budgets, the tail window cache, and session transport; axis names the resource behind utilization. The auditor fields appear only when one runs in the process.
pprof and health
/debug/pprof/ rides the same listener. A star that stalls without erroring is usually backpressure; the goroutine dump names the blocked stage:
curl -s localhost:9191/debug/pprof/goroutine?debug=1 | less
go tool pprof "localhost:9191/debug/pprof/profile?seconds=10"/healthz is liveness (process alive, no dependency checks); /readyz is readiness (registered, not draining). Node lifecycle has the exact answers; the admin dashboard is at /admin/ on the same port.
Source documents
dashboards/README.md,dashboards/nova-overview.json,dashboards/nova-oxia.json,dashboards/nova-objectcache.jsoninternal/daemon/summary.go,internal/daemon/stats.go,internal/daemon/observability.godocs/design/000-decisions.md(lifecycle logging entry)