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 parking_lot::RwLock;
6use std::sync::Arc;
7use tracing::{error, info, warn};
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 Default for HealthCheck {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl HealthCheck {
140    /// Create a new health check result
141    pub fn new() -> Self {
142        Self {
143            status: HealthStatus::Healthy,
144            components: Vec::new(),
145            timestamp: chrono::Utc::now(),
146        }
147    }
148
149    /// Add a component health status
150    pub fn add_component(&mut self, component: ComponentHealth) {
151        // Update overall status based on component
152        match component.status {
153            HealthStatus::Unhealthy => self.status = HealthStatus::Unhealthy,
154            HealthStatus::Degraded if self.status == HealthStatus::Healthy => {
155                self.status = HealthStatus::Degraded;
156            }
157            _ => {}
158        }
159        self.components.push(component);
160    }
161
162    /// Convert to JSON response
163    pub fn to_json(&self) -> serde_json::Value {
164        serde_json::json!({
165            "status": match self.status {
166                HealthStatus::Healthy => "healthy",
167                HealthStatus::Degraded => "degraded",
168                HealthStatus::Unhealthy => "unhealthy",
169            },
170            "timestamp": self.timestamp.to_rfc3339(),
171            "components": self.components.iter().map(|c| {
172                serde_json::json!({
173                    "name": c.name,
174                    "status": match c.status {
175                        HealthStatus::Healthy => "healthy",
176                        HealthStatus::Degraded => "degraded",
177                        HealthStatus::Unhealthy => "unhealthy",
178                    },
179                    "message": c.message,
180                    "last_success": c.last_success.map(|t| t.to_rfc3339()),
181                })
182            }).collect::<Vec<_>>(),
183        })
184    }
185}
186
187/// Readiness check result
188#[derive(Debug, Clone)]
189pub struct ReadinessCheck {
190    /// Is the service ready?
191    pub ready: bool,
192    /// Reason if not ready
193    pub reason: Option<String>,
194    /// Timestamp
195    pub timestamp: chrono::DateTime<chrono::Utc>,
196}
197
198impl ReadinessCheck {
199    /// Create a ready result
200    pub fn ready() -> Self {
201        Self {
202            ready: true,
203            reason: None,
204            timestamp: chrono::Utc::now(),
205        }
206    }
207
208    /// Create a not ready result
209    pub fn not_ready(reason: String) -> Self {
210        Self {
211            ready: false,
212            reason: Some(reason),
213            timestamp: chrono::Utc::now(),
214        }
215    }
216
217    /// Convert to JSON response
218    pub fn to_json(&self) -> serde_json::Value {
219        serde_json::json!({
220            "ready": self.ready,
221            "reason": self.reason,
222            "timestamp": self.timestamp.to_rfc3339(),
223        })
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_app_state() {
233        let state = AppState::new("test-123".to_string());
234
235        assert!(!state.is_ready());
236        assert!(state.is_healthy());
237
238        state.set_ready(true);
239        assert!(state.is_ready());
240
241        state.set_healthy(false);
242        assert!(!state.is_healthy());
243    }
244
245    #[test]
246    fn test_health_check() {
247        let mut check = HealthCheck::new();
248        assert_eq!(check.status, HealthStatus::Healthy);
249
250        check.add_component(ComponentHealth {
251            name: "upstream".to_string(),
252            status: HealthStatus::Healthy,
253            message: None,
254            last_success: Some(chrono::Utc::now()),
255        });
256        assert_eq!(check.status, HealthStatus::Healthy);
257
258        check.add_component(ComponentHealth {
259            name: "cache".to_string(),
260            status: HealthStatus::Degraded,
261            message: Some("High latency".to_string()),
262            last_success: Some(chrono::Utc::now()),
263        });
264        assert_eq!(check.status, HealthStatus::Degraded);
265
266        check.add_component(ComponentHealth {
267            name: "database".to_string(),
268            status: HealthStatus::Unhealthy,
269            message: Some("Connection failed".to_string()),
270            last_success: None,
271        });
272        assert_eq!(check.status, HealthStatus::Unhealthy);
273    }
274
275    #[test]
276    fn test_health_status_http_codes() {
277        assert_eq!(HealthStatus::Healthy.to_http_status(), 200);
278        assert_eq!(HealthStatus::Degraded.to_http_status(), 200);
279        assert_eq!(HealthStatus::Unhealthy.to_http_status(), 503);
280
281        assert!(HealthStatus::Healthy.is_ok());
282        assert!(HealthStatus::Degraded.is_ok());
283        assert!(!HealthStatus::Unhealthy.is_ok());
284    }
285}