use actix_web::{HttpResponse, Result, web}; use serde::{Deserialize, Serialize}; use std::time::{Duration, Instant}; #[derive(Debug, Serialize, Deserialize)] pub struct HealthStatus { pub status: String, pub timestamp: String, pub version: String, pub uptime_seconds: u64, pub checks: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct HealthCheck { pub name: String, pub status: String, pub response_time_ms: u64, pub message: Option, pub details: Option, } impl HealthStatus { pub fn new() -> Self { Self { status: "unknown".to_string(), timestamp: chrono::Utc::now().to_rfc3339(), version: env!("CARGO_PKG_VERSION").to_string(), uptime_seconds: 0, checks: Vec::new(), } } pub fn set_uptime(&mut self, uptime: Duration) { self.uptime_seconds = uptime.as_secs(); } pub fn add_check(&mut self, check: HealthCheck) { self.checks.push(check); } pub fn calculate_overall_status(&mut self) { let all_healthy = self.checks.iter().all(|check| check.status == "healthy"); let any_degraded = self.checks.iter().any(|check| check.status == "degraded"); self.status = if all_healthy { "healthy".to_string() } else if any_degraded { "degraded".to_string() } else { "unhealthy".to_string() }; } } impl HealthCheck { pub fn new(name: &str) -> Self { Self { name: name.to_string(), status: "unknown".to_string(), response_time_ms: 0, message: None, details: None, } } pub fn healthy(name: &str, response_time_ms: u64) -> Self { Self { name: name.to_string(), status: "healthy".to_string(), response_time_ms, message: Some("OK".to_string()), details: None, } } pub fn degraded(name: &str, response_time_ms: u64, message: &str) -> Self { Self { name: name.to_string(), status: "degraded".to_string(), response_time_ms, message: Some(message.to_string()), details: None, } } pub fn unhealthy(name: &str, response_time_ms: u64, error: &str) -> Self { Self { name: name.to_string(), status: "unhealthy".to_string(), response_time_ms, message: Some(error.to_string()), details: None, } } pub fn with_details(mut self, details: serde_json::Value) -> Self { self.details = Some(details); self } } /// Health check endpoint pub async fn health_check() -> Result { let start_time = Instant::now(); let mut status = HealthStatus::new(); // Set uptime (in a real app, you'd track this from startup) status.set_uptime(Duration::from_secs(3600)); // Placeholder // Check database connectivity let db_check = check_database_health().await; status.add_check(db_check); // Check Redis connectivity let redis_check = check_redis_health().await; status.add_check(redis_check); // Check Stripe connectivity let stripe_check = check_stripe_health().await; status.add_check(stripe_check); // Check file system let fs_check = check_filesystem_health().await; status.add_check(fs_check); // Check memory usage let memory_check = check_memory_health().await; status.add_check(memory_check); // Calculate overall status status.calculate_overall_status(); let response_code = match status.status.as_str() { "healthy" => 200, "degraded" => 200, // Still operational _ => 503, // Service unavailable }; log::info!( "Health check completed in {}ms - Status: {}", start_time.elapsed().as_millis(), status.status ); Ok( HttpResponse::build(actix_web::http::StatusCode::from_u16(response_code).unwrap()) .json(status), ) } /// Detailed health check endpoint for monitoring systems pub async fn health_check_detailed() -> Result { let start_time = Instant::now(); let mut status = HealthStatus::new(); // Set uptime status.set_uptime(Duration::from_secs(3600)); // Placeholder // Detailed database check let db_check = check_database_health_detailed().await; status.add_check(db_check); // Detailed Redis check let redis_check = check_redis_health_detailed().await; status.add_check(redis_check); // Detailed Stripe check let stripe_check = check_stripe_health_detailed().await; status.add_check(stripe_check); // Check external dependencies let external_check = check_external_dependencies().await; status.add_check(external_check); // Performance metrics let perf_check = check_performance_metrics().await; status.add_check(perf_check); status.calculate_overall_status(); log::info!( "Detailed health check completed in {}ms - Status: {}", start_time.elapsed().as_millis(), status.status ); Ok(HttpResponse::Ok().json(status)) } /// Simple readiness check for load balancers pub async fn readiness_check() -> Result { // Quick checks for essential services let db_ok = check_database_connectivity().await; let redis_ok = check_redis_connectivity().await; if db_ok && redis_ok { Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "ready", "timestamp": chrono::Utc::now().to_rfc3339() }))) } else { Ok(HttpResponse::ServiceUnavailable().json(serde_json::json!({ "status": "not_ready", "timestamp": chrono::Utc::now().to_rfc3339() }))) } } /// Simple liveness check pub async fn liveness_check() -> Result { Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "alive", "timestamp": chrono::Utc::now().to_rfc3339(), "version": env!("CARGO_PKG_VERSION") }))) } // Health check implementations async fn check_database_health() -> HealthCheck { let start = Instant::now(); match crate::db::db::get_db() { Ok(_) => HealthCheck::healthy("database", start.elapsed().as_millis() as u64), Err(e) => HealthCheck::unhealthy( "database", start.elapsed().as_millis() as u64, &format!("Database connection failed: {}", e), ), } } async fn check_database_health_detailed() -> HealthCheck { let start = Instant::now(); match crate::db::db::get_db() { Ok(db) => { // Try to perform a simple operation let details = serde_json::json!({ "connection_pool_size": "N/A", // Would need to expose from heromodels "active_connections": "N/A", "database_size": "N/A" }); HealthCheck::healthy("database", start.elapsed().as_millis() as u64) .with_details(details) } Err(e) => HealthCheck::unhealthy( "database", start.elapsed().as_millis() as u64, &format!("Database connection failed: {}", e), ), } } async fn check_redis_health() -> HealthCheck { let start = Instant::now(); // Try to connect to Redis match crate::utils::redis_service::get_connection() { Ok(_) => HealthCheck::healthy("redis", start.elapsed().as_millis() as u64), Err(e) => HealthCheck::unhealthy( "redis", start.elapsed().as_millis() as u64, &format!("Redis connection failed: {}", e), ), } } async fn check_redis_health_detailed() -> HealthCheck { let start = Instant::now(); match crate::utils::redis_service::get_connection() { Ok(_) => { let details = serde_json::json!({ "connection_status": "connected", "memory_usage": "N/A", "connected_clients": "N/A" }); HealthCheck::healthy("redis", start.elapsed().as_millis() as u64).with_details(details) } Err(e) => HealthCheck::unhealthy( "redis", start.elapsed().as_millis() as u64, &format!("Redis connection failed: {}", e), ), } } async fn check_stripe_health() -> HealthCheck { let start = Instant::now(); // Check if Stripe configuration is available let config = crate::config::get_config(); if !config.stripe.secret_key.is_empty() { HealthCheck::healthy("stripe", start.elapsed().as_millis() as u64) } else { HealthCheck::degraded( "stripe", start.elapsed().as_millis() as u64, "Stripe secret key not configured", ) } } async fn check_stripe_health_detailed() -> HealthCheck { let start = Instant::now(); let config = crate::config::get_config(); let has_secret = !config.stripe.secret_key.is_empty(); let has_webhook_secret = config.stripe.webhook_secret.is_some(); let details = serde_json::json!({ "secret_key_configured": has_secret, "webhook_secret_configured": has_webhook_secret, "api_version": "2023-10-16" // Current Stripe API version }); if has_secret && has_webhook_secret { HealthCheck::healthy("stripe", start.elapsed().as_millis() as u64).with_details(details) } else { HealthCheck::degraded( "stripe", start.elapsed().as_millis() as u64, "Stripe configuration incomplete", ) .with_details(details) } } async fn check_filesystem_health() -> HealthCheck { let start = Instant::now(); // Check if we can write to the data directory match std::fs::create_dir_all("data") { Ok(_) => { // Try to write a test file match std::fs::write("data/.health_check", "test") { Ok(_) => { // Clean up let _ = std::fs::remove_file("data/.health_check"); HealthCheck::healthy("filesystem", start.elapsed().as_millis() as u64) } Err(e) => HealthCheck::unhealthy( "filesystem", start.elapsed().as_millis() as u64, &format!("Cannot write to data directory: {}", e), ), } } Err(e) => HealthCheck::unhealthy( "filesystem", start.elapsed().as_millis() as u64, &format!("Cannot create data directory: {}", e), ), } } async fn check_memory_health() -> HealthCheck { let start = Instant::now(); // Basic memory check (in a real app, you'd use system metrics) let details = serde_json::json!({ "status": "basic_check_only", "note": "Detailed memory metrics require system integration" }); HealthCheck::healthy("memory", start.elapsed().as_millis() as u64).with_details(details) } async fn check_external_dependencies() -> HealthCheck { let start = Instant::now(); // Check external services (placeholder) let details = serde_json::json!({ "external_apis": "not_implemented", "third_party_services": "not_implemented" }); HealthCheck::healthy("external_dependencies", start.elapsed().as_millis() as u64) .with_details(details) } async fn check_performance_metrics() -> HealthCheck { let start = Instant::now(); let details = serde_json::json!({ "avg_response_time_ms": "N/A", "requests_per_second": "N/A", "error_rate": "N/A", "cpu_usage": "N/A" }); HealthCheck::healthy("performance", start.elapsed().as_millis() as u64).with_details(details) } // Quick connectivity checks for readiness async fn check_database_connectivity() -> bool { crate::db::db::get_db().is_ok() } async fn check_redis_connectivity() -> bool { crate::utils::redis_service::get_connection().is_ok() } /// Configure health check routes pub fn configure_health_routes(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/health") .route("", web::get().to(health_check)) .route("/detailed", web::get().to(health_check_detailed)) .route("/ready", web::get().to(readiness_check)) .route("/live", web::get().to(liveness_check)), ); }