hostbasket/actix_mvc_app/src/controllers/health.rs
Mahmoud-Emad d3a66d4fc8 feat: Add initial production deployment support
- Add .env.example file for environment variable setup
- Add .gitignore to manage sensitive files and directories
- Add Dockerfile.prod for production-ready Docker image
- Add PRODUCTION_CHECKLIST.md for pre/post deployment steps
- Add PRODUCTION_DEPLOYMENT.md for deployment instructions
- Add STRIPE_SETUP.md for Stripe payment configuration
- Add config/default.toml for default configuration settings
- Add config/local.toml.example for local configuration template
2025-06-25 18:32:20 +03:00

419 lines
12 KiB
Rust

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<HealthCheck>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HealthCheck {
pub name: String,
pub status: String,
pub response_time_ms: u64,
pub message: Option<String>,
pub details: Option<serde_json::Value>,
}
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<HttpResponse> {
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<HttpResponse> {
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<HttpResponse> {
// 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<HttpResponse> {
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)),
);
}