sentinel_proxy/
app.rs

1//! Application module for Sentinel proxy
2//!
3//! This module contains application-level logic and utilities for the proxy.
4
5use std::sync::Arc;
6use parking_lot::RwLock;
7use tracing::{info, warn, error};
8
9/// Application state for the proxy
10pub struct AppState {
11    /// Application version
12    pub version: String,
13    /// Instance ID
14    pub instance_id: String,
15    /// Startup time
16    pub start_time: std::time::Instant,
17    /// Is the proxy healthy?
18    pub is_healthy: Arc<RwLock<bool>>,
19    /// Is the proxy ready to serve traffic?
20    pub is_ready: Arc<RwLock<bool>>,
21}
22
23impl AppState {
24    /// Create new application state
25    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    /// Mark the application as ready
36    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    /// Mark the application as healthy/unhealthy
46    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    /// Check if the application is healthy
56    pub fn is_healthy(&self) -> bool {
57        *self.is_healthy.read()
58    }
59
60    /// Check if the application is ready
61    pub fn is_ready(&self) -> bool {
62        *self.is_ready.read()
63    }
64
65    /// Get uptime in seconds
66    pub fn uptime_seconds(&self) -> u64 {
67        self.start_time.elapsed().as_secs()
68    }
69
70    /// Get application info as JSON
71    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/// Health check status
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum HealthStatus {
85    /// Service is healthy
86    Healthy,
87    /// Service is degraded but operational
88    Degraded,
89    /// Service is unhealthy
90    Unhealthy,
91}
92
93impl HealthStatus {
94    /// Convert to HTTP status code
95    pub fn to_http_status(&self) -> u16 {
96        match self {
97            Self::Healthy => 200,
98            Self::Degraded => 200,  // Still return 200 for degraded
99            Self::Unhealthy => 503,
100        }
101    }
102
103    /// Check if the status is considered "ok" for serving traffic
104    pub fn is_ok(&self) -> bool {
105        matches!(self, Self::Healthy | Self::Degraded)
106    }
107}
108
109/// Health check result
110#[derive(Debug, Clone)]
111pub struct HealthCheck {
112    /// Overall status
113    pub status: HealthStatus,
114    /// Individual component checks
115    pub components: Vec<ComponentHealth>,
116    /// Timestamp of the check
117    pub timestamp: chrono::DateTime<chrono::Utc>,
118}
119
120/// Component health status
121#[derive(Debug, Clone)]
122pub struct ComponentHealth {
123    /// Component name
124    pub name: String,
125    /// Component status
126    pub status: HealthStatus,
127    /// Optional error message
128    pub message: Option<String>,
129    /// Last successful check time
130    pub last_success: Option<chrono::DateTime<chrono::Utc>>,
131}
132
133impl HealthCheck {
134    /// Create a new health check result
135    pub fn new() -> Self {
136        Self {
137            status: HealthStatus::Healthy,
138            components: Vec::new(),
139            timestamp: chrono::Utc::now(),
140        }
141    }
142
143    /// Add a component health status
144    pub fn add_component(&mut self, component: ComponentHealth) {
145        // Update overall status based on component
146        match component.status {
147            HealthStatus::Unhealthy => self.status = HealthStatus::Unhealthy,
148            HealthStatus::Degraded if self.status == HealthStatus::Healthy => {
149                self.status = HealthStatus::Degraded;
150            }
151            _ => {}
152        }
153        self.components.push(component);
154    }
155
156    /// Convert to JSON response
157    pub fn to_json(&self) -> serde_json::Value {
158        serde_json::json!({
159            "status": match self.status {
160                HealthStatus::Healthy => "healthy",
161                HealthStatus::Degraded => "degraded",
162                HealthStatus::Unhealthy => "unhealthy",
163            },
164            "timestamp": self.timestamp.to_rfc3339(),
165            "components": self.components.iter().map(|c| {
166                serde_json::json!({
167                    "name": c.name,
168                    "status": match c.status {
169                        HealthStatus::Healthy => "healthy",
170                        HealthStatus::Degraded => "degraded",
171                        HealthStatus::Unhealthy => "unhealthy",
172                    },
173                    "message": c.message,
174                    "last_success": c.last_success.map(|t| t.to_rfc3339()),
175                })
176            }).collect::<Vec<_>>(),
177        })
178    }
179}
180
181/// Readiness check result
182#[derive(Debug, Clone)]
183pub struct ReadinessCheck {
184    /// Is the service ready?
185    pub ready: bool,
186    /// Reason if not ready
187    pub reason: Option<String>,
188    /// Timestamp
189    pub timestamp: chrono::DateTime<chrono::Utc>,
190}
191
192impl ReadinessCheck {
193    /// Create a ready result
194    pub fn ready() -> Self {
195        Self {
196            ready: true,
197            reason: None,
198            timestamp: chrono::Utc::now(),
199        }
200    }
201
202    /// Create a not ready result
203    pub fn not_ready(reason: String) -> Self {
204        Self {
205            ready: false,
206            reason: Some(reason),
207            timestamp: chrono::Utc::now(),
208        }
209    }
210
211    /// Convert to JSON response
212    pub fn to_json(&self) -> serde_json::Value {
213        serde_json::json!({
214            "ready": self.ready,
215            "reason": self.reason,
216            "timestamp": self.timestamp.to_rfc3339(),
217        })
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_app_state() {
227        let state = AppState::new("test-123".to_string());
228
229        assert!(!state.is_ready());
230        assert!(state.is_healthy());
231
232        state.set_ready(true);
233        assert!(state.is_ready());
234
235        state.set_healthy(false);
236        assert!(!state.is_healthy());
237    }
238
239    #[test]
240    fn test_health_check() {
241        let mut check = HealthCheck::new();
242        assert_eq!(check.status, HealthStatus::Healthy);
243
244        check.add_component(ComponentHealth {
245            name: "upstream".to_string(),
246            status: HealthStatus::Healthy,
247            message: None,
248            last_success: Some(chrono::Utc::now()),
249        });
250        assert_eq!(check.status, HealthStatus::Healthy);
251
252        check.add_component(ComponentHealth {
253            name: "cache".to_string(),
254            status: HealthStatus::Degraded,
255            message: Some("High latency".to_string()),
256            last_success: Some(chrono::Utc::now()),
257        });
258        assert_eq!(check.status, HealthStatus::Degraded);
259
260        check.add_component(ComponentHealth {
261            name: "database".to_string(),
262            status: HealthStatus::Unhealthy,
263            message: Some("Connection failed".to_string()),
264            last_success: None,
265        });
266        assert_eq!(check.status, HealthStatus::Unhealthy);
267    }
268
269    #[test]
270    fn test_health_status_http_codes() {
271        assert_eq!(HealthStatus::Healthy.to_http_status(), 200);
272        assert_eq!(HealthStatus::Degraded.to_http_status(), 200);
273        assert_eq!(HealthStatus::Unhealthy.to_http_status(), 503);
274
275        assert!(HealthStatus::Healthy.is_ok());
276        assert!(HealthStatus::Degraded.is_ok());
277        assert!(!HealthStatus::Unhealthy.is_ok());
278    }
279}