Skip to content

Observability

A merod node is observable through two channels: a Prometheus /metrics endpoint for numeric time series, and structured tracing logs for event-level detail. This page documents both, grounded in the metric families the node actually registers.

The metrics service is mounted on the node’s HTTP server (the same listener as the admin API and JSON-RPC), at the fixed path /metrics. With the default [server] config that is:

Terminal window
curl http://127.0.0.1:2528/metrics

The body is Prometheus / OpenMetrics text format, produced by the prometheus-client crate. All node, sync, and HTTP metrics register against a single registry that the server encodes on each scrape.

Registered in crates/node/src/sync/prometheus_metrics.rs. protocol labels are sanitised against an allow-list (Snapshot, HashComparison, DeltaSync, SubtreePrefetch, LevelWise, BloomFilter, None), so cardinality stays bounded.

MetricTypeLabelsMeaning
sync_messages_sentCounterprotocolSync protocol messages sent.
sync_bytes_sentCounterprotocolBytes sent, by protocol.
sync_round_tripsCounterprotocolRound trips, by protocol.
sync_entities_transferredCounterEntities transferred during sync. Near-zero in steady state.
sync_mergesCountercrdt_typeCRDT merge operations (conflict resolutions).
sync_comparisonsCounterEntity hash comparisons performed.
sync_phase_duration_secondsHistogramphasePer-phase duration (buckets 1ms–16s).
sync_duration_secondsHistogramprotocol, outcomeEnd-to-end session duration (buckets 10ms–160s).
sync_attemptsCounterprotocolSync attempts started.
sync_successesCounterprotocolSuccessful syncs.
sync_failuresCounterprotocolFailed syncs.
sync_protocol_selectionsCounterprotocolWhich protocol the adaptive selector chose.

Safety / invariant counters (all should sit at zero in normal operation):

MetricTypeMeaning
sync_snapshot_blockedCounterSnapshot attempt blocked on an already-initialised node. Any increment warrants investigation.
sync_verification_failuresCounterPost-snapshot state-hash mismatch — possible corruption or non-determinism.
sync_lww_fallbackCounterLast-writer-wins fallback when CRDT-type metadata is missing. Occasional is tolerable; sustained growth is a bug signal.
sync_buffer_dropsCounterDeltas dropped from a bounded buffer (arriving faster than they apply). Any increment can mean catch-up sync is needed.

Registered in crates/node/src/node_metrics.rs. These are node-wide sums (no per-context label, by design — a node can host hundreds of contexts).

MetricTypeLabelsMeaning
delta_outcomes_totalCounteroutcomeDAG delta-apply outcomes. outcomeapplied, pending, cascaded, duplicate, error, plus absorbed_leaf_future_schema / absorbed_snapshot_entity_future_schema for stragglers buffered by a stale-schema reader.
delta_cascade_sizeHistogramPending deltas unblocked when a parent finally lands.
delta_missing_parents_totalCounterMissing-parent requests issued to peers.
dag_heads_countHistogramConcurrent DAG heads after each apply (buckets 1–256). Steady state is low (1–2).
dag_compaction_deltas_pruned_totalCounterDelta rows reclaimed by DAG compaction.
hc_leaf_drops_totalCounterreasonLeaves dropped by the apply-time membership check. reasonunauthorized (expected under churn) / lookup_error (rare, an I/O signal).
delta_stores_countGaugeContexts with a live in-memory delta store.
sync_sessions_activeGaugeContexts with an open snapshot-sync session (buffering deltas).
MetricTypeLabelsMeaning
governance_pending_contextsGaugeContexts holding at least one delta in the governance-pending buffer.
governance_pending_queue_depthGaugeSum of pending-buffer depths across contexts. Monotonic growth means the buffer cannot drain (governance has not caught up).
governance_drain_outcomes_totalCounteroutcomePer-delta drain outcomes. outcomeapplied, removed, never_member, rebuffered, dropped_max_attempts, lookup_error.
MetricTypeMeaning
blob_cache_entriesGaugeBlobs in the in-memory cache.
blob_cache_size_bytesGaugeResident bytes across the blob cache.
blob_cache_evictions_age_totalCounterEvicted for exceeding max age.
blob_cache_evictions_count_totalCounterEvicted to keep entry count under the cap.
blob_cache_evictions_memory_totalCounterEvicted to keep resident bytes under the cap.
MetricTypeLabelsSourceMeaning
network_event_channel_depthGaugenetwork channelEvents waiting in the network→node channel. Sustained high depth means the node manager can’t keep up.
network_event_channel_dropped_totalCounternetwork channelEvents dropped because the channel was full. Any increment can mean missed gossip / delayed sync.
network_event_channel_processing_latency_secondsHistogramnetwork channelEnqueue-to-dequeue latency.
http_requests_totalCountermethod, path, statusserverHTTP requests, by matched route template and status class (2xx/4xx/5xx).
http_request_duration_secondsHistogrammethod, path, statusserverRequest latency.
merod_build_infoGaugeversion, peer_idnodeConstant-1 beacon. Confirms the scrape pipeline works and which build / identity answered.
process_resident_memory_bytesGaugenodeRSS of the merod process (Linux only; 0 elsewhere).
process_virtual_memory_bytesGaugenodeVirtual memory size (Linux only).
process_threadsGaugenodeThread count (Linux only).
process_open_fdsGaugenodeOpen file descriptors (Linux only).

