bws_web_server/monitoring/
health.rs

1use crate::config::SiteConfig;
2use pingora::http::ResponseHeader;
3use pingora::prelude::*;
4
5pub struct HealthHandler {
6    start_time: std::time::Instant,
7}
8
9impl HealthHandler {
10    pub fn new() -> Self {
11        Self {
12            start_time: std::time::Instant::now(),
13        }
14    }
15
16    pub async fn handle(&self, session: &mut Session, _site: Option<&SiteConfig>) -> Result<()> {
17        let path = session.req_header().uri.path();
18
19        match path {
20            "/api/health" => self.handle_basic_health(session).await,
21            "/api/health/detailed" => self.handle_detailed_health(session).await,
22            "/api/health/ready" => self.handle_readiness(session).await,
23            "/api/health/live" => self.handle_liveness(session).await,
24            _ => self.handle_basic_health(session).await,
25        }
26    }
27
28    async fn handle_basic_health(&self, session: &mut Session) -> Result<()> {
29        let response = serde_json::json!({
30            "status": "ok",
31            "timestamp": chrono::Utc::now().to_rfc3339(),
32            "service": "bws-web-server",
33            "version": env!("CARGO_PKG_VERSION"),
34            "uptime_seconds": self.start_time.elapsed().as_secs()
35        });
36
37        self.send_json_response(session, 200, &response).await
38    }
39
40    async fn handle_detailed_health(&self, session: &mut Session) -> Result<()> {
41        let uptime = self.start_time.elapsed();
42        let memory_info = self.get_memory_info();
43        let system_info = self.get_system_info();
44
45        let response = serde_json::json!({
46            "status": "ok",
47            "timestamp": chrono::Utc::now().to_rfc3339(),
48            "service": {
49                "name": "bws-web-server",
50                "version": env!("CARGO_PKG_VERSION"),
51                "description": env!("CARGO_PKG_DESCRIPTION")
52            },
53            "uptime": {
54                "seconds": uptime.as_secs(),
55                "human_readable": self.format_duration(uptime)
56            },
57            "memory": memory_info,
58            "system": system_info,
59            "features": {
60                "ssl_support": true,
61                "auto_cert": true,
62                "multi_site": true,
63                "compression": true,
64                "caching": true
65            }
66        });
67
68        self.send_json_response(session, 200, &response).await
69    }
70
71    async fn handle_readiness(&self, session: &mut Session) -> Result<()> {
72        // Check if the service is ready to accept requests
73        let is_ready = self.check_readiness();
74
75        let status_code = if is_ready { 200 } else { 503 };
76        let response = serde_json::json!({
77            "status": if is_ready { "ready" } else { "not_ready" },
78            "timestamp": chrono::Utc::now().to_rfc3339(),
79            "checks": {
80                "configuration_loaded": true,
81                "ssl_manager_initialized": true,
82                "handlers_ready": true
83            }
84        });
85
86        self.send_json_response(session, status_code, &response)
87            .await
88    }
89
90    async fn handle_liveness(&self, session: &mut Session) -> Result<()> {
91        // Check if the service is alive and functioning
92        let is_alive = self.check_liveness();
93
94        let status_code = if is_alive { 200 } else { 503 };
95        let response = serde_json::json!({
96            "status": if is_alive { "alive" } else { "dead" },
97            "timestamp": chrono::Utc::now().to_rfc3339(),
98            "uptime_seconds": self.start_time.elapsed().as_secs()
99        });
100
101        self.send_json_response(session, status_code, &response)
102            .await
103    }
104
105    fn check_readiness(&self) -> bool {
106        // Implement readiness checks here
107        // For now, assume always ready
108        true
109    }
110
111    fn check_liveness(&self) -> bool {
112        // Implement liveness checks here
113        // For now, assume always alive
114        true
115    }
116
117    fn get_memory_info(&self) -> serde_json::Value {
118        // Get basic memory information
119        // This is a simplified implementation
120        serde_json::json!({
121            "note": "Memory information not available in this implementation"
122        })
123    }
124
125    fn get_system_info(&self) -> serde_json::Value {
126        serde_json::json!({
127            "rust_version": std::env::var("RUSTC_VERSION").unwrap_or_else(|_| "unknown".to_string()),
128            "target": std::env::var("TARGET").unwrap_or_else(|_| std::env::consts::ARCH.to_string()),
129            "build_timestamp": std::env::var("BUILD_TIMESTAMP").unwrap_or_else(|_| "unknown".to_string()),
130            "os": std::env::consts::OS,
131            "arch": std::env::consts::ARCH
132        })
133    }
134
135    fn format_duration(&self, duration: std::time::Duration) -> String {
136        let total_seconds = duration.as_secs();
137        let days = total_seconds / 86400;
138        let hours = (total_seconds % 86400) / 3600;
139        let minutes = (total_seconds % 3600) / 60;
140        let seconds = total_seconds % 60;
141
142        if days > 0 {
143            format!("{days}d {hours}h {minutes}m {seconds}s")
144        } else if hours > 0 {
145            format!("{hours}h {minutes}m {seconds}s")
146        } else if minutes > 0 {
147            format!("{minutes}m {seconds}s")
148        } else {
149            format!("{seconds}s")
150        }
151    }
152
153    async fn send_json_response(
154        &self,
155        session: &mut Session,
156        status: u16,
157        data: &serde_json::Value,
158    ) -> Result<()> {
159        let response_body = serde_json::to_string_pretty(data)
160            .unwrap_or_else(|_| r#"{"error": "Failed to serialize response"}"#.to_string());
161        let response_bytes = response_body.into_bytes();
162
163        let mut header = ResponseHeader::build(status, Some(4))?;
164        header.insert_header("Content-Type", "application/json; charset=utf-8")?;
165        header.insert_header("Content-Length", response_bytes.len().to_string())?;
166        header.insert_header("Cache-Control", "no-cache, no-store, must-revalidate")?;
167        header.insert_header("Pragma", "no-cache")?;
168
169        session
170            .write_response_header(Box::new(header), false)
171            .await?;
172        session
173            .write_response_body(Some(response_bytes.into()), true)
174            .await?;
175
176        Ok(())
177    }
178}
179
180impl Default for HealthHandler {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_health_handler_creation() {
192        let handler = HealthHandler::new();
193        assert!(handler.start_time.elapsed().as_millis() < 100); // Should be very recent
194    }
195
196    #[test]
197    fn test_duration_formatting() {
198        let handler = HealthHandler::new();
199
200        assert_eq!(
201            handler.format_duration(std::time::Duration::from_secs(30)),
202            "30s"
203        );
204        assert_eq!(
205            handler.format_duration(std::time::Duration::from_secs(90)),
206            "1m 30s"
207        );
208        assert_eq!(
209            handler.format_duration(std::time::Duration::from_secs(3661)),
210            "1h 1m 1s"
211        );
212        assert_eq!(
213            handler.format_duration(std::time::Duration::from_secs(90061)),
214            "1d 1h 1m 1s"
215        );
216    }
217
218    #[test]
219    fn test_readiness_check() {
220        let handler = HealthHandler::new();
221        assert!(handler.check_readiness());
222    }
223
224    #[test]
225    fn test_liveness_check() {
226        let handler = HealthHandler::new();
227        assert!(handler.check_liveness());
228    }
229}