Skip to main content

cqlite_cli/
status_metrics.rs

1//! Status Metrics Module for TUI and REPL Status Bars
2//!
3//! Provides shared infrastructure for collecting and formatting health,
4//! memory, and data metrics for display in both TUI and REPL modes.
5//!
6//! Issue #242: Enhanced Status Bar for CLI
7
8use cqlite_core::Database;
9use std::path::Path;
10use std::time::{Duration, Instant};
11
12/// Default metrics refresh interval (5 seconds)
13pub const METRICS_REFRESH_INTERVAL: Duration = Duration::from_secs(5);
14
15/// Health indicator for status bar display
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum HealthIndicator {
18    /// Everything is working correctly
19    Ok,
20    /// Minor issues or unconfigured state
21    Warning,
22    /// Critical errors or failures
23    Error,
24}
25
26/// Collected metrics for status bar display
27#[derive(Debug, Clone)]
28pub struct StatusMetrics {
29    /// Overall health status
30    pub health: HealthIndicator,
31    /// Current memory usage in bytes
32    pub memory_bytes: u64,
33    /// Total data size (SSTable bytes) visible
34    pub data_bytes: u64,
35    /// When these metrics were collected
36    pub last_updated: Instant,
37}
38
39impl Default for StatusMetrics {
40    fn default() -> Self {
41        Self {
42            health: HealthIndicator::Warning,
43            memory_bytes: 0,
44            data_bytes: 0,
45            last_updated: Instant::now(),
46        }
47    }
48}
49
50impl StatusMetrics {
51    /// Collect status metrics from the current state
52    ///
53    /// # Arguments
54    ///
55    /// * `data_dir` - Optional path to the data directory
56    /// * `database` - Optional reference to the Database for stats
57    ///
58    /// # Returns
59    ///
60    /// StatusMetrics with current health, memory, and data information
61    pub async fn collect(data_dir: Option<&Path>, database: Option<&Database>) -> Self {
62        let mut metrics = StatusMetrics::default();
63
64        // Determine health status based on data directory
65        metrics.health = Self::check_health(data_dir);
66
67        // Get process memory usage (more reliable than database internal stats)
68        metrics.memory_bytes = Self::get_process_memory();
69
70        // Get data stats from database if available
71        if let Some(db) = database {
72            if let Ok(stats) = db.stats().await {
73                metrics.data_bytes = stats.storage_stats.sstables.total_size;
74            }
75        }
76
77        metrics.last_updated = Instant::now();
78        metrics
79    }
80
81    /// Get current process memory usage in bytes
82    ///
83    /// Uses sysinfo crate for cross-platform memory reading.
84    /// Returns RSS (Resident Set Size) in bytes, or 0 if unavailable.
85    fn get_process_memory() -> u64 {
86        use sysinfo::{ProcessRefreshKind, System};
87
88        // Create system instance and refresh current process
89        let mut system = System::new();
90
91        // Get current process ID
92        let pid = match sysinfo::get_current_pid() {
93            Ok(pid) => pid,
94            Err(_) => return 0,
95        };
96
97        system.refresh_process_specifics(pid, ProcessRefreshKind::new().with_memory());
98
99        // Get memory usage in bytes (RSS)
100        system
101            .process(pid)
102            .map(|process| process.memory())
103            .unwrap_or(0)
104    }
105
106    /// Check health status based on data directory state
107    fn check_health(data_dir: Option<&Path>) -> HealthIndicator {
108        match data_dir {
109            None => HealthIndicator::Warning,
110            Some(path) => {
111                if !path.exists() {
112                    HealthIndicator::Error
113                } else if !path.is_dir() {
114                    HealthIndicator::Error
115                } else if std::fs::read_dir(path).is_err() {
116                    HealthIndicator::Error
117                } else {
118                    HealthIndicator::Ok
119                }
120            }
121        }
122    }
123
124    /// Format memory bytes as human-readable string
125    ///
126    /// Examples: "0 B", "512 B", "1.5 KB", "24.5 MB", "1.2 GB"
127    pub fn format_memory(&self) -> String {
128        format_bytes(self.memory_bytes)
129    }
130
131    /// Format data bytes as human-readable string
132    ///
133    /// Examples: "0 B", "512 B", "1.5 KB", "24.5 MB", "1.2 GB"
134    pub fn format_data(&self) -> String {
135        format_bytes(self.data_bytes)
136    }
137
138    /// Get the health indicator symbol for display
139    ///
140    /// Returns: "OK" (green), "WARN" (yellow), or "ERR" (red)
141    #[allow(dead_code)] // Part of public API, may be used in future enhancements
142    pub fn health_symbol(&self) -> &'static str {
143        match self.health {
144            HealthIndicator::Ok => "OK",
145            HealthIndicator::Warning => "WARN",
146            HealthIndicator::Error => "ERR",
147        }
148    }
149
150    /// Check if metrics are stale and need refresh
151    ///
152    /// # Arguments
153    ///
154    /// * `max_age` - Maximum age before metrics are considered stale
155    ///
156    /// # Returns
157    ///
158    /// true if metrics are older than max_age
159    pub fn is_stale(&self, max_age: Duration) -> bool {
160        self.last_updated.elapsed() > max_age
161    }
162}
163
164/// Format bytes as human-readable string with appropriate unit
165///
166/// Uses binary units (KiB = 1024) but displays as KB/MB/GB for readability
167pub(crate) fn format_bytes(bytes: u64) -> String {
168    const KB: u64 = 1024;
169    const MB: u64 = KB * 1024;
170    const GB: u64 = MB * 1024;
171    const TB: u64 = GB * 1024;
172
173    if bytes >= TB {
174        format!("{:.1} TB", bytes as f64 / TB as f64)
175    } else if bytes >= GB {
176        format!("{:.1} GB", bytes as f64 / GB as f64)
177    } else if bytes >= MB {
178        format!("{:.1} MB", bytes as f64 / MB as f64)
179    } else if bytes >= KB {
180        format!("{:.1} KB", bytes as f64 / KB as f64)
181    } else {
182        format!("{} B", bytes)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_format_bytes() {
192        assert_eq!(format_bytes(0), "0 B");
193        assert_eq!(format_bytes(512), "512 B");
194        assert_eq!(format_bytes(1024), "1.0 KB");
195        assert_eq!(format_bytes(1536), "1.5 KB");
196        assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
197        assert_eq!(format_bytes(25 * 1024 * 1024), "25.0 MB");
198        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
199        assert_eq!(format_bytes(1024 * 1024 * 1024 * 1024), "1.0 TB");
200    }
201
202    #[test]
203    fn test_health_symbol() {
204        let mut metrics = StatusMetrics::default();
205
206        metrics.health = HealthIndicator::Ok;
207        assert_eq!(metrics.health_symbol(), "OK");
208
209        metrics.health = HealthIndicator::Warning;
210        assert_eq!(metrics.health_symbol(), "WARN");
211
212        metrics.health = HealthIndicator::Error;
213        assert_eq!(metrics.health_symbol(), "ERR");
214    }
215
216    #[test]
217    fn test_is_stale() {
218        let metrics = StatusMetrics::default();
219
220        // Fresh metrics should not be stale
221        assert!(!metrics.is_stale(Duration::from_secs(5)));
222
223        // With zero duration, anything is stale
224        assert!(metrics.is_stale(Duration::from_nanos(0)));
225    }
226
227    #[test]
228    fn test_check_health_no_data_dir() {
229        let health = StatusMetrics::check_health(None);
230        assert_eq!(health, HealthIndicator::Warning);
231    }
232
233    #[test]
234    fn test_check_health_nonexistent_path() {
235        let health = StatusMetrics::check_health(Some(Path::new("/nonexistent/path/12345")));
236        assert_eq!(health, HealthIndicator::Error);
237    }
238
239    #[test]
240    fn test_default_metrics() {
241        let metrics = StatusMetrics::default();
242        assert_eq!(metrics.health, HealthIndicator::Warning);
243        assert_eq!(metrics.memory_bytes, 0);
244        assert_eq!(metrics.data_bytes, 0);
245    }
246}