leptos_sync_core/reliability/monitoring/
health.rs

1//! Health reporting and status monitoring
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7/// Health check definition
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct HealthCheck {
10    /// Unique identifier for the health check
11    pub id: String,
12    /// Human-readable name for the health check
13    pub name: String,
14    /// Description of what this health check monitors
15    pub description: String,
16    /// Function to perform the health check
17    pub check_fn: String, // In real implementation, this would be a function pointer
18    /// Timeout for the health check (in seconds)
19    pub timeout_seconds: u64,
20    /// Whether the health check is enabled
21    pub enabled: bool,
22    /// Interval between health checks (in seconds)
23    pub interval_seconds: u64,
24}
25
26impl HealthCheck {
27    /// Create a new health check
28    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    /// Set the timeout
46    pub fn with_timeout(mut self, timeout_seconds: u64) -> Self {
47        self.timeout_seconds = timeout_seconds;
48        self
49    }
50
51    /// Set the interval
52    pub fn with_interval(mut self, interval_seconds: u64) -> Self {
53        self.interval_seconds = interval_seconds;
54        self
55    }
56
57    /// Enable or disable the health check
58    pub fn set_enabled(mut self, enabled: bool) -> Self {
59        self.enabled = enabled;
60        self
61    }
62}
63
64/// Health status levels
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
66pub enum HealthStatus {
67    /// System is healthy
68    Healthy,
69    /// System has minor issues
70    Degraded,
71    /// System has major issues
72    Unhealthy,
73    /// System status is unknown
74    Unknown,
75}
76
77impl HealthStatus {
78    /// Get a human-readable description of the status
79    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    /// Get the status color for UI display
89    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/// System status information
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct SystemStatus {
102    /// Overall system health status
103    pub status: HealthStatus,
104    /// Timestamp when the status was last updated
105    pub last_updated: u64,
106    /// Individual health check results
107    pub health_checks: HashMap<String, HealthCheckResult>,
108    /// System uptime in seconds
109    pub uptime_seconds: u64,
110    /// Additional system information
111    pub system_info: HashMap<String, String>,
112}
113
114impl SystemStatus {
115    /// Create a new system status
116    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    /// Update the system status
130    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    /// Add a health check result
139    pub fn add_health_check_result(&mut self, check_id: String, result: HealthCheckResult) {
140        self.health_checks.insert(check_id, result);
141    }
142
143    /// Get the overall health status based on individual checks
144    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/// Result of a health check
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct HealthCheckResult {
174    /// Health check ID
175    pub check_id: String,
176    /// Status of the health check
177    pub status: HealthStatus,
178    /// Timestamp when the check was performed
179    pub checked_at: u64,
180    /// Duration the check took to complete (in milliseconds)
181    pub duration_ms: u64,
182    /// Optional message describing the result
183    pub message: Option<String>,
184    /// Additional metadata
185    pub metadata: HashMap<String, String>,
186}
187
188impl HealthCheckResult {
189    /// Create a new health check result
190    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    /// Add a message to the result
209    pub fn with_message(mut self, message: String) -> Self {
210        self.message = Some(message);
211        self
212    }
213
214    /// Add metadata to the result
215    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
216        self.metadata = metadata;
217        self
218    }
219}
220
221/// Health reporter for monitoring system health
222#[derive(Debug, Clone)]
223pub struct HealthReporter {
224    /// Health checks to perform
225    health_checks: HashMap<String, HealthCheck>,
226    /// Current system status
227    system_status: SystemStatus,
228    /// System start time for uptime calculation
229    system_start_time: u64,
230}
231
232impl HealthReporter {
233    /// Create a new health reporter
234    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    /// Create a health reporter with configuration
246    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    /// Add a health check
258    pub fn add_health_check(&mut self, health_check: HealthCheck) {
259        self.health_checks.insert(health_check.id.clone(), health_check);
260    }
261
262    /// Remove a health check
263    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    /// Get all health checks
269    pub fn get_health_checks(&self) -> Vec<&HealthCheck> {
270        self.health_checks.values().collect()
271    }
272
273    /// Get a specific health check
274    pub fn get_health_check(&self, check_id: &str) -> Option<&HealthCheck> {
275        self.health_checks.get(check_id)
276    }
277
278    /// Perform a health check
279    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        // In a real implementation, this would call the actual health check function
289        // For now, we'll simulate a health check
290        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        // Add the result to system status
302        self.system_status.add_health_check_result(check_id.to_string(), health_result.clone());
303        
304        // Update overall status
305        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    /// Perform all enabled health checks
312    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    /// Get the current system status
326    pub fn get_system_status(&self) -> &SystemStatus {
327        &self.system_status
328    }
329
330    /// Get the current system status with updated uptime
331    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    /// Get health check results by status
342    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    /// Get health statistics
351    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    /// Simulate a health check (for testing purposes)
370    fn simulate_health_check(&self, health_check: &HealthCheck) -> HealthStatus {
371        // In a real implementation, this would call the actual health check function
372        // For now, we'll return a random status for testing
373        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/// Health statistics
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct HealthStats {
392    /// Total number of health checks
393    pub total_checks: usize,
394    /// Number of healthy checks
395    pub healthy_checks: usize,
396    /// Number of degraded checks
397    pub degraded_checks: usize,
398    /// Number of unhealthy checks
399    pub unhealthy_checks: usize,
400    /// Number of unknown checks
401    pub unknown_checks: usize,
402    /// Overall system status
403    pub overall_status: HealthStatus,
404    /// System uptime in seconds
405    pub uptime_seconds: u64,
406}
407
408/// Configuration for health reporting
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct HealthConfig {
411    /// Default timeout for health checks (in seconds)
412    pub default_timeout_seconds: u64,
413    /// Default interval between health checks (in seconds)
414    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        // Add some health check results
471        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        // Calculate overall status
486        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        // Add a health check
495        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        // Perform the health check
504        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        // Check system status
511        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        // Add multiple health checks
520        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            // Simulate the health check result
537            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}