pinakes/crates/pinakes-server/src/routes/health.rs
NotAShelf eb6c0a3577
pinakes-server: update remaining route imports and handlers
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I67206fd813d514f8903041eea0a4cd266a6a6964
2026-03-08 00:43:28 +03:00

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,
},
})
}