ant_quic/
stats_dashboard.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Connection Statistics Dashboard
9//!
10//! This module provides a real-time dashboard for monitoring connection
11//! statistics, NAT traversal performance, and network health metrics.
12
13use crate::{
14    nat_traversal_api::{NatTraversalEvent, NatTraversalStatistics, PeerId},
15    terminal_ui,
16};
17
18/// Node statistics for dashboard display
19#[derive(Debug, Clone, Default)]
20pub struct NodeStats {
21    /// Number of currently active connections
22    pub active_connections: usize,
23    /// Total number of successful connections since startup
24    pub successful_connections: usize,
25    /// Total number of failed connections since startup
26    pub failed_connections: usize,
27}
28use std::{
29    collections::{HashMap, VecDeque},
30    net::SocketAddr,
31    sync::Arc,
32    time::{Duration, Instant},
33};
34use tokio::sync::RwLock;
35
36/// Box drawing style
37#[derive(Debug, Clone, Copy)]
38pub enum BoxStyle {
39    /// Single line borders
40    Single,
41    /// Double line borders
42    Double,
43    /// Rounded corners
44    Rounded,
45}
46
47/// Draw a box with title and content
48fn draw_box(title: &str, content: &str, _style: BoxStyle, width: usize) -> String {
49    let mut result = String::new();
50
51    // Top border with title
52    let padding = width.saturating_sub(title.len() + 4);
53    let left_pad = padding / 2;
54    let right_pad = padding - left_pad;
55
56    result.push_str(&format!(
57        "╭{} {} {}╮\n",
58        "─".repeat(left_pad),
59        title,
60        "─".repeat(right_pad)
61    ));
62
63    // Content lines
64    for line in content.lines() {
65        let line_len = line.chars().count();
66        let padding = width.saturating_sub(line_len + 2);
67        result.push_str(&format!("│ {}{} │\n", line, " ".repeat(padding)));
68    }
69
70    // Bottom border
71    result.push_str(&format!("╰{}╯", "─".repeat(width - 2)));
72
73    result
74}
75
76/// Dashboard configuration
77#[derive(Debug, Clone)]
78pub struct DashboardConfig {
79    /// Update interval for the dashboard
80    pub update_interval: Duration,
81    /// Maximum number of historical data points
82    pub history_size: usize,
83    /// Enable detailed connection tracking
84    pub detailed_tracking: bool,
85    /// Enable performance graphs
86    pub show_graphs: bool,
87}
88
89impl Default for DashboardConfig {
90    fn default() -> Self {
91        Self {
92            update_interval: Duration::from_secs(1),
93            history_size: 60, // 1 minute of second-by-second data
94            detailed_tracking: true,
95            show_graphs: true,
96        }
97    }
98}
99
100/// Connection information
101#[derive(Debug, Clone)]
102pub struct ConnectionInfo {
103    /// Peer identifier
104    pub peer_id: PeerId,
105    /// Remote socket address
106    pub remote_address: SocketAddr,
107    /// Timestamp when the connection was established
108    pub connected_at: Instant,
109    /// Total bytes sent
110    pub bytes_sent: u64,
111    /// Total bytes received
112    pub bytes_received: u64,
113    /// Timestamp of last activity
114    pub last_activity: Instant,
115    /// Measured round trip time
116    pub rtt: Option<Duration>,
117    /// Packet loss ratio [0.0-1.0]
118    pub packet_loss: f64,
119    /// NAT type inferred for the peer
120    pub nat_type: String,
121}
122
123/// Historical data point
124#[derive(Debug, Clone)]
125struct DataPoint {
126    #[allow(dead_code)]
127    timestamp: Instant,
128    active_connections: usize,
129    nat_success_rate: f64,
130    #[allow(dead_code)]
131    bytes_per_second: u64,
132    #[allow(dead_code)]
133    avg_rtt: Duration,
134}
135
136/// Statistics dashboard
137pub struct StatsDashboard {
138    config: DashboardConfig,
139    /// Current node statistics
140    node_stats: Arc<RwLock<NodeStats>>,
141    /// NAT traversal statistics
142    nat_stats: Arc<RwLock<NatTraversalStatistics>>,
143    /// Active connections
144    connections: Arc<RwLock<HashMap<PeerId, ConnectionInfo>>>,
145    /// Historical data
146    #[allow(dead_code)]
147    history: Arc<RwLock<VecDeque<DataPoint>>>,
148    /// Dashboard start time
149    start_time: Instant,
150    /// Last update time
151    last_update: Arc<RwLock<Instant>>,
152}
153
154impl StatsDashboard {
155    /// Create new statistics dashboard
156    pub fn new(config: DashboardConfig) -> Self {
157        let history_size = config.history_size;
158        Self {
159            config,
160            node_stats: Arc::new(RwLock::new(NodeStats::default())),
161            nat_stats: Arc::new(RwLock::new(NatTraversalStatistics::default())),
162            connections: Arc::new(RwLock::new(HashMap::new())),
163            history: Arc::new(RwLock::new(VecDeque::with_capacity(history_size))),
164            start_time: Instant::now(),
165            last_update: Arc::new(RwLock::new(Instant::now())),
166        }
167    }
168
169    /// Get the dashboard configuration
170    pub fn config(&self) -> &DashboardConfig {
171        &self.config
172    }
173
174    /// Update node statistics
175    pub async fn update_node_stats(&self, stats: NodeStats) {
176        *self.node_stats.write().await = stats;
177    }
178
179    /// Update NAT traversal statistics
180    pub async fn update_nat_stats(&self, stats: NatTraversalStatistics) {
181        *self.nat_stats.write().await = stats;
182    }
183
184    /// Handle NAT traversal event
185    pub async fn handle_nat_event(&self, event: &NatTraversalEvent) {
186        match event {
187            NatTraversalEvent::ConnectionEstablished {
188                peer_id,
189                remote_address,
190            } => {
191                let mut connections = self.connections.write().await;
192                connections.insert(
193                    *peer_id,
194                    ConnectionInfo {
195                        peer_id: *peer_id,
196                        remote_address: *remote_address,
197                        connected_at: Instant::now(),
198                        bytes_sent: 0,
199                        bytes_received: 0,
200                        last_activity: Instant::now(),
201                        rtt: None,
202                        packet_loss: 0.0,
203                        nat_type: "Unknown".to_string(),
204                    },
205                );
206            }
207            NatTraversalEvent::TraversalFailed { peer_id, .. } => {
208                let mut connections = self.connections.write().await;
209                connections.remove(peer_id);
210            }
211            _ => {}
212        }
213    }
214
215    /// Update connection metrics
216    pub async fn update_connection_metrics(
217        &self,
218        peer_id: PeerId,
219        bytes_sent: u64,
220        bytes_received: u64,
221        rtt: Option<Duration>,
222    ) {
223        let mut connections = self.connections.write().await;
224        if let Some(conn) = connections.get_mut(&peer_id) {
225            conn.bytes_sent = bytes_sent;
226            conn.bytes_received = bytes_received;
227            conn.rtt = rtt;
228            conn.last_activity = Instant::now();
229        }
230    }
231
232    /// Record historical data point
233    async fn record_data_point(&self) {
234        let _node_stats = self.node_stats.read().await;
235        let nat_stats = self.nat_stats.read().await;
236        let connections = self.connections.read().await;
237
238        let success_rate = if nat_stats.total_attempts > 0 {
239            nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64
240        } else {
241            0.0
242        };
243
244        let total_bytes: u64 = connections
245            .values()
246            .map(|c| c.bytes_sent + c.bytes_received)
247            .sum();
248
249        let avg_rtt = if connections.is_empty() {
250            Duration::from_millis(0)
251        } else {
252            let total_rtt: Duration = connections.values().filter_map(|c| c.rtt).sum();
253            let count = connections.values().filter(|c| c.rtt.is_some()).count();
254            if count > 0 {
255                total_rtt / count as u32
256            } else {
257                Duration::from_millis(0)
258            }
259        };
260
261        let data_point = DataPoint {
262            timestamp: Instant::now(),
263            active_connections: connections.len(),
264            nat_success_rate: success_rate,
265            bytes_per_second: total_bytes,
266            avg_rtt,
267        };
268
269        let mut history = self.history.write().await;
270        if history.len() >= self.config.history_size {
271            history.pop_front();
272        }
273        history.push_back(data_point);
274    }
275
276    /// Render the dashboard
277    pub async fn render(&self) -> String {
278        // Record current data point
279        self.record_data_point().await;
280
281        let mut output = String::new();
282
283        // Clear screen and move to top
284        output.push_str("\x1B[2J\x1B[H");
285
286        // Title
287        output.push_str(&format!(
288            "{}🚀 ant-quic Connection Statistics Dashboard\n\n{}",
289            terminal_ui::colors::BOLD,
290            terminal_ui::colors::RESET
291        ));
292
293        // System uptime
294        let uptime = self.start_time.elapsed();
295        output.push_str(&format!("⏱️  Uptime: {}\n\n", format_duration(uptime)));
296
297        // Render sections
298        output.push_str(&self.render_overview_section().await);
299        output.push_str(&self.render_nat_section().await);
300        output.push_str(&self.render_connections_section().await);
301
302        if self.config.show_graphs {
303            output.push_str(&self.render_graphs_section().await);
304        }
305
306        output.push_str(&self.render_footer().await);
307
308        output
309    }
310
311    /// Render overview section
312    async fn render_overview_section(&self) -> String {
313        let node_stats = self.node_stats.read().await;
314        let _connections = self.connections.read().await;
315
316        let mut section = String::new();
317
318        section.push_str(&draw_box(
319            "📊 Overview",
320            &format!(
321                "Active Connections: {}\n\
322                 Total Successful: {}\n\
323                 Total Failed: {}\n\
324                 Success Rate: {:.1}%",
325                format!(
326                    "{}{}{}",
327                    terminal_ui::colors::GREEN,
328                    node_stats.active_connections,
329                    terminal_ui::colors::RESET
330                ),
331                node_stats.successful_connections,
332                node_stats.failed_connections,
333                if node_stats.successful_connections + node_stats.failed_connections > 0 {
334                    (node_stats.successful_connections as f64
335                        / (node_stats.successful_connections + node_stats.failed_connections)
336                            as f64)
337                        * 100.0
338                } else {
339                    0.0
340                }
341            ),
342            BoxStyle::Single,
343            50,
344        ));
345
346        section.push('\n');
347        section
348    }
349
350    /// Render NAT traversal section
351    async fn render_nat_section(&self) -> String {
352        let nat_stats = self.nat_stats.read().await;
353
354        let mut section = String::new();
355
356        section.push_str(&draw_box(
357            "🌐 NAT Traversal",
358            &format!(
359                "Total Attempts: {}\n\
360                 Successful: {} ({:.1}%)\n\
361                 Direct Connections: {}\n\
362                 Relayed: {}\n\
363                 Average Time: {:?}\n\
364                 Active Sessions: {}",
365                nat_stats.total_attempts,
366                nat_stats.successful_connections,
367                if nat_stats.total_attempts > 0 {
368                    (nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64)
369                        * 100.0
370                } else {
371                    0.0
372                },
373                nat_stats.direct_connections,
374                nat_stats.relayed_connections,
375                nat_stats.average_coordination_time,
376                nat_stats.active_sessions,
377            ),
378            BoxStyle::Single,
379            50,
380        ));
381
382        section.push('\n');
383        section
384    }
385
386    /// Render connections section
387    async fn render_connections_section(&self) -> String {
388        let connections = self.connections.read().await;
389
390        let mut section = String::new();
391
392        if connections.is_empty() {
393            section.push_str(&draw_box(
394                "🔗 Active Connections",
395                "No active connections",
396                BoxStyle::Single,
397                50,
398            ));
399        } else {
400            let mut content = String::new();
401            for (i, (peer_id, conn)) in connections.iter().enumerate() {
402                if i > 0 {
403                    content.push_str("\n─────────────────────────────────────────────\n");
404                }
405
406                content.push_str(&format!(
407                    "Peer: {}\n\
408                     Address: {}\n\
409                     Duration: {}\n\
410                     Sent: {} | Received: {}\n\
411                     RTT: {} | Loss: {:.1}%",
412                    format!(
413                        "{}{}{}",
414                        terminal_ui::colors::DIM,
415                        hex::encode(&peer_id.0[..8]),
416                        terminal_ui::colors::RESET
417                    ),
418                    conn.remote_address,
419                    format_duration(conn.connected_at.elapsed()),
420                    format_bytes(conn.bytes_sent),
421                    format_bytes(conn.bytes_received),
422                    conn.rtt
423                        .map(|d| format!("{d:?}"))
424                        .unwrap_or_else(|| "N/A".to_string()),
425                    conn.packet_loss * 100.0,
426                ));
427            }
428
429            section.push_str(&draw_box(
430                &format!("🔗 Active Connections ({})", connections.len()),
431                &content,
432                BoxStyle::Single,
433                50,
434            ));
435        }
436
437        section.push('\n');
438        section
439    }
440
441    /// Render graphs section
442    async fn render_graphs_section(&self) -> String {
443        let history = self.history.read().await;
444
445        if history.len() < 2 {
446            return String::new();
447        }
448
449        let mut section = String::new();
450
451        // Connection count graph
452        let conn_data: Vec<usize> = history.iter().map(|d| d.active_connections).collect();
453
454        section.push_str(&draw_box(
455            "📈 Connection History",
456            &render_mini_graph(&conn_data, 20, 50),
457            BoxStyle::Single,
458            50,
459        ));
460        section.push('\n');
461
462        // Success rate graph
463        let success_data: Vec<f64> = history.iter().map(|d| d.nat_success_rate * 100.0).collect();
464
465        section.push_str(&draw_box(
466            "📈 NAT Success Rate %",
467            &render_mini_graph_float(&success_data, 20, 50),
468            BoxStyle::Single,
469            50,
470        ));
471        section.push('\n');
472
473        section
474    }
475
476    /// Render footer
477    async fn render_footer(&self) -> String {
478        let last_update = *self.last_update.read().await;
479
480        format!(
481            "\n{}\n{}",
482            format!(
483                "{}Last updated: {:?} ago{}",
484                terminal_ui::colors::DIM,
485                last_update.elapsed(),
486                terminal_ui::colors::RESET
487            ),
488            format!(
489                "{}Press Ctrl+C to exit{}",
490                terminal_ui::colors::DIM,
491                terminal_ui::colors::RESET
492            ),
493        )
494    }
495}
496
497/// Format duration in human-readable format
498fn format_duration(duration: Duration) -> String {
499    let secs = duration.as_secs();
500    if secs < 60 {
501        format!("{secs}s")
502    } else if secs < 3600 {
503        format!("{}m {}s", secs / 60, secs % 60)
504    } else {
505        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
506    }
507}
508
509/// Format bytes in human-readable format
510fn format_bytes(bytes: u64) -> String {
511    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
512    let mut size = bytes as f64;
513    let mut unit_index = 0;
514
515    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
516        size /= 1024.0;
517        unit_index += 1;
518    }
519
520    format!("{:.2} {}", size, UNITS[unit_index])
521}
522
523/// Render a simple ASCII graph
524fn render_mini_graph(data: &[usize], height: usize, width: usize) -> String {
525    if data.is_empty() {
526        return "No data".to_string();
527    }
528
529    let max_val = *data.iter().max().unwrap_or(&1).max(&1) as f64;
530    let step = data.len().max(1) / width.min(data.len()).max(1);
531
532    let mut graph = vec![vec![' '; width]; height];
533
534    for (i, chunk) in data.chunks(step).enumerate() {
535        if i >= width {
536            break;
537        }
538
539        let avg = chunk.iter().sum::<usize>() as f64 / chunk.len() as f64;
540        let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
541
542        for y in 0..=normalized {
543            let row = height - 1 - y;
544            graph[row][i] = '█';
545        }
546    }
547
548    let mut output = String::new();
549    for row in graph {
550        output.push_str(&row.iter().collect::<String>());
551        output.push('\n');
552    }
553
554    output.push_str(&format!(
555        "Max: {} | Latest: {}",
556        data.iter().max().unwrap_or(&0),
557        data.last().unwrap_or(&0)
558    ));
559
560    output
561}
562
563/// Render a simple ASCII graph for float values
564fn render_mini_graph_float(data: &[f64], height: usize, width: usize) -> String {
565    if data.is_empty() {
566        return "No data".to_string();
567    }
568
569    let max_val = data
570        .iter()
571        .cloned()
572        .fold(f64::NEG_INFINITY, f64::max)
573        .max(1.0);
574    let step = data.len().max(1) / width.min(data.len()).max(1);
575
576    let mut graph = vec![vec![' '; width]; height];
577
578    for (i, chunk) in data.chunks(step).enumerate() {
579        if i >= width {
580            break;
581        }
582
583        let avg = chunk.iter().sum::<f64>() / chunk.len() as f64;
584        let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
585
586        for y in 0..=normalized {
587            let row = height - 1 - y;
588            graph[row][i] = '█';
589        }
590    }
591
592    let mut output = String::new();
593    for row in graph {
594        output.push_str(&row.iter().collect::<String>());
595        output.push('\n');
596    }
597
598    output.push_str(&format!(
599        "Max: {:.1}% | Latest: {:.1}%",
600        data.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
601        data.last().unwrap_or(&0.0)
602    ));
603
604    output
605}