summaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-27 05:02:39 +0100
committertv <tv@krebsco.de>2026-10-27 05:03:29 +0100
commitb5f756c3f623c41830aa6129a0fd63bd3f99a7ba (patch)
tree9faeb383fc899bf499749e843d4d4fb65b08adfd /src/api
parent16d7da71c49b62dd975957e5694088b8b084f244 (diff)
add transport stats
Diffstat (limited to 'src/api')
-rw-r--r--src/api/peers.rs6
-rw-r--r--src/api/stats.rs4
-rw-r--r--src/api/stats/prometheus.rs174
3 files changed, 184 insertions, 0 deletions
diff --git a/src/api/peers.rs b/src/api/peers.rs
index 9e907ac..ac73296 100644
--- a/src/api/peers.rs
+++ b/src/api/peers.rs
@@ -17,6 +17,7 @@ use crate::document::store;
use crate::locks::MutexExt;
use crate::state::{AppState, HelloState, Incidents, Liveness};
use crate::swarm::peer::forgotten_path;
+use crate::transport::Destination;
use crate::transport::traffic::PeerTraffic;
#[derive(Debug, Serialize)]
@@ -50,6 +51,9 @@ pub(crate) struct PeerStatus {
alias: Option<String>,
/// Where this node reaches it, or would (`Transport::addresses_of`).
addresses: Vec<String>,
+ /// Where messages to it go now: the address, whether the peer proved itself there,
+ /// and the connection there (libp2p: direct or relayed). Absent if nowhere.
+ destination: Option<Destination>,
validated_swarm: Option<String>,
status: PeerLivenessStatus,
handshake_complete: bool,
@@ -146,11 +150,13 @@ pub(crate) async fn list_peers(
let alias = aliases.remove(&peer);
let addresses = state.transport.addresses_of(&peer);
+ let destination = state.transport.destination(&peer);
peers.insert(
peer,
PeerStatus {
alias,
addresses,
+ destination,
validated_swarm: ps.standing.swarm_id().map(str::to_owned),
status,
handshake_complete: ps.hello_state == HelloState::Established,
diff --git a/src/api/stats.rs b/src/api/stats.rs
index fa64600..45d91da 100644
--- a/src/api/stats.rs
+++ b/src/api/stats.rs
@@ -16,6 +16,7 @@ use serde::Serialize;
use super::peers::EpochInfo;
use crate::state::{AppState, Incidents};
use crate::swarm::{members, stability, topology};
+use crate::transport::stats::TransportStats;
use crate::transport::traffic::PeerTraffic;
#[derive(Debug, Serialize)]
@@ -44,6 +45,8 @@ pub(crate) struct StatsResponse {
propagation: PropagationStats,
/// This node's own files (`files`), by kind.
disk: BTreeMap<&'static str, DiskStats>,
+ /// How the transports find and reach peers (`Transport::stats`).
+ transports: TransportStats,
}
/// Reads and writes of one kind of file, since the node started.
@@ -316,5 +319,6 @@ async fn collect(state: &AppState) -> StatsResponse {
(kind.as_str(), stats)
})
.collect(),
+ transports: state.transport.stats(),
}
}
diff --git a/src/api/stats/prometheus.rs b/src/api/stats/prometheus.rs
index 01cd271..8c6e3e6 100644
--- a/src/api/stats/prometheus.rs
+++ b/src/api/stats/prometheus.rs
@@ -9,6 +9,8 @@ use tokio::time::Instant;
use super::StatsResponse;
use crate::locks::MutexExt;
use crate::state::{AppState, Incidents, Liveness};
+use crate::transport::Destination;
+use crate::transport::stats::{Reachability, TransportStats};
use crate::transport::traffic::{ByKind, Counter, PeerTraffic};
pub(super) const CONTENT_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8";
@@ -118,6 +120,7 @@ struct Peer {
incidents: Incidents,
rtt_secs: Option<f64>,
throughput: Option<f64>,
+ destination: Option<Destination>,
}
async fn peers(state: &AppState) -> Vec<Peer> {
@@ -155,6 +158,7 @@ async fn peers(state: &AppState) -> Vec<Peer> {
rtt_secs,
throughput,
alias: aliases.remove(&key),
+ destination: state.transport.destination(&key),
key,
});
}
@@ -182,6 +186,147 @@ fn traffic_series(traffic: &PeerTraffic) -> Vec<(&'static str, &'static str, Cou
series
}
+/// The transports' figures (`TransportStats`).
+fn transports(out: &mut Out, stats: &TransportStats) {
+ if let Some(identity) = &stats.identity {
+ out.family(
+ "identity_proven_nodes",
+ "gauge",
+ "Nodes proven at an address, by transport.",
+ );
+ for (transport, nodes) in &identity.proven {
+ out.sample(
+ "identity_proven_nodes",
+ &[("transport", transport)],
+ *nodes as f64,
+ );
+ }
+ out.single(
+ "identity_hinted_nodes",
+ "gauge",
+ "Nodes known only from hints, proven nowhere.",
+ identity.hinted as f64,
+ );
+ let counts = &identity.counts;
+ for (name, help, value) in [
+ (
+ "identity_proofs_rejected_total",
+ "Identity proofs that didn't hold.",
+ counts.proofs_rejected,
+ ),
+ (
+ "identity_address_changes_total",
+ "Nodes that proved themselves at another address on a transport than before.",
+ counts.address_changes,
+ ),
+ (
+ "identity_dropped_unproven_total",
+ "Messages dropped because no node proved itself at their address.",
+ counts.dropped_unproven,
+ ),
+ (
+ "identity_refused_unproven_total",
+ "Sends refused because the peer hadn't proven where it is yet.",
+ counts.refused_unproven,
+ ),
+ (
+ "identity_seeds_unanswered_total",
+ "Seeds no node answered at in time.",
+ counts.seeds_unanswered,
+ ),
+ ] {
+ out.single(name, "counter", help, value as f64);
+ }
+ }
+ if let Some(libp2p) = &stats.libp2p {
+ out.family(
+ "libp2p_reachability",
+ "gauge",
+ "What AutoNAT says: 1 for the current status.",
+ );
+ for status in Reachability::ALL {
+ out.sample(
+ "libp2p_reachability",
+ &[("status", status.as_str())],
+ flag(libp2p.reachability == status),
+ );
+ }
+ out.family(
+ "libp2p_connections",
+ "gauge",
+ "Open libp2p connections, direct or through a relay.",
+ );
+ for (kind, count) in [
+ ("direct", libp2p.connections.direct),
+ ("relayed", libp2p.connections.relayed),
+ ] {
+ out.sample("libp2p_connections", &[("kind", kind)], count as f64);
+ }
+ let slots = &libp2p.relay_slots;
+ out.single(
+ "libp2p_relay_slots",
+ "gauge",
+ "Slots held on relays, to be reachable through them.",
+ slots.held as f64,
+ );
+ out.single(
+ "libp2p_relays_paused",
+ "gauge",
+ "Relays picked that never granted a slot, not picked again for a while.",
+ slots.paused as f64,
+ );
+ out.family(
+ "libp2p_relay_slot_events_total",
+ "counter",
+ "Relay slots granted, lost after being granted, and never granted.",
+ );
+ for (event, count) in [
+ ("granted", slots.granted),
+ ("lost", slots.lost),
+ ("not_granted", slots.not_granted),
+ ] {
+ out.sample(
+ "libp2p_relay_slot_events_total",
+ &[("event", event)],
+ count as f64,
+ );
+ }
+ out.family(
+ "libp2p_hole_punches_total",
+ "counter",
+ "Direct connections tried over a relayed one (DCUtR), by result.",
+ );
+ for (result, count) in [
+ ("succeeded", libp2p.hole_punches.succeeded),
+ ("failed", libp2p.hole_punches.failed),
+ ] {
+ out.sample(
+ "libp2p_hole_punches_total",
+ &[("result", result)],
+ count as f64,
+ );
+ }
+ let failures = &libp2p.dial_failures;
+ out.family(
+ "libp2p_dial_failures_total",
+ "counter",
+ "libp2p connections that couldn't be made, by cause.",
+ );
+ for (cause, count) in [
+ ("unreachable", failures.unreachable),
+ ("wrong_peer_id", failures.wrong_peer_id),
+ ("no_addresses", failures.no_addresses),
+ ("other", failures.other),
+ ] {
+ out.sample(
+ "libp2p_dial_failures_total",
+ &[("cause", cause)],
+ count as f64,
+ );
+ }
+ }
+}
+
/// A per-peer incident counter: its name, its help and how to read it.
type IncidentSeries = (&'static str, &'static str, fn(&Incidents) -> f64);
@@ -503,6 +648,8 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
);
}
+ transports(&mut out, &stats.transports);
+
let peers = peers(state).await;
out.family(
@@ -521,6 +668,33 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
);
}
+ out.family(
+ "peer_destination_info",
+ "gauge",
+ "Where messages to the peer go: its transport, whether it proved itself there, and the connection (always 1).",
+ );
+ for peer in &peers {
+ if let Some(destination) = &peer.destination {
+ let transport = destination
+ .address
+ .split_once(':')
+ .map_or("", |(scheme, _)| scheme);
+ out.sample(
+ "peer_destination_info",
+ &[
+ ("peer", &peer.key),
+ ("transport", transport),
+ ("proven", if destination.proven { "true" } else { "false" }),
+ (
+ "connection",
+ destination.connection.map_or("", |c| c.as_str()),
+ ),
+ ],
+ 1.0,
+ );
+ }
+ }
+
out.family("peer_neighbour", "gauge", "1 if the peer is a neighbour.");
for peer in &peers {
out.sample(