use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
Unavailable,
}
impl std::fmt::Display for HealthStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HealthStatus::Healthy => write!(f, "healthy"),
HealthStatus::Degraded => write!(f, "degraded"),
HealthStatus::Unhealthy => write!(f, "unhealthy"),
HealthStatus::Unavailable => write!(f, "unavailable"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentHealth {
pub name: String,
pub status: HealthStatus,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<std::collections::HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checked_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub check_duration_ms: Option<u64>,
}
impl ComponentHealth {
pub fn new(name: impl Into<String>, status: HealthStatus, message: impl Into<String>) -> Self {
let checked_at = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.ok();
Self {
name: name.into(),
status,
message: message.into(),
details: None,
checked_at,
check_duration_ms: None,
}
}
pub fn with_details(mut self, details: std::collections::HashMap<String, String>) -> Self {
self.details = Some(details);
self
}
pub fn with_duration(mut self, duration: Duration) -> Self {
self.check_duration_ms = Some(duration.as_millis() as u64);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheckResult {
pub status: HealthStatus,
pub components: Vec<ComponentHealth>,
pub version: String,
}
impl HealthCheckResult {
pub fn from_components(components: Vec<ComponentHealth>) -> Self {
let status = components
.iter()
.map(|c| c.status)
.max_by_key(|s| match s {
HealthStatus::Healthy => 0,
HealthStatus::Degraded => 1,
HealthStatus::Unavailable => 2,
HealthStatus::Unhealthy => 3,
})
.unwrap_or(HealthStatus::Healthy);
Self {
status,
components,
version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
pub fn network_health_check() -> ComponentHealth {
ComponentHealth::new(
"network",
HealthStatus::Unavailable,
"Network module is disabled. P2P functionality is not compiled into this build.",
)
}
pub fn health_check() -> HealthCheckResult {
let components = vec![
network_health_check(),
ComponentHealth::new(
"schema",
HealthStatus::Healthy,
"Schema validation is operational.",
),
storage_health_check(),
];
HealthCheckResult::from_components(components)
}
fn storage_health_check() -> ComponentHealth {
use std::collections::HashMap;
let mut details = HashMap::new();
details.insert("type".to_string(), "filesystem".to_string());
ComponentHealth::new(
"storage",
HealthStatus::Healthy,
"Storage subsystem is operational.",
)
.with_details(details)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_network_health_unavailable() {
let health = network_health_check();
assert_eq!(health.status, HealthStatus::Unavailable);
assert_eq!(health.name, "network");
}
#[test]
fn test_overall_health_check() {
let result = health_check();
assert!(matches!(
result.status,
HealthStatus::Unavailable | HealthStatus::Healthy | HealthStatus::Degraded
));
assert!(!result.components.is_empty());
}
#[test]
fn test_health_status_display() {
assert_eq!(format!("{}", HealthStatus::Healthy), "healthy");
assert_eq!(format!("{}", HealthStatus::Degraded), "degraded");
assert_eq!(format!("{}", HealthStatus::Unhealthy), "unhealthy");
assert_eq!(format!("{}", HealthStatus::Unavailable), "unavailable");
}
#[test]
fn test_component_health_builder() {
let mut details = std::collections::HashMap::new();
details.insert("key".to_string(), "value".to_string());
let health = ComponentHealth::new("test", HealthStatus::Healthy, "Test message")
.with_details(details)
.with_duration(Duration::from_millis(100));
assert_eq!(health.name, "test");
assert_eq!(health.status, HealthStatus::Healthy);
assert_eq!(health.check_duration_ms, Some(100));
assert!(health.details.is_some());
}
#[test]
fn test_health_result_worst_status() {
let components = vec![
ComponentHealth::new("a", HealthStatus::Healthy, "ok"),
ComponentHealth::new("b", HealthStatus::Degraded, "degraded"),
];
let result = HealthCheckResult::from_components(components);
assert_eq!(result.status, HealthStatus::Degraded);
let components = vec![
ComponentHealth::new("a", HealthStatus::Healthy, "ok"),
ComponentHealth::new("b", HealthStatus::Unhealthy, "bad"),
];
let result = HealthCheckResult::from_components(components);
assert_eq!(result.status, HealthStatus::Unhealthy);
}
}