From ac65f6a5225d0077296b5af55086cbb54d17f2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:01:52 -0300 Subject: [PATCH 1/3] feat: add other current state metrics from leanMetrics --- crates/blockchain/src/lib.rs | 43 ++++++++++----------- crates/blockchain/src/metrics.rs | 66 ++++++++++++++++++++++++++++++++ crates/blockchain/src/store.rs | 11 +++++- 3 files changed, 95 insertions(+), 25 deletions(-) create mode 100644 crates/blockchain/src/metrics.rs diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 8a4a5b4..afd61d9 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -14,6 +14,7 @@ use tokio::sync::mpsc; use tracing::{error, info, warn}; pub mod key_manager; +mod metrics; pub mod store; /// Messages sent from the blockchain to the P2P layer for publishing. @@ -90,6 +91,9 @@ impl BlockChainServer { let slot = time_since_genesis / SECONDS_PER_SLOT; let interval = time_since_genesis % SECONDS_PER_SLOT; + // Update current slot metric + metrics::update_current_slot(slot); + // Produce attestations at interval 1 if interval == 1 { self.produce_attestations(slot); @@ -99,17 +103,14 @@ impl BlockChainServer { let has_proposal = false; self.store.on_tick(timestamp, has_proposal); + + // Update safe target slot metric (updated by store.on_tick at interval 2) + metrics::update_safe_target_slot(self.store.safe_target_slot()); } fn produce_attestations(&mut self, slot: u64) { // Get the head state to determine number of validators - let head_state = match self.store.head_state() { - Some(state) => state, - None => { - warn!(%slot, "Cannot produce attestations: no head state"); - return; - } - }; + let head_state = self.store.head_state(); let num_validators = head_state.validators.len() as u64; @@ -149,14 +150,16 @@ impl BlockChainServer { }; // Publish to gossip network - if let Err(err) = self + let Ok(_) = self .p2p_tx .send(OutboundGossip::PublishAttestation(signed_attestation)) - { - error!(%slot, %validator_id, %err, "Failed to publish attestation"); - } else { - info!(%slot, %validator_id, "Published attestation"); - } + .inspect_err( + |err| error!(%slot, %validator_id, %err, "Failed to publish attestation"), + ) + else { + continue; + }; + info!(%slot, %validator_id, "Published attestation"); } } @@ -166,7 +169,10 @@ impl BlockChainServer { warn!(%slot, %err, "Failed to process block"); return; } - update_head_slot(slot); + metrics::update_head_slot(slot); + metrics::update_latest_justified_slot(self.store.latest_justified().slot); + metrics::update_latest_finalized_slot(self.store.latest_finalized().slot); + metrics::update_validators_count(self.store.head_state().validators.len() as u64); } fn on_gossip_attestation(&mut self, attestation: SignedAttestation) { @@ -228,12 +234,3 @@ impl GenServer for BlockChainServer { CastResponse::NoReply } } - -fn update_head_slot(slot: u64) { - static LEAN_HEAD_SLOT: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - prometheus::register_int_gauge!("lean_head_slot", "Latest slot of the lean chain") - .unwrap() - }); - LEAN_HEAD_SLOT.set(slot.try_into().unwrap()); -} diff --git a/crates/blockchain/src/metrics.rs b/crates/blockchain/src/metrics.rs new file mode 100644 index 0000000..e07a863 --- /dev/null +++ b/crates/blockchain/src/metrics.rs @@ -0,0 +1,66 @@ +//! Prometheus metrics for the blockchain module. + +pub fn update_head_slot(slot: u64) { + static LEAN_HEAD_SLOT: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_gauge!("lean_head_slot", "Latest slot of the lean chain") + .unwrap() + }); + LEAN_HEAD_SLOT.set(slot.try_into().unwrap()); +} + +pub fn update_latest_justified_slot(slot: u64) { + static LEAN_LATEST_JUSTIFIED_SLOT: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_gauge!( + "lean_latest_justified_slot", + "Latest justified slot" + ) + .unwrap() + }); + LEAN_LATEST_JUSTIFIED_SLOT.set(slot.try_into().unwrap()); +} + +pub fn update_latest_finalized_slot(slot: u64) { + static LEAN_LATEST_FINALIZED_SLOT: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_gauge!( + "lean_latest_finalized_slot", + "Latest finalized slot" + ) + .unwrap() + }); + LEAN_LATEST_FINALIZED_SLOT.set(slot.try_into().unwrap()); +} + +pub fn update_current_slot(slot: u64) { + static LEAN_CURRENT_SLOT: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_gauge!( + "lean_current_slot", + "Current slot of the lean chain" + ) + .unwrap() + }); + LEAN_CURRENT_SLOT.set(slot.try_into().unwrap()); +} + +pub fn update_validators_count(count: u64) { + static LEAN_VALIDATORS_COUNT: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_gauge!( + "lean_validators_count", + "Number of validators managed by a node" + ) + .unwrap() + }); + LEAN_VALIDATORS_COUNT.set(count.try_into().unwrap()); +} + +pub fn update_safe_target_slot(slot: u64) { + static LEAN_SAFE_TARGET_SLOT: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_gauge!("lean_safe_target_slot", "Safe target slot").unwrap() + }); + LEAN_SAFE_TARGET_SLOT.set(slot.try_into().unwrap()); +} diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index b4b6438..3b9cdb5 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -676,8 +676,15 @@ impl Store { } /// Returns a reference to the head state if it exists. - pub fn head_state(&self) -> Option<&State> { - self.states.get(&self.head) + pub fn head_state(&self) -> &State { + self.states + .get(&self.head) + .expect("head state is always available") + } + + /// Returns the slot of the current safe target block. + pub fn safe_target_slot(&self) -> u64 { + self.blocks[&self.safe_target].slot } } From a39614d200f66d1fb5e1fec834269aba217e0a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:22:25 -0300 Subject: [PATCH 2/3] docs: add document explaining metrics setup --- docs/metrics.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 docs/metrics.md diff --git a/docs/metrics.md b/docs/metrics.md new file mode 100644 index 0000000..a944d82 --- /dev/null +++ b/docs/metrics.md @@ -0,0 +1,68 @@ +# Metrics + +We collect various metrics and serve them via a Prometheus-compatible HTTP endpoint at `http://:/metrics` (default: `http://127.0.0.1:5054/metrics`). + +The exposed metrics follow [the leanMetrics specification](https://github.com/leanEthereum/leanMetrics/blob/3b32b300cca5ed7a7a2b3f142273fae9dbc171bf/metrics.md), with some metrics not yet implemented. We have a full list of implemented metrics below, with a checkbox indicating whether each metric is currently supported or not. + +## Node Info Metrics + +| Name | Type | Usage | Sample collection event | Labels | Supported | +|--------|-------|-------|-------------------------|--------|---------------| +| `lean_node_info` | Gauge | Node information (always 1) | On node start | name, version | □ | +| `lean_node_start_time_seconds` | Gauge | Start timestamp | On node start | | □ | + + +## PQ Signature Metrics + +| Name | Type | Usage | Sample collection event | Labels | Buckets | Supported | +|--------|-------|-------|-------------------------|--------|---------|-----------| +| `lean_pq_sig_attestation_signing_time_seconds` | Histogram | Time taken to sign an attestation | On each attestation signing | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +| `lean_pq_sig_attestation_verification_time_seconds` | Histogram | Time taken to verify an attestation signature | On each `signature.verify()` on an attestation | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +| `lean_pq_sig_aggregated_signatures_total` | Counter | Total number of aggregated signatures | On `build_attestation_signatures()` | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +| `lean_pq_sig_attestations_in_aggregated_signatures_total` | Counter | Total number of attestations included into aggregated signatures | On `build_attestation_signatures()` | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +| `lean_pq_sig_attestation_signatures_building_time_seconds` | Histogram | Time taken to verify an aggregated attestation signature | On `build_attestation_signatures()` | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +| `lean_pq_sig_aggregated_signatures_verification_time_seconds` | Histogram | Time taken to verify an aggregated attestation signature | On validate aggregated signature | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +| `lean_pq_sig_aggregated_signatures_valid_total`| Counter | Total number of valid aggregated signatures | On validate aggregated signature | | | □ | +| `lean_pq_sig_aggregated_signatures_invalid_total`| Counter | Total number of invalid aggregated signatures | On validate aggregated signature | | | □ | + +## Fork-Choice Metrics + +| Name | Type | Usage | Sample collection event | Labels | Buckets | Supported | +|--------|-------|-------|-------------------------|--------|---------|-----------| +| `lean_head_slot` | Gauge | Latest slot of the lean chain | On get fork choice head | | | ✅ | +| `lean_current_slot` | Gauge | Current slot of the lean chain | On scrape | | | ✅ | +| `lean_safe_target_slot` | Gauge | Safe target slot | On safe target update | | | ✅ | +|`lean_fork_choice_block_processing_time_seconds`| Histogram | Time taken to process block | On fork choice process block | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +|`lean_attestations_valid_total`| Counter | Total number of valid attestations | On validate attestation | source=block,gossip | | □ | +|`lean_attestations_invalid_total`| Counter | Total number of invalid attestations | On validate attestation | source=block,gossip | | □ | +|`lean_attestation_validation_time_seconds`| Histogram | Time taken to validate attestation | On validate attestation | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +| `lean_fork_choice_reorgs_total` | Counter | Total number of fork choice reorgs | On fork choice reorg | | | □ | +| `lean_fork_choice_reorg_depth` | Histogram | Depth of fork choice reorgs (in blocks) | On fork choice reorg | | 1, 2, 3, 5, 7, 10, 20, 30, 50, 100 | □ | + +## State Transition Metrics + +| Name | Type | Usage | Sample collection event | Labels | Buckets | Supported | +|--------|-------|-------|-------------------------|--------|---------|-----------| +| `lean_latest_justified_slot` | Gauge | Latest justified slot | On state transition | | | ✅ | +| `lean_latest_finalized_slot` | Gauge | Latest finalized slot | On state transition | | | ✅ | +| `lean_finalizations_total` | Counter | Total number of finalization attempts | On finalization attempt | result=success,error | | □ | +|`lean_state_transition_time_seconds`| Histogram | Time to process state transition | On state transition | | 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3, 4 | □ | +|`lean_state_transition_slots_processed_total`| Counter | Total number of processed slots | On state transition process slots | | | □ | +|`lean_state_transition_slots_processing_time_seconds`| Histogram | Time taken to process slots | On state transition process slots | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +|`lean_state_transition_block_processing_time_seconds`| Histogram | Time taken to process block | On state transition process block | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +|`lean_state_transition_attestations_processed_total`| Counter | Total number of processed attestations | On state transition process attestations | | | □ | +|`lean_state_transition_attestations_processing_time_seconds`| Histogram | Time taken to process attestations | On state transition process attestations | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | + +## Validator Metrics + +| Name | Type | Usage | Sample collection event | Labels | Supported | +|--------|-------|-------|-------------------------|--------|-----------| +|`lean_validators_count`| Gauge | Number of validators managed by a node | On scrape | | ✅ | + +## Network Metrics + +| Name | Type | Usage | Sample collection event | Labels | Supported | +|--------|-------|-------|-------------------------|--------|-----------| +|`lean_connected_peers`| Gauge | Number of connected peers | On scrape | client=lantern,qlean,ream,zeam | □ | +|`lean_peer_connection_events_total`| Counter | Total number of peer connection events | On peer connection | direction=inbound,outbound
result=success,timeout,error | □ | +|`lean_peer_disconnection_events_total`| Counter | Total number of peer disconnection events | On peer disconnection | direction=inbound,outbound
reason=timeout,remote_close,local_close,error | □ | From 5de93a9169f8a90e9c50765db1decc90836247b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:28:16 -0300 Subject: [PATCH 3/3] chore: fmt --- crates/blockchain/src/metrics.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/crates/blockchain/src/metrics.rs b/crates/blockchain/src/metrics.rs index e07a863..43fc70d 100644 --- a/crates/blockchain/src/metrics.rs +++ b/crates/blockchain/src/metrics.rs @@ -12,11 +12,8 @@ pub fn update_head_slot(slot: u64) { pub fn update_latest_justified_slot(slot: u64) { static LEAN_LATEST_JUSTIFIED_SLOT: std::sync::LazyLock = std::sync::LazyLock::new(|| { - prometheus::register_int_gauge!( - "lean_latest_justified_slot", - "Latest justified slot" - ) - .unwrap() + prometheus::register_int_gauge!("lean_latest_justified_slot", "Latest justified slot") + .unwrap() }); LEAN_LATEST_JUSTIFIED_SLOT.set(slot.try_into().unwrap()); } @@ -24,11 +21,8 @@ pub fn update_latest_justified_slot(slot: u64) { pub fn update_latest_finalized_slot(slot: u64) { static LEAN_LATEST_FINALIZED_SLOT: std::sync::LazyLock = std::sync::LazyLock::new(|| { - prometheus::register_int_gauge!( - "lean_latest_finalized_slot", - "Latest finalized slot" - ) - .unwrap() + prometheus::register_int_gauge!("lean_latest_finalized_slot", "Latest finalized slot") + .unwrap() }); LEAN_LATEST_FINALIZED_SLOT.set(slot.try_into().unwrap()); } @@ -36,11 +30,8 @@ pub fn update_latest_finalized_slot(slot: u64) { pub fn update_current_slot(slot: u64) { static LEAN_CURRENT_SLOT: std::sync::LazyLock = std::sync::LazyLock::new(|| { - prometheus::register_int_gauge!( - "lean_current_slot", - "Current slot of the lean chain" - ) - .unwrap() + prometheus::register_int_gauge!("lean_current_slot", "Current slot of the lean chain") + .unwrap() }); LEAN_CURRENT_SLOT.set(slot.try_into().unwrap()); }