1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct HealthCheck {
10 pub id: String,
12 pub name: String,
14 pub description: String,
16 pub check_fn: String, pub timeout_seconds: u64,
20 pub enabled: bool,
22 pub interval_seconds: u64,
24}
25
26impl HealthCheck {
27 pub fn new(
29 id: String,
30 name: String,
31 description: String,
32 check_fn: String,
33 ) -> Self {
34 Self {
35 id,
36 name,
37 description,
38 check_fn,
39 timeout_seconds: 30,
40 enabled: true,
41 interval_seconds: 60,
42 }
43 }
44
45 pub fn with_timeout(mut self, timeout_seconds: u64) -> Self {
47 self.timeout_seconds = timeout_seconds;
48 self
49 }
50
51 pub fn with_interval(mut self, interval_seconds: u64) -> Self {
53 self.interval_seconds = interval_seconds;
54 self
55 }
56
57 pub fn set_enabled(mut self, enabled: bool) -> Self {
59 self.enabled = enabled;
60 self
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
66pub enum HealthStatus {
67 Healthy,
69 Degraded,
71 Unhealthy,
73 Unknown,
75}
76
77impl HealthStatus {
78 pub fn description(&self) -> &'static str {
80 match self {
81 HealthStatus::Healthy => "System is operating normally",
82 HealthStatus::Degraded => "System is experiencing minor issues",
83 HealthStatus::Unhealthy => "System is experiencing major issues",
84 HealthStatus::Unknown => "System status is unknown",
85 }
86 }
87
88 pub fn color(&self) -> &'static str {
90 match self {
91 HealthStatus::Healthy => "green",
92 HealthStatus::Degraded => "yellow",
93 HealthStatus::Unhealthy => "red",
94 HealthStatus::Unknown => "gray",
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct SystemStatus {
102 pub status: HealthStatus,
104 pub last_updated: u64,
106 pub health_checks: HashMap<String, HealthCheckResult>,
108 pub uptime_seconds: u64,
110 pub system_info: HashMap<String, String>,
112}
113
114impl SystemStatus {
115 pub fn new() -> Self {
117 Self {
118 status: HealthStatus::Unknown,
119 last_updated: SystemTime::now()
120 .duration_since(UNIX_EPOCH)
121 .unwrap()
122 .as_secs(),
123 health_checks: HashMap::new(),
124 uptime_seconds: 0,
125 system_info: HashMap::new(),
126 }
127 }
128
129 pub fn update_status(&mut self, status: HealthStatus) {
131 self.status = status;
132 self.last_updated = SystemTime::now()
133 .duration_since(UNIX_EPOCH)
134 .unwrap()
135 .as_secs();
136 }
137
138 pub fn add_health_check_result(&mut self, check_id: String, result: HealthCheckResult) {
140 self.health_checks.insert(check_id, result);
141 }
142
143 pub fn calculate_overall_status(&self) -> HealthStatus {
145 if self.health_checks.is_empty() {
146 return HealthStatus::Unknown;
147 }
148
149 let mut has_unhealthy = false;
150 let mut has_degraded = false;
151
152 for result in self.health_checks.values() {
153 match result.status {
154 HealthStatus::Unhealthy => has_unhealthy = true,
155 HealthStatus::Degraded => has_degraded = true,
156 HealthStatus::Healthy => continue,
157 HealthStatus::Unknown => has_degraded = true,
158 }
159 }
160
161 if has_unhealthy {
162 HealthStatus::Unhealthy
163 } else if has_degraded {
164 HealthStatus::Degraded
165 } else {
166 HealthStatus::Healthy
167 }
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct HealthCheckResult {
174 pub check_id: String,
176 pub status: HealthStatus,
178 pub checked_at: u64,
180 pub duration_ms: u64,
182 pub message: Option<String>,
184 pub metadata: HashMap<String, String>,
186}
187
188impl HealthCheckResult {
189 pub fn new(
191 check_id: String,
192 status: HealthStatus,
193 duration_ms: u64,
194 ) -> Self {
195 Self {
196 check_id,
197 status,
198 checked_at: SystemTime::now()
199 .duration_since(UNIX_EPOCH)
200 .unwrap()
201 .as_secs(),
202 duration_ms,
203 message: None,
204 metadata: HashMap::new(),
205 }
206 }
207
208 pub fn with_message(mut self, message: String) -> Self {
210 self.message = Some(message);
211 self
212 }
213
214 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
216 self.metadata = metadata;
217 self
218 }
219}
220
221#[derive(Debug, Clone)]
223pub struct HealthReporter {
224 health_checks: HashMap<String, HealthCheck>,
226 system_status: SystemStatus,
228 system_start_time: u64,
230}
231
232impl HealthReporter {
233 pub fn new() -> Self {
235 Self {
236 health_checks: HashMap::new(),
237 system_status: SystemStatus::new(),
238 system_start_time: SystemTime::now()
239 .duration_since(UNIX_EPOCH)
240 .unwrap()
241 .as_secs(),
242 }
243 }
244
245 pub fn with_config(config: HealthConfig) -> Self {
247 Self {
248 health_checks: HashMap::new(),
249 system_status: SystemStatus::new(),
250 system_start_time: SystemTime::now()
251 .duration_since(UNIX_EPOCH)
252 .unwrap()
253 .as_secs(),
254 }
255 }
256
257 pub fn add_health_check(&mut self, health_check: HealthCheck) {
259 self.health_checks.insert(health_check.id.clone(), health_check);
260 }
261
262 pub fn remove_health_check(&mut self, check_id: &str) {
264 self.health_checks.remove(check_id);
265 self.system_status.health_checks.remove(check_id);
266 }
267
268 pub fn get_health_checks(&self) -> Vec<&HealthCheck> {
270 self.health_checks.values().collect()
271 }
272
273 pub fn get_health_check(&self, check_id: &str) -> Option<&HealthCheck> {
275 self.health_checks.get(check_id)
276 }
277
278 pub fn perform_health_check(&mut self, check_id: &str) -> Option<HealthCheckResult> {
280 let health_check = self.health_checks.get(check_id)?;
281
282 if !health_check.enabled {
283 return None;
284 }
285
286 let start_time = SystemTime::now();
287
288 let result = self.simulate_health_check(health_check);
291
292 let duration = start_time.elapsed().unwrap_or_default();
293 let duration_ms = duration.as_millis() as u64;
294
295 let mut health_result = HealthCheckResult::new(
296 check_id.to_string(),
297 result,
298 duration_ms,
299 );
300
301 self.system_status.add_health_check_result(check_id.to_string(), health_result.clone());
303
304 let overall_status = self.system_status.calculate_overall_status();
306 self.system_status.update_status(overall_status);
307
308 Some(health_result)
309 }
310
311 pub fn perform_all_health_checks(&mut self) -> Vec<HealthCheckResult> {
313 let mut results = Vec::new();
314
315 let check_ids: Vec<String> = self.health_checks.keys().cloned().collect();
316 for check_id in check_ids {
317 if let Some(result) = self.perform_health_check(&check_id) {
318 results.push(result);
319 }
320 }
321
322 results
323 }
324
325 pub fn get_system_status(&self) -> &SystemStatus {
327 &self.system_status
328 }
329
330 pub fn get_current_system_status(&mut self) -> SystemStatus {
332 let current_time = SystemTime::now()
333 .duration_since(UNIX_EPOCH)
334 .unwrap()
335 .as_secs();
336
337 self.system_status.uptime_seconds = current_time - self.system_start_time;
338 self.system_status.clone()
339 }
340
341 pub fn get_health_checks_by_status(&self, status: &HealthStatus) -> Vec<&HealthCheckResult> {
343 self.system_status
344 .health_checks
345 .values()
346 .filter(|result| &result.status == status)
347 .collect()
348 }
349
350 pub fn get_health_stats(&self) -> HealthStats {
352 let total_checks = self.health_checks.len();
353 let healthy_checks = self.get_health_checks_by_status(&HealthStatus::Healthy).len();
354 let degraded_checks = self.get_health_checks_by_status(&HealthStatus::Degraded).len();
355 let unhealthy_checks = self.get_health_checks_by_status(&HealthStatus::Unhealthy).len();
356 let unknown_checks = self.get_health_checks_by_status(&HealthStatus::Unknown).len();
357
358 HealthStats {
359 total_checks,
360 healthy_checks,
361 degraded_checks,
362 unhealthy_checks,
363 unknown_checks,
364 overall_status: self.system_status.status.clone(),
365 uptime_seconds: self.system_status.uptime_seconds,
366 }
367 }
368
369 fn simulate_health_check(&self, health_check: &HealthCheck) -> HealthStatus {
371 match health_check.id.as_str() {
374 "database" => HealthStatus::Healthy,
375 "redis" => HealthStatus::Healthy,
376 "external_api" => HealthStatus::Degraded,
377 "disk_space" => HealthStatus::Unhealthy,
378 _ => HealthStatus::Unknown,
379 }
380 }
381}
382
383impl Default for HealthReporter {
384 fn default() -> Self {
385 Self::new()
386 }
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct HealthStats {
392 pub total_checks: usize,
394 pub healthy_checks: usize,
396 pub degraded_checks: usize,
398 pub unhealthy_checks: usize,
400 pub unknown_checks: usize,
402 pub overall_status: HealthStatus,
404 pub uptime_seconds: u64,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct HealthConfig {
411 pub default_timeout_seconds: u64,
413 pub default_interval_seconds: u64,
415}
416
417impl Default for HealthConfig {
418 fn default() -> Self {
419 Self {
420 default_timeout_seconds: 30,
421 default_interval_seconds: 60,
422 }
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn test_health_check_creation() {
432 let health_check = HealthCheck::new(
433 "database".to_string(),
434 "Database Health".to_string(),
435 "Checks database connectivity".to_string(),
436 "check_database".to_string(),
437 );
438
439 assert_eq!(health_check.id, "database");
440 assert_eq!(health_check.name, "Database Health");
441 assert!(health_check.enabled);
442 }
443
444 #[test]
445 fn test_health_status() {
446 assert_eq!(HealthStatus::Healthy.description(), "System is operating normally");
447 assert_eq!(HealthStatus::Healthy.color(), "green");
448 assert!(HealthStatus::Healthy < HealthStatus::Degraded);
449 assert!(HealthStatus::Degraded < HealthStatus::Unhealthy);
450 }
451
452 #[test]
453 fn test_health_check_result() {
454 let result = HealthCheckResult::new(
455 "database".to_string(),
456 HealthStatus::Healthy,
457 150,
458 ).with_message("Database is responding normally".to_string());
459
460 assert_eq!(result.check_id, "database");
461 assert_eq!(result.status, HealthStatus::Healthy);
462 assert_eq!(result.duration_ms, 150);
463 assert!(result.message.is_some());
464 }
465
466 #[test]
467 fn test_system_status() {
468 let mut status = SystemStatus::new();
469
470 let result1 = HealthCheckResult::new(
472 "database".to_string(),
473 HealthStatus::Healthy,
474 100,
475 );
476 let result2 = HealthCheckResult::new(
477 "redis".to_string(),
478 HealthStatus::Degraded,
479 200,
480 );
481
482 status.add_health_check_result("database".to_string(), result1);
483 status.add_health_check_result("redis".to_string(), result2);
484
485 let overall_status = status.calculate_overall_status();
487 assert_eq!(overall_status, HealthStatus::Degraded);
488 }
489
490 #[test]
491 fn test_health_reporter() {
492 let mut reporter = HealthReporter::new();
493
494 let health_check = HealthCheck::new(
496 "database".to_string(),
497 "Database Health".to_string(),
498 "Checks database connectivity".to_string(),
499 "check_database".to_string(),
500 );
501 reporter.add_health_check(health_check);
502
503 let result = reporter.perform_health_check("database");
505 assert!(result.is_some());
506
507 let result = result.unwrap();
508 assert_eq!(result.check_id, "database");
509
510 let status = reporter.get_current_system_status();
512 assert!(status.uptime_seconds > 0);
513 }
514
515 #[test]
516 fn test_health_stats() {
517 let mut reporter = HealthReporter::new();
518
519 let checks = vec![
521 ("database", HealthStatus::Healthy),
522 ("redis", HealthStatus::Healthy),
523 ("external_api", HealthStatus::Degraded),
524 ("disk_space", HealthStatus::Unhealthy),
525 ];
526
527 for (id, status) in checks {
528 let health_check = HealthCheck::new(
529 id.to_string(),
530 format!("{} Health", id),
531 format!("Checks {} connectivity", id),
532 format!("check_{}", id),
533 );
534 reporter.add_health_check(health_check);
535
536 let result = HealthCheckResult::new(id.to_string(), status, 100);
538 reporter.system_status.add_health_check_result(id.to_string(), result);
539 }
540
541 let stats = reporter.get_health_stats();
542 assert_eq!(stats.total_checks, 4);
543 assert_eq!(stats.healthy_checks, 2);
544 assert_eq!(stats.degraded_checks, 1);
545 assert_eq!(stats.unhealthy_checks, 1);
546 }
547}