Repository: op-rs/kona
Pull request #1527: chore(node/p2p): Peer Count Metric
Author: login: refcell
Labels: name: K-feature
description: Kind: feature
name: A-node
description: Area: cl node (eq. Go op-node) handles single-chain consensus
name: A-p2p
description: Area: p2p
Created: 2025-04-25T17:29:11Z | Merged: 2025-04-25T18:19:28Z

### Description

Introduces a peer count metric.

Closes #1528 

Linked issue #1528: feat(node/p2p): Peer Count Gauge Metric
### Description

Add a gauge metric to the `kona-p2p` crate behind a `metrics` feature flag.

This follows the [`op-node`'s peer count metric](https://github.com/ethereum-optimism/optimism/blob/develop/op-node/metrics/metrics.go#L237-L242).

It should be incremented when a new connection is established, or decremented when a connection is dropped. (to ensure notifications aren't missed, the gauge can just be _set_ to the current peer count value when one of these notifications is encountered).

Diff:
diff --git a/Cargo.lock b/Cargo.lock
index f847395265..4349a6c6e1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4803,6 +4803,7 @@ dependencies = [
  "op-alloy-consensus",
  "op-alloy-rpc-types-engine",
  "openssl",
+ "prometheus",
  "rand 0.9.1",
  "secp256k1",
  "serde",
@@ -6682,6 +6683,44 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "procfs"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f"
+dependencies = [
+ "bitflags 2.9.0",
+ "hex",
+ "procfs-core",
+ "rustix 0.38.44",
+]
+
+[[package]]
+name = "procfs-core"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec"
+dependencies = [
+ "bitflags 2.9.0",
+ "hex",
+]
+
+[[package]]
+name = "prometheus"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a"
+dependencies = [
+ "cfg-if",
+ "fnv",
+ "lazy_static",
+ "libc",
+ "memchr",
+ "parking_lot",
+ "procfs",
+ "thiserror 2.0.12",
+]
+
 [[package]]
 name = "prometheus-client"
 version = "0.22.3"
diff --git a/Cargo.toml b/Cargo.toml
index f0ccf927e1..a714084377 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -155,6 +155,7 @@ futures = "0.3.31"
 reqwest = "0.12.15"
 tempfile = "3.19.1"
 arbitrary = "1.4.1"
+multihash = "0.19.3"
 crossterm = "0.29.0"
 color-eyre = "0.6.3"
 jsonrpsee = "0.25.1"
@@ -191,6 +192,7 @@ openssl = "0.10.72"
 tracing-loki = "0.2.6"
 tracing-subscriber = "0.3.19"
 tracing = { version = "0.1.41", default-features = false }
+prometheus = { version = "0.14.0", default-features = false }
 metrics-exporter-prometheus = { version = "0.17.0", default-features = false }
 
 # Testing
diff --git a/crates/node/p2p/Cargo.toml b/crates/node/p2p/Cargo.toml
index 983ae72eb1..26b64a17ae 100644
--- a/crates/node/p2p/Cargo.toml
+++ b/crates/node/p2p/Cargo.toml
@@ -58,20 +58,24 @@ derive_more = { workspace = true, features = ["display", "deref", "debug"] }
 # `arbitrary` feature dependencies
 arbitrary = { workspace = true, features = ["derive"], optional = true }
 
+# `metrics` feature
+prometheus = { workspace = true, features = ["process"], optional = true }
+
 [dev-dependencies]
-kona-cli.workspace = true
-multihash = "0.19.3"
-tempfile.workspace = true
 arbtest.workspace = true
+tempfile.workspace = true
+kona-cli.workspace = true
+multihash.workspace = true
+alloy-eips.workspace = true
+
+rand = { workspace = true, features = ["thread_rng"] }
 arbitrary = { workspace = true, features = ["derive"] }
 alloy-primitives = { workspace = true, features = ["arbitrary"] }
 alloy-rpc-types-engine = { workspace = true, features = ["std"] }
 alloy-consensus = { workspace = true, features = ["arbitrary", "k256"] }
-alloy-eips.workspace = true
-
 op-alloy-consensus = { workspace = true, features = ["arbitrary", "k256"] }
-rand = { workspace = true, features = ["thread_rng"] }
 
 [features]
 default = []
+metrics = ["dep:prometheus"]
 arbitrary = ["dep:arbitrary", "alloy-primitives/arbitrary"]
diff --git a/crates/node/p2p/src/gossip/driver.rs b/crates/node/p2p/src/gossip/driver.rs
index 266309e008..78c6cec3d8 100644
--- a/crates/node/p2p/src/gossip/driver.rs
+++ b/crates/node/p2p/src/gossip/driver.rs
@@ -221,6 +221,9 @@ impl GossipDriver {
     /// Handles the [`SwarmEvent<Event>`].
     pub fn handle_event(&mut self, event: SwarmEvent<Event>) -> Option<OpNetworkPayloadEnvelope> {
         if let SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } = event {
+            let peer_count = self.swarm.connected_peers().count();
+            trace!(target: "gossip", "Connection established: {:?} | Peer Count: {}", peer_id, peer_count);
+            crate::set!(PEER_COUNT, peer_count as i64);
             self.peerstore.insert(peer_id, endpoint.get_remote_address().clone());
             return None;
         }
@@ -232,7 +235,9 @@ impl GossipDriver {
             return None;
         }
         if let SwarmEvent::ConnectionClosed { peer_id, cause, .. } = event {
-            trace!(target: "gossip", "Connection closed, redialing peer: {:?} | {:?}", peer_id, cause);
+            let peer_count = self.swarm.connected_peers().count();
+            trace!(target: "gossip", "Connection closed, redialing peer: {:?} | {:?} | Peer Count: {}", peer_id, cause, peer_count);
+            crate::set!(PEER_COUNT, peer_count as i64);
             self.redial(peer_id);
             return None;
         }
diff --git a/crates/node/p2p/src/lib.rs b/crates/node/p2p/src/lib.rs
index aa548606ff..13f64fa152 100644
--- a/crates/node/p2p/src/lib.rs
+++ b/crates/node/p2p/src/lib.rs
@@ -10,6 +10,13 @@
 #[macro_use]
 extern crate tracing;
 
+mod macros;
+
+#[cfg(feature = "metrics")]
+mod metrics;
+#[cfg(feature = "metrics")]
+pub use metrics::{PEER_COUNT, PEER_SCORES};
+
 mod net;
 pub use net::{Broadcast, Config, Network, NetworkBuilder, NetworkBuilderError};
 
diff --git a/crates/node/p2p/src/macros.rs b/crates/node/p2p/src/macros.rs
new file mode 100644
index 0000000000..3e19f63236
--- /dev/null
+++ b/crates/node/p2p/src/macros.rs
@@ -0,0 +1,10 @@
+//! Macros for recording metrics.
+
+/// Sets a metric value.
+#[macro_export]
+macro_rules! set {
+    ($metric:ident, $value:expr) => {
+        #[cfg(feature = "metrics")]
+        $crate::metrics::$metric.set($value);
+    };
+}
diff --git a/crates/node/p2p/src/metrics/mod.rs b/crates/node/p2p/src/metrics/mod.rs
new file mode 100644
index 0000000000..f0aa8135c2
--- /dev/null
+++ b/crates/node/p2p/src/metrics/mod.rs
@@ -0,0 +1,20 @@
+//! Metrics for the P2P stack.
+
+use lazy_static::lazy_static;
+use prometheus::{self, HistogramVec, IntGauge, register_histogram_vec, register_int_gauge};
+
+lazy_static! {
+    /// Gauge of the number of connected peers.
+    pub static ref PEER_COUNT: IntGauge = register_int_gauge!(
+        "kona_node_peer_count",
+        "Count of currently connected p2p peers"
+    ).expect("Peer count failed to register");
+
+    /// Histogram of currently connected peer scores.
+    pub static ref PEER_SCORES: HistogramVec = register_histogram_vec!(
+        "kona_node_peer_scores",
+        "Histogram of currently connected peer scores",
+        &["type"],
+        vec![-100.0, -40.0, -20.0, -10.0, -5.0, -2.0, -1.0, -0.5, -0.05, 0.0, 0.05, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 40.0]
+    ).expect("Peer scores failed to register");
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a peer count metric into the P2P module. Key changes include:
- Adding a metrics recording block in the RPC request handling.
- Defining new metrics (PEER_COUNT and PEER_SCORES) in the metrics module.
- Updating configuration in Cargo.toml files to include the Prometheus dependency and a new "metrics" feature.

### Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                      | Description                                        |
| ----------------------------------------- | -------------------------------------------------- |
| crates/node/p2p/src/rpc/request.rs         | Recorded the peer count metric using a new macro.  |
| crates/node/p2p/src/metrics/mod.rs          | Introduced metric definitions for peer count and scores. |
| crates/node/p2p/src/macros.rs               | Added a macro for setting metric values.           |
| crates/node/p2p/src/lib.rs                  | Integrated the metrics module and re-exported PEER_SCORES. |
| crates/node/p2p/Cargo.toml                  | Updated dependencies and enabled the metrics feature. |
| Cargo.toml                                | Added global dependencies for multihash and Prometheus. |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T17:29:53Z
user: login: theochap
body: Looks great, it could be useful to think of a way to emit that kind of metrics inside the kona-node binary as well
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-25T17:40:53Z
user: login: emhane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T17:45:05Z

Review comments:
diff_hunk: @@ -10,6 +10,13 @@
 #[macro_use]
 extern crate tracing;
 
+mod macros;
+
+#[cfg(feature = "metrics")]
+mod metrics;
+#[cfg(feature = "metrics")]
+pub use metrics::PEER_SCORES;
path: crates/node/p2p/src/lib.rs
user: login: Copilot
body: If the peer count metric is intended for external use, consider re-exporting PEER_COUNT alongside PEER_SCORES to maintain a consistent public API.
```suggestion
pub use metrics::{PEER_SCORES, PEER_COUNT};
```
created_at: 2025-04-25T17:29:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,10 @@
+//! Macros for recording metrics.
+
+/// Sets a metric value.
+#[macro_export]
+macro_rules! set {
path: crates/node/p2p/src/macros.rs
user: login: emhane
body: ```suggestion
macro_rules! record_metric {
```
a more helpful name would be nice
created_at: 2025-04-25T17:44:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: refcell
created_at: 2025-04-25T17:29:14Z
body: ## 📚 $\text{Stack Overview}$

Pulls submitted in this stack:
* #1527 👈
* `main`

_This comment was automatically generated by [`st`](https://github.com/clabby/st)._
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-25T17:32:11Z
body: ## [Codecov](https://app.codecov.io/gh/op-rs/kona/pull/1527?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=op-rs) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 92.8%. Comparing base [(`b3fb0a0`)](https://app.codecov.io/gh/op-rs/kona/commit/b3fb0a0093fbe3f8067545a5acf55b3b441e2833?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=op-rs) to head [(`ba04864`)](https://app.codecov.io/gh/op-rs/kona/commit/ba0486413c44f998462e7698323a93e2e4e0d679?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=op-rs).
> Report is 1 commits behind head on main.

:white_check_mark: All tests successful. No failed tests found.


[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/op-rs/kona/pull/1527?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=op-rs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=op-rs).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: refcell
created_at: 2025-04-25T17:45:30Z
body: > Looks great, it could be useful to think of a way to emit that kind of metrics inside the kona-node binary as well

We should be able to pick up the metrics through the exported prometheus metrics. Planning on setting up a simple grafana dashboard so we can start to look at these metrics. Maybe it'll help us understand what's going on here.
author_association: CONTRIBUTOR