use std::time::Instant; use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; use serde::{Deserialize, Serialize}; use crate::state::AppState; /// Basic health check response #[derive(Debug, Serialize, Deserialize)] pub struct HealthResponse { pub status: String, pub version: String, #[serde(skip_serializing_if = "Option::is_none")] pub database: Option, #[serde(skip_serializing_if = "Option::is_none")] pub filesystem: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct DatabaseHealth { pub status: String, pub latency_ms: u64, #[serde(skip_serializing_if = "Option::is_none")] pub media_count: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct FilesystemHealth { pub status: String, pub roots_configured: usize, pub roots_accessible: usize, } #[derive(Debug, Serialize, Deserialize)] pub struct CacheHealth { pub hit_rate: f64, pub total_entries: u64, pub responses_size: u64, pub queries_size: u64, pub media_size: u64, } /// Comprehensive health check - includes database, filesystem, and cache status pub async fn health(State(state): State) -> Json { let mut response = HealthResponse { status: "ok".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), database: None, filesystem: None, cache: None, }; // Check database health let db_start = Instant::now(); let db_health = match state.storage.count_media().await { Ok(count) => { DatabaseHealth { status: "ok".to_string(), latency_ms: db_start.elapsed().as_millis() as u64, media_count: Some(count), } }, Err(e) => { response.status = "degraded".to_string(); DatabaseHealth { status: format!("error: {e}"), latency_ms: db_start.elapsed().as_millis() as u64, media_count: None, } }, }; response.database = Some(db_health); // Check filesystem health (root directories) let roots: Vec = state.storage.list_root_dirs().await.unwrap_or_default(); let roots_accessible = roots.iter().filter(|r| r.exists()).count(); if roots_accessible < roots.len() { response.status = "degraded".to_string(); } response.filesystem = Some(FilesystemHealth { status: if roots_accessible == roots.len() { "ok" } else { "degraded" } .to_string(), roots_configured: roots.len(), roots_accessible, }); // Get cache statistics let cache_stats = state.cache.stats(); response.cache = Some(CacheHealth { hit_rate: cache_stats.overall_hit_rate(), total_entries: cache_stats.total_entries(), responses_size: cache_stats.responses.size, queries_size: cache_stats.queries.size, media_size: cache_stats.media.size, }); Json(response) } /// Liveness probe - just checks if the server is running /// Returns 200 OK if the server process is alive pub async fn liveness() -> impl IntoResponse { ( StatusCode::OK, Json(serde_json::json!({ "status": "alive" })), ) } /// Readiness probe - checks if the server can serve requests /// Returns 200 OK if database is accessible pub async fn readiness(State(state): State) -> impl IntoResponse { // Check database connectivity let db_start = Instant::now(); match state.storage.count_media().await { Ok(_) => { let latency = db_start.elapsed().as_millis() as u64; ( StatusCode::OK, Json(serde_json::json!({ "status": "ready", "database_latency_ms": latency })), ) }, Err(e) => { ( StatusCode::SERVICE_UNAVAILABLE, Json(serde_json::json!({ "status": "not_ready", "reason": e.to_string() })), ) }, } } /// Detailed health check for monitoring dashboards #[derive(Debug, Serialize, Deserialize)] pub struct DetailedHealthResponse { pub status: String, pub version: String, pub uptime_seconds: u64, pub database: DatabaseHealth, pub filesystem: FilesystemHealth, pub cache: CacheHealth, pub jobs: JobsHealth, } #[derive(Debug, Serialize, Deserialize)] pub struct JobsHealth { pub pending: usize, pub running: usize, } pub async fn health_detailed( State(state): State, ) -> Json { // Check database let db_start = Instant::now(); let (db_status, media_count) = match state.storage.count_media().await { Ok(count) => ("ok".to_string(), Some(count)), Err(e) => (format!("error: {e}"), None), }; let db_latency = db_start.elapsed().as_millis() as u64; // Check filesystem let roots = state.storage.list_root_dirs().await.unwrap_or_default(); let roots_accessible = roots.iter().filter(|r| r.exists()).count(); // Get cache stats let cache_stats = state.cache.stats(); // Get job queue stats let job_stats = state.job_queue.stats().await; let overall_status = if db_status == "ok" && roots_accessible == roots.len() { "ok" } else { "degraded" }; Json(DetailedHealthResponse { status: overall_status.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), uptime_seconds: 0, // Could track server start time database: DatabaseHealth { status: db_status, latency_ms: db_latency, media_count, }, filesystem: FilesystemHealth { status: if roots_accessible == roots.len() { "ok" } else { "degraded" } .to_string(), roots_configured: roots.len(), roots_accessible, }, cache: CacheHealth { hit_rate: cache_stats.overall_hit_rate(), total_entries: cache_stats.total_entries(), responses_size: cache_stats.responses.size, queries_size: cache_stats.queries.size, media_size: cache_stats.media.size, }, jobs: JobsHealth { pending: job_stats.pending, running: job_stats.running, }, }) }