Skip to main content

minifly_api/
health.rs

1//! Health check system for Minifly API server
2
3use axum::{extract::State, response::Json, http::StatusCode};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use tracing::{info, warn, error};
7use crate::state::AppState;
8use anyhow::Result;
9use sqlx::Row;
10
11/// Overall health status
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum HealthStatus {
15    Healthy,
16    Degraded,
17    Unhealthy,
18}
19
20/// Individual service health information
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ServiceHealth {
23    pub status: HealthStatus,
24    pub message: String,
25    pub last_checked: String,
26    pub response_time_ms: Option<u64>,
27    pub details: HashMap<String, serde_json::Value>,
28}
29
30/// Complete health check response
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct HealthResponse {
33    pub status: HealthStatus,
34    pub version: String,
35    pub uptime_seconds: u64,
36    pub timestamp: String,
37    pub services: HashMap<String, ServiceHealth>,
38    pub summary: String,
39}
40
41/// Health check manager
42pub struct HealthChecker {
43    start_time: std::time::Instant,
44}
45
46impl HealthChecker {
47    pub fn new() -> Self {
48        Self {
49            start_time: std::time::Instant::now(),
50        }
51    }
52
53    /// Perform comprehensive health check
54    pub async fn check_health(&self, state: &AppState) -> HealthResponse {
55        info!("Performing comprehensive health check");
56        let start = std::time::Instant::now();
57        
58        let mut services = HashMap::new();
59        
60        // Check database connectivity
61        services.insert("database".to_string(), self.check_database_health(state).await);
62        
63        // Check Docker daemon
64        services.insert("docker".to_string(), self.check_docker_health(state).await);
65        
66        // Check LiteFS (if configured)
67        services.insert("litefs".to_string(), self.check_litefs_health(state).await);
68        
69        // Check file system access
70        services.insert("filesystem".to_string(), self.check_filesystem_health().await);
71        
72        // Determine overall status
73        let overall_status = self.determine_overall_status(&services);
74        let summary = self.generate_summary(&services, &overall_status);
75        
76        let health_check_duration = start.elapsed();
77        
78        info!(
79            status = ?overall_status,
80            duration_ms = health_check_duration.as_millis(),
81            services_checked = services.len(),
82            "Health check completed"
83        );
84        
85        HealthResponse {
86            status: overall_status,
87            version: env!("CARGO_PKG_VERSION").to_string(),
88            uptime_seconds: self.start_time.elapsed().as_secs(),
89            timestamp: chrono::Utc::now().to_rfc3339(),
90            services,
91            summary,
92        }
93    }
94
95    /// Check database health
96    async fn check_database_health(&self, state: &AppState) -> ServiceHealth {
97        let start = std::time::Instant::now();
98        
99        match self.test_database_connection(state).await {
100            Ok(_) => {
101                ServiceHealth {
102                    status: HealthStatus::Healthy,
103                    message: "Database connection successful".to_string(),
104                    last_checked: chrono::Utc::now().to_rfc3339(),
105                    response_time_ms: Some(start.elapsed().as_millis() as u64),
106                    details: HashMap::new(),
107                }
108            }
109            Err(e) => {
110                error!(error = %e, "Database health check failed");
111                ServiceHealth {
112                    status: HealthStatus::Unhealthy,
113                    message: format!("Database connection failed: {}", e),
114                    last_checked: chrono::Utc::now().to_rfc3339(),
115                    response_time_ms: Some(start.elapsed().as_millis() as u64),
116                    details: HashMap::new(),
117                }
118            }
119        }
120    }
121
122    /// Check Docker daemon health
123    async fn check_docker_health(&self, state: &AppState) -> ServiceHealth {
124        let start = std::time::Instant::now();
125        
126        match self.test_docker_connection(state).await {
127            Ok(info) => {
128                let mut details = HashMap::new();
129                details.insert("version".to_string(), serde_json::Value::String(info.version));
130                details.insert("containers_running".to_string(), serde_json::Value::Number(info.containers_running.into()));
131                
132                ServiceHealth {
133                    status: HealthStatus::Healthy,
134                    message: "Docker daemon accessible".to_string(),
135                    last_checked: chrono::Utc::now().to_rfc3339(),
136                    response_time_ms: Some(start.elapsed().as_millis() as u64),
137                    details,
138                }
139            }
140            Err(e) => {
141                error!(error = %e, "Docker health check failed");
142                ServiceHealth {
143                    status: HealthStatus::Unhealthy,
144                    message: format!("Docker daemon unavailable: {}", e),
145                    last_checked: chrono::Utc::now().to_rfc3339(),
146                    response_time_ms: Some(start.elapsed().as_millis() as u64),
147                    details: HashMap::new(),
148                }
149            }
150        }
151    }
152
153    /// Check LiteFS health
154    async fn check_litefs_health(&self, _state: &AppState) -> ServiceHealth {
155        let start = std::time::Instant::now();
156        
157        // Check if LiteFS mount points are accessible
158        match self.test_litefs_mounts().await {
159            Ok(mount_info) => {
160                ServiceHealth {
161                    status: HealthStatus::Healthy,
162                    message: "LiteFS mounts accessible".to_string(),
163                    last_checked: chrono::Utc::now().to_rfc3339(),
164                    response_time_ms: Some(start.elapsed().as_millis() as u64),
165                    details: mount_info,
166                }
167            }
168            Err(e) => {
169                warn!(error = %e, "LiteFS health check failed");
170                ServiceHealth {
171                    status: HealthStatus::Degraded,
172                    message: format!("LiteFS issues detected: {}", e),
173                    last_checked: chrono::Utc::now().to_rfc3339(),
174                    response_time_ms: Some(start.elapsed().as_millis() as u64),
175                    details: HashMap::new(),
176                }
177            }
178        }
179    }
180
181    /// Check filesystem health
182    async fn check_filesystem_health(&self) -> ServiceHealth {
183        let start = std::time::Instant::now();
184        
185        match self.test_filesystem_access().await {
186            Ok(fs_info) => {
187                ServiceHealth {
188                    status: HealthStatus::Healthy,
189                    message: "Filesystem access normal".to_string(),
190                    last_checked: chrono::Utc::now().to_rfc3339(),
191                    response_time_ms: Some(start.elapsed().as_millis() as u64),
192                    details: fs_info,
193                }
194            }
195            Err(e) => {
196                error!(error = %e, "Filesystem health check failed");
197                ServiceHealth {
198                    status: HealthStatus::Unhealthy,
199                    message: format!("Filesystem access issues: {}", e),
200                    last_checked: chrono::Utc::now().to_rfc3339(),
201                    response_time_ms: Some(start.elapsed().as_millis() as u64),
202                    details: HashMap::new(),
203                }
204            }
205        }
206    }
207
208    /// Test database connection
209    async fn test_database_connection(&self, state: &AppState) -> Result<()> {
210        // Simple query to test database connectivity
211        let result = sqlx::query("SELECT 1 as test")
212            .fetch_one(&state.db)
213            .await?;
214        
215        let _test_value: i32 = result.get("test");
216        Ok(())
217    }
218
219    /// Test Docker daemon connection
220    async fn test_docker_connection(&self, state: &AppState) -> Result<DockerInfo> {
221        let version = state.docker.version().await?;
222        let containers = state.docker.list_containers(None).await?;
223        
224        let running_containers = containers.iter()
225            .filter(|c| c.state.as_ref().map(|s| s == "running").unwrap_or(false))
226            .count();
227
228        Ok(DockerInfo {
229            version: version.version.unwrap_or_else(|| "unknown".to_string()),
230            containers_running: running_containers,
231        })
232    }
233
234    /// Test LiteFS mount accessibility
235    async fn test_litefs_mounts(&self) -> Result<HashMap<String, serde_json::Value>> {
236        let mut details = HashMap::new();
237        
238        // Check if data directory exists and is writable
239        let data_dir = std::path::Path::new("./data");
240        if data_dir.exists() {
241            details.insert("data_dir_exists".to_string(), serde_json::Value::Bool(true));
242            
243            // Try to write a test file
244            let test_file = data_dir.join("health_check.tmp");
245            match std::fs::write(&test_file, "health_check") {
246                Ok(_) => {
247                    details.insert("data_dir_writable".to_string(), serde_json::Value::Bool(true));
248                    let _ = std::fs::remove_file(&test_file); // Clean up
249                }
250                Err(_) => {
251                    details.insert("data_dir_writable".to_string(), serde_json::Value::Bool(false));
252                }
253            }
254        } else {
255            details.insert("data_dir_exists".to_string(), serde_json::Value::Bool(false));
256        }
257        
258        Ok(details)
259    }
260
261    /// Test filesystem access
262    async fn test_filesystem_access(&self) -> Result<HashMap<String, serde_json::Value>> {
263        let mut details = HashMap::new();
264        
265        // Check current directory access
266        let current_dir = std::env::current_dir()?;
267        details.insert("current_dir".to_string(), 
268                      serde_json::Value::String(current_dir.display().to_string()));
269        
270        // Check available disk space (simplified)
271        if let Ok(metadata) = std::fs::metadata(&current_dir) {
272            details.insert("current_dir_accessible".to_string(), serde_json::Value::Bool(true));
273        } else {
274            details.insert("current_dir_accessible".to_string(), serde_json::Value::Bool(false));
275        }
276        
277        Ok(details)
278    }
279
280    /// Determine overall health status from individual services
281    fn determine_overall_status(&self, services: &HashMap<String, ServiceHealth>) -> HealthStatus {
282        let mut healthy_count = 0;
283        let mut degraded_count = 0;
284        let mut unhealthy_count = 0;
285        
286        for service in services.values() {
287            match service.status {
288                HealthStatus::Healthy => healthy_count += 1,
289                HealthStatus::Degraded => degraded_count += 1,
290                HealthStatus::Unhealthy => unhealthy_count += 1,
291            }
292        }
293        
294        // Determine overall status based on service health
295        if unhealthy_count > 0 {
296            HealthStatus::Unhealthy
297        } else if degraded_count > 0 {
298            HealthStatus::Degraded
299        } else {
300            HealthStatus::Healthy
301        }
302    }
303
304    /// Generate health summary message
305    fn generate_summary(&self, services: &HashMap<String, ServiceHealth>, overall_status: &HealthStatus) -> String {
306        let total_services = services.len();
307        let healthy_services = services.values()
308            .filter(|s| matches!(s.status, HealthStatus::Healthy))
309            .count();
310        
311        match overall_status {
312            HealthStatus::Healthy => {
313                format!("All {} services are healthy", total_services)
314            }
315            HealthStatus::Degraded => {
316                format!("{}/{} services healthy, some degraded", healthy_services, total_services)
317            }
318            HealthStatus::Unhealthy => {
319                format!("{}/{} services healthy, critical issues detected", healthy_services, total_services)
320            }
321        }
322    }
323}
324
325/// Docker information for health checks
326#[derive(Debug)]
327struct DockerInfo {
328    version: String,
329    containers_running: usize,
330}
331
332/// Health check endpoint handler
333pub async fn health_handler(State(state): State<AppState>) -> Result<Json<HealthResponse>, StatusCode> {
334    let health_checker = HealthChecker::new();
335    let health_response = health_checker.check_health(&state).await;
336    
337    // Return appropriate HTTP status based on health
338    match health_response.status {
339        HealthStatus::Healthy => Ok(Json(health_response)),
340        HealthStatus::Degraded => {
341            // Return 200 but log the degraded state
342            warn!("Platform is in degraded state: {}", health_response.summary);
343            Ok(Json(health_response))
344        }
345        HealthStatus::Unhealthy => {
346            // Return 503 Service Unavailable for unhealthy state
347            error!("Platform is unhealthy: {}", health_response.summary);
348            Err(StatusCode::SERVICE_UNAVAILABLE)
349        }
350    }
351}
352
353/// Simple liveness probe endpoint
354pub async fn liveness_handler() -> StatusCode {
355    // Simple liveness check - if the server can respond, it's alive
356    StatusCode::OK
357}
358
359/// Readiness probe endpoint
360pub async fn readiness_handler(State(state): State<AppState>) -> StatusCode {
361    let health_checker = HealthChecker::new();
362    
363    // Quick readiness check focusing on critical services
364    let db_ready = health_checker.test_database_connection(&state).await.is_ok();
365    let docker_ready = health_checker.test_docker_connection(&state).await.is_ok();
366    
367    if db_ready && docker_ready {
368        StatusCode::OK
369    } else {
370        StatusCode::SERVICE_UNAVAILABLE
371    }
372}