use crate::error::Result;
use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
pub struct HealthCheckManager {
checks: Arc<RwLock<HashMap<String, Box<dyn HealthCheck + Send + Sync>>>>,
status: Arc<RwLock<HealthStatus>>,
}
pub trait HealthCheck: Send + Sync {
fn check(&self) -> Result<CheckResult>;
fn name(&self) -> &str;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResult {
pub name: String,
pub status: ComponentStatus,
pub message: Option<String>,
pub checked_at: DateTime<Utc>,
pub duration_ms: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComponentStatus {
Healthy,
Degraded,
Unhealthy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
pub status: ComponentStatus,
pub checks: Vec<CheckResult>,
pub checked_at: DateTime<Utc>,
}
impl HealthCheckManager {
pub fn new() -> Self {
Self {
checks: Arc::new(RwLock::new(HashMap::new())),
status: Arc::new(RwLock::new(HealthStatus {
status: ComponentStatus::Healthy,
checks: Vec::new(),
checked_at: Utc::now(),
})),
}
}
pub fn register(&self, check: Box<dyn HealthCheck + Send + Sync>) {
let name = check.name().to_string();
self.checks.write().insert(name, check);
}
pub fn check_all(&self) -> Result<HealthStatus> {
let checks = self.checks.read();
let mut results = Vec::new();
let mut overall_status = ComponentStatus::Healthy;
for check in checks.values() {
let result = check.check()?;
match result.status {
ComponentStatus::Unhealthy => overall_status = ComponentStatus::Unhealthy,
ComponentStatus::Degraded if overall_status == ComponentStatus::Healthy => {
overall_status = ComponentStatus::Degraded
}
_ => {}
}
results.push(result);
}
let status = HealthStatus {
status: overall_status,
checks: results,
checked_at: Utc::now(),
};
*self.status.write() = status.clone();
Ok(status)
}
pub fn get_status(&self) -> HealthStatus {
self.status.read().clone()
}
}
impl Default for HealthCheckManager {
fn default() -> Self {
Self::new()
}
}
pub struct DatabaseHealthCheck {
name: String,
}
impl DatabaseHealthCheck {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
}
impl HealthCheck for DatabaseHealthCheck {
fn check(&self) -> Result<CheckResult> {
let start = std::time::Instant::now();
let status = ComponentStatus::Healthy;
Ok(CheckResult {
name: self.name.clone(),
status,
message: Some("Database connection successful".to_string()),
checked_at: Utc::now(),
duration_ms: start.elapsed().as_secs_f64() * 1000.0,
})
}
fn name(&self) -> &str {
&self.name
}
}
pub struct CacheHealthCheck {
name: String,
}
impl CacheHealthCheck {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
}
impl HealthCheck for CacheHealthCheck {
fn check(&self) -> Result<CheckResult> {
let start = std::time::Instant::now();
let status = ComponentStatus::Healthy;
Ok(CheckResult {
name: self.name.clone(),
status,
message: Some("Cache operational".to_string()),
checked_at: Utc::now(),
duration_ms: start.elapsed().as_secs_f64() * 1000.0,
})
}
fn name(&self) -> &str {
&self.name
}
}
pub struct DiskSpaceHealthCheck {
name: String,
#[allow(dead_code)]
path: String,
warning_threshold: f64,
critical_threshold: f64,
}
impl DiskSpaceHealthCheck {
pub fn new(
name: impl Into<String>,
path: impl Into<String>,
warning_threshold: f64,
critical_threshold: f64,
) -> Self {
Self {
name: name.into(),
path: path.into(),
warning_threshold,
critical_threshold,
}
}
}
impl HealthCheck for DiskSpaceHealthCheck {
fn check(&self) -> Result<CheckResult> {
let start = std::time::Instant::now();
let usage_percent = 50.0;
let status = if usage_percent >= self.critical_threshold {
ComponentStatus::Unhealthy
} else if usage_percent >= self.warning_threshold {
ComponentStatus::Degraded
} else {
ComponentStatus::Healthy
};
Ok(CheckResult {
name: self.name.clone(),
status,
message: Some(format!("Disk usage: {:.1}%", usage_percent)),
checked_at: Utc::now(),
duration_ms: start.elapsed().as_secs_f64() * 1000.0,
})
}
fn name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_health_check_manager() {
let manager = HealthCheckManager::new();
manager.register(Box::new(DatabaseHealthCheck::new("postgres")));
manager.register(Box::new(CacheHealthCheck::new("redis")));
let status = manager.check_all().expect("Health check failed");
assert_eq!(status.checks.len(), 2);
assert_eq!(status.status, ComponentStatus::Healthy);
}
#[test]
fn test_disk_space_check() {
let check = DiskSpaceHealthCheck::new("/", "/", 80.0, 95.0);
let result = check.check().expect("Check failed");
assert_eq!(result.status, ComponentStatus::Healthy);
}
}