1use std::sync::Arc;
6use parking_lot::RwLock;
7use tracing::{info, warn, error};
8
9pub struct AppState {
11 pub version: String,
13 pub instance_id: String,
15 pub start_time: std::time::Instant,
17 pub is_healthy: Arc<RwLock<bool>>,
19 pub is_ready: Arc<RwLock<bool>>,
21}
22
23impl AppState {
24 pub fn new(instance_id: String) -> Self {
26 Self {
27 version: env!("CARGO_PKG_VERSION").to_string(),
28 instance_id,
29 start_time: std::time::Instant::now(),
30 is_healthy: Arc::new(RwLock::new(true)),
31 is_ready: Arc::new(RwLock::new(false)),
32 }
33 }
34
35 pub fn set_ready(&self, ready: bool) {
37 *self.is_ready.write() = ready;
38 if ready {
39 info!("Application marked as ready");
40 } else {
41 warn!("Application marked as not ready");
42 }
43 }
44
45 pub fn set_healthy(&self, healthy: bool) {
47 *self.is_healthy.write() = healthy;
48 if healthy {
49 info!("Application marked as healthy");
50 } else {
51 error!("Application marked as unhealthy");
52 }
53 }
54
55 pub fn is_healthy(&self) -> bool {
57 *self.is_healthy.read()
58 }
59
60 pub fn is_ready(&self) -> bool {
62 *self.is_ready.read()
63 }
64
65 pub fn uptime_seconds(&self) -> u64 {
67 self.start_time.elapsed().as_secs()
68 }
69
70 pub fn info(&self) -> serde_json::Value {
72 serde_json::json!({
73 "version": self.version,
74 "instance_id": self.instance_id,
75 "uptime_seconds": self.uptime_seconds(),
76 "is_healthy": self.is_healthy(),
77 "is_ready": self.is_ready(),
78 })
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum HealthStatus {
85 Healthy,
87 Degraded,
89 Unhealthy,
91}
92
93impl HealthStatus {
94 pub fn to_http_status(&self) -> u16 {
96 match self {
97 Self::Healthy => 200,
98 Self::Degraded => 200, Self::Unhealthy => 503,
100 }
101 }
102
103 pub fn is_ok(&self) -> bool {
105 matches!(self, Self::Healthy | Self::Degraded)
106 }
107}
108
109#[derive(Debug, Clone)]
111pub struct HealthCheck {
112 pub status: HealthStatus,
114 pub components: Vec<ComponentHealth>,
116 pub timestamp: chrono::DateTime<chrono::Utc>,
118}
119
120#[derive(Debug, Clone)]
122pub struct ComponentHealth {
123 pub name: String,
125 pub status: HealthStatus,
127 pub message: Option<String>,
129 pub last_success: Option<chrono::DateTime<chrono::Utc>>,
131}
132
133impl HealthCheck {
134 pub fn new() -> Self {
136 Self {
137 status: HealthStatus::Healthy,
138 components: Vec::new(),
139 timestamp: chrono::Utc::now(),
140 }
141 }
142
143 pub fn add_component(&mut self, component: ComponentHealth) {
145 match component.status {
147 HealthStatus::Unhealthy => self.status = HealthStatus::Unhealthy,
148 HealthStatus::Degraded if self.status == HealthStatus::Healthy => {
149 self.status = HealthStatus::Degraded;
150 }
151 _ => {}
152 }
153 self.components.push(component);
154 }
155
156 pub fn to_json(&self) -> serde_json::Value {
158 serde_json::json!({
159 "status": match self.status {
160 HealthStatus::Healthy => "healthy",
161 HealthStatus::Degraded => "degraded",
162 HealthStatus::Unhealthy => "unhealthy",
163 },
164 "timestamp": self.timestamp.to_rfc3339(),
165 "components": self.components.iter().map(|c| {
166 serde_json::json!({
167 "name": c.name,
168 "status": match c.status {
169 HealthStatus::Healthy => "healthy",
170 HealthStatus::Degraded => "degraded",
171 HealthStatus::Unhealthy => "unhealthy",
172 },
173 "message": c.message,
174 "last_success": c.last_success.map(|t| t.to_rfc3339()),
175 })
176 }).collect::<Vec<_>>(),
177 })
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct ReadinessCheck {
184 pub ready: bool,
186 pub reason: Option<String>,
188 pub timestamp: chrono::DateTime<chrono::Utc>,
190}
191
192impl ReadinessCheck {
193 pub fn ready() -> Self {
195 Self {
196 ready: true,
197 reason: None,
198 timestamp: chrono::Utc::now(),
199 }
200 }
201
202 pub fn not_ready(reason: String) -> Self {
204 Self {
205 ready: false,
206 reason: Some(reason),
207 timestamp: chrono::Utc::now(),
208 }
209 }
210
211 pub fn to_json(&self) -> serde_json::Value {
213 serde_json::json!({
214 "ready": self.ready,
215 "reason": self.reason,
216 "timestamp": self.timestamp.to_rfc3339(),
217 })
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_app_state() {
227 let state = AppState::new("test-123".to_string());
228
229 assert!(!state.is_ready());
230 assert!(state.is_healthy());
231
232 state.set_ready(true);
233 assert!(state.is_ready());
234
235 state.set_healthy(false);
236 assert!(!state.is_healthy());
237 }
238
239 #[test]
240 fn test_health_check() {
241 let mut check = HealthCheck::new();
242 assert_eq!(check.status, HealthStatus::Healthy);
243
244 check.add_component(ComponentHealth {
245 name: "upstream".to_string(),
246 status: HealthStatus::Healthy,
247 message: None,
248 last_success: Some(chrono::Utc::now()),
249 });
250 assert_eq!(check.status, HealthStatus::Healthy);
251
252 check.add_component(ComponentHealth {
253 name: "cache".to_string(),
254 status: HealthStatus::Degraded,
255 message: Some("High latency".to_string()),
256 last_success: Some(chrono::Utc::now()),
257 });
258 assert_eq!(check.status, HealthStatus::Degraded);
259
260 check.add_component(ComponentHealth {
261 name: "database".to_string(),
262 status: HealthStatus::Unhealthy,
263 message: Some("Connection failed".to_string()),
264 last_success: None,
265 });
266 assert_eq!(check.status, HealthStatus::Unhealthy);
267 }
268
269 #[test]
270 fn test_health_status_http_codes() {
271 assert_eq!(HealthStatus::Healthy.to_http_status(), 200);
272 assert_eq!(HealthStatus::Degraded.to_http_status(), 200);
273 assert_eq!(HealthStatus::Unhealthy.to_http_status(), 503);
274
275 assert!(HealthStatus::Healthy.is_ok());
276 assert!(HealthStatus::Degraded.is_ok());
277 assert!(!HealthStatus::Unhealthy.is_ok());
278 }
279}