1use parking_lot::RwLock;
6use std::sync::Arc;
7use tracing::{error, info, warn};
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 Default for HealthCheck {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl HealthCheck {
140 pub fn new() -> Self {
142 Self {
143 status: HealthStatus::Healthy,
144 components: Vec::new(),
145 timestamp: chrono::Utc::now(),
146 }
147 }
148
149 pub fn add_component(&mut self, component: ComponentHealth) {
151 match component.status {
153 HealthStatus::Unhealthy => self.status = HealthStatus::Unhealthy,
154 HealthStatus::Degraded if self.status == HealthStatus::Healthy => {
155 self.status = HealthStatus::Degraded;
156 }
157 _ => {}
158 }
159 self.components.push(component);
160 }
161
162 pub fn to_json(&self) -> serde_json::Value {
164 serde_json::json!({
165 "status": match self.status {
166 HealthStatus::Healthy => "healthy",
167 HealthStatus::Degraded => "degraded",
168 HealthStatus::Unhealthy => "unhealthy",
169 },
170 "timestamp": self.timestamp.to_rfc3339(),
171 "components": self.components.iter().map(|c| {
172 serde_json::json!({
173 "name": c.name,
174 "status": match c.status {
175 HealthStatus::Healthy => "healthy",
176 HealthStatus::Degraded => "degraded",
177 HealthStatus::Unhealthy => "unhealthy",
178 },
179 "message": c.message,
180 "last_success": c.last_success.map(|t| t.to_rfc3339()),
181 })
182 }).collect::<Vec<_>>(),
183 })
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct ReadinessCheck {
190 pub ready: bool,
192 pub reason: Option<String>,
194 pub timestamp: chrono::DateTime<chrono::Utc>,
196}
197
198impl ReadinessCheck {
199 pub fn ready() -> Self {
201 Self {
202 ready: true,
203 reason: None,
204 timestamp: chrono::Utc::now(),
205 }
206 }
207
208 pub fn not_ready(reason: String) -> Self {
210 Self {
211 ready: false,
212 reason: Some(reason),
213 timestamp: chrono::Utc::now(),
214 }
215 }
216
217 pub fn to_json(&self) -> serde_json::Value {
219 serde_json::json!({
220 "ready": self.ready,
221 "reason": self.reason,
222 "timestamp": self.timestamp.to_rfc3339(),
223 })
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_app_state() {
233 let state = AppState::new("test-123".to_string());
234
235 assert!(!state.is_ready());
236 assert!(state.is_healthy());
237
238 state.set_ready(true);
239 assert!(state.is_ready());
240
241 state.set_healthy(false);
242 assert!(!state.is_healthy());
243 }
244
245 #[test]
246 fn test_health_check() {
247 let mut check = HealthCheck::new();
248 assert_eq!(check.status, HealthStatus::Healthy);
249
250 check.add_component(ComponentHealth {
251 name: "upstream".to_string(),
252 status: HealthStatus::Healthy,
253 message: None,
254 last_success: Some(chrono::Utc::now()),
255 });
256 assert_eq!(check.status, HealthStatus::Healthy);
257
258 check.add_component(ComponentHealth {
259 name: "cache".to_string(),
260 status: HealthStatus::Degraded,
261 message: Some("High latency".to_string()),
262 last_success: Some(chrono::Utc::now()),
263 });
264 assert_eq!(check.status, HealthStatus::Degraded);
265
266 check.add_component(ComponentHealth {
267 name: "database".to_string(),
268 status: HealthStatus::Unhealthy,
269 message: Some("Connection failed".to_string()),
270 last_success: None,
271 });
272 assert_eq!(check.status, HealthStatus::Unhealthy);
273 }
274
275 #[test]
276 fn test_health_status_http_codes() {
277 assert_eq!(HealthStatus::Healthy.to_http_status(), 200);
278 assert_eq!(HealthStatus::Degraded.to_http_status(), 200);
279 assert_eq!(HealthStatus::Unhealthy.to_http_status(), 503);
280
281 assert!(HealthStatus::Healthy.is_ok());
282 assert!(HealthStatus::Degraded.is_ok());
283 assert!(!HealthStatus::Unhealthy.is_ok());
284 }
285}