use crate::{DhtHealth, DhtHealthStatus, NetworkMetrics};
use serde::Serialize;
use std::time::Instant;
#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum NetworkHealthStatus {
Healthy,
Degraded,
Unhealthy,
Unknown,
}
#[derive(Debug, Clone, Serialize)]
pub struct ComponentHealth {
pub name: String,
pub status: NetworkHealthStatus,
pub message: Option<String>,
pub score: f64,
}
#[derive(Debug, Clone, Serialize)]
pub struct NetworkHealth {
pub status: NetworkHealthStatus,
pub score: f64,
pub components: Vec<ComponentHealth>,
pub timestamp: u64,
pub uptime_secs: u64,
}
impl NetworkHealth {
pub fn is_healthy(&self) -> bool {
self.status == NetworkHealthStatus::Healthy
}
pub fn is_degraded(&self) -> bool {
self.status == NetworkHealthStatus::Degraded
}
pub fn is_unhealthy(&self) -> bool {
self.status == NetworkHealthStatus::Unhealthy
}
}
pub struct HealthChecker {
last_check: parking_lot::RwLock<Option<NetworkHealth>>,
history: parking_lot::RwLock<Vec<(Instant, NetworkHealthStatus)>>,
max_history: usize,
}
impl HealthChecker {
pub fn new() -> Self {
Self {
last_check: parking_lot::RwLock::new(None),
history: parking_lot::RwLock::new(Vec::new()),
max_history: 100,
}
}
pub fn check_health(
&self,
metrics: &NetworkMetrics,
dht_health: Option<&DhtHealth>,
) -> NetworkHealth {
let mut components = Vec::new();
let mut total_score = 0.0;
let mut component_count = 0;
let connection_health = self.check_connection_health(metrics);
total_score += connection_health.score;
component_count += 1;
components.push(connection_health);
if let Some(dht) = dht_health {
let dht_component = self.check_dht_health(dht);
total_score += dht_component.score;
component_count += 1;
components.push(dht_component);
}
let bandwidth_health = self.check_bandwidth_health(metrics);
total_score += bandwidth_health.score;
component_count += 1;
components.push(bandwidth_health);
let overall_score = if component_count > 0 {
total_score / component_count as f64
} else {
0.0
};
let overall_status = if overall_score >= 0.8 {
NetworkHealthStatus::Healthy
} else if overall_score >= 0.5 {
NetworkHealthStatus::Degraded
} else if overall_score > 0.0 {
NetworkHealthStatus::Unhealthy
} else {
NetworkHealthStatus::Unknown
};
let health = NetworkHealth {
status: overall_status.clone(),
score: overall_score,
components,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time is after UNIX epoch")
.as_secs(),
uptime_secs: metrics.uptime().as_secs(),
};
let mut history = self.history.write();
history.push((Instant::now(), overall_status));
if history.len() > self.max_history {
history.remove(0);
}
*self.last_check.write() = Some(health.clone());
health
}
pub fn last_health(&self) -> Option<NetworkHealth> {
self.last_check.read().clone()
}
pub fn health_history(&self) -> HealthHistory {
let history = self.history.read();
let total = history.len();
if total == 0 {
return HealthHistory {
total_checks: 0,
healthy_count: 0,
degraded_count: 0,
unhealthy_count: 0,
unknown_count: 0,
healthy_percentage: 0.0,
};
}
let mut healthy_count = 0;
let mut degraded_count = 0;
let mut unhealthy_count = 0;
let mut unknown_count = 0;
for (_, status) in history.iter() {
match status {
NetworkHealthStatus::Healthy => healthy_count += 1,
NetworkHealthStatus::Degraded => degraded_count += 1,
NetworkHealthStatus::Unhealthy => unhealthy_count += 1,
NetworkHealthStatus::Unknown => unknown_count += 1,
}
}
HealthHistory {
total_checks: total,
healthy_count,
degraded_count,
unhealthy_count,
unknown_count,
healthy_percentage: (healthy_count as f64 / total as f64) * 100.0,
}
}
fn check_connection_health(&self, metrics: &NetworkMetrics) -> ComponentHealth {
let snapshot = metrics.connections().snapshot();
let total = snapshot.total_established;
let failed = snapshot.total_failed;
let active = snapshot.active;
let success_rate = if total > 0 {
(total - failed) as f64 / total as f64
} else {
1.0 };
let has_connections = active > 0;
let score = if !has_connections && total == 0 {
0.5 } else if !has_connections {
0.3 } else {
success_rate
};
let status = if score >= 0.8 {
NetworkHealthStatus::Healthy
} else if score >= 0.5 {
NetworkHealthStatus::Degraded
} else {
NetworkHealthStatus::Unhealthy
};
let message = if !has_connections && total > 0 {
Some("No active connections".to_string())
} else if success_rate < 0.5 {
Some(format!(
"High connection failure rate: {:.1}%",
(1.0 - success_rate) * 100.0
))
} else {
None
};
ComponentHealth {
name: "connections".to_string(),
status,
message,
score,
}
}
fn check_dht_health(&self, dht_health: &DhtHealth) -> ComponentHealth {
let status = match dht_health.status {
DhtHealthStatus::Healthy => NetworkHealthStatus::Healthy,
DhtHealthStatus::Degraded => NetworkHealthStatus::Degraded,
DhtHealthStatus::Unhealthy => NetworkHealthStatus::Unhealthy,
DhtHealthStatus::Unknown => NetworkHealthStatus::Unknown,
};
let message = if dht_health.peer_count == 0 {
Some("No peers in routing table".to_string())
} else if dht_health.query_success_rate < 0.5 {
Some(format!(
"Low query success rate: {:.1}%",
dht_health.query_success_rate * 100.0
))
} else {
None
};
ComponentHealth {
name: "dht".to_string(),
status,
message,
score: dht_health.health_score,
}
}
fn check_bandwidth_health(&self, metrics: &NetworkMetrics) -> ComponentHealth {
let snapshot = metrics.bandwidth().snapshot();
let total_traffic = snapshot.total_sent + snapshot.total_received;
let connections = metrics.connections().active();
let score = if connections == 0 {
0.8 } else if total_traffic == 0 {
0.5 } else {
1.0 };
let status = if score >= 0.8 {
NetworkHealthStatus::Healthy
} else if score >= 0.5 {
NetworkHealthStatus::Degraded
} else {
NetworkHealthStatus::Unhealthy
};
ComponentHealth {
name: "bandwidth".to_string(),
status,
message: None,
score,
}
}
}
impl Default for HealthChecker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct HealthHistory {
pub total_checks: usize,
pub healthy_count: usize,
pub degraded_count: usize,
pub unhealthy_count: usize,
pub unknown_count: usize,
pub healthy_percentage: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metrics::NetworkMetrics;
#[test]
fn test_health_checker_creation() {
let checker = HealthChecker::new();
assert!(checker.last_health().is_none());
}
#[test]
fn test_health_check_no_connections() {
let checker = HealthChecker::new();
let metrics = NetworkMetrics::new();
let health = checker.check_health(&metrics, None);
assert!(
health.status == NetworkHealthStatus::Degraded
|| health.status == NetworkHealthStatus::Unknown
);
}
#[test]
fn test_health_check_with_connections() {
let checker = HealthChecker::new();
let metrics = NetworkMetrics::new();
metrics.connections().connection_established(true);
metrics.connections().connection_established(false);
let health = checker.check_health(&metrics, None);
assert_eq!(health.components.len(), 2); assert_eq!(health.components[0].name, "connections");
}
#[test]
fn test_health_history() {
let checker = HealthChecker::new();
let metrics = NetworkMetrics::new();
for _ in 0..5 {
checker.check_health(&metrics, None);
}
let history = checker.health_history();
assert_eq!(history.total_checks, 5);
}
#[test]
fn test_health_status_determination() {
let checker = HealthChecker::new();
let metrics = NetworkMetrics::new();
metrics.connections().connection_established(true);
metrics.bandwidth().record_sent(1000);
metrics.bandwidth().record_received(2000);
let health = checker.check_health(&metrics, None);
assert!(health.score > 0.5);
}
#[test]
fn test_last_health_stored() {
let checker = HealthChecker::new();
let metrics = NetworkMetrics::new();
let health1 = checker.check_health(&metrics, None);
let last = checker
.last_health()
.expect("test: last health should be stored after check");
assert_eq!(health1.timestamp, last.timestamp);
assert_eq!(health1.score, last.score);
}
}