use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Instant;
static HEALTH_REGISTRY: OnceCell<Arc<HealthRegistry>> = OnceCell::new();
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheck {
pub name: String,
pub status: HealthStatus,
pub message: Option<String>,
#[serde(skip)]
pub last_check: Option<Instant>,
pub metadata: HashMap<String, String>,
}
pub struct HealthRegistry {
checks: RwLock<HashMap<String, HealthCheck>>,
}
impl HealthRegistry {
fn new() -> Self {
Self { checks: RwLock::new(HashMap::new()) }
}
pub fn register(&self, name: String, check: HealthCheck) {
let mut checks = self.checks.write().unwrap_or_else(|e| e.into_inner());
checks.insert(name, check);
}
pub fn update(&self, name: &str, status: HealthStatus, message: Option<String>) {
let mut checks = self.checks.write().unwrap_or_else(|e| e.into_inner());
if let Some(check) = checks.get_mut(name) {
check.status = status;
check.message = message;
check.last_check = Some(Instant::now());
}
}
pub fn overall_status(&self) -> HealthStatus {
let checks = self.checks.read().unwrap_or_else(|e| e.into_inner());
if checks.is_empty() {
return HealthStatus::Healthy;
}
let has_unhealthy = checks.values().any(|c| c.status == HealthStatus::Unhealthy);
let has_degraded = checks.values().any(|c| c.status == HealthStatus::Degraded);
if has_unhealthy {
HealthStatus::Unhealthy
} else if has_degraded {
HealthStatus::Degraded
} else {
HealthStatus::Healthy
}
}
pub fn get_all(&self) -> Vec<HealthCheck> {
let checks = self.checks.read().unwrap_or_else(|e| e.into_inner());
checks.values().cloned().collect()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HealthResponse {
pub status: HealthStatus,
pub timestamp: u64,
pub version: String,
pub checks: Vec<HealthCheck>,
}
pub fn init(port: u16) -> Result<(), Box<dyn std::error::Error>> {
let registry = Arc::new(HealthRegistry::new());
HEALTH_REGISTRY.set(registry).map_err(|_| "Health checks already initialized")?;
register_default_checks(port);
Ok(())
}
fn register_default_checks(port: u16) {
if let Some(registry) = HEALTH_REGISTRY.get() {
let mut process_metadata = HashMap::new();
process_metadata.insert("configured_port".to_string(), port.to_string());
registry.register(
"sdk_process".to_string(),
HealthCheck {
name: "SDK Process".to_string(),
status: HealthStatus::Healthy,
message: Some("Health registry initialized".to_string()),
last_check: Some(Instant::now()),
metadata: process_metadata,
},
);
registry.register(
"memory".to_string(),
HealthCheck {
name: "Memory Usage".to_string(),
status: HealthStatus::Degraded,
message: Some("Memory check has not run yet".to_string()),
last_check: Some(Instant::now()),
metadata: HashMap::new(),
},
);
check_memory_health(registry);
}
}
#[allow(dead_code)]
fn check_rpc_health(registry: &Arc<HealthRegistry>) {
registry.update(
"rpc_connection",
HealthStatus::Degraded,
Some("No RPC probe result has been registered".to_string()),
);
}
#[allow(dead_code)]
fn check_memory_health(registry: &Arc<HealthRegistry>) {
let Some(memory_usage_percent) = current_memory_usage_percent() else {
registry.update(
"memory",
HealthStatus::Degraded,
Some("Memory usage is unavailable on this platform".to_string()),
);
return;
};
let (status, message) = if memory_usage_percent < 70 {
(HealthStatus::Healthy, format!("Memory usage at {}%", memory_usage_percent))
} else if memory_usage_percent < 85 {
(HealthStatus::Degraded, format!("Memory usage elevated at {}%", memory_usage_percent))
} else {
(HealthStatus::Unhealthy, format!("Memory usage critical at {}%", memory_usage_percent))
};
registry.update("memory", status, Some(message));
}
#[cfg(target_os = "linux")]
fn current_memory_usage_percent() -> Option<u64> {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
let mut total_kb = None;
let mut available_kb = None;
for line in meminfo.lines() {
let mut parts = line.split_whitespace();
match parts.next()? {
"MemTotal:" => total_kb = parts.next()?.parse::<u64>().ok(),
"MemAvailable:" => available_kb = parts.next()?.parse::<u64>().ok(),
_ => {},
}
}
let total = total_kb?;
let available = available_kb?;
if total == 0 {
return None;
}
Some(((total - available) * 100) / total)
}
#[cfg(not(target_os = "linux"))]
fn current_memory_usage_percent() -> Option<u64> {
None
}
#[allow(dead_code)]
fn check_blockchain_health(registry: &Arc<HealthRegistry>) {
registry.update(
"blockchain_sync",
HealthStatus::Degraded,
Some("No blockchain sync probe result has been registered".to_string()),
);
}
pub fn update_health(name: &str, status: HealthStatus, message: Option<String>) {
if let Some(registry) = HEALTH_REGISTRY.get() {
registry.update(name, status, message);
}
}
pub fn register_health_check(name: String, initial_status: HealthStatus) {
if let Some(registry) = HEALTH_REGISTRY.get() {
registry.register(
name.clone(),
HealthCheck {
name,
status: initial_status,
message: None,
last_check: Some(Instant::now()),
metadata: HashMap::new(),
},
);
}
}
pub fn shutdown() {
if let Some(registry) = HEALTH_REGISTRY.get() {
registry.update("sdk_process", HealthStatus::Degraded, Some("Shutting down".to_string()));
}
}