- 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
419 lines
12 KiB
Rust
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)),
|
|
);
|
|
}
|