feat(observability): validator health metrics + readiness probe - #102
feat(observability): validator health metrics + readiness probe#102luishsr wants to merge 5 commits into
Conversation
P2 of the Network Observability Initiative (axyl-private#428). Adds the validator/
network-health signals the audit found missing — the node already exports Narwhal/
Mysten's Prometheus suite, but the highest-value per-validator and readiness signals
were tracked in-memory and never exposed.
Readiness probe: the healthcheck now serves /readyz — 200 only when the node is voting
(CvvActive) or an operational observer, 503 while a validator is still catching up
(CvvInactive). Liveness (any other path) stays 200. Reads the live NodeMode via a
watch::Receiver from the consensus bus, threaded at the spawn site. Lets a load balancer /
on-call distinguish "process up" from "actually voting in the current epoch".
New metrics (registered into the consensus default_registry, served on --metrics):
- validator_participation{authority} — committed certificates per validator, counted at
the bullshark commit site. A direct per-validator liveness signal, consistent across
nodes (every node commits the same sub-dags). This is the audit's biggest gap
("did validator X vote/certify?").
- vote_request_rejections{authority, reason} — this node's header vote requests rejected
by a peer (from VoteFailureTracker), by rejecting validator and reason
(too_old | epoch_mismatch). Node-local: rising totals mean this node is falling behind.
- committee_size — committee size for the current epoch, set per epoch.
Dashboard: etc/monitoring/.../rayls-consensus.json — a starter Grafana dashboard (epoch,
committee size, bad nodes, round progress, commit-latency p95, leader election, the two
new per-validator series, peers) with auto-provisioning, portable via a datasource
variable. Composes with the etc/monitoring stack from axyl#99.
Refs: raylsnetwork/axyl-private#428
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @luishsr's task in 2m 25s —— View job PR Review: feat(observability): validator health metrics + readiness probeTasks:
SummaryThis is well-scoped observability work. The readiness probe design is sound (watch-channel-based mode propagation, path routing, proper HTTP formatting), and the metrics follow the established Issues1. Startup bind error is silently swallowed —
|
…ion metric Follow-up to the P2 review on #102: - Propagate a healthcheck bind failure (core.rs) instead of `let _ =`-swallowing it. A port already in use would previously start the node with no health endpoint and no signal — defeating the purpose of an explicitly-requested probe. Now returns the error. - Handle each healthcheck connection in its own task (health.rs). The accept loop bounded each request read with a 200ms timeout but still ran it inline, so a client that connects and sends nothing held the loop for the full timeout and delayed every other probe. A per-connection task keeps the accept loop free; the watch::Receiver is cloned per conn. - Reset the accept-loop backoff on a healthy accept (health.rs) so a past transient error doesn't keep the retry delay elevated. - Count cert-covered-skipped too-old rejections under a distinct `too_old_skipped` reason (certifier.rs) rather than dropping them from vote_request_rejections. Otherwise the metric reads zero while a node continuously trips the skip path, masking real trouble. Doc comment on the metric updated to list the three reason values. - Document the `/ready` alias alongside `/readyz` in the health module docs. Reviewer's other two points verified as non-issues, no change: the `authority` label is a ~44-char bs58 of a 32-byte AuthorityIdentifier with cardinality bounded by committee size (not a raw BLS key); and `current_round` (dashboard panel 4) is a real registered PrimaryMetrics gauge on the same default_registry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @luishsr's task in 1m 54s —— View job PR Re-Review: feat(observability): validator health metrics + readiness probeTasks:
Follow-up ReviewThe fix commit ( Issue 1 — Silent bind error ✅ Fixed
Issue 2 — Accept loop blocking for 200ms ✅ Fixed
One minor observation (not blocking): the Issue 3 — Backoff not reset on success ✅ Fixed
Issue 4 — Authority label cardinality ✅ Verified non-issueCommit message confirms Issue 5 —
|
…ric with too_old_skipped) The re-review noted the epoch-mismatch stale-peer early-return dropped the rejection from vote_request_rejections — the same gap just fixed for too_old. Count it under a distinct `epoch_mismatch_stale` reason so the raw rejection rate stays visible across all four paths. Metric doc updated to list all reason values. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @luishsr's task in 1m 14s —— View job PR Re-Review (commit
|
| Path | reason label |
Counts toward demotion |
|---|---|---|
| Too-old, cert store covers it | too_old_skipped |
No |
| Too-old, counted | too_old |
Yes |
| Epoch mismatch, stale peer | epoch_mismatch_stale |
No |
| Epoch mismatch, counted | epoch_mismatch |
Yes |
Doc comment (consensus.rs) — updated to list all four reason values and explain the *_skipped / *_stale distinction. The explanation is clear and accurate.
No further gaps found. I re-read the DagError::TooOldRejectedByPeer and DagError::EpochRejectedByPeer match arms end-to-end — there are no remaining early-return paths that bypass the metric.
Summary
All issues from both prior reviews are now resolved. The PR is clean, well-tested, and the metric coverage is complete. Ready to merge.
…lidator-metrics # Conflicts: # crates/middleware/orchestrator/src/epoch_manager/core.rs
|
Claude finished @luishsr's task in 1m 34s —— View job PR Re-Review (post-merge-sync
|
Security Scan - CodeSeverity: HIGH, CRITICAL No vulnerabilities found View scan results |
…omment_width CI's Format job runs `cargo +nightly fmt --all --check`, whose rustfmt.toml sets comment_width=100 + wrap_comments (nightly-only). One doc-comment line in the readiness module wrapped one word early; rewrap it so the nightly check passes. No code change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @luishsr's task in 34s —— View job PR Re-Review (post-sync
|
| /// handled in the accept loop and closed immediately after the response. | ||
| /// | ||
| /// To enable on node startup, use `rayls-network node --enable-healthcheck`. | ||
| /// To enable on node startup, use `rayls-network node --healthcheck <PORT>`. |
There was a problem hiding this comment.
As you're changing parameter names - flagging again that this might break the test env as well as deployments?
There was a problem hiding this comment.
Dug into this — the CLI flag itself is unchanged by this PR: --healthcheck <PORT> (pub healthcheck: Option<u16> in node.rs) already exists on main today, and nothing in this PR renames or retypes it. What changed here is only the doc comment on health.rs, which said --enable-healthcheck — a name that doesn't match the actual flag. I traced that mismatch back to c08dd6e ("Porting codebase from old repository", the original import) — it's been stale since before any of us touched this file; this PR is the first to fix the doc text to match the real flag. I also grepped axyl-automation-testing and etc/ for --enable-healthcheck/enable_healthcheck — zero hits, so nothing in the test env references the old (never-actually-real) name either. No Prometheus metric names were renamed in this PR either (the new ones — vote_request_rejections, committee_size, validator_participation — are net-new, nothing existing renamed). So I don't think there's a live break here, but let me know if you had a specific test-env or deployment reference in mind that I should check directly — happy to look at anything specific.
| //! - **liveness** (any path, e.g. `GET /`): always `200 OK` while the process is accepting | ||
| //! connections — the pre-existing behaviour. | ||
| //! - **readiness** (`GET /readyz`, alias `GET /ready`): `200 OK` only when the node is actually | ||
| //! participating — voting (`CvvActive`) or an operational observer — and `503 Service |
There was a problem hiding this comment.
It seems pretty odd to me to use 503 when the validator is CvvInactive. What's the reason behind this decision?
| tokio::spawn(async move { | ||
| let mut buf = [0u8; 256]; | ||
| let wants_readiness = matches!( | ||
| tokio::time::timeout( | ||
| Duration::from_millis(200), | ||
| socket.read(&mut buf), | ||
| ) | ||
| .await, | ||
| Ok(Ok(n)) if n > 0 && request_targets_readiness(&buf[..n]) | ||
| ); | ||
|
|
||
| let response = if wants_readiness { | ||
| readiness_response(*node_mode.borrow()) | ||
| } else { | ||
| // Any other path (incl. `/`) is a liveness probe: process is up. | ||
| LIVE_200 | ||
| }; |
There was a problem hiding this comment.
could you have used some framework for exposing this data instead of doing it the raw way string-matching the request that comes from the socket - i'd also argue that this makes the API very fragile later to extend.
What
P2 of the Network Observability Initiative (
axyl-private#428). The research audit found the node already exports Narwhal/Mysten's full Prometheus suite — but the highest-value per-validator and readiness signals were tracked in-memory and never exposed. This PR surfaces them.Readiness probe
The healthcheck now serves
/readyz—200only when the node is voting (CvvActive) or an operational observer, and503while a validator is still catching up (CvvInactive). Liveness (any other path) stays200. It reads the liveNodeModevia awatch::Receiverfrom the consensus bus, threaded in at the spawn site. This lets a load balancer / on-call distinguish "process up" from "actually voting in the current epoch" — which the unconditional-200healthcheck couldn't.New metrics (consensus registry, served on
--metrics/metrics_address)validator_participation{authority}— committed certificates per validator, counted at the bullshark commit site (where the sub-dag certificates are already iterated). A direct per-validator liveness signal, and consistent across nodes since every node commits the same sub-dags. This is the audit's biggest gap — "did validator X vote/certify?"vote_request_rejections{authority, reason}— this node's header vote requests rejected by a peer (fromVoteFailureTracker), by rejecting validator and reason (too_old/epoch_mismatch). Node-local: rising totals mean this node is falling behind. Complements the network-consistentvalidator_participation.committee_size— committee size for the current epoch, set at the per-epoch metrics hook.Dashboard
etc/monitoring/.../rayls-consensus.json— a starter Grafana dashboard (9 panels: epoch, committee size, bad nodes, round progress, commit-latency p95, leader election, the two new per-validator series, peers) with auto-provisioning, portable via a datasource variable.Tests
3 tests on the readiness probe (path routing, mode→status mapping, and a live server test flipping
CvvInactive→CvvActiveand seeing503→200). The metrics follow the establishedregister_*_with_registry!idiom; compiles clean acrossconsensus-primary,primary-metrics, andorchestrator.Composition / scope
etc/monitoring/dashboard composes with the monitoring stack in feat(observability): enable metrics via config + operator monitoring setup #99 (this branch is offmain, so feat(observability): enable metrics via config + operator monitoring setup #99'sprometheus.yml/ docker-compose aren't on it yet; they merge cleanly as separate files).ValidatorStatusexport (Active / PendingActivation / Exited — needs an EVMConsensusRegistryread joined viaCommittee) and round-lag as its own metric (currently derived in the dashboard). Tracked under#428.🤖 Generated with Claude Code