Observability¶
Each etcfuse-meta daemon exposes a Prometheus endpoint when started with
--metrics-addr:
etcfuse-meta --metrics-addr=:9090 ...
curl http://localhost:9090/metrics
The metrics are backed by prometheus/client_golang, so the endpoint also
carries the standard Go runtime and process collectors alongside the EtcFS
series below.
Health and readiness¶
The same listener serves two endpoints an orchestrator can probe, and the distinction between them is the point:
| Path | Answers | Meaning |
|---|---|---|
/healthz |
always 200 while the process runs | The daemon is alive and restarting it fixes nothing. |
/readyz |
200, or 503 with the reason | The daemon will serve I/O rather than fail it. |
/readyz reports not-ready in three cases: the IPC socket is not yet being
served (the daemon is still starting), the membership lease is not live (peers
may already be fencing this node), or self-fencing has triggered (every write
will be rejected). None of those is a reason to kill the process — a fenced
node is doing exactly what it should — which is why they do not affect
/healthz.
The server sets read, write and idle timeouts. The listener is reachable by anything that can route to the node, and without them a client that opens a connection and never finishes its request holds a goroutine and a file descriptor indefinitely.
Instrumentation model¶
Metrics are declared once, as package-level variables in pkg/metrics, and
registered with the default Prometheus registry. A subsystem instruments itself
by referring to the metric it owns; nothing is threaded through constructors and
no registry is passed around. The trade-off is deliberate: a global registry
makes a metric impossible to forget to wire, which is the failure mode that
matters here — a daemon whose /metrics endpoint answers but reports nothing is
worse than one that has no endpoint at all, because it looks healthy.
Metric names are an API. Dashboards and alert rules are written against them, so
renaming one is a breaking change; test/harness/metrics_test.go pins the list.
One latency series is exported, and only one. The histograms that decomposed a request into its etcd, device and handler stages existed to tune the data path, and that question is answered; end-to-end handler latency stays, because it is the one an operator asks during an incident and the one no counter can reconstruct. For the stages beneath it, etcd and the device both export their own latency.
The series¶
| Metric | Type | Labels | Meaning |
|---|---|---|---|
etcfuse_fuse_ops_total |
Counter | op |
FUSE operations served, by operation name |
etcfuse_fuse_errors_total |
Counter | op |
Operations that returned an errno |
etcfuse_fuse_op_duration_seconds |
Histogram | op |
End-to-end handler latency |
etcfuse_etcd_txn_total |
Counter | outcome |
etcd transactions, by outcome (committed, rejected, error) |
etcfuse_etcd_txn_origin_total |
Counter | origin |
Committed transactions by the operation that asked for them (create, setattr, extent_flush, lock_acquire, lock_release, write_commit, times_batch, times_single, reclaim). A commit is the unit of cost in this filesystem, so this is what a per-operation cost model is built from; other is a path that has not been labelled |
etcfuse_metadata_cache_total |
Counter | result |
Data-path metadata lookups, by whether the lock-held snapshot answered them (hit, miss) |
etcfuse_lock_handover_hold_seconds |
Histogram | — | How long a cached inode lock is held before a peer's recall is honoured. Pinned at the ceiling means an inode is fought over continuously and the waiters pay for it; pinned at the floor means recalls arrive after the holder had finished |
etcfuse_dirent_cache_total |
Counter | result |
Lookups of a name that is not there, by whether this node's cached set of the directory's names answered it (hit) or it had to read etcd (miss). Sustained misses mean the directories are past the per-directory cap, or the dirent watch is not delivering and the cache is disarmed |
etcfuse_readdir_page_total |
Counter | result |
READDIR pages, by whether the listing resumed where the last reply stopped (resumed) or had to re-read the directory from the start (rescanned) |
etcfuse_pending_extents |
Gauge | — | Metadata keys written by acknowledged writes and not yet published to etcd |
etcfuse_pending_bytes |
Gauge | — | Acknowledged write payload those keys stand for, summed across every inode |
etcfuse_metadata_flush_total |
Counter | trigger |
Publications of deferred metadata (interval, buffer_full, memory_pressure, sync_write, operation, recall, eviction, shutdown) |
etcfuse_metadata_flush_batch_total |
Counter | — | Transactions that published one or more inodes' deferred metadata together |
etcfuse_metadata_flush_batch_inodes_total |
Counter | — | Inodes those transactions carried. Divided by the batch count it is what batching the sweep is worth: one inode per transaction means the batch is a single flush under another name |
etcfuse_metadata_flush_failures_total |
Counter | reason |
Flushes that did not publish (error, rejected, fenced, device) |
etcfuse_block_io_total |
Counter | op |
Block device operations (read, write) |
etcfuse_block_io_bytes_total |
Counter | op |
Bytes transferred to and from the device |
etcfuse_block_io_duration_seconds |
Histogram | op |
Device read and write latency. Shares its buckets with etcfuse_fuse_op_duration_seconds, so the two can be read against each other to say how much of an operation's latency was the device rather than the daemon |
etcfuse_scrub_anomalies_total |
Counter | type |
Anomalies found by the scrubber |
etcfuse_scrub_passes_total |
Counter | — | Completed scrub passes |
etcfuse_scrub_last_run_seconds |
Gauge | — | Unix timestamp of the last completed pass |
etcfuse_arena_utilization |
Gauge | — | Fraction of blocks in use across this node's arenas |
etcfuse_arenas_owned |
Gauge | — | Arenas currently owned by this node |
etcfuse_membership_count |
Gauge | — | Live cluster members as last observed by this node |
etcfuse_fencing_generation |
Gauge | — | This node's current fencing generation |
etcfuse_fenced_nodes_total |
Counter | outcome |
Departures this node's controller acted on (fenced, failed, departed — the last being an intentional leave that was not fenced) |
Where each is updated:
- The FUSE series come from the IPC dispatch wrapper, so every operation the daemon serves is counted and no new handler can be added without being instrumented.
- The etcd series come from the single transaction path every store mutation funnels through.
etcfuse_arena_utilizationandetcfuse_arenas_ownedare sampled by the arena reaper's tick rather than updated per allocation: both are derived by walking the arena bitmaps under the allocator lock, which is on the write path, and a gauge one tick stale is worth more than the contention.etcfuse_membership_countis counted by the fencing controller's reconciliation sweep, which already reads every known node's membership key.
Per-anomaly and per-inode detail is deliberately not exported: a series per
affected inode is how a metrics backend gets taken down by a filesystem fault.
That detail stays in the daemon's logs and in fsck output.
What to alert on¶
rate(etcfuse_scrub_anomalies_total{type!~"orphan|dead"}[5m]) > 0— orphan and dead findings are routine and auto-remediated; the rest need a human.changes(etcfuse_fencing_generation[10m]) > 0— this node was fenced.increase(etcfuse_metadata_flush_failures_total[5m]) > 0— acknowledged writes are not reaching etcd.errormeans they are still buffered and everyfsyncon those inodes is returningEIO;rejectedandfencedmean they were discarded, which is data loss and is logged as such.etcfuse_pending_byteswell above what the flush interval should allow — the flusher has stalled, and that figure is what a crash would lose right now.increase(etcfuse_fenced_nodes_total{outcome="failed"}[10m]) > 0— a fence was attempted and could not be confirmed, which leaves the target in the limbo state described in the external fencing controller page.etcfuse_arena_utilization > 0.9— this node is close to needing an arena it may not be able to claim.time() - etcfuse_scrub_last_run_seconds > 300— the scrubber has stopped, which the anomaly counters cannot show; they simply stop rising.
These five are wired as Prometheus alerting rules in
deploy/prometheus/etcfs-alerts.yml. A matching Grafana dashboard covering all
of the series above lives at deploy/grafana/etcfs-dashboard.json — import it
directly, or provision it via Grafana's dashboard-provisioning config pointed
at deploy/grafana/.