use parking_lot::RwLock;
use std::sync::Arc;
use tracing::{error, info, warn};
pub struct AppState {
pub version: String,
pub instance_id: String,
pub start_time: std::time::Instant,
pub is_healthy: Arc<RwLock<bool>>,
pub is_ready: Arc<RwLock<bool>>,
}
impl AppState {
pub fn new(instance_id: String) -> Self {
Self {
version: env!("CARGO_PKG_VERSION").to_string(),
instance_id,
start_time: std::time::Instant::now(),
is_healthy: Arc::new(RwLock::new(true)),
is_ready: Arc::new(RwLock::new(false)),
}
}
pub fn set_ready(&self, ready: bool) {
*self.is_ready.write() = ready;
if ready {
info!("Application marked as ready");
} else {
warn!("Application marked as not ready");
}
}
pub fn set_healthy(&self, healthy: bool) {
*self.is_healthy.write() = healthy;
if healthy {
info!("Application marked as healthy");
} else {
error!("Application marked as unhealthy");
}
}
pub fn is_healthy(&self) -> bool {
*self.is_healthy.read()
}
pub fn is_ready(&self) -> bool {
*self.is_ready.read()
}
pub fn uptime_seconds(&self) -> u64 {
self.start_time.elapsed().as_secs()
}
pub fn info(&self) -> serde_json::Value {
serde_json::json!({
"version": self.version,
"instance_id": self.instance_id,
"uptime_seconds": self.uptime_seconds(),
"is_healthy": self.is_healthy(),
"is_ready": self.is_ready(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
}
impl HealthStatus {
pub fn to_http_status(&self) -> u16 {
match self {
Self::Healthy => 200,
Self::Degraded => 200, Self::Unhealthy => 503,
}
}
pub fn is_ok(&self) -> bool {
matches!(self, Self::Healthy | Self::Degraded)
}
}
#[derive(Debug, Clone)]
pub struct HealthCheck {
pub status: HealthStatus,
pub components: Vec<ComponentHealth>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone)]
pub struct ComponentHealth {
pub name: String,
pub status: HealthStatus,
pub message: Option<String>,
pub last_success: Option<chrono::DateTime<chrono::Utc>>,
}
impl Default for HealthCheck {
fn default() -> Self {
Self::new()
}
}
impl HealthCheck {
pub fn new() -> Self {
Self {
status: HealthStatus::Healthy,
components: Vec::new(),
timestamp: chrono::Utc::now(),
}
}
pub fn add_component(&mut self, component: ComponentHealth) {
match component.status {
HealthStatus::Unhealthy => self.status = HealthStatus::Unhealthy,
HealthStatus::Degraded if self.status == HealthStatus::Healthy => {
self.status = HealthStatus::Degraded;
}
_ => {}
}
self.components.push(component);
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"status": match self.status {
HealthStatus::Healthy => "healthy",
HealthStatus::Degraded => "degraded",
HealthStatus::Unhealthy => "unhealthy",
},
"timestamp": self.timestamp.to_rfc3339(),
"components": self.components.iter().map(|c| {
serde_json::json!({
"name": c.name,
"status": match c.status {
HealthStatus::Healthy => "healthy",
HealthStatus::Degraded => "degraded",
HealthStatus::Unhealthy => "unhealthy",
},
"message": c.message,
"last_success": c.last_success.map(|t| t.to_rfc3339()),
})
}).collect::<Vec<_>>(),
})
}
}
#[derive(Debug, Clone)]
pub struct ReadinessCheck {
pub ready: bool,
pub reason: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl ReadinessCheck {
pub fn ready() -> Self {
Self {
ready: true,
reason: None,
timestamp: chrono::Utc::now(),
}
}
pub fn not_ready(reason: String) -> Self {
Self {
ready: false,
reason: Some(reason),
timestamp: chrono::Utc::now(),
}
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"ready": self.ready,
"reason": self.reason,
"timestamp": self.timestamp.to_rfc3339(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_state() {
let state = AppState::new("test-123".to_string());
assert!(!state.is_ready());
assert!(state.is_healthy());
state.set_ready(true);
assert!(state.is_ready());
state.set_healthy(false);
assert!(!state.is_healthy());
}
#[test]
fn test_health_check() {
let mut check = HealthCheck::new();
assert_eq!(check.status, HealthStatus::Healthy);
check.add_component(ComponentHealth {
name: "upstream".to_string(),
status: HealthStatus::Healthy,
message: None,
last_success: Some(chrono::Utc::now()),
});
assert_eq!(check.status, HealthStatus::Healthy);
check.add_component(ComponentHealth {
name: "cache".to_string(),
status: HealthStatus::Degraded,
message: Some("High latency".to_string()),
last_success: Some(chrono::Utc::now()),
});
assert_eq!(check.status, HealthStatus::Degraded);
check.add_component(ComponentHealth {
name: "database".to_string(),
status: HealthStatus::Unhealthy,
message: Some("Connection failed".to_string()),
last_success: None,
});
assert_eq!(check.status, HealthStatus::Unhealthy);
}
#[test]
fn test_health_status_http_codes() {
assert_eq!(HealthStatus::Healthy.to_http_status(), 200);
assert_eq!(HealthStatus::Degraded.to_http_status(), 200);
assert_eq!(HealthStatus::Unhealthy.to_http_status(), 503);
assert!(HealthStatus::Healthy.is_ok());
assert!(HealthStatus::Degraded.is_ok());
assert!(!HealthStatus::Unhealthy.is_ok());
}
}