mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Add historical node and per-peer statistics with btop-style graphs (#64)
In-memory time-series history on the daemon: fast ring (1s × 3600) plus slow ring (1m × 1440) per metric, covering node-level gauges (mesh size, tree depth, peer count, active sessions), counters (parent switches, aggregate bytes/packets in/out), loss rate, and seven per-peer metrics keyed by NodeAddr (srtt_ms, loss_rate, bytes_in/out, packets_in/out, ecn_ce). The slow ring is produced by downsampling the fast ring on minute boundaries with Last / Sum / Mean aggregation chosen per metric type. Missing data is first-class. New peers back-fill NaN so every ring shares a time axis with the node rings; peers absent from a tick sample NaN (keeps alignment, shows as a visible gap); counter metrics emit NaN on decrease (new link_stats baseline after reconnect) so deltas aren't polluted. Peers are evicted 24h after last contact. Downsampling is NaN-aware: mean skips NaN, all-NaN slow windows stay NaN. Each history window always returns its full span at the chosen density (1m / 10m / 1h / 24h), front-padded with NaN when the ring hasn't yet accumulated enough samples, so switching between windows feels like zooming in or out rather than clipping to whatever has arrived. NaN serializes to JSON null via a custom serializer. Control socket queries: - show_stats_list enumerates registered metrics plus scope field and peer_retention_seconds. - show_stats_history returns one metric's series for a given window and granularity; accepts optional peer (npub) for per-peer metrics. - show_stats_all_history returns every metric in a single round trip; accepts optional peer to fetch all seven per-peer metrics. - show_stats_peers enumerates tracked peers with lifecycle metadata. - show_stats_history_all_peers returns one metric across all peers for grid rendering. - show_status carries short sparkline windows for the dashboard so the client can render without extra fetches. fipsctl gains `stats list`, `stats peers`, and `stats history <metric>` with `--peer` (hostname or npub) and `--plot` for a Unicode block sparkline. Plot header reports sample count, granularity, window, and gap count; NaN renders as a blank cell. fipstop dashboard grows inline sparklines (peer count, mesh size, aggregate bytes in/out). A new Graphs tab stacks every metric as an independent mini plot with its own autoscaled range; each plot uses btop's braille 2×4 filled-area algorithm (25-entry lookup table packing two samples per character, per-row gradient coloring for the characteristic btop vertical-band look, rounded borders with embedded titles). Three modes are cycled with `m`: Node (node-level stack), MetricByPeer (small-multiples grid, 1 / 2 / 3 columns by terminal width), PeerByMetric (existing stack scoped to one peer). `n` / `N` cycles the mode-specific selector (metric or peer), a selector row shows the current choice, and Graphs-tab refreshes re-fetch show_stats_peers so selectors track peer churn. Up / Down scrolls the stack, Left / Right cycles the window, `g` jumps to the tab. Implements IDEA-0084 (TASK-2026-0062).
This commit is contained in:
@@ -100,7 +100,7 @@ impl Node {
|
||||
}
|
||||
Some((request, response_tx)) = control_rx.recv() => {
|
||||
let response = if request.command.starts_with("show_") {
|
||||
queries::dispatch(self, &request.command)
|
||||
queries::dispatch(self, &request.command, request.params.as_ref())
|
||||
} else {
|
||||
commands::dispatch(
|
||||
self,
|
||||
@@ -125,6 +125,7 @@ impl Node {
|
||||
self.check_tree_state().await;
|
||||
self.check_bloom_state().await;
|
||||
self.compute_mesh_size();
|
||||
self.record_stats_history();
|
||||
self.check_mmp_reports().await;
|
||||
self.check_session_mmp_reports().await;
|
||||
self.check_link_heartbeats().await;
|
||||
|
||||
@@ -14,6 +14,7 @@ mod routing_error_rate_limit;
|
||||
pub(crate) mod session;
|
||||
pub(crate) mod session_wire;
|
||||
pub(crate) mod stats;
|
||||
pub(crate) mod stats_history;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod tree;
|
||||
@@ -358,6 +359,9 @@ pub struct Node {
|
||||
/// Routing, forwarding, discovery, and error signal counters.
|
||||
stats: stats::NodeStats,
|
||||
|
||||
/// Time-series history of node-level metrics (1s/1m rings).
|
||||
stats_history: stats_history::StatsHistory,
|
||||
|
||||
// === TUN Interface ===
|
||||
/// TUN device state.
|
||||
tun_state: TunState,
|
||||
@@ -535,6 +539,7 @@ impl Node {
|
||||
next_link_id: 1,
|
||||
next_transport_id: 1,
|
||||
stats: stats::NodeStats::new(),
|
||||
stats_history: stats_history::StatsHistory::new(),
|
||||
tun_state,
|
||||
tun_name: None,
|
||||
tun_tx: None,
|
||||
@@ -644,6 +649,7 @@ impl Node {
|
||||
next_link_id: 1,
|
||||
next_transport_id: 1,
|
||||
stats: stats::NodeStats::new(),
|
||||
stats_history: stats_history::StatsHistory::new(),
|
||||
tun_state,
|
||||
tun_name: None,
|
||||
tun_tx: None,
|
||||
@@ -1115,6 +1121,70 @@ impl Node {
|
||||
&mut self.stats
|
||||
}
|
||||
|
||||
/// Get the stats history collector.
|
||||
pub fn stats_history(&self) -> &stats_history::StatsHistory {
|
||||
&self.stats_history
|
||||
}
|
||||
|
||||
/// Sample the current node state into the stats history ring.
|
||||
/// Called once per tick from the RX loop.
|
||||
pub(crate) fn record_stats_history(&mut self) {
|
||||
let fwd = &self.stats.forwarding;
|
||||
let peers_with_mmp: Vec<f64> = self
|
||||
.peers
|
||||
.values()
|
||||
.filter_map(|p| p.mmp().map(|m| m.metrics.loss_rate()))
|
||||
.collect();
|
||||
let loss_rate = if peers_with_mmp.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
peers_with_mmp.iter().sum::<f64>() / peers_with_mmp.len() as f64
|
||||
};
|
||||
|
||||
let snap = stats_history::Snapshot {
|
||||
mesh_size: self.estimated_mesh_size,
|
||||
tree_depth: self.tree_state.my_coords().depth() as u32,
|
||||
peer_count: self.peers.len() as u64,
|
||||
parent_switches_total: self.stats.tree.parent_switches,
|
||||
bytes_in_total: fwd.received_bytes,
|
||||
bytes_out_total: fwd.forwarded_bytes + fwd.originated_bytes,
|
||||
packets_in_total: fwd.received_packets,
|
||||
packets_out_total: fwd.forwarded_packets + fwd.originated_packets,
|
||||
loss_rate,
|
||||
active_sessions: self.sessions.len() as u64,
|
||||
};
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
let peer_snaps: Vec<stats_history::PeerSnapshot> = self
|
||||
.peers
|
||||
.values()
|
||||
.map(|p| {
|
||||
let stats = p.link_stats();
|
||||
let (srtt_ms, loss_rate, ecn_ce) = match p.mmp() {
|
||||
Some(m) => (
|
||||
m.metrics.srtt_ms(),
|
||||
Some(m.metrics.loss_rate()),
|
||||
m.receiver.ecn_ce_count() as u64,
|
||||
),
|
||||
None => (None, None, 0),
|
||||
};
|
||||
stats_history::PeerSnapshot {
|
||||
node_addr: *p.node_addr(),
|
||||
last_seen: now,
|
||||
srtt_ms,
|
||||
loss_rate,
|
||||
bytes_in_total: stats.bytes_recv,
|
||||
bytes_out_total: stats.bytes_sent,
|
||||
packets_in_total: stats.packets_recv,
|
||||
packets_out_total: stats.packets_sent,
|
||||
ecn_ce_total: ecn_ce,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.stats_history.tick(now, &snap, &peer_snaps);
|
||||
}
|
||||
|
||||
// === TUN Interface ===
|
||||
|
||||
/// Get the TUN state.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user