use crate::store::VecStore;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
}
impl HealthStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Healthy => "healthy",
Self::Degraded => "degraded",
Self::Unhealthy => "unhealthy",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthReport {
pub status: HealthStatus,
pub timestamp: SystemTime,
pub database: DatabaseHealth,
pub index: IndexHealth,
pub performance: PerformanceHealth,
pub resources: ResourceHealth,
pub alerts: Vec<Alert>,
pub uptime: Option<Duration>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseHealth {
pub total_vectors: usize,
pub active_vectors: usize,
pub deleted_vectors: usize,
pub dimension: usize,
pub deletion_ratio: f64,
pub storage_efficiency: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexHealth {
pub index_type: String,
pub integrity_ok: bool,
pub avg_degree: Option<f64>,
pub fragmentation: f64,
pub last_rebuild: Option<SystemTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceHealth {
pub avg_query_latency_ms: Option<f64>,
pub p95_query_latency_ms: Option<f64>,
pub qps: Option<f64>,
pub insert_throughput: Option<f64>,
pub performance_score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceHealth {
pub memory_bytes: usize,
pub disk_bytes: usize,
pub memory_per_vector: f64,
pub memory_utilization: f64,
pub disk_utilization: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Alert {
pub severity: AlertSeverity,
pub category: AlertCategory,
pub message: String,
pub value: Option<f64>,
pub recommendation: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AlertSeverity {
Info,
Warning,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AlertCategory {
Performance,
Storage,
Index,
Resource,
Capacity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheckConfig {
pub deletion_ratio_warning: f64,
pub deletion_ratio_critical: f64,
pub fragmentation_warning: f64,
pub memory_warning: f64,
pub latency_warning_ms: f64,
pub min_performance_score: f64,
}
impl Default for HealthCheckConfig {
fn default() -> Self {
Self {
deletion_ratio_warning: 0.3,
deletion_ratio_critical: 0.5,
fragmentation_warning: 50.0,
memory_warning: 80.0,
latency_warning_ms: 100.0,
min_performance_score: 70.0,
}
}
}
pub struct HealthChecker {
config: HealthCheckConfig,
start_time: SystemTime,
}
impl HealthChecker {
pub fn new(config: HealthCheckConfig) -> Self {
Self {
config,
start_time: SystemTime::now(),
}
}
pub fn default() -> Self {
Self::new(HealthCheckConfig::default())
}
pub fn check(&self, store: &VecStore) -> Result<HealthReport> {
let database = self.check_database(store);
let index = self.check_index(store);
let performance = self.check_performance(store);
let resources = self.check_resources(store, &database);
let mut alerts = Vec::new();
self.generate_database_alerts(&database, &mut alerts);
self.generate_index_alerts(&index, &mut alerts);
self.generate_performance_alerts(&performance, &mut alerts);
self.generate_resource_alerts(&resources, &mut alerts);
let status = self.determine_status(&alerts);
let uptime = SystemTime::now().duration_since(self.start_time).ok();
Ok(HealthReport {
status,
timestamp: SystemTime::now(),
database,
index,
performance,
resources,
alerts,
uptime,
})
}
fn check_database(&self, store: &VecStore) -> DatabaseHealth {
let total_vectors = store.len() + store.deleted_count();
let active_vectors = store.active_count();
let deleted_vectors = store.deleted_count();
let dimension = store.dimension();
let deletion_ratio = if total_vectors > 0 {
deleted_vectors as f64 / total_vectors as f64
} else {
0.0
};
let storage_efficiency = if total_vectors > 0 {
(active_vectors as f64 / total_vectors as f64) * 100.0
} else {
100.0
};
DatabaseHealth {
total_vectors,
active_vectors,
deleted_vectors,
dimension,
deletion_ratio,
storage_efficiency,
}
}
fn check_index(&self, _store: &VecStore) -> IndexHealth {
IndexHealth {
index_type: "HNSW".to_string(),
integrity_ok: true,
avg_degree: None,
fragmentation: 0.0,
last_rebuild: None,
}
}
fn check_performance(&self, _store: &VecStore) -> PerformanceHealth {
PerformanceHealth {
avg_query_latency_ms: None,
p95_query_latency_ms: None,
qps: None,
insert_throughput: None,
performance_score: 85.0, }
}
fn check_resources(&self, _store: &VecStore, db: &DatabaseHealth) -> ResourceHealth {
let vector_size = db.dimension * 4; let vectors_memory = db.active_vectors * vector_size;
let index_overhead = db.active_vectors * 64; let memory_bytes = vectors_memory + index_overhead;
let memory_per_vector = if db.active_vectors > 0 {
memory_bytes as f64 / db.active_vectors as f64
} else {
0.0
};
ResourceHealth {
memory_bytes,
disk_bytes: 0, memory_per_vector,
memory_utilization: 0.0, disk_utilization: 0.0,
}
}
fn generate_database_alerts(&self, db: &DatabaseHealth, alerts: &mut Vec<Alert>) {
if db.deletion_ratio >= self.config.deletion_ratio_critical {
alerts.push(Alert {
severity: AlertSeverity::Critical,
category: AlertCategory::Storage,
message: format!(
"High deletion ratio: {:.1}% of vectors are deleted",
db.deletion_ratio * 100.0
),
value: Some(db.deletion_ratio * 100.0),
recommendation: Some("Run compaction to reclaim space".to_string()),
});
} else if db.deletion_ratio >= self.config.deletion_ratio_warning {
alerts.push(Alert {
severity: AlertSeverity::Warning,
category: AlertCategory::Storage,
message: format!(
"Elevated deletion ratio: {:.1}% of vectors are deleted",
db.deletion_ratio * 100.0
),
value: Some(db.deletion_ratio * 100.0),
recommendation: Some("Consider running compaction soon".to_string()),
});
}
if db.active_vectors == 0 && db.total_vectors > 0 {
alerts.push(Alert {
severity: AlertSeverity::Warning,
category: AlertCategory::Capacity,
message: "Database has no active vectors".to_string(),
value: None,
recommendation: Some("All vectors have been deleted".to_string()),
});
}
if db.storage_efficiency < 50.0 && db.total_vectors > 100 {
alerts.push(Alert {
severity: AlertSeverity::Warning,
category: AlertCategory::Storage,
message: format!("Low storage efficiency: {:.1}%", db.storage_efficiency),
value: Some(db.storage_efficiency),
recommendation: Some("Run compaction to improve efficiency".to_string()),
});
}
}
fn generate_index_alerts(&self, index: &IndexHealth, alerts: &mut Vec<Alert>) {
if !index.integrity_ok {
alerts.push(Alert {
severity: AlertSeverity::Critical,
category: AlertCategory::Index,
message: "Index integrity check failed".to_string(),
value: None,
recommendation: Some("Rebuild index from scratch".to_string()),
});
}
if index.fragmentation >= self.config.fragmentation_warning {
alerts.push(Alert {
severity: AlertSeverity::Warning,
category: AlertCategory::Index,
message: format!("High index fragmentation: {:.1}", index.fragmentation),
value: Some(index.fragmentation),
recommendation: Some("Consider rebuilding index".to_string()),
});
}
}
fn generate_performance_alerts(&self, perf: &PerformanceHealth, alerts: &mut Vec<Alert>) {
if let Some(latency) = perf.p95_query_latency_ms {
if latency >= self.config.latency_warning_ms {
alerts.push(Alert {
severity: AlertSeverity::Warning,
category: AlertCategory::Performance,
message: format!("High query latency: {:.2}ms (p95)", latency),
value: Some(latency),
recommendation: Some(
"Check index parameters or consider upgrading hardware".to_string(),
),
});
}
}
if perf.performance_score < self.config.min_performance_score {
alerts.push(Alert {
severity: AlertSeverity::Warning,
category: AlertCategory::Performance,
message: format!("Low performance score: {:.1}", perf.performance_score),
value: Some(perf.performance_score),
recommendation: Some("Review configuration and system resources".to_string()),
});
}
}
fn generate_resource_alerts(&self, resources: &ResourceHealth, alerts: &mut Vec<Alert>) {
if resources.memory_utilization >= self.config.memory_warning {
alerts.push(Alert {
severity: AlertSeverity::Warning,
category: AlertCategory::Resource,
message: format!(
"High memory utilization: {:.1}%",
resources.memory_utilization
),
value: Some(resources.memory_utilization),
recommendation: Some("Consider adding memory or reducing dataset size".to_string()),
});
}
if resources.disk_utilization >= 90.0 {
alerts.push(Alert {
severity: AlertSeverity::Critical,
category: AlertCategory::Resource,
message: format!(
"Critical disk utilization: {:.1}%",
resources.disk_utilization
),
value: Some(resources.disk_utilization),
recommendation: Some("Free up disk space immediately".to_string()),
});
}
}
fn determine_status(&self, alerts: &[Alert]) -> HealthStatus {
let has_critical = alerts.iter().any(|a| a.severity == AlertSeverity::Critical);
let has_warning = alerts.iter().any(|a| a.severity == AlertSeverity::Warning);
if has_critical {
HealthStatus::Unhealthy
} else if has_warning {
HealthStatus::Degraded
} else {
HealthStatus::Healthy
}
}
}
pub fn print_health_report(report: &HealthReport) {
println!("\n{}", "=".repeat(80));
println!("VecStore Health Report");
println!("{}", "=".repeat(80));
let status_icon = match report.status {
HealthStatus::Healthy => "â
",
HealthStatus::Degraded => "â ī¸",
HealthStatus::Unhealthy => "â",
};
println!(
"\n{} Overall Status: {}",
status_icon,
report.status.as_str().to_uppercase()
);
if let Some(uptime) = report.uptime {
println!("âąī¸ Uptime: {:?}", uptime);
}
println!("\nđ Database:");
println!(" Total vectors: {}", report.database.total_vectors);
println!(" Active vectors: {}", report.database.active_vectors);
println!(" Deleted vectors: {}", report.database.deleted_vectors);
println!(" Dimension: {}", report.database.dimension);
println!(
" Deletion ratio: {:.1}%",
report.database.deletion_ratio * 100.0
);
println!(
" Storage efficiency: {:.1}%",
report.database.storage_efficiency
);
println!("\nđ Index:");
println!(" Type: {}", report.index.index_type);
println!(
" Integrity: {}",
if report.index.integrity_ok {
"â OK"
} else {
"â Failed"
}
);
println!(" Fragmentation: {:.1}", report.index.fragmentation);
println!("\n⥠Performance:");
println!(
" Performance score: {:.1}/100",
report.performance.performance_score
);
if let Some(latency) = report.performance.avg_query_latency_ms {
println!(" Avg query latency: {:.2}ms", latency);
}
if let Some(p95) = report.performance.p95_query_latency_ms {
println!(" P95 query latency: {:.2}ms", p95);
}
if let Some(qps) = report.performance.qps {
println!(" QPS: {:.0}", qps);
}
println!("\nđž Resources:");
println!(
" Memory usage: {:.2} MB",
report.resources.memory_bytes as f64 / 1_000_000.0
);
println!(
" Memory per vector: {:.1} bytes",
report.resources.memory_per_vector
);
if !report.alerts.is_empty() {
println!("\nđ¨ Alerts ({}):", report.alerts.len());
for alert in &report.alerts {
let icon = match alert.severity {
AlertSeverity::Info => "âšī¸",
AlertSeverity::Warning => "â ī¸",
AlertSeverity::Critical => "â",
};
println!(
" {} [{}] {}",
icon,
format!("{:?}", alert.category),
alert.message
);
if let Some(rec) = &alert.recommendation {
println!(" â {}", rec);
}
}
} else {
println!("\nâ
No alerts");
}
println!("\n{}", "=".repeat(80));
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_health_check() -> Result<()> {
let temp_dir = TempDir::new()?;
let store = VecStore::open(temp_dir.path().join("test.db"))?;
let checker = HealthChecker::default();
let report = checker.check(&store)?;
assert_eq!(report.status, HealthStatus::Healthy);
assert_eq!(report.database.active_vectors, 0);
assert!(report.alerts.is_empty());
Ok(())
}
#[test]
fn test_deletion_ratio_alert() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut store = VecStore::open(temp_dir.path().join("test.db"))?;
for i in 0..10 {
store.upsert(
format!("vec_{}", i),
vec![1.0, 2.0, 3.0],
crate::store::Metadata {
fields: std::collections::HashMap::new(),
},
)?;
}
for i in 0..6 {
store.soft_delete(&format!("vec_{}", i))?;
}
let checker = HealthChecker::default();
let report = checker.check(&store)?;
assert_eq!(report.status, HealthStatus::Unhealthy);
assert!(report
.alerts
.iter()
.any(|a| a.severity == AlertSeverity::Critical));
Ok(())
}
}