cqlite_cli/repl/commands/
health.rs1use anyhow::Result;
6use colored::Colorize;
7use std::fs;
8use std::path::Path;
9
10#[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
26pub 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 checks.extend(check_data_directory(data_dir));
42
43 checks.extend(check_config(
45 config_file,
46 page_size,
47 timing_enabled,
48 colors_enabled,
49 ));
50
51 checks.extend(check_compression_codecs());
53
54 checks.extend(check_platform());
56
57 display_health_results(&checks);
59
60 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 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 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 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 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 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 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 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 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 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 available_codecs.push("LZ4");
283 available_codecs.push("Snappy");
284 available_codecs.push("Deflate");
285 available_codecs.push("Zstd");
286
287 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 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 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 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 #[cfg(unix)]
344 {
345 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 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}