A node and its peers converge on a shared root hash per context; group and namespace heartbeats periodically exchange those roots (and DAG heads) so that divergence is detected without a full sync. See Sync & Convergence and Governance for the protocol detail.

  • dag_heads_count histogram sits in the low buckets (1–2 concurrent heads).
  • sync_buffer_drops_total, sync_snapshot_blocked_total, sync_verification_failures_total all stay at 0.
  • governance_pending_queue_depth is flat or trending to 0 (the buffer drains as governance catches up).
  • sync_successes_total / sync_attempts_total is high; the selector mostly picks HashComparison.
  • delta_outcomes_total{outcome="error"} and hc_leaf_drops_total{reason="lookup_error"} are near zero.
  • merod_build_info is present (scrape pipeline alive, identity confirmed).

Not every convergence signal is a Prometheus series. Two typed state machines surface a node’s sync health at finer granularity than the counters above — both are pushed as events and readable on demand, so a client or operator can tell “actively syncing” from “waiting for a peer” from “stuck behind backoff”.

Each context reports a coarse sync_status phase, published both as the sync_status JSON-RPC response and as a SyncStatus WebSocket event. The phase is recomputed from the context’s SyncState and only re-emitted on a real transition, so the event stream stays quiet between changes (crates/node/src/sync/session.rs, produced from crates/primitives/src/sync_status.rs).

PhaseCarriesMeaning
IdleSettled — no sync in flight, nothing pending.
WaitingForPeersNot yet initialized and nothing actively syncing — typically waiting for a co-member peer to appear. Set explicitly on the benign NoPeersAvailable / PeerNotMaterialized outcomes, which otherwise look idle.
SyncingA sync attempt is in flight (handshake / delta exchange).
ReceivingSnapshotrecords_received, percent, eta_secsReceiving an initial-state snapshot. percent / eta_secs are None against a peer too old to advertise the snapshot’s total entity count, degrading to the raw records_received liveness count.
BackingOffretry_in_secsThe last attempt failed; the next retry is gated behind the exponential backoffretry_in_secs is the estimated wait.

Separately, each namespace runs a readiness FSM that governs when the node will participate in the three-phase governance contract. Peers exchange signed beacons advertising how far each has applied; the tier is a pure function of local progress, peer beacons, and elapsed boot grace (crates/node/src/readiness.rs, evaluate_readiness).

TierCarriesMeaning
BootstrappingJust subscribed — no peer beacon yet and still within boot_grace, or an empty-DAG joiner with no known target.
LocallyReadyBoot grace elapsed with no peer beacons; the node self-promotes on local state alone.
PeerValidatedReadyHeard a fresh peer beacon and the local applied_through is within applied_through_grace of the peer’s tip. The fully-converged tier.
CatchingUptarget_applied_throughA peer beacon names a tip the node is still behind; carries the target to catch up to.
DegradedreasonDemoted — PendingOps(n) while ops are blocking promotion, or NoRecentPeers when a once-fresh namespace has heard no beacon within ttl_heartbeat.

Readiness config defaults (ReadinessConfig::default): beacon_interval 5 s, ttl_heartbeat 60 s, boot_grace 10 s, applied_through_grace 2 ops. Any local pending op demotes to Degraded immediately — pending ops always win over a tip-fresh peer comparison.

merod initialises tracing with an EnvFilter driven by the RUST_LOG environment variable. When RUST_LOG is unset or empty, the default directive is:

Terminal window
RUST_LOG="merod=info,calimero_=info"

Set it to raise or lower verbosity per module before launching the node:

Terminal window
RUST_LOG="merod=debug,calimero_node=debug,calimero_=info" \
merod --home data/ --node node1 run