Implement comprehensive node and transport statistics

Add 71 new counters (84 values) across three categories:

Node statistics (NodeStats, plain u64 — single handler context):
- Forwarding: 9 counters x (packets + bytes) = 18 values. Covers
  received, decode_error, ttl_exhausted, delivered, forwarded,
  drop_no_route, drop_mtu_exceeded, drop_send_error, originated.
- Discovery: 17 counters (packets only). Request path: received,
  decode_error, duplicate, already_visited, target_is_us, forwarded,
  ttl_exhausted, initiated, deduplicated. Response path: received,
  decode_error, forwarded, identity_miss, proof_failed, accepted,
  timed_out.
- Error signals: 3 counters — coords_required, path_broken,
  mtu_exceeded.
- Spanning tree: 16 counters. Inbound announce handling (received
  through accepted, parent switch, loop detection, ancestry change),
  outbound (sent, rate limited, send failed), cumulative events
  (parent switches/losses, flap dampening).
- Bloom filter: 10 counters. Inbound (received through accepted),
  outbound (sent, debounce suppressed, send failed).

Transport statistics (AtomicU64 + Arc — shared with spawned tasks):
- UDP (6 counters, 8 values): packets/bytes sent/recv, send_errors,
  recv_errors, mtu_exceeded, kernel_drops (stub for SO_MEMINFO).
- TCP (10 counters, 12 values): packets/bytes sent/recv, send_errors,
  recv_errors, mtu_exceeded, plus connection lifecycle counters
  (established, accepted, rejected, timeouts, refused).

Control socket integration:
- show_routing: forwarding, discovery, error signal stats
- show_tree: spanning tree stats + per-peer bloom metrics
  (estimated_count, set_bits, fill_ratio) and coordinate paths
- show_bloom: bloom filter stats + per-peer snapshots
- show_transports: per-transport stats snapshots

