chie_core/
network_diag.rs

1//! Network diagnostics and monitoring utilities.
2//!
3//! This module provides tools for monitoring network health, measuring latency,
4//! detecting connectivity issues, and tracking network quality metrics.
5//!
6//! # Features
7//!
8//! - Latency measurement and tracking
9//! - Connection quality monitoring
10//! - Packet loss estimation
11//! - Bandwidth estimation
12//! - Network health scoring
13//!
14//! # Example
15//!
16//! ```
17//! use chie_core::network_diag::{NetworkMonitor, ConnectionQuality};
18//!
19//! let mut monitor = NetworkMonitor::new();
20//!
21//! // Record latency measurements
22//! monitor.record_latency("peer1".to_string(), 50);
23//! monitor.record_latency("peer1".to_string(), 55);
24//!
25//! // Get connection quality
26//! let quality = monitor.get_quality("peer1");
27//! println!("Connection quality: {:?}", quality);
28//!
29//! // Get health score
30//! let score = monitor.health_score("peer1");
31//! println!("Health score: {:.2}", score);
32//! ```
33
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36use std::time::{Duration, SystemTime};
37
38/// Connection quality levels.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum ConnectionQuality {
41    /// Excellent connection (latency < 50ms).
42    Excellent,
43    /// Good connection (latency 50-150ms).
44    Good,
45    /// Fair connection (latency 150-300ms).
46    Fair,
47    /// Poor connection (latency 300-500ms).
48    Poor,
49    /// Very poor connection (latency > 500ms).
50    VeryPoor,
51}
52
53impl ConnectionQuality {
54    /// Get connection quality from latency.
55    #[must_use]
56    pub const fn from_latency(latency_ms: u64) -> Self {
57        if latency_ms < 50 {
58            Self::Excellent
59        } else if latency_ms < 150 {
60            Self::Good
61        } else if latency_ms < 300 {
62            Self::Fair
63        } else if latency_ms < 500 {
64            Self::Poor
65        } else {
66            Self::VeryPoor
67        }
68    }
69
70    /// Get health score (0.0 to 1.0).
71    #[must_use]
72    pub const fn health_score(&self) -> f64 {
73        match self {
74            Self::Excellent => 1.0,
75            Self::Good => 0.8,
76            Self::Fair => 0.6,
77            Self::Poor => 0.4,
78            Self::VeryPoor => 0.2,
79        }
80    }
81}
82
83/// Network statistics for a connection.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct NetworkStats {
86    /// Peer ID.
87    pub peer_id: String,
88
89    /// Average latency in milliseconds.
90    pub avg_latency_ms: f64,
91
92    /// Minimum latency observed.
93    pub min_latency_ms: u64,
94
95    /// Maximum latency observed.
96    pub max_latency_ms: u64,
97
98    /// Latency standard deviation.
99    pub latency_stddev: f64,
100
101    /// Total measurements.
102    pub sample_count: u64,
103
104    /// Estimated packet loss rate (0.0 to 1.0).
105    pub packet_loss_rate: f64,
106
107    /// Estimated bandwidth in bytes/sec.
108    pub estimated_bandwidth_bps: f64,
109
110    /// Connection quality.
111    pub quality: ConnectionQuality,
112
113    /// Last measurement timestamp.
114    pub last_update: SystemTime,
115}
116
117impl NetworkStats {
118    /// Create new network statistics.
119    fn new(peer_id: String) -> Self {
120        Self {
121            peer_id,
122            avg_latency_ms: 0.0,
123            min_latency_ms: u64::MAX,
124            max_latency_ms: 0,
125            latency_stddev: 0.0,
126            sample_count: 0,
127            packet_loss_rate: 0.0,
128            estimated_bandwidth_bps: 0.0,
129            quality: ConnectionQuality::Good,
130            last_update: SystemTime::now(),
131        }
132    }
133
134    /// Calculate health score (0.0 to 1.0).
135    #[must_use]
136    #[inline]
137    pub fn health_score(&self) -> f64 {
138        let latency_score = self.quality.health_score();
139        let packet_loss_penalty = self.packet_loss_rate;
140        let stability_score = if self.sample_count > 10 {
141            1.0 - (self.latency_stddev / 100.0).min(1.0)
142        } else {
143            0.5
144        };
145
146        (latency_score * 0.5 + (1.0 - packet_loss_penalty) * 0.3 + stability_score * 0.2)
147            .clamp(0.0, 1.0)
148    }
149
150    /// Check if connection is stable.
151    #[must_use]
152    #[inline]
153    pub const fn is_stable(&self) -> bool {
154        self.latency_stddev < 50.0 && self.packet_loss_rate < 0.05
155    }
156
157    /// Check if connection is healthy.
158    #[must_use]
159    #[inline]
160    pub fn is_healthy(&self) -> bool {
161        self.health_score() >= 0.6
162    }
163}
164
165/// Network monitor for tracking connection health.
166pub struct NetworkMonitor {
167    /// Per-peer statistics.
168    stats: HashMap<String, NetworkStats>,
169
170    /// Maximum history size per peer.
171    max_history: usize,
172
173    /// Latency history for variance calculation.
174    latency_history: HashMap<String, Vec<u64>>,
175}
176
177impl NetworkMonitor {
178    /// Create a new network monitor.
179    #[must_use]
180    #[inline]
181    pub fn new() -> Self {
182        Self::with_history_size(100)
183    }
184
185    /// Create a network monitor with custom history size.
186    #[must_use]
187    #[inline]
188    pub fn with_history_size(max_history: usize) -> Self {
189        Self {
190            stats: HashMap::new(),
191            max_history,
192            latency_history: HashMap::new(),
193        }
194    }
195
196    /// Record a latency measurement.
197    pub fn record_latency(&mut self, peer_id: String, latency_ms: u64) {
198        let stats = self
199            .stats
200            .entry(peer_id.clone())
201            .or_insert_with(|| NetworkStats::new(peer_id.clone()));
202
203        // Update basic stats
204        stats.sample_count += 1;
205        stats.min_latency_ms = stats.min_latency_ms.min(latency_ms);
206        stats.max_latency_ms = stats.max_latency_ms.max(latency_ms);
207        stats.last_update = SystemTime::now();
208
209        // Update moving average
210        let alpha = 0.3;
211        if stats.sample_count == 1 {
212            stats.avg_latency_ms = latency_ms as f64;
213        } else {
214            stats.avg_latency_ms = alpha * latency_ms as f64 + (1.0 - alpha) * stats.avg_latency_ms;
215        }
216
217        // Update quality
218        stats.quality = ConnectionQuality::from_latency(stats.avg_latency_ms as u64);
219
220        // Update history for variance calculation
221        let history = self.latency_history.entry(peer_id).or_default();
222        history.push(latency_ms);
223        if history.len() > self.max_history {
224            history.remove(0);
225        }
226
227        // Calculate standard deviation
228        if history.len() > 1 {
229            let mean = history.iter().sum::<u64>() as f64 / history.len() as f64;
230            let variance = history
231                .iter()
232                .map(|&x| {
233                    let diff = x as f64 - mean;
234                    diff * diff
235                })
236                .sum::<f64>()
237                / history.len() as f64;
238            stats.latency_stddev = variance.sqrt();
239        }
240    }
241
242    /// Record bandwidth measurement.
243    pub fn record_bandwidth(&mut self, peer_id: &str, bytes_per_sec: f64) {
244        if let Some(stats) = self.stats.get_mut(peer_id) {
245            // Update moving average
246            let alpha = 0.2;
247            if stats.estimated_bandwidth_bps == 0.0 {
248                stats.estimated_bandwidth_bps = bytes_per_sec;
249            } else {
250                stats.estimated_bandwidth_bps =
251                    alpha * bytes_per_sec + (1.0 - alpha) * stats.estimated_bandwidth_bps;
252            }
253        }
254    }
255
256    /// Record packet loss event.
257    pub fn record_packet_loss(&mut self, peer_id: &str, lost_packets: u64, total_packets: u64) {
258        if let Some(stats) = self.stats.get_mut(peer_id) {
259            let loss_rate = lost_packets as f64 / total_packets as f64;
260
261            // Update moving average
262            let alpha = 0.3;
263            stats.packet_loss_rate = alpha * loss_rate + (1.0 - alpha) * stats.packet_loss_rate;
264        }
265    }
266
267    /// Get network statistics for a peer.
268    #[must_use]
269    #[inline]
270    pub fn get_stats(&self, peer_id: &str) -> Option<&NetworkStats> {
271        self.stats.get(peer_id)
272    }
273
274    /// Get connection quality for a peer.
275    #[must_use]
276    #[inline]
277    pub fn get_quality(&self, peer_id: &str) -> ConnectionQuality {
278        self.stats
279            .get(peer_id)
280            .map(|s| s.quality)
281            .unwrap_or(ConnectionQuality::Good)
282    }
283
284    /// Get health score for a peer.
285    #[must_use]
286    #[inline]
287    pub fn health_score(&self, peer_id: &str) -> f64 {
288        self.stats
289            .get(peer_id)
290            .map(|s| s.health_score())
291            .unwrap_or(0.5)
292    }
293
294    /// Get all healthy peers.
295    #[must_use]
296    #[inline]
297    pub fn get_healthy_peers(&self) -> Vec<String> {
298        self.stats
299            .values()
300            .filter(|s| s.is_healthy())
301            .map(|s| s.peer_id.clone())
302            .collect()
303    }
304
305    /// Get peers with excellent connections.
306    #[must_use]
307    #[inline]
308    pub fn get_excellent_peers(&self) -> Vec<String> {
309        self.stats
310            .values()
311            .filter(|s| s.quality == ConnectionQuality::Excellent)
312            .map(|s| s.peer_id.clone())
313            .collect()
314    }
315
316    /// Get average network health across all peers.
317    #[must_use]
318    #[inline]
319    pub fn average_health(&self) -> f64 {
320        if self.stats.is_empty() {
321            return 0.5;
322        }
323
324        let sum: f64 = self.stats.values().map(|s| s.health_score()).sum();
325        sum / self.stats.len() as f64
326    }
327
328    /// Clean up old peer statistics.
329    pub fn cleanup_old_peers(&mut self, max_age_secs: u64) {
330        let now = SystemTime::now();
331        self.stats.retain(|peer_id, stats| {
332            let age = now
333                .duration_since(stats.last_update)
334                .unwrap_or(Duration::from_secs(0))
335                .as_secs();
336
337            if age >= max_age_secs {
338                self.latency_history.remove(peer_id);
339                false
340            } else {
341                true
342            }
343        });
344    }
345
346    /// Get total number of monitored peers.
347    #[must_use]
348    #[inline]
349    pub fn peer_count(&self) -> usize {
350        self.stats.len()
351    }
352
353    /// Get all peer IDs.
354    #[must_use]
355    #[inline]
356    pub fn get_all_peer_ids(&self) -> Vec<String> {
357        self.stats.keys().cloned().collect()
358    }
359}
360
361impl Default for NetworkMonitor {
362    fn default() -> Self {
363        Self::new()
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_connection_quality_from_latency() {
373        assert_eq!(
374            ConnectionQuality::from_latency(30),
375            ConnectionQuality::Excellent
376        );
377        assert_eq!(
378            ConnectionQuality::from_latency(100),
379            ConnectionQuality::Good
380        );
381        assert_eq!(
382            ConnectionQuality::from_latency(200),
383            ConnectionQuality::Fair
384        );
385        assert_eq!(
386            ConnectionQuality::from_latency(400),
387            ConnectionQuality::Poor
388        );
389        assert_eq!(
390            ConnectionQuality::from_latency(600),
391            ConnectionQuality::VeryPoor
392        );
393    }
394
395    #[test]
396    fn test_connection_quality_health_score() {
397        assert_eq!(ConnectionQuality::Excellent.health_score(), 1.0);
398        assert_eq!(ConnectionQuality::Good.health_score(), 0.8);
399        assert_eq!(ConnectionQuality::Fair.health_score(), 0.6);
400        assert_eq!(ConnectionQuality::Poor.health_score(), 0.4);
401        assert_eq!(ConnectionQuality::VeryPoor.health_score(), 0.2);
402    }
403
404    #[test]
405    fn test_network_monitor_basic() {
406        let mut monitor = NetworkMonitor::new();
407
408        monitor.record_latency("peer1".to_string(), 50);
409        monitor.record_latency("peer1".to_string(), 55);
410        monitor.record_latency("peer1".to_string(), 45);
411
412        let stats = monitor.get_stats("peer1").unwrap();
413        assert_eq!(stats.sample_count, 3);
414        assert_eq!(stats.min_latency_ms, 45);
415        assert_eq!(stats.max_latency_ms, 55);
416    }
417
418    #[test]
419    fn test_network_monitor_quality() {
420        let mut monitor = NetworkMonitor::new();
421
422        monitor.record_latency("peer1".to_string(), 30);
423        assert_eq!(monitor.get_quality("peer1"), ConnectionQuality::Excellent);
424
425        monitor.record_latency("peer2".to_string(), 200);
426        assert_eq!(monitor.get_quality("peer2"), ConnectionQuality::Fair);
427    }
428
429    #[test]
430    fn test_bandwidth_tracking() {
431        let mut monitor = NetworkMonitor::new();
432
433        monitor.record_latency("peer1".to_string(), 50);
434        monitor.record_bandwidth("peer1", 1_000_000.0);
435        monitor.record_bandwidth("peer1", 1_500_000.0);
436
437        let stats = monitor.get_stats("peer1").unwrap();
438        assert!(stats.estimated_bandwidth_bps > 0.0);
439    }
440
441    #[test]
442    fn test_packet_loss() {
443        let mut monitor = NetworkMonitor::new();
444
445        monitor.record_latency("peer1".to_string(), 50);
446        monitor.record_packet_loss("peer1", 5, 100); // 5% loss
447
448        let stats = monitor.get_stats("peer1").unwrap();
449        assert!(stats.packet_loss_rate > 0.0);
450    }
451
452    #[test]
453    fn test_health_score() {
454        let mut monitor = NetworkMonitor::new();
455
456        // Excellent connection
457        for _ in 0..10 {
458            monitor.record_latency("peer1".to_string(), 40);
459        }
460
461        let score = monitor.health_score("peer1");
462        assert!(score > 0.8);
463    }
464
465    #[test]
466    fn test_get_healthy_peers() {
467        let mut monitor = NetworkMonitor::new();
468
469        monitor.record_latency("peer1".to_string(), 40);
470        monitor.record_latency("peer2".to_string(), 600);
471        monitor.record_latency("peer3".to_string(), 80);
472
473        let healthy = monitor.get_healthy_peers();
474        assert!(healthy.contains(&"peer1".to_string()));
475        assert!(healthy.contains(&"peer3".to_string()));
476    }
477
478    #[test]
479    fn test_get_excellent_peers() {
480        let mut monitor = NetworkMonitor::new();
481
482        monitor.record_latency("peer1".to_string(), 30);
483        monitor.record_latency("peer2".to_string(), 100);
484
485        let excellent = monitor.get_excellent_peers();
486        assert_eq!(excellent.len(), 1);
487        assert_eq!(excellent[0], "peer1");
488    }
489
490    #[test]
491    fn test_average_health() {
492        let mut monitor = NetworkMonitor::new();
493
494        monitor.record_latency("peer1".to_string(), 40);
495        monitor.record_latency("peer2".to_string(), 150);
496
497        let avg = monitor.average_health();
498        assert!(avg > 0.5 && avg < 1.0);
499    }
500
501    #[test]
502    fn test_cleanup_old_peers() {
503        let mut monitor = NetworkMonitor::new();
504
505        monitor.record_latency("peer1".to_string(), 50);
506        assert_eq!(monitor.peer_count(), 1);
507
508        monitor.cleanup_old_peers(0);
509        assert_eq!(monitor.peer_count(), 0);
510    }
511
512    #[test]
513    fn test_network_stats_stability() {
514        let mut monitor = NetworkMonitor::new();
515
516        // Stable connection
517        for _ in 0..20 {
518            monitor.record_latency("peer1".to_string(), 50);
519        }
520
521        let stats = monitor.get_stats("peer1").unwrap();
522        assert!(stats.is_stable());
523
524        // Unstable connection
525        for i in 0..20 {
526            monitor.record_latency("peer2".to_string(), 50 + (i * 20));
527        }
528
529        let stats2 = monitor.get_stats("peer2").unwrap();
530        assert!(!stats2.is_stable());
531    }
532}