summaryrefslogtreecommitdiffstats
path: root/src/api/stats
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/stats
parent16d7da71c49b62dd975957e5694088b8b084f244 (diff)
add transport stats
Diffstat (limited to 'src/api/stats')
-rw-r--r--src/api/stats/prometheus.rs174
1 files changed, 174 insertions, 0 deletions
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(