allframe_core/health/
mod.rs

1//! Health check infrastructure for AllFrame applications
2//!
3//! This module provides a standardized health check pattern with support for
4//! dependency health monitoring, configurable timeouts, and critical vs
5//! non-critical dependency classification.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use allframe_core::health::{HealthCheck, Dependency, DependencyStatus, HealthReport};
11//! use std::time::Duration;
12//!
13//! struct RedisHealth {
14//!     client: redis::Client,
15//! }
16//!
17//! impl Dependency for RedisHealth {
18//!     fn name(&self) -> &str { "redis" }
19//!
20//!     async fn check(&self) -> DependencyStatus {
21//!         match self.client.ping().await {
22//!             Ok(_) => DependencyStatus::Healthy,
23//!             Err(e) => DependencyStatus::Unhealthy(e.to_string()),
24//!         }
25//!     }
26//!
27//!     fn is_critical(&self) -> bool { true }
28//!     fn timeout(&self) -> Duration { Duration::from_secs(5) }
29//! }
30//! ```
31
32mod server;
33mod types;
34
35use std::{
36    future::Future,
37    pin::Pin,
38    sync::Arc,
39    time::{Duration, Instant},
40};
41
42pub use server::HealthServer;
43pub use types::*;
44
45/// A dependency that can be health-checked
46///
47/// Implement this trait for each external dependency (database, cache, API,
48/// etc.) that your application relies on.
49pub trait Dependency: Send + Sync {
50    /// The name of this dependency (e.g., "redis", "postgres", "kraken-api")
51    fn name(&self) -> &str;
52
53    /// Check the health of this dependency
54    ///
55    /// This method should perform an actual health check (e.g., ping, query)
56    /// and return the current status.
57    fn check(&self) -> Pin<Box<dyn Future<Output = DependencyStatus> + Send + '_>>;
58
59    /// Whether this dependency is critical for the application to function
60    ///
61    /// If a critical dependency is unhealthy, the overall health status will be
62    /// Unhealthy. Non-critical dependencies being unhealthy will result in
63    /// a Degraded status.
64    fn is_critical(&self) -> bool {
65        true
66    }
67
68    /// The timeout for health checks on this dependency
69    ///
70    /// If the check takes longer than this duration, it will be considered
71    /// unhealthy.
72    fn timeout(&self) -> Duration {
73        Duration::from_secs(5)
74    }
75}
76
77/// A collection of dependencies that can be health-checked together
78pub trait HealthCheck: Send + Sync {
79    /// Get all dependencies to check
80    fn dependencies(&self) -> Vec<Arc<dyn Dependency>>;
81
82    /// Check all dependencies and return a comprehensive health report
83    fn check_all(&self) -> Pin<Box<dyn Future<Output = HealthReport> + Send + '_>> {
84        let deps = self.dependencies();
85        Box::pin(async move {
86            let start = Instant::now();
87            let mut reports = Vec::with_capacity(deps.len());
88            let mut has_critical_failure = false;
89            let mut has_degradation = false;
90
91            for dep in deps {
92                let dep_start = Instant::now();
93                let timeout = dep.timeout();
94                let is_critical = dep.is_critical();
95                let name = dep.name().to_string();
96
97                let status = match tokio::time::timeout(timeout, dep.check()).await {
98                    Ok(status) => status,
99                    Err(_) => DependencyStatus::Unhealthy(format!(
100                        "Health check timed out after {:?}",
101                        timeout
102                    )),
103                };
104
105                let duration = dep_start.elapsed();
106
107                match &status {
108                    DependencyStatus::Unhealthy(_) if is_critical => {
109                        has_critical_failure = true;
110                    }
111                    DependencyStatus::Unhealthy(_) | DependencyStatus::Degraded(_) => {
112                        has_degradation = true;
113                    }
114                    _ => {}
115                }
116
117                reports.push(DependencyReport {
118                    name,
119                    status,
120                    duration,
121                    critical: is_critical,
122                });
123            }
124
125            let overall_status = if has_critical_failure {
126                OverallStatus::Unhealthy
127            } else if has_degradation {
128                OverallStatus::Degraded
129            } else {
130                OverallStatus::Healthy
131            };
132
133            HealthReport {
134                status: overall_status,
135                dependencies: reports,
136                total_duration: start.elapsed(),
137                timestamp: std::time::SystemTime::now(),
138            }
139        })
140    }
141}
142
143/// A simple health checker that holds a list of dependencies
144pub struct SimpleHealthCheck {
145    dependencies: Vec<Arc<dyn Dependency>>,
146}
147
148impl SimpleHealthCheck {
149    /// Create a new health checker with no dependencies
150    pub fn new() -> Self {
151        Self {
152            dependencies: Vec::new(),
153        }
154    }
155
156    /// Add a dependency to check
157    pub fn add_dependency<D: Dependency + 'static>(mut self, dep: D) -> Self {
158        self.dependencies.push(Arc::new(dep));
159        self
160    }
161
162    /// Add an already Arc-wrapped dependency
163    pub fn add_arc_dependency(mut self, dep: Arc<dyn Dependency>) -> Self {
164        self.dependencies.push(dep);
165        self
166    }
167}
168
169impl Default for SimpleHealthCheck {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175impl HealthCheck for SimpleHealthCheck {
176    fn dependencies(&self) -> Vec<Arc<dyn Dependency>> {
177        self.dependencies.clone()
178    }
179}
180
181/// A dependency that always reports healthy (useful for testing)
182pub struct AlwaysHealthy {
183    name: String,
184}
185
186impl AlwaysHealthy {
187    /// Create a new always-healthy dependency
188    pub fn new(name: impl Into<String>) -> Self {
189        Self { name: name.into() }
190    }
191}
192
193impl Dependency for AlwaysHealthy {
194    fn name(&self) -> &str {
195        &self.name
196    }
197
198    fn check(&self) -> Pin<Box<dyn Future<Output = DependencyStatus> + Send + '_>> {
199        Box::pin(async { DependencyStatus::Healthy })
200    }
201}
202
203/// A dependency that always reports unhealthy (useful for testing)
204pub struct AlwaysUnhealthy {
205    name: String,
206    message: String,
207}
208
209impl AlwaysUnhealthy {
210    /// Create a new always-unhealthy dependency
211    pub fn new(name: impl Into<String>, message: impl Into<String>) -> Self {
212        Self {
213            name: name.into(),
214            message: message.into(),
215        }
216    }
217}
218
219impl Dependency for AlwaysUnhealthy {
220    fn name(&self) -> &str {
221        &self.name
222    }
223
224    fn check(&self) -> Pin<Box<dyn Future<Output = DependencyStatus> + Send + '_>> {
225        let msg = self.message.clone();
226        Box::pin(async move { DependencyStatus::Unhealthy(msg) })
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[tokio::test]
235    async fn test_always_healthy() {
236        let dep = AlwaysHealthy::new("test");
237        assert_eq!(dep.name(), "test");
238        assert!(matches!(dep.check().await, DependencyStatus::Healthy));
239    }
240
241    #[tokio::test]
242    async fn test_always_unhealthy() {
243        let dep = AlwaysUnhealthy::new("test", "connection failed");
244        assert_eq!(dep.name(), "test");
245        match dep.check().await {
246            DependencyStatus::Unhealthy(msg) => assert_eq!(msg, "connection failed"),
247            _ => panic!("Expected unhealthy status"),
248        }
249    }
250
251    #[tokio::test]
252    async fn test_simple_health_check_empty() {
253        let checker = SimpleHealthCheck::new();
254        let report = checker.check_all().await;
255        assert_eq!(report.status, OverallStatus::Healthy);
256        assert!(report.dependencies.is_empty());
257    }
258
259    #[tokio::test]
260    async fn test_simple_health_check_all_healthy() {
261        let checker = SimpleHealthCheck::new()
262            .add_dependency(AlwaysHealthy::new("dep1"))
263            .add_dependency(AlwaysHealthy::new("dep2"));
264
265        let report = checker.check_all().await;
266        assert_eq!(report.status, OverallStatus::Healthy);
267        assert_eq!(report.dependencies.len(), 2);
268    }
269
270    #[tokio::test]
271    async fn test_simple_health_check_critical_failure() {
272        let checker = SimpleHealthCheck::new()
273            .add_dependency(AlwaysHealthy::new("healthy"))
274            .add_dependency(AlwaysUnhealthy::new("critical", "down"));
275
276        let report = checker.check_all().await;
277        assert_eq!(report.status, OverallStatus::Unhealthy);
278    }
279
280    #[tokio::test]
281    async fn test_default_timeout() {
282        let dep = AlwaysHealthy::new("test");
283        assert_eq!(dep.timeout(), Duration::from_secs(5));
284    }
285
286    #[tokio::test]
287    async fn test_default_critical() {
288        let dep = AlwaysHealthy::new("test");
289        assert!(dep.is_critical());
290    }
291}