diff --git a/CHANGELOG.md b/CHANGELOG.md index 08863da..f61d363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - When a route is used which is about to expire, we now send a route request to try and refresh its duration before it expires. +- We now track when a peer was fist discovered and when we last connected to it. + This info is displayed in the CLI when listing peers. ### Fixed diff --git a/mycelium-cli/src/peer.rs b/mycelium-cli/src/peer.rs index fb94ae1..a981bcc 100644 --- a/mycelium-cli/src/peer.rs +++ b/mycelium-cli/src/peer.rs @@ -37,7 +37,9 @@ pub async fn list_peers( "Type", "Connection", "Rx total", - "Tx total" + "Tx total", + "Discovered", + "Last connection" ]); for peer in peers.iter() { table.add_row(row![ @@ -47,6 +49,10 @@ pub async fn list_peers( peer.connection_state, format_bytes(peer.rx_bytes), format_bytes(peer.tx_bytes), + format_seconds(peer.discovered), + peer.last_connected + .map(format_seconds) + .unwrap_or("Never connected".to_string()), ]); } table.printstd(); @@ -69,6 +75,24 @@ fn format_bytes(bytes: u64) -> String { ) } +/// Convert an amount of seconds into a human readable string. +fn format_seconds(total_seconds: u64) -> String { + let seconds = total_seconds % 60; + let minutes = (total_seconds / 60) % 60; + let hours = (total_seconds / 3600) % 60; + let days = (total_seconds / 86400) % 60; + + if days > 0 { + format!("{days}d {hours}h {minutes}m {seconds}s") + } else if hours > 0 { + format!("{hours}h {minutes}m {seconds}s") + } else if minutes > 0 { + format!("{minutes}m {seconds}s") + } else { + format!("{seconds}s") + } +} + /// Remove peer(s) by (underlay) IP pub async fn remove_peers( server_addr: SocketAddr, diff --git a/mycelium/src/peer_manager.rs b/mycelium/src/peer_manager.rs index a1dfd66..0e2f5ae 100644 --- a/mycelium/src/peer_manager.rs +++ b/mycelium/src/peer_manager.rs @@ -27,7 +27,7 @@ use std::{collections::hash_map::Entry, future::IntoFuture}; use tokio::net::TcpStream; use tokio::net::{TcpListener, UdpSocket}; use tokio::task::AbortHandle; -use tokio::time::MissedTickBehavior; +use tokio::time::{Instant, MissedTickBehavior}; use tracing::{debug, error, info, instrument, trace, warn}; /// Magic bytes to identify a multicast UDP packet used in link local peer discovery. @@ -88,6 +88,10 @@ struct PeerInfo { connection_attempts: usize, /// Keep track of the amount of bytes we've sent to and received from this peer. con_traffic: ConnectionTraffic, + /// The moment in time we learned about this peer. + discovered: Instant, + /// The moment we last connected to this peer. + connected: Option, } /// Counters for the amount of traffic written to and received from a [`Peer`]. @@ -126,6 +130,10 @@ pub struct PeerStats { pub tx_bytes: u64, /// Amount of bytes received from this [`Peer`]. pub rx_bytes: u64, + /// Amount of time which passed since the system learned about this [`Peer`], in seconds. + pub discovered: u64, + /// Amount of seconds since the last succesfull connection to this [`Peer`]. + pub last_connected: Option, } impl PeerInfo { @@ -212,6 +220,7 @@ where // loop will perform the actual check and figure out they are dead, then // (re)connect. .map(|s| { + let now = tokio::time::Instant::now(); ( s, PeerInfo { @@ -223,6 +232,8 @@ where tx_bytes: Arc::new(AtomicU64::new(0)), rx_bytes: Arc::new(AtomicU64::new(0)), }, + discovered: now, + connected: None, }, ) }) @@ -281,6 +292,7 @@ where if peer_map.contains_key(&peer) { return Err(PeerExists); } + let now = tokio::time::Instant::now(); peer_map.insert( peer, PeerInfo { @@ -292,6 +304,8 @@ where tx_bytes: Arc::new(AtomicU64::new(0)), rx_bytes: Arc::new(AtomicU64::new(0)), }, + discovered: now, + connected: None, }, ); @@ -333,6 +347,8 @@ where connection_state, tx_bytes: peer_info.written(), rx_bytes: peer_info.read(), + discovered: peer_info.discovered.elapsed().as_secs(), + last_connected: peer_info.connected.map(|i| i.elapsed().as_secs()), }); } pi @@ -382,6 +398,7 @@ where // We successfully connected, reset the connection_attempts counter to 0 pi.connection_attempts = 0; + pi.connected = Some(tokio::time::Instant::now()); } else { // Only log with error level on the first connection failure, to avoid spamming the logs if pi.connection_attempts == 0 { @@ -916,6 +933,7 @@ where } } // Only if we don't know it yet. + let now = tokio::time::Instant::now(); if let Entry::Vacant(e) = peers.entry(endpoint) { e.insert(PeerInfo { pt: discovery_type, @@ -927,6 +945,8 @@ where }, connection_attempts: 0, con_traffic, + discovered: now, + connected: if peer.is_some() { Some(now) } else { None }, }); if let Some(p) = peer { self.router.lock().unwrap().add_peer_interface(p); @@ -936,6 +956,10 @@ where // We got an inbound peer with a duplicate entry. This is possible if the sending port // is the same as the previous one, which generally happens with our Quic setup. In // this case, the old connection needs to be replaced. + let discovered = peers + .get(&endpoint) + .map(|epi| epi.discovered) + .unwrap_or(now); let old_peer_info = peers.insert( endpoint, PeerInfo { @@ -948,6 +972,8 @@ where }, connection_attempts: 0, con_traffic, + discovered, + connected: Some(now), }, ); // If we have a new peer notify insert the new one in the router, then notify it that