use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
use tokio::sync::RwLock;
use tracing::{debug, error, warn};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheckResult {
pub name: String,
pub status: HealthStatus,
pub message: Option<String>,
pub timestamp: SystemTime,
pub duration: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthReport {
pub status: HealthStatus,
pub checks: Vec<HealthCheckResult>,
pub timestamp: SystemTime,
pub total_duration: Duration,
}
#[async_trait::async_trait]
pub trait HealthCheck: Send + Sync {
fn name(&self) -> &str;
async fn check(&self) -> HealthCheckResult;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceMetrics {
pub total_requests: u64,
pub successful_requests: u64,
pub failed_requests: u64,
pub avg_response_time_ms: f64,
pub p95_response_time_ms: f64,
pub p99_response_time_ms: f64,
pub active_connections: u64,
pub memory_usage_bytes: u64,
pub cpu_usage_percent: f64,
pub timestamp: SystemTime,
}
#[derive(Debug, Clone)]
pub struct RequestMetrics {
pub start_time: Instant,
pub method: String,
pub success: bool,
pub response_time: Duration,
pub error_message: Option<String>,
}
pub struct MonitoringSystem {
health_checks: Arc<RwLock<Vec<Box<dyn HealthCheck>>>>,
metrics: Arc<RwLock<PerformanceMetrics>>,
response_times: Arc<RwLock<Vec<Duration>>>,
config: MonitoringConfig,
}
#[derive(Debug, Clone)]
pub struct MonitoringConfig {
pub max_response_times: usize,
pub health_check_interval: Duration,
pub detailed_logging: bool,
}
impl Default for MonitoringConfig {
fn default() -> Self {
Self {
max_response_times: 10000,
health_check_interval: Duration::from_secs(30),
detailed_logging: true,
}
}
}
impl MonitoringSystem {
pub fn new(config: MonitoringConfig) -> Self {
Self {
health_checks: Arc::new(RwLock::new(Vec::new())),
metrics: Arc::new(RwLock::new(PerformanceMetrics::default())),
response_times: Arc::new(RwLock::new(Vec::new())),
config,
}
}
pub async fn register_health_check(&self, check: Box<dyn HealthCheck>) {
let mut checks = self.health_checks.write().await;
checks.push(check);
}
pub async fn health_check(&self) -> HealthReport {
let start_time = Instant::now();
let mut results = Vec::new();
let mut overall_status = HealthStatus::Healthy;
let checks = self.health_checks.read().await;
for check in checks.iter() {
let result = check.check().await;
match (&overall_status, &result.status) {
(HealthStatus::Healthy, HealthStatus::Degraded) => {
overall_status = HealthStatus::Degraded
}
(HealthStatus::Healthy | HealthStatus::Degraded, HealthStatus::Unhealthy) => {
overall_status = HealthStatus::Unhealthy
}
(
HealthStatus::Healthy | HealthStatus::Degraded | HealthStatus::Unhealthy,
HealthStatus::Unknown,
) => overall_status = HealthStatus::Unknown,
_ => {}
}
results.push(result);
}
let total_duration = start_time.elapsed();
HealthReport {
status: overall_status,
checks: results,
timestamp: SystemTime::now(),
total_duration,
}
}
pub async fn record_request(&self, request: RequestMetrics) {
let mut metrics = self.metrics.write().await;
let mut response_times = self.response_times.write().await;
metrics.total_requests += 1;
if request.success {
metrics.successful_requests += 1;
} else {
metrics.failed_requests += 1;
}
response_times.push(request.response_time);
let current_len = response_times.len();
if current_len > self.config.max_response_times {
response_times.drain(0..current_len - self.config.max_response_times);
}
let mut sorted_times = response_times.clone();
sorted_times.sort();
if !sorted_times.is_empty() {
let avg_ms = sorted_times.iter().sum::<Duration>().as_secs_f64() * 1000.0
/ sorted_times.len() as f64;
let p95_idx = (sorted_times.len() as f64 * 0.95) as usize;
let p99_idx = (sorted_times.len() as f64 * 0.99) as usize;
metrics.avg_response_time_ms = avg_ms;
metrics.p95_response_time_ms = sorted_times
.get(p95_idx)
.unwrap_or(&Duration::ZERO)
.as_secs_f64()
* 1000.0;
metrics.p99_response_time_ms = sorted_times
.get(p99_idx)
.unwrap_or(&Duration::ZERO)
.as_secs_f64()
* 1000.0;
}
metrics.timestamp = SystemTime::now();
if self.config.detailed_logging {
if request.success {
debug!(
"Request completed: {} in {:?}",
request.method, request.response_time
);
} else {
warn!(
"Request failed: {} in {:?} - {}",
request.method,
request.response_time,
request
.error_message
.unwrap_or_else(|| "Unknown error".to_string())
);
}
}
}
pub async fn get_metrics(&self) -> PerformanceMetrics {
self.metrics.read().await.clone()
}
pub async fn start_periodic_health_checks(&self) {
let health_checks = self.health_checks.clone();
let interval = self.config.health_check_interval;
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(interval);
loop {
interval_timer.tick().await;
let checks = health_checks.read().await;
for check in checks.iter() {
let result = check.check().await;
match result.status {
HealthStatus::Healthy => {
debug!("Health check '{}' passed", result.name);
}
HealthStatus::Degraded => {
warn!(
"Health check '{}' degraded: {}",
result.name,
result.message.unwrap_or_else(|| "No details".to_string())
);
}
HealthStatus::Unhealthy => {
error!(
"Health check '{}' failed: {}",
result.name,
result.message.unwrap_or_else(|| "No details".to_string())
);
}
HealthStatus::Unknown => {
warn!(
"Health check '{}' status unknown: {}",
result.name,
result.message.unwrap_or_else(|| "No details".to_string())
);
}
}
}
}
});
}
pub async fn update_system_metrics(&self, active_connections: u64) {
let mut metrics = self.metrics.write().await;
metrics.active_connections = active_connections;
#[cfg(target_os = "linux")]
{
if let Ok(usage) = self.get_system_usage().await {
metrics.memory_usage_bytes = usage.memory_bytes;
metrics.cpu_usage_percent = usage.cpu_percent;
}
}
}
#[cfg(target_os = "linux")]
async fn get_system_usage(&self) -> Result<SystemUsage, Box<dyn std::error::Error>> {
use std::fs;
let status = fs::read_to_string("/proc/self/status")?;
let memory_kb = status
.lines()
.find(|line| line.starts_with("VmRSS:"))
.and_then(|line| line.split_whitespace().nth(1))
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
let stat = fs::read_to_string("/proc/self/stat")?;
let fields: Vec<&str> = stat.split_whitespace().collect();
let utime = fields
.get(13)
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
let stime = fields
.get(14)
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
let cpu_percent = ((utime + stime) as f64 / 100.0) * 0.1;
Ok(SystemUsage {
memory_bytes: memory_kb * 1024,
cpu_percent,
})
}
}
#[cfg(target_os = "linux")]
struct SystemUsage {
memory_bytes: u64,
cpu_percent: f64,
}
impl Default for PerformanceMetrics {
fn default() -> Self {
Self {
total_requests: 0,
successful_requests: 0,
failed_requests: 0,
avg_response_time_ms: 0.0,
p95_response_time_ms: 0.0,
p99_response_time_ms: 0.0,
active_connections: 0,
memory_usage_bytes: 0,
cpu_usage_percent: 0.0,
timestamp: SystemTime::now(),
}
}
}
pub struct BasicHealthCheck {
name: String,
}
impl BasicHealthCheck {
pub fn new(name: String) -> Self {
Self { name }
}
}
#[async_trait::async_trait]
impl HealthCheck for BasicHealthCheck {
fn name(&self) -> &str {
&self.name
}
async fn check(&self) -> HealthCheckResult {
let start_time = Instant::now();
let status = if std::env::var("HEALTH_CHECK_FAIL").is_ok() {
HealthStatus::Unhealthy
} else {
HealthStatus::Healthy
};
let duration = start_time.elapsed();
HealthCheckResult {
name: self.name.clone(),
status,
message: Some("Basic system health check".to_string()),
timestamp: SystemTime::now(),
duration,
}
}
}
pub struct FileSystemHealthCheck {
test_path: std::path::PathBuf,
}
impl FileSystemHealthCheck {
pub fn new(test_path: std::path::PathBuf) -> Self {
Self { test_path }
}
}
#[async_trait::async_trait]
impl HealthCheck for FileSystemHealthCheck {
fn name(&self) -> &str {
"filesystem"
}
async fn check(&self) -> HealthCheckResult {
let start_time = Instant::now();
let (status, message) = match std::fs::metadata(&self.test_path) {
Ok(_) => (HealthStatus::Healthy, "File system accessible".to_string()),
Err(e) => (
HealthStatus::Unhealthy,
format!("File system check failed: {}", e),
),
};
let duration = start_time.elapsed();
HealthCheckResult {
name: "filesystem".to_string(),
status,
message: Some(message),
timestamp: SystemTime::now(),
duration,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn test_monitoring_system() {
let config = MonitoringConfig::default();
let monitoring = MonitoringSystem::new(config);
let health_check = Box::new(BasicHealthCheck::new("test".to_string()));
monitoring.register_health_check(health_check).await;
let report = monitoring.health_check().await;
assert_eq!(report.status, HealthStatus::Healthy);
assert_eq!(report.checks.len(), 1);
let request = RequestMetrics {
start_time: Instant::now(),
method: "test_method".to_string(),
success: true,
response_time: Duration::from_millis(100),
error_message: None,
};
monitoring.record_request(request).await;
let metrics = monitoring.get_metrics().await;
assert_eq!(metrics.total_requests, 1);
assert_eq!(metrics.successful_requests, 1);
assert_eq!(metrics.failed_requests, 0);
}
#[tokio::test]
async fn test_health_check_aggregation() {
let config = MonitoringConfig::default();
let monitoring = MonitoringSystem::new(config);
let healthy_check = Box::new(BasicHealthCheck::new("healthy".to_string()));
let degraded_check = Box::new(FileSystemHealthCheck::new(std::path::PathBuf::from(
"/nonexistent",
)));
monitoring.register_health_check(healthy_check).await;
monitoring.register_health_check(degraded_check).await;
let report = monitoring.health_check().await;
assert_eq!(report.checks.len(), 2);
assert_eq!(report.status, HealthStatus::Unhealthy);
}
}