cqlite_cli/
status_metrics.rs1use cqlite_core::Database;
9use std::path::Path;
10use std::time::{Duration, Instant};
11
12pub const METRICS_REFRESH_INTERVAL: Duration = Duration::from_secs(5);
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum HealthIndicator {
18 Ok,
20 Warning,
22 Error,
24}
25
26#[derive(Debug, Clone)]
28pub struct StatusMetrics {
29 pub health: HealthIndicator,
31 pub memory_bytes: u64,
33 pub data_bytes: u64,
35 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 pub async fn collect(data_dir: Option<&Path>, database: Option<&Database>) -> Self {
62 let mut metrics = StatusMetrics::default();
63
64 metrics.health = Self::check_health(data_dir);
66
67 metrics.memory_bytes = Self::get_process_memory();
69
70 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 fn get_process_memory() -> u64 {
86 use sysinfo::{ProcessRefreshKind, System};
87
88 let mut system = System::new();
90
91 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 system
101 .process(pid)
102 .map(|process| process.memory())
103 .unwrap_or(0)
104 }
105
106 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 pub fn format_memory(&self) -> String {
128 format_bytes(self.memory_bytes)
129 }
130
131 pub fn format_data(&self) -> String {
135 format_bytes(self.data_bytes)
136 }
137
138 #[allow(dead_code)] 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 pub fn is_stale(&self, max_age: Duration) -> bool {
160 self.last_updated.elapsed() > max_age
161 }
162}
163
164pub(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 assert!(!metrics.is_stale(Duration::from_secs(5)));
222
223 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}