Also refactor UDP transport from flat files (udp.rs + udp_stats.rs)
into directory module (udp/mod.rs + udp/stats.rs) matching TCP
structure, and fix pre-existing clippy warnings in tree/tests.rs.
This commit is contained in:
Johnathan Corgan
2026-02-28 18:23:04 +00:00
parent 5c1cbb4c30
commit 71a5c68fa9
15 changed files with 864 additions and 49 deletions
+44 -18
View File
@@ -48,6 +48,7 @@ impl Node {
if !peer.can_send_tree_announce(now_ms) {
peer.mark_tree_announce_pending();
self.stats_mut().tree.rate_limited += 1;
debug!(
peer = %self.peer_display_name(peer_addr),
"TreeAnnounce rate-limited, marking pending"
@@ -63,7 +64,12 @@ impl Node {
})?;
// Send
self.send_encrypted_link_message(peer_addr, &encoded).await?;
if let Err(e) = self.send_encrypted_link_message(peer_addr, &encoded).await {
self.stats_mut().tree.send_failed += 1;
return Err(e);
}
self.stats_mut().tree.sent += 1;
// Record send time
if let Some(peer) = self.peers.get_mut(peer_addr) {
@@ -122,9 +128,12 @@ impl Node {
/// 4. Re-evaluate parent selection
/// 5. If parent changed: increment seq, sign, recompute coords, announce to all
pub(super) async fn handle_tree_announce(&mut self, from: &NodeAddr, payload: &[u8]) {
self.stats_mut().tree.received += 1;
let announce = match TreeAnnounce::decode(payload) {
Ok(a) => a,
Err(e) => {
self.stats_mut().tree.decode_error += 1;
debug!(from = %self.peer_display_name(from), error = %e, "Malformed TreeAnnounce");
return;
}
@@ -134,6 +143,7 @@ impl Node {
let pubkey = match self.peers.get(from) {
Some(peer) => peer.pubkey(),
None => {
self.stats_mut().tree.unknown_peer += 1;
debug!(from = %self.peer_display_name(from), "TreeAnnounce from unknown peer");
return;
}
@@ -141,6 +151,7 @@ impl Node {
// The declaring node_addr in the announce should match the sender
if announce.declaration.node_addr() != from {
self.stats_mut().tree.addr_mismatch += 1;
debug!(
from = %self.peer_display_name(from),
declared = %announce.declaration.node_addr(),
@@ -150,6 +161,7 @@ impl Node {
}
if let Err(e) = announce.declaration.verify(&pubkey) {
self.stats_mut().tree.sig_failed += 1;
warn!(
from = %self.peer_display_name(from),
error = %e,
@@ -179,10 +191,13 @@ impl Node {
);
if !updated {
self.stats_mut().tree.stale += 1;
debug!(from = %self.peer_display_name(from), "TreeAnnounce not fresher than existing, ignored");
return;
}
self.stats_mut().tree.accepted += 1;
info!(
from = %self.peer_display_name(from),
seq = announce.declaration.sequence(),
@@ -215,6 +230,9 @@ impl Node {
self.tree_state.recompute_coords();
self.coord_cache.clear();
self.stats_mut().tree.parent_switched += 1;
self.stats_mut().tree.parent_switches += 1;
info!(
new_parent = %self.peer_display_name(&new_parent),
new_seq = new_seq,
@@ -223,6 +241,7 @@ impl Node {
"Parent switched, flushed coord cache, announcing to all peers"
);
if flap_dampened {
self.stats_mut().tree.flap_dampened += 1;
warn!("Flap dampening engaged: excessive parent switches detected");
}
@@ -235,25 +254,26 @@ impl Node {
&& *self.tree_state.my_declaration().parent_id() == *from
{
// Check for loop: if parent's ancestry now contains us, drop parent
if let Some(parent_coords) = self.tree_state.peer_coords(from) {
if parent_coords.contains(self.identity.node_addr()) {
warn!(
parent = %self.peer_display_name(from),
"Parent ancestry contains us — loop detected, dropping parent"
);
let peer_costs: HashMap<NodeAddr, f64> = self.peers.iter()
.map(|(addr, peer)| (*addr, peer.link_cost()))
.collect();
if self.tree_state.handle_parent_lost(&peer_costs) {
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
warn!(error = %e, "Failed to sign declaration after loop detection");
return;
}
self.coord_cache.clear();
self.send_tree_announce_to_all().await;
if let Some(parent_coords) = self.tree_state.peer_coords(from)
&& parent_coords.contains(self.identity.node_addr())
{
self.stats_mut().tree.loop_detected += 1;
warn!(
parent = %self.peer_display_name(from),
"Parent ancestry contains us — loop detected, dropping parent"
);
let peer_costs: HashMap<NodeAddr, f64> = self.peers.iter()
.map(|(addr, peer)| (*addr, peer.link_cost()))
.collect();
if self.tree_state.handle_parent_lost(&peer_costs) {
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
warn!(error = %e, "Failed to sign declaration after loop detection");
return;
}
return;
self.coord_cache.clear();
self.send_tree_announce_to_all().await;
}
return;
}
// Our parent's ancestry changed but we're keeping the same parent.
@@ -280,6 +300,7 @@ impl Node {
let new_depth = self.tree_state.my_coords().depth();
if new_root != old_root || new_depth != old_depth {
self.stats_mut().tree.ancestry_changed += 1;
info!(
parent = %self.peer_display_name(from),
old_root = %old_root,
@@ -351,6 +372,9 @@ impl Node {
self.tree_state.recompute_coords();
self.coord_cache.clear();
self.stats_mut().tree.parent_switched += 1;
self.stats_mut().tree.parent_switches += 1;
info!(
new_parent = %self.peer_display_name(&new_parent),
new_seq = new_seq,
@@ -360,6 +384,7 @@ impl Node {
"Parent switched via periodic cost re-evaluation"
);
if flap_dampened {
self.stats_mut().tree.flap_dampened += 1;
warn!("Flap dampening engaged: excessive parent switches detected");
}
@@ -383,6 +408,7 @@ impl Node {
self.tree_state.remove_peer(node_addr);
if was_parent {
self.stats_mut().tree.parent_losses += 1;
let peer_costs: HashMap<NodeAddr, f64> = self.peers.iter()
.map(|(addr, peer)| (*addr, peer.link_cost()))
.collect();