aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/stats
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/stats')
-rw-r--r--src/api/stats/prometheus.rs234
1 files changed, 142 insertions, 92 deletions
diff --git a/src/api/stats/prometheus.rs b/src/api/stats/prometheus.rs
index 690effa..0a1d75d 100644
--- a/src/api/stats/prometheus.rs
+++ b/src/api/stats/prometheus.rs
@@ -4,12 +4,12 @@
use std::fmt::Write;
-use tokio::time::Instant;
-
use super::StatsResponse;
-use crate::locks::MutexExt;
-use crate::state::{AppState, Incidents, Liveness};
+use crate::api::media_type;
+use crate::api::peers::{PeerListResponse, PeerLivenessStatus};
+use crate::state::Incidents;
use crate::transport::Destination;
+use crate::transport::paths::ClockOffset;
use crate::transport::stats::{Reachability, TransportStats};
use crate::transport::traffic::{ByKind, Counter, PeerTraffic};
@@ -19,10 +19,10 @@ pub(super) const CONTENT_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8"
/// plain text; browsers and curl don't name either.
pub(super) fn wanted(accept: Option<&str>) -> bool {
accept.is_some_and(|accept| {
- accept.split(',').any(|item| {
- let media = item.split(';').next().unwrap_or("").trim();
- media == "application/openmetrics-text" || media == "text/plain"
- })
+ accept
+ .split(',')
+ .map(media_type)
+ .any(|media| media == "application/openmetrics-text" || media == "text/plain")
})
}
@@ -50,6 +50,32 @@ impl Out {
let _ = writeln!(self.0, " {value}");
}
+ /// One histogram's series (its family declared before): each bucket counting what
+ /// fell into it and every one before (upper bounds in seconds, the last unbounded),
+ /// the sum (seconds) and the count.
+ fn histogram(
+ &mut self,
+ name: &str,
+ labels: &[(&str, &str)],
+ buckets: impl Iterator<Item = (Option<f64>, u64)>,
+ sum: f64,
+ count: u64,
+ ) {
+ let mut cumulative = 0;
+ for (le, in_bucket) in buckets {
+ cumulative += in_bucket;
+ let le = match le {
+ Some(seconds) => seconds.to_string(),
+ None => "+Inf".to_owned(),
+ };
+ let mut with_le = labels.to_vec();
+ with_le.push(("le", &le));
+ self.sample(&format!("{name}_bucket"), &with_le, cumulative as f64);
+ }
+ self.sample(&format!("{name}_sum"), labels, sum);
+ self.sample(&format!("{name}_count"), labels, count as f64);
+ }
+
/// A family with one unlabelled sample.
fn single(&mut self, name: &str, kind: &str, help: &str, value: f64) {
self.family(name, kind, help);
@@ -126,45 +152,30 @@ struct Peer {
incidents: Incidents,
throughput: Option<f64>,
destination: Option<Destination>,
+ clock: Option<ClockOffset>,
}
-async fn peers(state: &AppState) -> Vec<Peer> {
- let now = Instant::now();
- let snapshot = state.peers_snapshot().await;
- let mut aliases = crate::swarm::members::aliases(&state.doc.read().await.doc);
- let mut peers = Vec::with_capacity(snapshot.len());
- for (key, peer_state) in snapshot {
- let neighbour = crate::swarm::topology::is_neighbour(state, &key);
- let throughput = state
- .throughput
- .lock_or_recover()
- .get(&key)
- .map(|estimate| estimate.bytes_per_sec);
- let traffic = state.traffic.peer(&key).unwrap_or_default();
- let peer_state = peer_state.lock().await;
- peers.push(Peer {
- neighbour,
- unknown: peer_state.liveness == Liveness::Unknown,
- dormant: matches!(peer_state.liveness, Liveness::Dormant { .. }),
- last_seen_secs: peer_state
- .last_seen
- .map(|t| now.saturating_duration_since(t).as_secs_f64()),
- last_sync_secs: peer_state
- .last_sync_rx
- .map(|t| now.saturating_duration_since(t).as_secs_f64()),
- diverged_secs: peer_state
- .diverged_since
- .map(|t| now.saturating_duration_since(t).as_secs_f64()),
- traffic,
- incidents: peer_state.incidents,
- throughput,
- alias: aliases.remove(&key),
- destination: state.transport.destination(&key),
- key,
- });
- }
- peers.sort_by(|a, b| a.key.cmp(&b.key));
- peers
+/// The rows of `/peers` (`peers::peer_list`), as the series need them.
+fn peers(list: &PeerListResponse) -> Vec<Peer> {
+ let secs = |secs: Option<u64>| secs.map(|secs| secs as f64);
+ list.peers
+ .iter()
+ .map(|(key, peer)| Peer {
+ key: key.clone(),
+ alias: peer.alias.clone(),
+ neighbour: peer.neighbour,
+ unknown: matches!(peer.status, PeerLivenessStatus::Unknown),
+ dormant: matches!(peer.status, PeerLivenessStatus::Dormant),
+ last_seen_secs: secs(peer.last_seen_secs_ago),
+ last_sync_secs: secs(peer.last_sync_secs_ago),
+ diverged_secs: secs(peer.diverged_for_secs),
+ traffic: peer.traffic,
+ incidents: peer.incidents,
+ throughput: peer.sync_throughput.as_ref().map(|t| t.bytes_per_sec),
+ destination: peer.destination.clone(),
+ clock: peer.clock,
+ })
+ .collect()
}
/// Every (direction, kind, counter) of `traffic`.
@@ -179,6 +190,7 @@ fn traffic_series(traffic: &PeerTraffic) -> Vec<(&'static str, &'static str, Cou
(direction, "blob", by.blob),
(direction, "identity", by.identity),
(direction, "paths", by.paths),
+ (direction, "links", by.links),
]
};
let mut series = Vec::new();
@@ -203,6 +215,19 @@ fn transports(out: &mut Out, stats: &TransportStats) {
*nodes as f64,
);
}
+ out.family(
+ "identity_relayed_nodes",
+ "gauge",
+ "Proven nodes a transport reaches only through a relay, by transport.",
+ );
+ for transport in identity.proven.keys() {
+ let relayed = identity.relayed.get(transport).copied().unwrap_or(0);
+ out.sample(
+ "identity_relayed_nodes",
+ &[("transport", transport)],
+ relayed as f64,
+ );
+ }
out.single(
"identity_hinted_nodes",
"gauge",
@@ -245,7 +270,7 @@ fn transports(out: &mut Out, stats: &TransportStats) {
out.family(
"paths_probes_total",
"counter",
- "PINGs to paths of peers with several, by result.",
+ "PINGs to paths in use, by result.",
);
for (result, count) in [
("sent", counts.probes_sent),
@@ -385,7 +410,8 @@ pub(super) fn render_relay_only(stats: &super::RelayOnlyStats) -> String {
/// A per-peer incident counter: its name, its help and how to read it.
type IncidentSeries = (&'static str, &'static str, fn(&Incidents) -> f64);
-pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
+/// `stats` and `list` (`/peers`) as series: nothing read anew.
+pub(super) fn render(stats: &StatsResponse, list: &PeerListResponse) -> String {
let mut out = Out::default();
// Names for joining (`* on(peer) group_left(alias) continuum_peer_info`), kept out
@@ -416,7 +442,7 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
"epoch_generation",
"gauge",
"Generation of the local epoch (remakes so far).",
- state.local_epoch().generation as f64,
+ stats.epoch.generation as f64,
);
let document = &stats.document;
@@ -448,28 +474,14 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
);
let lock_wait = &document.lock_wait;
for (mode, waits) in [("read", &lock_wait.read), ("write", &lock_wait.write)] {
- let mut cumulative = 0;
- for (le_us, count) in waits.buckets() {
- cumulative += count;
- let le = match le_us {
- Some(us) => (us as f64 / 1e6).to_string(),
- None => "+Inf".to_owned(),
- };
- out.sample(
- "document_lock_wait_seconds_bucket",
- &[("mode", mode), ("le", &le)],
- cumulative as f64,
- );
- }
- out.sample(
- "document_lock_wait_seconds_sum",
+ out.histogram(
+ "document_lock_wait_seconds",
&[("mode", mode)],
+ waits
+ .buckets()
+ .map(|(le_us, count)| (le_us.map(|us| us as f64 / 1e6), count)),
waits.sum_us as f64 / 1e6,
- );
- out.sample(
- "document_lock_wait_seconds_count",
- &[("mode", mode)],
- waits.count as f64,
+ waits.count,
);
}
out.family(
@@ -732,38 +744,47 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
out.family(
"propagation_delay_seconds",
"histogram",
- "Time from a change's commit elsewhere until applied here (includes clock skew).",
+ "Time from a change's commit elsewhere until applied here, by the peers' clocks (includes the writer's clock skew).",
);
- let mut cumulative = 0;
- for bucket in &propagation.buckets {
- cumulative += bucket.count;
- let le = match bucket.le_ms {
- Some(ms) => (ms as f64 / 1000.0).to_string(),
- None => "+Inf".to_owned(),
- };
- out.sample(
- "propagation_delay_seconds_bucket",
- &[("le", &le)],
- cumulative as f64,
- );
- }
- out.sample(
- "propagation_delay_seconds_sum",
+ out.histogram(
+ "propagation_delay_seconds",
&[],
+ propagation
+ .buckets
+ .iter()
+ .map(|bucket| (bucket.le_ms.map(|ms| ms as f64 / 1000.0), bucket.count)),
propagation.sum_ms as f64 / 1000.0,
- );
- out.sample(
- "propagation_delay_seconds_count",
- &[],
- propagation.count as f64,
+ propagation.count,
);
out.single(
"propagation_clock_ahead_total",
"counter",
- "Changes stamped later than this node's clock on arrival.",
+ "Changes stamped later than their arrival, by the peers' clocks.",
propagation.ahead as f64,
);
+ let clock = &stats.clock;
+ out.single(
+ "clock_peers_compared",
+ "gauge",
+ "Active peers whose clocks were compared with ours (from their PONGs).",
+ clock.peers as f64,
+ );
+ if let Some(offset_ms) = clock.offset_ms {
+ out.single(
+ "clock_offset_seconds",
+ "gauge",
+ "How far this node's clock is ahead of its peers' (behind: negative), the median.",
+ offset_ms as f64 / 1000.0,
+ );
+ }
+ out.single(
+ "clock_off",
+ "gauge",
+ "1 while this node's clock differs from most of its peers' by more than --clock-skew-warn.",
+ flag(clock.off),
+ );
+
// Node totals, including peers no longer listed.
let totals = traffic_series(&stats.traffic);
out.family(
@@ -793,7 +814,7 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
transports(&mut out, &stats.transports);
- let peers = peers(state).await;
+ let peers = peers(list);
out.family(
"peer_info",
@@ -838,7 +859,7 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
out.family(
"peer_path_rtt_seconds",
"gauge",
- "Smoothed round-trip time of the PINGs on each path to a peer with several.",
+ "Smoothed round-trip time of the PINGs on each path to a peer.",
);
for peer in &peers {
for path in peer.destination.iter().flat_map(|d| &d.paths) {
@@ -932,6 +953,35 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
}
out.family(
+ "peer_clock_offset_seconds",
+ "gauge",
+ "How far the peer's clock is ahead of ours (behind: negative), from its PONGs.",
+ );
+ for peer in &peers {
+ if let Some(clock) = peer.clock {
+ out.sample(
+ "peer_clock_offset_seconds",
+ &[("peer", &peer.key)],
+ clock.offset_ms as f64 / 1000.0,
+ );
+ }
+ }
+ out.family(
+ "peer_clock_offset_within_seconds",
+ "gauge",
+ "Give or take this much: half the round trip it was measured over.",
+ );
+ for peer in &peers {
+ if let Some(clock) = peer.clock {
+ out.sample(
+ "peer_clock_offset_within_seconds",
+ &[("peer", &peer.key)],
+ clock.within_ms as f64 / 1000.0,
+ );
+ }
+ }
+
+ out.family(
"peer_messages_total",
"counter",
"Messages by direction and kind.",