use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheckResult {
pub status: HealthStatus,
pub message: Option<String>,
pub checked_at: i64,
pub response_time_ms: u64,
}
impl HealthCheckResult {
pub fn healthy(message: impl Into<String>, response_time_ms: u64) -> Self {
Self {
status: HealthStatus::Healthy,
message: Some(message.into()),
checked_at: chrono::Utc::now().timestamp(),
response_time_ms,
}
}
pub fn degraded(message: impl Into<String>, response_time_ms: u64) -> Self {
Self {
status: HealthStatus::Degraded,
message: Some(message.into()),
checked_at: chrono::Utc::now().timestamp(),
response_time_ms,
}
}
pub fn unhealthy(message: impl Into<String>, response_time_ms: u64) -> Self {
Self {
status: HealthStatus::Unhealthy,
message: Some(message.into()),
checked_at: chrono::Utc::now().timestamp(),
response_time_ms,
}
}
}
pub trait HealthChecker: Send + Sync {
fn check(&self) -> HealthCheckResult;
fn name(&self) -> &str;
}
pub struct DatabaseHealthChecker {
name: String,
timeout: Duration,
}
impl DatabaseHealthChecker {
pub fn new(name: impl Into<String>, timeout: Duration) -> Self {
Self {
name: name.into(),
timeout,
}
}
}
impl HealthChecker for DatabaseHealthChecker {
fn check(&self) -> HealthCheckResult {
let start = Instant::now();
let elapsed = start.elapsed().as_millis() as u64;
if elapsed > self.timeout.as_millis() as u64 {
HealthCheckResult::degraded(
format!("Database response time {}ms exceeds timeout", elapsed),
elapsed,
)
} else {
HealthCheckResult::healthy("Database connection OK", elapsed)
}
}
fn name(&self) -> &str {
&self.name
}
}
pub struct CacheHealthChecker {
name: String,
timeout: Duration,
}
impl CacheHealthChecker {
pub fn new(name: impl Into<String>, timeout: Duration) -> Self {
Self {
name: name.into(),
timeout,
}
}
}
impl HealthChecker for CacheHealthChecker {
fn check(&self) -> HealthCheckResult {
let start = Instant::now();
let elapsed = start.elapsed().as_millis() as u64;
if elapsed > self.timeout.as_millis() as u64 {
HealthCheckResult::degraded("Cache response time exceeded", elapsed)
} else {
HealthCheckResult::healthy("Cache connection OK", elapsed)
}
}
fn name(&self) -> &str {
&self.name
}
}
pub struct ResourceHealthChecker {
name: String,
cpu_threshold: f64,
memory_threshold: f64,
}
impl ResourceHealthChecker {
pub fn new(name: impl Into<String>, cpu_threshold: f64, memory_threshold: f64) -> Self {
Self {
name: name.into(),
cpu_threshold,
memory_threshold,
}
}
}
impl HealthChecker for ResourceHealthChecker {
fn check(&self) -> HealthCheckResult {
let start = Instant::now();
let cpu_usage = 0.0; let memory_usage = 0.0;
let elapsed = start.elapsed().as_millis() as u64;
if cpu_usage > self.cpu_threshold || memory_usage > self.memory_threshold {
HealthCheckResult::degraded(
format!(
"High resource usage: CPU {:.1}%, Memory {:.1}%",
cpu_usage, memory_usage
),
elapsed,
)
} else {
HealthCheckResult::healthy("Resource usage OK", elapsed)
}
}
fn name(&self) -> &str {
&self.name
}
}
pub struct CircuitBreakerHealthChecker {
name: String,
is_open: Arc<AtomicBool>,
}
impl CircuitBreakerHealthChecker {
pub fn new(name: impl Into<String>, is_open: Arc<AtomicBool>) -> Self {
Self {
name: name.into(),
is_open,
}
}
}
impl HealthChecker for CircuitBreakerHealthChecker {
fn check(&self) -> HealthCheckResult {
let start = Instant::now();
let elapsed = start.elapsed().as_millis() as u64;
if self.is_open.load(Ordering::Relaxed) {
HealthCheckResult::degraded("Circuit breaker is open", elapsed)
} else {
HealthCheckResult::healthy("Circuit breaker is closed", elapsed)
}
}
fn name(&self) -> &str {
&self.name
}
}
pub struct HealthCheckManager {
checkers: Arc<RwLock<Vec<Arc<dyn HealthChecker>>>>,
cache: Arc<RwLock<HashMap<String, (HealthCheckResult, Instant)>>>,
cache_ttl: Duration,
}
impl HealthCheckManager {
pub fn new() -> Self {
Self {
checkers: Arc::new(RwLock::new(Vec::new())),
cache: Arc::new(RwLock::new(HashMap::new())),
cache_ttl: Duration::from_secs(5),
}
}
pub fn with_cache_ttl(mut self, ttl: Duration) -> Self {
self.cache_ttl = ttl;
self
}
pub fn register(&self, checker: Arc<dyn HealthChecker>) {
self.checkers.write().unwrap().push(checker);
}
pub fn check_all(&self) -> HealthReport {
let checkers = self.checkers.read().unwrap();
let mut results = HashMap::new();
let mut overall_status = HealthStatus::Healthy;
for checker in checkers.iter() {
let name = checker.name().to_string();
let result = {
let cache = self.cache.read().unwrap();
if let Some((cached_result, cached_at)) = cache.get(&name) {
if cached_at.elapsed() < self.cache_ttl {
Some(cached_result.clone())
} else {
None
}
} else {
None
}
};
let result = result.unwrap_or_else(|| {
let fresh_result = checker.check();
self.cache
.write()
.unwrap()
.insert(name.clone(), (fresh_result.clone(), Instant::now()));
fresh_result
});
match result.status {
HealthStatus::Unhealthy => overall_status = HealthStatus::Unhealthy,
HealthStatus::Degraded if overall_status != HealthStatus::Unhealthy => {
overall_status = HealthStatus::Degraded
}
_ => {}
}
results.insert(name, result);
}
HealthReport {
status: overall_status,
components: results,
timestamp: chrono::Utc::now().timestamp(),
}
}
pub fn liveness(&self) -> bool {
true
}
pub fn readiness(&self) -> bool {
let report = self.check_all();
matches!(
report.status,
HealthStatus::Healthy | HealthStatus::Degraded
)
}
pub fn clear_cache(&self) {
self.cache.write().unwrap().clear();
}
}
impl Default for HealthCheckManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthReport {
pub status: HealthStatus,
pub components: HashMap<String, HealthCheckResult>,
pub timestamp: i64,
}
impl HealthReport {
pub fn is_healthy(&self) -> bool {
self.status == HealthStatus::Healthy
}
pub fn is_unhealthy(&self) -> bool {
self.status == HealthStatus::Unhealthy
}
pub fn unhealthy_components(&self) -> Vec<String> {
self.components
.iter()
.filter(|(_, r)| r.status == HealthStatus::Unhealthy)
.map(|(n, _)| n.clone())
.collect()
}
pub fn degraded_components(&self) -> Vec<String> {
self.components
.iter()
.filter(|(_, r)| r.status == HealthStatus::Degraded)
.map(|(n, _)| n.clone())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct AlwaysHealthyChecker {
name: String,
}
impl HealthChecker for AlwaysHealthyChecker {
fn check(&self) -> HealthCheckResult {
HealthCheckResult::healthy("All good", 10)
}
fn name(&self) -> &str {
&self.name
}
}
struct AlwaysUnhealthyChecker {
name: String,
}
impl HealthChecker for AlwaysUnhealthyChecker {
fn check(&self) -> HealthCheckResult {
HealthCheckResult::unhealthy("Not good", 10)
}
fn name(&self) -> &str {
&self.name
}
}
#[test]
fn test_health_status() {
assert_eq!(HealthStatus::Healthy, HealthStatus::Healthy);
assert_ne!(HealthStatus::Healthy, HealthStatus::Unhealthy);
}
#[test]
fn test_health_check_result() {
let result = HealthCheckResult::healthy("OK", 100);
assert_eq!(result.status, HealthStatus::Healthy);
assert_eq!(result.response_time_ms, 100);
}
#[test]
fn test_database_health_checker() {
let checker = DatabaseHealthChecker::new("postgres", Duration::from_secs(5));
let result = checker.check();
assert_eq!(result.status, HealthStatus::Healthy);
assert_eq!(checker.name(), "postgres");
}
#[test]
fn test_cache_health_checker() {
let checker = CacheHealthChecker::new("redis", Duration::from_secs(1));
let result = checker.check();
assert!(matches!(
result.status,
HealthStatus::Healthy | HealthStatus::Degraded
));
}
#[test]
fn test_resource_health_checker() {
let checker = ResourceHealthChecker::new("system", 80.0, 90.0);
let result = checker.check();
assert_eq!(result.status, HealthStatus::Healthy);
}
#[test]
fn test_circuit_breaker_health_checker() {
let is_open = Arc::new(AtomicBool::new(false));
let checker = CircuitBreakerHealthChecker::new("circuit", is_open.clone());
let result = checker.check();
assert_eq!(result.status, HealthStatus::Healthy);
is_open.store(true, Ordering::Relaxed);
let result = checker.check();
assert_eq!(result.status, HealthStatus::Degraded);
}
#[test]
fn test_health_check_manager() {
let manager = HealthCheckManager::new();
manager.register(Arc::new(AlwaysHealthyChecker {
name: "test1".to_string(),
}));
manager.register(Arc::new(AlwaysHealthyChecker {
name: "test2".to_string(),
}));
let report = manager.check_all();
assert_eq!(report.status, HealthStatus::Healthy);
assert_eq!(report.components.len(), 2);
}
#[test]
fn test_health_check_manager_unhealthy() {
let manager = HealthCheckManager::new();
manager.register(Arc::new(AlwaysHealthyChecker {
name: "healthy".to_string(),
}));
manager.register(Arc::new(AlwaysUnhealthyChecker {
name: "unhealthy".to_string(),
}));
let report = manager.check_all();
assert_eq!(report.status, HealthStatus::Unhealthy);
assert!(!report.is_healthy());
assert!(report.is_unhealthy());
let unhealthy = report.unhealthy_components();
assert_eq!(unhealthy.len(), 1);
assert_eq!(unhealthy[0], "unhealthy");
}
#[test]
fn test_liveness_readiness() {
let manager = HealthCheckManager::new();
manager.register(Arc::new(AlwaysHealthyChecker {
name: "test".to_string(),
}));
assert!(manager.liveness());
assert!(manager.readiness());
}
#[test]
fn test_health_report_helpers() {
let manager = HealthCheckManager::new();
manager.register(Arc::new(AlwaysHealthyChecker {
name: "test1".to_string(),
}));
let report = manager.check_all();
assert!(report.is_healthy());
assert!(!report.is_unhealthy());
assert_eq!(report.unhealthy_components().len(), 0);
assert_eq!(report.degraded_components().len(), 0);
}
#[test]
fn test_cache_ttl() {
use std::thread;
let manager = HealthCheckManager::new().with_cache_ttl(Duration::from_millis(50));
manager.register(Arc::new(AlwaysHealthyChecker {
name: "test".to_string(),
}));
let report1 = manager.check_all();
let report2 = manager.check_all();
assert_eq!(
report1.components["test"].checked_at,
report2.components["test"].checked_at
);
thread::sleep(Duration::from_millis(200));
let report3 = manager.check_all();
assert!(
report3.components["test"].checked_at >= report1.components["test"].checked_at,
"Expected fresh check timestamp to be newer or equal"
);
manager.clear_cache();
let report4 = manager.check_all();
assert!(report4.components["test"].checked_at >= report3.components["test"].checked_at);
}
}