mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
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:
+16
-1
@@ -57,6 +57,7 @@ impl Node {
|
||||
|
||||
// Check debounce
|
||||
if !self.bloom_state.should_send_update(peer_addr, now_ms) {
|
||||
self.stats_mut().bloom.debounce_suppressed += 1;
|
||||
// Either not pending or rate-limited; will retry on tick
|
||||
return Ok(());
|
||||
}
|
||||
@@ -70,7 +71,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().bloom.send_failed += 1;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
self.stats_mut().bloom.sent += 1;
|
||||
|
||||
// Record send and store the filter for change detection
|
||||
debug!(
|
||||
@@ -123,9 +129,12 @@ impl Node {
|
||||
/// 3. Store the filter on the peer
|
||||
/// 4. Mark other peers for outgoing filter update
|
||||
pub(super) async fn handle_filter_announce(&mut self, from: &NodeAddr, payload: &[u8]) {
|
||||
self.stats_mut().bloom.received += 1;
|
||||
|
||||
let announce = match FilterAnnounce::decode(payload) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
self.stats_mut().bloom.decode_error += 1;
|
||||
debug!(from = %self.peer_display_name(from), error = %e, "Malformed FilterAnnounce");
|
||||
return;
|
||||
}
|
||||
@@ -133,10 +142,12 @@ impl Node {
|
||||
|
||||
// Validate
|
||||
if !announce.is_valid() {
|
||||
self.stats_mut().bloom.invalid += 1;
|
||||
debug!(from = %self.peer_display_name(from), "FilterAnnounce filter/size_class mismatch");
|
||||
return;
|
||||
}
|
||||
if !announce.is_v1_compliant() {
|
||||
self.stats_mut().bloom.non_v1 += 1;
|
||||
debug!(from = %self.peer_display_name(from), size_class = announce.size_class, "Non-v1 FilterAnnounce rejected");
|
||||
return;
|
||||
}
|
||||
@@ -145,6 +156,7 @@ impl Node {
|
||||
let current_seq = match self.peers.get(from) {
|
||||
Some(peer) => peer.filter_sequence(),
|
||||
None => {
|
||||
self.stats_mut().bloom.unknown_peer += 1;
|
||||
debug!(from = %self.peer_display_name(from), "FilterAnnounce from unknown peer");
|
||||
return;
|
||||
}
|
||||
@@ -152,6 +164,7 @@ impl Node {
|
||||
|
||||
// Reject stale/replay
|
||||
if announce.sequence <= current_seq {
|
||||
self.stats_mut().bloom.stale += 1;
|
||||
debug!(
|
||||
from = %self.peer_display_name(from),
|
||||
received_seq = announce.sequence,
|
||||
@@ -161,6 +174,8 @@ impl Node {
|
||||
return;
|
||||
}
|
||||
|
||||
self.stats_mut().bloom.accepted += 1;
|
||||
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
|
||||
@@ -25,9 +25,12 @@ impl Node {
|
||||
from: &NodeAddr,
|
||||
payload: &[u8],
|
||||
) {
|
||||
self.stats_mut().discovery.req_received += 1;
|
||||
|
||||
let request = match LookupRequest::decode(payload) {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
self.stats_mut().discovery.req_decode_error += 1;
|
||||
debug!(from = %self.peer_display_name(from), error = %e, "Malformed LookupRequest");
|
||||
return;
|
||||
}
|
||||
@@ -37,6 +40,7 @@ impl Node {
|
||||
|
||||
// Dedup: drop if we've already seen this request_id
|
||||
if self.recent_requests.contains_key(&request.request_id) {
|
||||
self.stats_mut().discovery.req_duplicate += 1;
|
||||
trace!(
|
||||
request_id = request.request_id,
|
||||
from = %self.peer_display_name(from),
|
||||
@@ -56,6 +60,7 @@ impl Node {
|
||||
|
||||
// Loop prevention: drop if we've already been visited
|
||||
if request.was_visited(self.node_addr()) {
|
||||
self.stats_mut().discovery.req_already_visited += 1;
|
||||
trace!(
|
||||
request_id = request.request_id,
|
||||
target = %self.peer_display_name(&request.target),
|
||||
@@ -66,6 +71,7 @@ impl Node {
|
||||
|
||||
// Are we the target?
|
||||
if request.target == *self.node_addr() {
|
||||
self.stats_mut().discovery.req_target_is_us += 1;
|
||||
debug!(
|
||||
request_id = request.request_id,
|
||||
origin = %self.peer_display_name(&request.origin),
|
||||
@@ -77,8 +83,10 @@ impl Node {
|
||||
|
||||
// Forward if TTL permits
|
||||
if request.can_forward() {
|
||||
self.stats_mut().discovery.req_forwarded += 1;
|
||||
self.forward_lookup_request(request).await;
|
||||
} else {
|
||||
self.stats_mut().discovery.req_ttl_exhausted += 1;
|
||||
trace!(
|
||||
request_id = request.request_id,
|
||||
target = %self.peer_display_name(&request.target),
|
||||
@@ -99,9 +107,12 @@ impl Node {
|
||||
from: &NodeAddr,
|
||||
payload: &[u8],
|
||||
) {
|
||||
self.stats_mut().discovery.resp_received += 1;
|
||||
|
||||
let mut response = match LookupResponse::decode(payload) {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
self.stats_mut().discovery.resp_decode_error += 1;
|
||||
debug!(from = %self.peer_display_name(from), error = %e, "Malformed LookupResponse");
|
||||
return;
|
||||
}
|
||||
@@ -113,6 +124,7 @@ impl Node {
|
||||
if let Some(recent) = self.recent_requests.get(&response.request_id) {
|
||||
// Transit node: reverse-path forward
|
||||
let from_peer = recent.from_peer;
|
||||
self.stats_mut().discovery.resp_forwarded += 1;
|
||||
|
||||
// Apply path_mtu min() from the outgoing link's transport MTU
|
||||
if let Some(peer) = self.peers.get(&from_peer)
|
||||
@@ -153,6 +165,7 @@ impl Node {
|
||||
let target_pubkey = match self.lookup_by_fips_prefix(&prefix) {
|
||||
Some((_addr, pubkey)) => pubkey,
|
||||
None => {
|
||||
self.stats_mut().discovery.resp_identity_miss += 1;
|
||||
warn!(
|
||||
request_id = response.request_id,
|
||||
target = %self.peer_display_name(&target),
|
||||
@@ -171,6 +184,7 @@ impl Node {
|
||||
&response.target_coords,
|
||||
);
|
||||
if !peer_id.verify(&proof_data, &response.proof) {
|
||||
self.stats_mut().discovery.resp_proof_failed += 1;
|
||||
warn!(
|
||||
request_id = response.request_id,
|
||||
target = %self.peer_display_name(&target),
|
||||
@@ -179,6 +193,8 @@ impl Node {
|
||||
return;
|
||||
}
|
||||
|
||||
self.stats_mut().discovery.resp_accepted += 1;
|
||||
|
||||
debug!(
|
||||
request_id = response.request_id,
|
||||
target = %self.peer_display_name(&target),
|
||||
@@ -335,6 +351,8 @@ impl Node {
|
||||
/// response arrives, it's recognized as "our request" and the
|
||||
/// target's coordinates are cached in coord_cache.
|
||||
pub(in crate::node) async fn initiate_lookup(&mut self, target: &NodeAddr, ttl: u8) {
|
||||
self.stats_mut().discovery.req_initiated += 1;
|
||||
|
||||
let origin = *self.node_addr();
|
||||
let origin_coords = self.tree_state().my_coords().clone();
|
||||
let mut request = LookupRequest::generate(*target, origin, origin_coords, ttl, 0);
|
||||
@@ -375,6 +393,7 @@ impl Node {
|
||||
if let Some(&initiated_at) = self.pending_lookups.get(dest)
|
||||
&& now_ms.saturating_sub(initiated_at) < lookup_timeout_ms
|
||||
{
|
||||
self.stats_mut().discovery.req_deduplicated += 1;
|
||||
return;
|
||||
}
|
||||
self.pending_lookups.insert(*dest, now_ms);
|
||||
@@ -396,6 +415,7 @@ impl Node {
|
||||
.collect();
|
||||
|
||||
for addr in timed_out {
|
||||
self.stats_mut().discovery.resp_timed_out += 1;
|
||||
self.pending_lookups.remove(&addr);
|
||||
if let Some(packets) = self.pending_tun_packets.remove(&addr) {
|
||||
for pkt in &packets {
|
||||
|
||||
@@ -22,9 +22,12 @@ impl Node {
|
||||
/// Called by `dispatch_link_message` for msg_type 0x00. The payload
|
||||
/// has already had its msg_type byte stripped by dispatch.
|
||||
pub(in crate::node) async fn handle_session_datagram(&mut self, _from: &NodeAddr, payload: &[u8]) {
|
||||
self.stats_mut().forwarding.record_received(payload.len());
|
||||
|
||||
let mut datagram = match SessionDatagram::decode(payload) {
|
||||
Ok(dg) => dg,
|
||||
Err(e) => {
|
||||
self.stats_mut().forwarding.record_decode_error(payload.len());
|
||||
debug!(error = %e, "Malformed SessionDatagram");
|
||||
return;
|
||||
}
|
||||
@@ -32,6 +35,7 @@ impl Node {
|
||||
|
||||
// TTL enforcement: decrement and drop if exhausted
|
||||
if !datagram.decrement_ttl() {
|
||||
self.stats_mut().forwarding.record_ttl_exhausted(payload.len());
|
||||
debug!(
|
||||
src = %datagram.src_addr,
|
||||
dest = %datagram.dest_addr,
|
||||
@@ -45,6 +49,7 @@ impl Node {
|
||||
|
||||
// Local delivery: dispatch to session layer handlers
|
||||
if datagram.dest_addr == *self.node_addr() {
|
||||
self.stats_mut().forwarding.record_delivered(payload.len());
|
||||
self.handle_session_payload(&datagram.src_addr, &datagram.payload, datagram.path_mtu)
|
||||
.await;
|
||||
return;
|
||||
@@ -54,6 +59,7 @@ impl Node {
|
||||
let next_hop_addr = match self.find_next_hop(&datagram.dest_addr) {
|
||||
Some(peer) => *peer.node_addr(),
|
||||
None => {
|
||||
self.stats_mut().forwarding.record_drop_no_route(payload.len());
|
||||
self.send_routing_error(&datagram).await;
|
||||
return;
|
||||
}
|
||||
@@ -79,9 +85,11 @@ impl Node {
|
||||
{
|
||||
match e {
|
||||
NodeError::MtuExceeded { mtu, .. } => {
|
||||
self.stats_mut().forwarding.record_drop_mtu_exceeded(payload.len());
|
||||
self.send_mtu_exceeded_error(&datagram, mtu).await;
|
||||
}
|
||||
_ => {
|
||||
self.stats_mut().forwarding.record_drop_send_error(payload.len());
|
||||
debug!(
|
||||
next_hop = %next_hop_addr,
|
||||
dest = %datagram.dest_addr,
|
||||
@@ -90,6 +98,8 @@ impl Node {
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.stats_mut().forwarding.record_forwarded(encoded.len());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -703,6 +703,8 @@ impl Node {
|
||||
/// immediately (rate-limited), trigger discovery, and reset the
|
||||
/// warmup counter for subsequent data packets.
|
||||
async fn handle_coords_required(&mut self, inner: &[u8]) {
|
||||
self.stats_mut().errors.coords_required += 1;
|
||||
|
||||
let msg = match CoordsRequired::decode(inner) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
@@ -759,6 +761,8 @@ impl Node {
|
||||
/// Send a standalone CoordsWarmup immediately (rate-limited), invalidate
|
||||
/// cached coordinates, trigger re-discovery, and reset the warmup counter.
|
||||
async fn handle_path_broken(&mut self, inner: &[u8]) {
|
||||
self.stats_mut().errors.path_broken += 1;
|
||||
|
||||
let msg = match PathBroken::decode(inner) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
@@ -820,6 +824,8 @@ impl Node {
|
||||
/// next-hop transport MTU. Apply the reported bottleneck MTU to our
|
||||
/// PathMtuState for the affected session, causing an immediate decrease.
|
||||
async fn handle_mtu_exceeded(&mut self, inner: &[u8]) {
|
||||
self.stats_mut().errors.mtu_exceeded += 1;
|
||||
|
||||
let msg = match MtuExceeded::decode(inner) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
@@ -1231,7 +1237,9 @@ impl Node {
|
||||
}
|
||||
|
||||
let encoded = datagram.encode();
|
||||
self.send_encrypted_link_message(&next_hop_addr, &encoded).await
|
||||
self.send_encrypted_link_message(&next_hop_addr, &encoded).await?;
|
||||
self.stats_mut().forwarding.record_originated(encoded.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look up destination coordinates from available caches.
|
||||
|
||||
@@ -13,6 +13,7 @@ mod routing_error_rate_limit;
|
||||
pub(crate) mod session;
|
||||
pub(crate) mod session_wire;
|
||||
pub(crate) mod wire;
|
||||
pub(crate) mod stats;
|
||||
mod tree;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -299,6 +300,10 @@ pub struct Node {
|
||||
/// Next transport ID to allocate.
|
||||
next_transport_id: u32,
|
||||
|
||||
// === Node Statistics ===
|
||||
/// Routing, forwarding, discovery, and error signal counters.
|
||||
stats: stats::NodeStats,
|
||||
|
||||
// === TUN Interface ===
|
||||
/// TUN device state.
|
||||
tun_state: TunState,
|
||||
@@ -433,6 +438,7 @@ impl Node {
|
||||
max_links,
|
||||
next_link_id: 1,
|
||||
next_transport_id: 1,
|
||||
stats: stats::NodeStats::new(),
|
||||
tun_state,
|
||||
tun_name: None,
|
||||
tun_tx: None,
|
||||
@@ -526,6 +532,7 @@ impl Node {
|
||||
max_links,
|
||||
next_link_id: 1,
|
||||
next_transport_id: 1,
|
||||
stats: stats::NodeStats::new(),
|
||||
tun_state,
|
||||
tun_name: None,
|
||||
tun_tx: None,
|
||||
@@ -803,6 +810,18 @@ impl Node {
|
||||
&mut self.coord_cache
|
||||
}
|
||||
|
||||
// === Node Statistics ===
|
||||
|
||||
/// Get the node statistics.
|
||||
pub fn stats(&self) -> &stats::NodeStats {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
/// Get mutable node statistics.
|
||||
pub(crate) fn stats_mut(&mut self) -> &mut stats::NodeStats {
|
||||
&mut self.stats
|
||||
}
|
||||
|
||||
// === TUN Interface ===
|
||||
|
||||
/// Get the TUN state.
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
//! Node-level statistics for routing, forwarding, and discovery operations.
|
||||
//!
|
||||
//! Unlike `EthernetStats` (which uses `AtomicU64` + `Arc` for cross-task
|
||||
//! sharing), these counters use plain `u64` because `Node` handlers run
|
||||
//! on a single `&mut self` context. A `snapshot()` method produces a
|
||||
//! copyable struct for control socket queries.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Forwarding statistics — packets and bytes for each outcome.
|
||||
#[derive(Default)]
|
||||
pub struct ForwardingStats {
|
||||
pub received_packets: u64,
|
||||
pub received_bytes: u64,
|
||||
pub decode_error_packets: u64,
|
||||
pub decode_error_bytes: u64,
|
||||
pub ttl_exhausted_packets: u64,
|
||||
pub ttl_exhausted_bytes: u64,
|
||||
pub delivered_packets: u64,
|
||||
pub delivered_bytes: u64,
|
||||
pub forwarded_packets: u64,
|
||||
pub forwarded_bytes: u64,
|
||||
pub drop_no_route_packets: u64,
|
||||
pub drop_no_route_bytes: u64,
|
||||
pub drop_mtu_exceeded_packets: u64,
|
||||
pub drop_mtu_exceeded_bytes: u64,
|
||||
pub drop_send_error_packets: u64,
|
||||
pub drop_send_error_bytes: u64,
|
||||
pub originated_packets: u64,
|
||||
pub originated_bytes: u64,
|
||||
}
|
||||
|
||||
impl ForwardingStats {
|
||||
pub fn record_received(&mut self, bytes: usize) {
|
||||
self.received_packets += 1;
|
||||
self.received_bytes += bytes as u64;
|
||||
}
|
||||
|
||||
pub fn record_decode_error(&mut self, bytes: usize) {
|
||||
self.decode_error_packets += 1;
|
||||
self.decode_error_bytes += bytes as u64;
|
||||
}
|
||||
|
||||
pub fn record_ttl_exhausted(&mut self, bytes: usize) {
|
||||
self.ttl_exhausted_packets += 1;
|
||||
self.ttl_exhausted_bytes += bytes as u64;
|
||||
}
|
||||
|
||||
pub fn record_delivered(&mut self, bytes: usize) {
|
||||
self.delivered_packets += 1;
|
||||
self.delivered_bytes += bytes as u64;
|
||||
}
|
||||
|
||||
pub fn record_forwarded(&mut self, bytes: usize) {
|
||||
self.forwarded_packets += 1;
|
||||
self.forwarded_bytes += bytes as u64;
|
||||
}
|
||||
|
||||
pub fn record_drop_no_route(&mut self, bytes: usize) {
|
||||
self.drop_no_route_packets += 1;
|
||||
self.drop_no_route_bytes += bytes as u64;
|
||||
}
|
||||
|
||||
pub fn record_drop_mtu_exceeded(&mut self, bytes: usize) {
|
||||
self.drop_mtu_exceeded_packets += 1;
|
||||
self.drop_mtu_exceeded_bytes += bytes as u64;
|
||||
}
|
||||
|
||||
pub fn record_drop_send_error(&mut self, bytes: usize) {
|
||||
self.drop_send_error_packets += 1;
|
||||
self.drop_send_error_bytes += bytes as u64;
|
||||
}
|
||||
|
||||
pub fn record_originated(&mut self, bytes: usize) {
|
||||
self.originated_packets += 1;
|
||||
self.originated_bytes += bytes as u64;
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> ForwardingStatsSnapshot {
|
||||
ForwardingStatsSnapshot {
|
||||
received_packets: self.received_packets,
|
||||
received_bytes: self.received_bytes,
|
||||
decode_error_packets: self.decode_error_packets,
|
||||
decode_error_bytes: self.decode_error_bytes,
|
||||
ttl_exhausted_packets: self.ttl_exhausted_packets,
|
||||
ttl_exhausted_bytes: self.ttl_exhausted_bytes,
|
||||
delivered_packets: self.delivered_packets,
|
||||
delivered_bytes: self.delivered_bytes,
|
||||
forwarded_packets: self.forwarded_packets,
|
||||
forwarded_bytes: self.forwarded_bytes,
|
||||
drop_no_route_packets: self.drop_no_route_packets,
|
||||
drop_no_route_bytes: self.drop_no_route_bytes,
|
||||
drop_mtu_exceeded_packets: self.drop_mtu_exceeded_packets,
|
||||
drop_mtu_exceeded_bytes: self.drop_mtu_exceeded_bytes,
|
||||
drop_send_error_packets: self.drop_send_error_packets,
|
||||
drop_send_error_bytes: self.drop_send_error_bytes,
|
||||
originated_packets: self.originated_packets,
|
||||
originated_bytes: self.originated_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Discovery statistics — packet counts for request and response handling.
|
||||
#[derive(Default)]
|
||||
pub struct DiscoveryStats {
|
||||
// Request counters
|
||||
pub req_received: u64,
|
||||
pub req_decode_error: u64,
|
||||
pub req_duplicate: u64,
|
||||
pub req_already_visited: u64,
|
||||
pub req_target_is_us: u64,
|
||||
pub req_forwarded: u64,
|
||||
pub req_ttl_exhausted: u64,
|
||||
pub req_initiated: u64,
|
||||
pub req_deduplicated: u64,
|
||||
// Response counters
|
||||
pub resp_received: u64,
|
||||
pub resp_decode_error: u64,
|
||||
pub resp_forwarded: u64,
|
||||
pub resp_identity_miss: u64,
|
||||
pub resp_proof_failed: u64,
|
||||
pub resp_accepted: u64,
|
||||
pub resp_timed_out: u64,
|
||||
}
|
||||
|
||||
impl DiscoveryStats {
|
||||
pub fn snapshot(&self) -> DiscoveryStatsSnapshot {
|
||||
DiscoveryStatsSnapshot {
|
||||
req_received: self.req_received,
|
||||
req_decode_error: self.req_decode_error,
|
||||
req_duplicate: self.req_duplicate,
|
||||
req_already_visited: self.req_already_visited,
|
||||
req_target_is_us: self.req_target_is_us,
|
||||
req_forwarded: self.req_forwarded,
|
||||
req_ttl_exhausted: self.req_ttl_exhausted,
|
||||
req_initiated: self.req_initiated,
|
||||
req_deduplicated: self.req_deduplicated,
|
||||
resp_received: self.resp_received,
|
||||
resp_decode_error: self.resp_decode_error,
|
||||
resp_forwarded: self.resp_forwarded,
|
||||
resp_identity_miss: self.resp_identity_miss,
|
||||
resp_proof_failed: self.resp_proof_failed,
|
||||
resp_accepted: self.resp_accepted,
|
||||
resp_timed_out: self.resp_timed_out,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spanning tree statistics — announce handling and parent tracking.
|
||||
#[derive(Default)]
|
||||
pub struct TreeStats {
|
||||
// Inbound announce handling
|
||||
pub received: u64,
|
||||
pub decode_error: u64,
|
||||
pub unknown_peer: u64,
|
||||
pub addr_mismatch: u64,
|
||||
pub sig_failed: u64,
|
||||
pub stale: u64,
|
||||
pub accepted: u64,
|
||||
pub parent_switched: u64,
|
||||
pub loop_detected: u64,
|
||||
pub ancestry_changed: u64,
|
||||
// Outbound announce sending
|
||||
pub sent: u64,
|
||||
pub rate_limited: u64,
|
||||
pub send_failed: u64,
|
||||
// Cumulative events
|
||||
pub parent_switches: u64,
|
||||
pub parent_losses: u64,
|
||||
pub flap_dampened: u64,
|
||||
}
|
||||
|
||||
impl TreeStats {
|
||||
pub fn snapshot(&self) -> TreeStatsSnapshot {
|
||||
TreeStatsSnapshot {
|
||||
received: self.received,
|
||||
decode_error: self.decode_error,
|
||||
unknown_peer: self.unknown_peer,
|
||||
addr_mismatch: self.addr_mismatch,
|
||||
sig_failed: self.sig_failed,
|
||||
stale: self.stale,
|
||||
accepted: self.accepted,
|
||||
parent_switched: self.parent_switched,
|
||||
loop_detected: self.loop_detected,
|
||||
ancestry_changed: self.ancestry_changed,
|
||||
sent: self.sent,
|
||||
rate_limited: self.rate_limited,
|
||||
send_failed: self.send_failed,
|
||||
parent_switches: self.parent_switches,
|
||||
parent_losses: self.parent_losses,
|
||||
flap_dampened: self.flap_dampened,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bloom filter statistics — filter announce handling.
|
||||
#[derive(Default)]
|
||||
pub struct BloomStats {
|
||||
// Inbound announce handling
|
||||
pub received: u64,
|
||||
pub decode_error: u64,
|
||||
pub invalid: u64,
|
||||
pub non_v1: u64,
|
||||
pub unknown_peer: u64,
|
||||
pub stale: u64,
|
||||
pub accepted: u64,
|
||||
// Outbound announce sending
|
||||
pub sent: u64,
|
||||
pub debounce_suppressed: u64,
|
||||
pub send_failed: u64,
|
||||
}
|
||||
|
||||
impl BloomStats {
|
||||
pub fn snapshot(&self) -> BloomStatsSnapshot {
|
||||
BloomStatsSnapshot {
|
||||
received: self.received,
|
||||
decode_error: self.decode_error,
|
||||
invalid: self.invalid,
|
||||
non_v1: self.non_v1,
|
||||
unknown_peer: self.unknown_peer,
|
||||
stale: self.stale,
|
||||
accepted: self.accepted,
|
||||
sent: self.sent,
|
||||
debounce_suppressed: self.debounce_suppressed,
|
||||
send_failed: self.send_failed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error signal statistics — counts of each error signal type received.
|
||||
#[derive(Default)]
|
||||
pub struct ErrorSignalStats {
|
||||
pub coords_required: u64,
|
||||
pub path_broken: u64,
|
||||
pub mtu_exceeded: u64,
|
||||
}
|
||||
|
||||
impl ErrorSignalStats {
|
||||
pub fn snapshot(&self) -> ErrorSignalStatsSnapshot {
|
||||
ErrorSignalStatsSnapshot {
|
||||
coords_required: self.coords_required,
|
||||
path_broken: self.path_broken,
|
||||
mtu_exceeded: self.mtu_exceeded,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Aggregate node statistics.
|
||||
#[derive(Default)]
|
||||
pub struct NodeStats {
|
||||
pub forwarding: ForwardingStats,
|
||||
pub discovery: DiscoveryStats,
|
||||
pub tree: TreeStats,
|
||||
pub bloom: BloomStats,
|
||||
pub errors: ErrorSignalStats,
|
||||
}
|
||||
|
||||
impl NodeStats {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> NodeStatsSnapshot {
|
||||
NodeStatsSnapshot {
|
||||
forwarding: self.forwarding.snapshot(),
|
||||
discovery: self.discovery.snapshot(),
|
||||
tree: self.tree.snapshot(),
|
||||
bloom: self.bloom.snapshot(),
|
||||
errors: self.errors.snapshot(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Snapshot types (copyable, serializable) ---
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct ForwardingStatsSnapshot {
|
||||
pub received_packets: u64,
|
||||
pub received_bytes: u64,
|
||||
pub decode_error_packets: u64,
|
||||
pub decode_error_bytes: u64,
|
||||
pub ttl_exhausted_packets: u64,
|
||||
pub ttl_exhausted_bytes: u64,
|
||||
pub delivered_packets: u64,
|
||||
pub delivered_bytes: u64,
|
||||
pub forwarded_packets: u64,
|
||||
pub forwarded_bytes: u64,
|
||||
pub drop_no_route_packets: u64,
|
||||
pub drop_no_route_bytes: u64,
|
||||
pub drop_mtu_exceeded_packets: u64,
|
||||
pub drop_mtu_exceeded_bytes: u64,
|
||||
pub drop_send_error_packets: u64,
|
||||
pub drop_send_error_bytes: u64,
|
||||
pub originated_packets: u64,
|
||||
pub originated_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct DiscoveryStatsSnapshot {
|
||||
pub req_received: u64,
|
||||
pub req_decode_error: u64,
|
||||
pub req_duplicate: u64,
|
||||
pub req_already_visited: u64,
|
||||
pub req_target_is_us: u64,
|
||||
pub req_forwarded: u64,
|
||||
pub req_ttl_exhausted: u64,
|
||||
pub req_initiated: u64,
|
||||
pub req_deduplicated: u64,
|
||||
pub resp_received: u64,
|
||||
pub resp_decode_error: u64,
|
||||
pub resp_forwarded: u64,
|
||||
pub resp_identity_miss: u64,
|
||||
pub resp_proof_failed: u64,
|
||||
pub resp_accepted: u64,
|
||||
pub resp_timed_out: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct TreeStatsSnapshot {
|
||||
pub received: u64,
|
||||
pub decode_error: u64,
|
||||
pub unknown_peer: u64,
|
||||
pub addr_mismatch: u64,
|
||||
pub sig_failed: u64,
|
||||
pub stale: u64,
|
||||
pub accepted: u64,
|
||||
pub parent_switched: u64,
|
||||
pub loop_detected: u64,
|
||||
pub ancestry_changed: u64,
|
||||
pub sent: u64,
|
||||
pub rate_limited: u64,
|
||||
pub send_failed: u64,
|
||||
pub parent_switches: u64,
|
||||
pub parent_losses: u64,
|
||||
pub flap_dampened: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct BloomStatsSnapshot {
|
||||
pub received: u64,
|
||||
pub decode_error: u64,
|
||||
pub invalid: u64,
|
||||
pub non_v1: u64,
|
||||
pub unknown_peer: u64,
|
||||
pub stale: u64,
|
||||
pub accepted: u64,
|
||||
pub sent: u64,
|
||||
pub debounce_suppressed: u64,
|
||||
pub send_failed: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct ErrorSignalStatsSnapshot {
|
||||
pub coords_required: u64,
|
||||
pub path_broken: u64,
|
||||
pub mtu_exceeded: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct NodeStatsSnapshot {
|
||||
pub forwarding: ForwardingStatsSnapshot,
|
||||
pub discovery: DiscoveryStatsSnapshot,
|
||||
pub tree: TreeStatsSnapshot,
|
||||
pub bloom: BloomStatsSnapshot,
|
||||
pub errors: ErrorSignalStatsSnapshot,
|
||||
}
|
||||
+44
-18
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user