rustapi_core/
health.rs

1//! Health check system for monitoring application health
2//!
3//! This module provides a flexible health check system for monitoring
4//! the health and readiness of your application and its dependencies.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use rustapi_core::health::{HealthCheck, HealthCheckBuilder, HealthStatus};
10//!
11//! #[tokio::main]
12//! async fn main() {
13//!     let health = HealthCheckBuilder::new(true)
14//!         .add_check("database", || async {
15//!             // Check database connection
16//!             HealthStatus::healthy()
17//!         })
18//!         .add_check("redis", || async {
19//!             // Check Redis connection
20//!             HealthStatus::healthy()
21//!         })
22//!         .build();
23//!
24//!     // Use health.execute().await to get results
25//! }
26//! ```
27
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30use std::future::Future;
31use std::pin::Pin;
32use std::sync::Arc;
33
34/// Health status of a component
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36#[serde(rename_all = "lowercase")]
37pub enum HealthStatus {
38    /// Component is healthy
39    #[serde(rename = "healthy")]
40    Healthy,
41    /// Component is unhealthy
42    #[serde(rename = "unhealthy")]
43    Unhealthy { reason: String },
44    /// Component is degraded but functional
45    #[serde(rename = "degraded")]
46    Degraded { reason: String },
47}
48
49impl HealthStatus {
50    /// Create a healthy status
51    pub fn healthy() -> Self {
52        Self::Healthy
53    }
54
55    /// Create an unhealthy status with a reason
56    pub fn unhealthy(reason: impl Into<String>) -> Self {
57        Self::Unhealthy {
58            reason: reason.into(),
59        }
60    }
61
62    /// Create a degraded status with a reason
63    pub fn degraded(reason: impl Into<String>) -> Self {
64        Self::Degraded {
65            reason: reason.into(),
66        }
67    }
68
69    /// Check if the status is healthy
70    pub fn is_healthy(&self) -> bool {
71        matches!(self, Self::Healthy)
72    }
73
74    /// Check if the status is unhealthy
75    pub fn is_unhealthy(&self) -> bool {
76        matches!(self, Self::Unhealthy { .. })
77    }
78
79    /// Check if the status is degraded
80    pub fn is_degraded(&self) -> bool {
81        matches!(self, Self::Degraded { .. })
82    }
83}
84
85/// Overall health check result
86#[derive(Debug, Serialize, Deserialize)]
87pub struct HealthCheckResult {
88    /// Overall status
89    pub status: HealthStatus,
90    /// Individual component checks
91    pub checks: HashMap<String, HealthStatus>,
92    /// Application version (if provided)
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub version: Option<String>,
95    /// Timestamp of check (ISO 8601)
96    pub timestamp: String,
97}
98
99/// Type alias for async health check functions
100pub type HealthCheckFn =
101    Arc<dyn Fn() -> Pin<Box<dyn Future<Output = HealthStatus> + Send>> + Send + Sync>;
102
103/// Health check configuration
104#[derive(Clone)]
105pub struct HealthCheck {
106    checks: HashMap<String, HealthCheckFn>,
107    version: Option<String>,
108}
109
110impl HealthCheck {
111    /// Execute all health checks
112    pub async fn execute(&self) -> HealthCheckResult {
113        let mut results = HashMap::new();
114        let mut overall_status = HealthStatus::Healthy;
115
116        for (name, check) in &self.checks {
117            let status = check().await;
118
119            // Determine overall status
120            match &status {
121                HealthStatus::Unhealthy { .. } => {
122                    overall_status = HealthStatus::unhealthy("one or more checks failed");
123                }
124                HealthStatus::Degraded { .. } => {
125                    if overall_status.is_healthy() {
126                        overall_status = HealthStatus::degraded("one or more checks degraded");
127                    }
128                }
129                _ => {}
130            }
131
132            results.insert(name.clone(), status);
133        }
134
135        // Use UTC timestamp formatted as ISO 8601
136        let timestamp = std::time::SystemTime::now()
137            .duration_since(std::time::UNIX_EPOCH)
138            .map(|d| {
139                let secs = d.as_secs();
140                let nanos = d.subsec_nanos();
141                format!("{}.{:09}Z", secs, nanos)
142            })
143            .unwrap_or_else(|_| "unknown".to_string());
144
145        HealthCheckResult {
146            status: overall_status,
147            checks: results,
148            version: self.version.clone(),
149            timestamp,
150        }
151    }
152}
153
154/// Builder for health check configuration
155pub struct HealthCheckBuilder {
156    checks: HashMap<String, HealthCheckFn>,
157    version: Option<String>,
158}
159
160impl HealthCheckBuilder {
161    /// Create a new health check builder
162    ///
163    /// # Arguments
164    ///
165    /// * `include_default` - Whether to include a default "self" check that always returns healthy
166    pub fn new(include_default: bool) -> Self {
167        let mut checks = HashMap::new();
168
169        if include_default {
170            let check: HealthCheckFn = Arc::new(|| Box::pin(async { HealthStatus::healthy() }));
171            checks.insert("self".to_string(), check);
172        }
173
174        Self {
175            checks,
176            version: None,
177        }
178    }
179
180    /// Add a health check
181    ///
182    /// # Example
183    ///
184    /// ```rust
185    /// use rustapi_core::health::{HealthCheckBuilder, HealthStatus};
186    ///
187    /// let health = HealthCheckBuilder::new(false)
188    ///     .add_check("database", || async {
189    ///         // Simulate database check
190    ///         HealthStatus::healthy()
191    ///     })
192    ///     .build();
193    /// ```
194    pub fn add_check<F, Fut>(mut self, name: impl Into<String>, check: F) -> Self
195    where
196        F: Fn() -> Fut + Send + Sync + 'static,
197        Fut: Future<Output = HealthStatus> + Send + 'static,
198    {
199        let check_fn = Arc::new(move || {
200            Box::pin(check()) as Pin<Box<dyn Future<Output = HealthStatus> + Send>>
201        });
202        self.checks.insert(name.into(), check_fn);
203        self
204    }
205
206    /// Set the application version
207    pub fn version(mut self, version: impl Into<String>) -> Self {
208        self.version = Some(version.into());
209        self
210    }
211
212    /// Build the health check
213    pub fn build(self) -> HealthCheck {
214        HealthCheck {
215            checks: self.checks,
216            version: self.version,
217        }
218    }
219}
220
221impl Default for HealthCheckBuilder {
222    fn default() -> Self {
223        Self::new(true)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[tokio::test]
232    async fn health_check_all_healthy() {
233        let health = HealthCheckBuilder::new(false)
234            .add_check("db", || async { HealthStatus::healthy() })
235            .add_check("cache", || async { HealthStatus::healthy() })
236            .version("1.0.0")
237            .build();
238
239        let result = health.execute().await;
240
241        assert!(result.status.is_healthy());
242        assert_eq!(result.checks.len(), 2);
243        assert_eq!(result.version, Some("1.0.0".to_string()));
244    }
245
246    #[tokio::test]
247    async fn health_check_one_unhealthy() {
248        let health = HealthCheckBuilder::new(false)
249            .add_check("db", || async { HealthStatus::healthy() })
250            .add_check("cache", || async {
251                HealthStatus::unhealthy("connection failed")
252            })
253            .build();
254
255        let result = health.execute().await;
256
257        assert!(result.status.is_unhealthy());
258        assert_eq!(result.checks.len(), 2);
259    }
260
261    #[tokio::test]
262    async fn health_check_one_degraded() {
263        let health = HealthCheckBuilder::new(false)
264            .add_check("db", || async { HealthStatus::healthy() })
265            .add_check("cache", || async { HealthStatus::degraded("high latency") })
266            .build();
267
268        let result = health.execute().await;
269
270        assert!(result.status.is_degraded());
271        assert_eq!(result.checks.len(), 2);
272    }
273
274    #[tokio::test]
275    async fn health_check_with_default() {
276        let health = HealthCheckBuilder::new(true).build();
277
278        let result = health.execute().await;
279
280        assert!(result.status.is_healthy());
281        assert_eq!(result.checks.len(), 1);
282        assert!(result.checks.contains_key("self"));
283    }
284}