diff --git a/Cargo.lock b/Cargo.lock index 570a2099..f0c4e741 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,6 +576,9 @@ dependencies = [ "rand 0.8.5", "tempfile", "thiserror 2.0.17", + "tokio", + "vise", + "vise-exporter", ] [[package]] diff --git a/crates/charon-p2p/Cargo.toml b/crates/charon-p2p/Cargo.toml index 42e10b31..21dd70bc 100644 --- a/crates/charon-p2p/Cargo.toml +++ b/crates/charon-p2p/Cargo.toml @@ -7,17 +7,20 @@ license.workspace = true publish.workspace = true [dependencies] +chrono.workspace = true libp2p.workspace = true thiserror.workspace = true k256.workspace = true charon-eth2.workspace = true charon-k1util.workspace = true -chrono.workspace = true +vise.workspace = true +tokio.workspace = true rand.workspace = true [dev-dependencies] charon-testutil.workspace = true tempfile.workspace = true +vise-exporter.workspace = true [lints] workspace = true diff --git a/crates/charon-p2p/examples/metrics.rs b/crates/charon-p2p/examples/metrics.rs new file mode 100644 index 00000000..16d6e583 --- /dev/null +++ b/crates/charon-p2p/examples/metrics.rs @@ -0,0 +1,58 @@ +//! Example demonstrating the charon-p2p metrics functionality. +//! +//! To run this example, run the local Prometheus and Grafana containers: +//! ```bash +//! docker compose -f test-infra/docker-compose.yml up -d +//! ``` +//! +//! Then run the example: +//! ```bash +//! cargo run --example metrics -p charon-p2p +//! ``` +//! +//! Metrics will be available in Grafana at http://localhost:3000. + +use std::net::SocketAddr; + +use charon_p2p::metrics::{ + ConnectionType, Direction, P2P_METRICS, PeerConnectionLabels, PeerNetworkLabels, + PeerStreamLabels, Protocol, RelayConnectionLabels, +}; +use vise_exporter::MetricsExporter; + +#[tokio::main] +async fn main() { + let bind_address = SocketAddr::from(([0, 0, 0, 0], 9464)); + + let exporter = MetricsExporter::default() + .bind(bind_address) + .await + .expect("Failed to bind metrics exporter"); + tokio::spawn(async move { + exporter + .start() + .await + .expect("Failed to start metrics exporter"); + }); + + P2P_METRICS.ping_latency_secs["rust"].observe(1.0); + P2P_METRICS.ping_error_total["rust"].inc(); + P2P_METRICS.ping_success["rust"].set(1); + P2P_METRICS.reachability_status.set(1); + P2P_METRICS.relay_connections["rust"].set(1); + P2P_METRICS.peer_connection_types + [&PeerConnectionLabels::new("rust", ConnectionType::Direct, Protocol::Tcp)] + .set(1); + P2P_METRICS.relay_connection_types + [&RelayConnectionLabels::new("rust", ConnectionType::Direct, Protocol::Tcp)] + .set(1); + P2P_METRICS.peer_streams[&PeerStreamLabels::new("rust", Direction::Inbound, Protocol::Tcp)] + .set(1); + P2P_METRICS.peer_connection_total["rust"].inc(); + P2P_METRICS.peer_network_receive_bytes_total[&PeerNetworkLabels::new("rust", Protocol::Tcp)] + .inc(); + P2P_METRICS.peer_network_sent_bytes_total[&PeerNetworkLabels::new("rust", Protocol::Tcp)].inc(); + + // Wait for 20 seconds to see the logs in Loki + std::thread::sleep(std::time::Duration::from_secs(20)); +} diff --git a/crates/charon-p2p/src/lib.rs b/crates/charon-p2p/src/lib.rs index 2e513476..f7d40c86 100644 --- a/crates/charon-p2p/src/lib.rs +++ b/crates/charon-p2p/src/lib.rs @@ -14,5 +14,8 @@ pub mod name; /// P2P configuration. pub mod config; +/// Metrics. +pub mod metrics; + /// K1 utilities. pub mod k1; diff --git a/crates/charon-p2p/src/metrics.rs b/crates/charon-p2p/src/metrics.rs new file mode 100644 index 00000000..bfadff76 --- /dev/null +++ b/crates/charon-p2p/src/metrics.rs @@ -0,0 +1,149 @@ +use vise::*; + +const BUCKETS: [f64; 11] = [ + 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, +]; + +/// Metrics for the P2P layer. +#[derive(Debug, Metrics)] +#[metrics(prefix = "p2p")] +pub struct P2PMetrics { + /// Ping latencies in seconds per peer + #[metrics(buckets = &BUCKETS, labels = ["peer"])] + pub ping_latency_secs: LabeledFamily, + + /// Total number of ping errors per peer + #[metrics(labels = ["peer"])] + pub ping_error_total: LabeledFamily, + + /// Whether the last ping was successful (1) or not (0). Can be used as + /// proxy for connected peers + #[metrics(labels = ["peer"])] + pub ping_success: LabeledFamily, + + /// Current libp2p reachability status of this node as detected by autonat: + /// unknown(0), public(1) or private(2). + pub reachability_status: Gauge, + + /// Connected relays by name + #[metrics(labels = ["peer"])] + pub relay_connections: LabeledFamily, + + /// Current number of libp2p connections by peer, type (`direct` or + /// `relay`), and protocol (`tcp`, `quic`). Note that peers may have + /// multiple connections. + pub peer_connection_types: Family, + + /// Current number of libp2p connections by relay, type (`direct` or + /// `relay`), and protocol (`tcp`, `quic`). Note that peers may have + /// multiple connections. + pub relay_connection_types: Family, + + /// Current number of libp2p streams by peer, direction ('inbound' or + /// 'outbound' or 'unknown') and protocol. + pub peer_streams: Family, + + /// Total number of libp2p connections per peer. + #[metrics(labels = ["peer"])] + pub peer_connection_total: LabeledFamily, + + /// Total number of network bytes received from the peer by protocol. + pub peer_network_receive_bytes_total: Family, + + /// Total number of network bytes sent to the peer by protocol. + pub peer_network_sent_bytes_total: Family, +} + +/// The type of connection. +#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelValue)] +#[metrics(rename_all = "snake_case")] +pub enum ConnectionType { + /// A direct connection to a peer. + Direct, + /// A connection to a relay. + Relay, +} + +/// The direction of a connection. +#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelValue)] +#[metrics(rename_all = "snake_case")] +pub enum Direction { + /// An inbound connection. + Inbound, + /// An outbound connection. + Outbound, + /// An unknown connection. + Unknown, +} + +/// The protocol of a connection. +#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelValue)] +#[metrics(rename_all = "snake_case")] +pub enum Protocol { + /// A TCP connection. + Tcp, + /// A QUIC connection. + Quic, +} + +/// Labels for peer connections. +#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelSet)] +pub struct PeerConnectionLabels { + peer: String, + r#type: ConnectionType, + protocol: Protocol, +} + +impl PeerConnectionLabels { + /// Creates a new peer connection labels. + pub fn new(peer: &str, r#type: ConnectionType, protocol: Protocol) -> Self { + Self { + peer: peer.to_string(), + r#type, + protocol, + } + } +} + +/// Relay connection labels +pub type RelayConnectionLabels = PeerConnectionLabels; + +/// Labels for peer streams. +#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelSet)] +pub struct PeerStreamLabels { + peer: String, + direction: Direction, + protocol: Protocol, +} + +impl PeerStreamLabels { + /// Creates a new peer stream labels. + pub fn new(peer: &str, direction: Direction, protocol: Protocol) -> Self { + Self { + peer: peer.to_string(), + direction, + protocol, + } + } +} + +/// Labels for peer network. +#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelSet)] +pub struct PeerNetworkLabels { + peer: String, + protocol: Protocol, +} + +impl PeerNetworkLabels { + /// Creates a new peer network labels. + pub fn new(peer: &str, protocol: Protocol) -> Self { + Self { + peer: peer.to_string(), + protocol, + } + } +} + +/// Global metrics for the P2P layer. +#[vise::register] +pub static P2P_METRICS: Global = Global::new(); diff --git a/crates/tracing/examples/test-infra/docker-compose.yml b/test-infra/docker-compose.yml similarity index 100% rename from crates/tracing/examples/test-infra/docker-compose.yml rename to test-infra/docker-compose.yml diff --git a/crates/tracing/examples/test-infra/prometheus.yml b/test-infra/prometheus.yml similarity index 100% rename from crates/tracing/examples/test-infra/prometheus.yml rename to test-infra/prometheus.yml diff --git a/crates/tracing/examples/test-infra/promtail-config.yml b/test-infra/promtail-config.yml similarity index 100% rename from crates/tracing/examples/test-infra/promtail-config.yml rename to test-infra/promtail-config.yml