1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum HealthStatus {
15 Healthy,
16 Degraded,
17 Unhealthy,
18}
19
20#[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#[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
41pub 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 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 services.insert("database".to_string(), self.check_database_health(state).await);
62
63 services.insert("docker".to_string(), self.check_docker_health(state).await);
65
66 services.insert("litefs".to_string(), self.check_litefs_health(state).await);
68
69 services.insert("filesystem".to_string(), self.check_filesystem_health().await);
71
72 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 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 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 async fn check_litefs_health(&self, _state: &AppState) -> ServiceHealth {
155 let start = std::time::Instant::now();
156
157 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 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 async fn test_database_connection(&self, state: &AppState) -> Result<()> {
210 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 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 async fn test_litefs_mounts(&self) -> Result<HashMap<String, serde_json::Value>> {
236 let mut details = HashMap::new();
237
238 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 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); }
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 async fn test_filesystem_access(&self) -> Result<HashMap<String, serde_json::Value>> {
263 let mut details = HashMap::new();
264
265 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 if let Ok(metadata) = std::fs::metadata(¤t_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 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 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 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#[derive(Debug)]
327struct DockerInfo {
328 version: String,
329 containers_running: usize,
330}
331
332pub 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 match health_response.status {
339 HealthStatus::Healthy => Ok(Json(health_response)),
340 HealthStatus::Degraded => {
341 warn!("Platform is in degraded state: {}", health_response.summary);
343 Ok(Json(health_response))
344 }
345 HealthStatus::Unhealthy => {
346 error!("Platform is unhealthy: {}", health_response.summary);
348 Err(StatusCode::SERVICE_UNAVAILABLE)
349 }
350 }
351}
352
353pub async fn liveness_handler() -> StatusCode {
355 StatusCode::OK
357}
358
359pub async fn readiness_handler(State(state): State<AppState>) -> StatusCode {
361 let health_checker = HealthChecker::new();
362
363 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}