Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I67206fd813d514f8903041eea0a4cd266a6a6964
222 lines
6.1 KiB
Rust
222 lines
6.1 KiB
Rust
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<DatabaseHealth>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub filesystem: Option<FilesystemHealth>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub cache: Option<CacheHealth>,
|
|
}
|
|
|
|
#[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<u64>,
|
|
}
|
|
|
|
#[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<AppState>) -> Json<HealthResponse> {
|
|
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<std::path::PathBuf> =
|
|
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<AppState>) -> 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<AppState>,
|
|
) -> Json<DetailedHealthResponse> {
|
|
// 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,
|
|
},
|
|
})
|
|
}
|