Skip to main content

cqlite_cli/repl/commands/
health.rs

1//! :health meta-command implementation
2//!
3//! Performs configuration and environment diagnostics
4
5use anyhow::Result;
6use colored::Colorize;
7use std::fs;
8use std::path::Path;
9
10/// Health check result
11#[derive(Debug, Clone)]
12pub struct HealthCheckResult {
13    pub category: String,
14    pub status: HealthStatus,
15    pub message: String,
16    pub hint: Option<String>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum HealthStatus {
21    Ok,
22    Warning,
23    Error,
24}
25
26/// Execute the :health command
27pub async fn execute_health(
28    data_dir: Option<&Path>,
29    config_file: Option<&Path>,
30    page_size: usize,
31    timing_enabled: bool,
32    colors_enabled: bool,
33) -> Result<()> {
34    println!();
35    println!("{}", "=== Health Diagnostics ===".green().bold());
36    println!();
37
38    let mut checks = Vec::new();
39
40    // 1. Data directory checks
41    checks.extend(check_data_directory(data_dir));
42
43    // 2. Config checks
44    checks.extend(check_config(
45        config_file,
46        page_size,
47        timing_enabled,
48        colors_enabled,
49    ));
50
51    // 3. Compression codec checks
52    checks.extend(check_compression_codecs());
53
54    // 4. Platform checks
55    checks.extend(check_platform());
56
57    // Display results
58    display_health_results(&checks);
59
60    // Collect and display actionable hints
61    display_hints(&checks);
62
63    Ok(())
64}
65
66fn check_data_directory(data_dir: Option<&Path>) -> Vec<HealthCheckResult> {
67    let mut results = Vec::new();
68
69    // Check if data directory is set
70    let Some(dir) = data_dir else {
71        results.push(HealthCheckResult {
72            category: "data-dir".to_string(),
73            status: HealthStatus::Warning,
74            message: "Data directory not configured".to_string(),
75            hint: Some("Use :config data-dir=<PATH> to set data directory".to_string()),
76        });
77        return results;
78    };
79
80    // Check if directory exists
81    if !dir.exists() {
82        results.push(HealthCheckResult {
83            category: "data-dir".to_string(),
84            status: HealthStatus::Error,
85            message: format!("Data directory does not exist: {}", dir.display()),
86            hint: Some(format!(
87                "Create directory or use :config data-dir=<PATH> to set valid directory"
88            )),
89        });
90        return results;
91    }
92
93    // Check if it's a directory
94    if !dir.is_dir() {
95        results.push(HealthCheckResult {
96            category: "data-dir".to_string(),
97            status: HealthStatus::Error,
98            message: format!("Path is not a directory: {}", dir.display()),
99            hint: Some("Provide a directory path, not a file".to_string()),
100        });
101        return results;
102    }
103
104    // Check if directory is readable
105    match fs::read_dir(dir) {
106        Ok(_) => {
107            results.push(HealthCheckResult {
108                category: "data-dir".to_string(),
109                status: HealthStatus::Ok,
110                message: format!("Data directory readable: {}", dir.display()),
111                hint: None,
112            });
113        }
114        Err(e) => {
115            results.push(HealthCheckResult {
116                category: "data-dir".to_string(),
117                status: HealthStatus::Error,
118                message: format!("Cannot read data directory: {}", e),
119                hint: Some("Check directory permissions".to_string()),
120            });
121            return results;
122        }
123    }
124
125    // Check directory layout sanity (look for keyspace-like subdirectories)
126    match check_directory_layout(dir) {
127        Ok(layout_info) => {
128            if layout_info.has_keyspaces {
129                results.push(HealthCheckResult {
130                    category: "data-dir layout".to_string(),
131                    status: HealthStatus::Ok,
132                    message: format!(
133                        "Found {} keyspace-like directories",
134                        layout_info.keyspace_count
135                    ),
136                    hint: None,
137                });
138            } else {
139                results.push(HealthCheckResult {
140                    category: "data-dir layout".to_string(),
141                    status: HealthStatus::Warning,
142                    message: "No keyspace directories found".to_string(),
143                    hint: Some(
144                        "Directory may be empty or not a valid Cassandra data directory"
145                            .to_string(),
146                    ),
147                });
148            }
149        }
150        Err(e) => {
151            results.push(HealthCheckResult {
152                category: "data-dir layout".to_string(),
153                status: HealthStatus::Warning,
154                message: format!("Could not analyze directory layout: {}", e),
155                hint: None,
156            });
157        }
158    }
159
160    results
161}
162
163#[derive(Debug)]
164struct DirectoryLayoutInfo {
165    has_keyspaces: bool,
166    keyspace_count: usize,
167}
168
169fn check_directory_layout(dir: &Path) -> Result<DirectoryLayoutInfo> {
170    let mut keyspace_count = 0;
171    let mut has_keyspaces = false;
172
173    for entry in fs::read_dir(dir)? {
174        let entry = entry?;
175        let path = entry.path();
176
177        if path.is_dir() {
178            // Count directories that might be keyspaces
179            // (simple heuristic: any subdirectory could be a keyspace)
180            keyspace_count += 1;
181            has_keyspaces = true;
182        }
183    }
184
185    Ok(DirectoryLayoutInfo {
186        has_keyspaces,
187        keyspace_count,
188    })
189}
190
191fn check_config(
192    config_file: Option<&Path>,
193    page_size: usize,
194    timing_enabled: bool,
195    colors_enabled: bool,
196) -> Vec<HealthCheckResult> {
197    let mut results = Vec::new();
198
199    // Config file check
200    if let Some(file) = config_file {
201        if file.exists() {
202            results.push(HealthCheckResult {
203                category: "config".to_string(),
204                status: HealthStatus::Ok,
205                message: format!("Config file loaded: {}", file.display()),
206                hint: None,
207            });
208        } else {
209            results.push(HealthCheckResult {
210                category: "config".to_string(),
211                status: HealthStatus::Warning,
212                message: format!("Config file not found: {}", file.display()),
213                hint: Some("Check config file path".to_string()),
214            });
215        }
216    } else {
217        results.push(HealthCheckResult {
218            category: "config".to_string(),
219            status: HealthStatus::Ok,
220            message: "Using default configuration".to_string(),
221            hint: None,
222        });
223    }
224
225    // Page size sanity check
226    if page_size > 0 && page_size <= 10000 {
227        results.push(HealthCheckResult {
228            category: "page-size".to_string(),
229            status: HealthStatus::Ok,
230            message: format!("Page size: {}", page_size),
231            hint: None,
232        });
233    } else if page_size > 10000 {
234        results.push(HealthCheckResult {
235            category: "page-size".to_string(),
236            status: HealthStatus::Warning,
237            message: format!("Page size very large: {}", page_size),
238            hint: Some("Consider reducing page size for better memory usage".to_string()),
239        });
240    } else {
241        results.push(HealthCheckResult {
242            category: "page-size".to_string(),
243            status: HealthStatus::Error,
244            message: format!("Invalid page size: {}", page_size),
245            hint: Some("Use :config page_size=<N> to set valid page size".to_string()),
246        });
247    }
248
249    // Report timing and colors settings (informational)
250    results.push(HealthCheckResult {
251        category: "settings".to_string(),
252        status: HealthStatus::Ok,
253        message: format!(
254            "Timing: {}, Colors: {}",
255            if timing_enabled {
256                "enabled"
257            } else {
258                "disabled"
259            },
260            if colors_enabled {
261                "enabled"
262            } else {
263                "disabled"
264            }
265        ),
266        hint: None,
267    });
268
269    results
270}
271
272fn check_compression_codecs() -> Vec<HealthCheckResult> {
273    let mut results = Vec::new();
274    let mut available_codecs: Vec<&str> = Vec::new();
275
276    // Note: Compression features are defined in cqlite-core, not cqlite-cli
277    // We check what the core library was built with by checking its dependencies
278    // For now, we assume standard build with all-compression feature enabled
279
280    // The standard build includes all compression codecs
281    // These are compiled into cqlite-core by default
282    available_codecs.push("LZ4");
283    available_codecs.push("Snappy");
284    available_codecs.push("Deflate");
285    available_codecs.push("Zstd");
286
287    // Report available codecs
288    if !available_codecs.is_empty() {
289        results.push(HealthCheckResult {
290            category: "compression".to_string(),
291            status: HealthStatus::Ok,
292            message: format!("Available codecs: {}", available_codecs.join(", ")),
293            hint: None,
294        });
295    } else {
296        // If no codecs at all (unlikely with default features)
297        results.push(HealthCheckResult {
298            category: "compression".to_string(),
299            status: HealthStatus::Error,
300            message: "No compression codecs available".to_string(),
301            hint: Some("Rebuild with --features all-compression".to_string()),
302        });
303    }
304
305    results
306}
307
308fn check_platform() -> Vec<HealthCheckResult> {
309    let mut results = Vec::new();
310
311    // Platform detection
312    let platform = std::env::consts::OS;
313    let arch = std::env::consts::ARCH;
314
315    results.push(HealthCheckResult {
316        category: "platform".to_string(),
317        status: HealthStatus::Ok,
318        message: format!("Platform: {} ({})", platform, arch),
319        hint: None,
320    });
321
322    // Check if we're on a supported platform
323    match platform {
324        "linux" | "macos" | "windows" => {
325            results.push(HealthCheckResult {
326                category: "platform".to_string(),
327                status: HealthStatus::Ok,
328                message: format!("{} is supported", platform),
329                hint: None,
330            });
331        }
332        _ => {
333            results.push(HealthCheckResult {
334                category: "platform".to_string(),
335                status: HealthStatus::Warning,
336                message: format!("{} may have limited support", platform),
337                hint: Some("Report issues at https://github.com/cqlite/cqlite/issues".to_string()),
338            });
339        }
340    }
341
342    // File descriptor limits (Unix-like systems only)
343    #[cfg(unix)]
344    {
345        // This is a simple check - in production you might use libc to get actual limits
346        results.push(HealthCheckResult {
347            category: "platform".to_string(),
348            status: HealthStatus::Ok,
349            message: "Unix-like platform detected".to_string(),
350            hint: None,
351        });
352    }
353
354    results
355}
356
357fn display_health_results(checks: &[HealthCheckResult]) {
358    println!("{}", "Checks:".cyan().bold());
359    println!();
360
361    for check in checks {
362        let (icon, status_text) = match check.status {
363            HealthStatus::Ok => ("✅", "OK".green()),
364            HealthStatus::Warning => ("⚠️ ", "WARNING".yellow()),
365            HealthStatus::Error => ("❌", "ERROR".red()),
366        };
367
368        println!(
369            "  {} {} [{}] {}",
370            icon,
371            check.category.cyan(),
372            status_text,
373            check.message
374        );
375    }
376
377    println!();
378}
379
380fn display_hints(checks: &[HealthCheckResult]) {
381    let hints: Vec<_> = checks
382        .iter()
383        .filter_map(|check| {
384            check
385                .hint
386                .as_ref()
387                .map(|hint| (check.category.clone(), check.status, hint.clone()))
388        })
389        .collect();
390
391    if !hints.is_empty() {
392        println!("{}", "Tips:".yellow().bold());
393        println!();
394
395        for (category, status, hint) in hints {
396            let prefix = match status {
397                HealthStatus::Error => "❌",
398                HealthStatus::Warning => "💡",
399                HealthStatus::Ok => "ℹ️ ",
400            };
401
402            println!("  {} {}: {}", prefix, category.cyan(), hint);
403        }
404
405        println!();
406    }
407
408    // Summary
409    let ok_count = checks
410        .iter()
411        .filter(|c| c.status == HealthStatus::Ok)
412        .count();
413    let warning_count = checks
414        .iter()
415        .filter(|c| c.status == HealthStatus::Warning)
416        .count();
417    let error_count = checks
418        .iter()
419        .filter(|c| c.status == HealthStatus::Error)
420        .count();
421
422    println!("{}", "Summary:".cyan().bold());
423    println!(
424        "  {} checks: {} OK, {} warnings, {} errors",
425        checks.len(),
426        ok_count.to_string().green(),
427        warning_count.to_string().yellow(),
428        error_count.to_string().red()
429    );
430    println!();
431}