things3_cli/
health.rs

1async fn health_check(State(state): State<AppState>) -> Result<Json<HealthResponse>, StatusCode> {
2    let health_status = state.observability.health_status();
3
4    let response = HealthResponse {
5        status: health_status.status,
6        timestamp: health_status.timestamp.to_string(),
7        uptime: health_status.uptime,
8        version: health_status.version,
9        environment: "production".to_string(),
10        checks: std::collections::HashMap::new(),
11    };
12
13    Ok(Json(response))
14}
15
16async fn readiness_check(
17    State(state): State<AppState>,
18) -> Result<Json<HealthResponse>, StatusCode> {
19    let health_status = state.observability.health_status();
20
21    let response = HealthResponse {
22        status: health_status.status,
23        timestamp: health_status.timestamp.to_string(),
24        uptime: health_status.uptime,
25        version: health_status.version,
26        environment: "production".to_string(),
27        checks: std::collections::HashMap::new(),
28    };
29
30    Ok(Json(response))
31}
32
33async fn liveness_check(State(state): State<AppState>) -> Result<Json<HealthResponse>, StatusCode> {
34    let health_status = state.observability.health_status();
35
36    let response = HealthResponse {
37        status: health_status.status,
38        timestamp: health_status.timestamp.to_string(),
39        uptime: health_status.uptime,
40        version: health_status.version,
41        environment: "production".to_string(),
42        checks: std::collections::HashMap::new(),
43    };
44
45    Ok(Json(response))
46}
47
48async fn metrics_endpoint(State(state): State<AppState>) -> Result<String, StatusCode> {
49    let health_status = state.observability.health_status();
50
51    let metrics = format!(
52        "# HELP health_status Current health status\n\
53         # TYPE health_status gauge\n\
54         health_status{{status=\"{}\"}} {}\n\
55         # HELP uptime_seconds Current uptime in seconds\n\
56         # TYPE uptime_seconds counter\n\
57         uptime_seconds {}\n",
58        health_status.status,
59        i32::from(health_status.status == "healthy"),
60        health_status.uptime.as_secs()
61    );
62
63    Ok(metrics)
64}
65
66use axum::{extract::State, http::StatusCode, response::Json, routing::get, Router};
67use serde::{Deserialize, Serialize};
68use std::sync::Arc;
69use things3_core::{ObservabilityManager, ThingsDatabase};
70use tokio::net::TcpListener;
71use tower_http::cors::CorsLayer;
72use tracing::{info, instrument};
73
74// Struct definitions - must come after all functions to avoid items_after_statements
75/// Application state
76#[derive(Clone)]
77pub struct AppState {
78    pub observability: Arc<ObservabilityManager>,
79    pub database: Arc<ThingsDatabase>,
80}
81
82/// Health response
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct HealthResponse {
85    pub status: String,
86    pub timestamp: String,
87    pub uptime: std::time::Duration,
88    pub version: String,
89    pub environment: String,
90    pub checks: std::collections::HashMap<String, CheckResponse>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct CheckResponse {
95    pub status: String,
96    pub message: Option<String>,
97    pub duration_ms: u64,
98}
99
100impl HealthServer {
101    /// Create a new health check server
102    #[must_use]
103    pub fn new(
104        port: u16,
105        observability: Arc<ObservabilityManager>,
106        database: Arc<ThingsDatabase>,
107    ) -> Self {
108        Self {
109            port,
110            observability,
111            database,
112        }
113    }
114
115    /// Start the health check server
116    ///
117    /// # Errors
118    /// Returns an error if the server fails to start or bind to the port
119    #[instrument(skip(self))]
120    pub async fn start(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
121        let state = AppState {
122            observability: self.observability,
123            database: self.database,
124        };
125
126        let app = Router::new()
127            .route("/health", get(health_check))
128            .route("/ready", get(readiness_check))
129            .route("/live", get(liveness_check))
130            .route("/metrics", get(metrics_endpoint))
131            .layer(CorsLayer::permissive())
132            .with_state(state);
133
134        let listener = TcpListener::bind(format!("0.0.0.0:{}", self.port)).await?;
135        info!("Health check server running on port {}", self.port);
136
137        axum::serve(listener, app).await?;
138        Ok(())
139    }
140}
141
142/// Health check server
143pub struct HealthServer {
144    port: u16,
145    observability: Arc<ObservabilityManager>,
146    database: Arc<ThingsDatabase>,
147}
148
149/// Start the health check server
150///
151/// # Errors
152/// Returns an error if the server fails to start or bind to the port
153#[instrument(skip(observability, database))]
154pub async fn start_health_server(
155    port: u16,
156    observability: Arc<ObservabilityManager>,
157    database: Arc<ThingsDatabase>,
158) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
159    let server = HealthServer::new(port, observability, database);
160    server.start().await
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use tempfile::NamedTempFile;
167
168    #[test]
169    fn test_health_server_creation() {
170        let temp_file = NamedTempFile::new().unwrap();
171        let db_path = temp_file.path();
172
173        let config = things3_core::ThingsConfig::new(db_path, false);
174        let rt = tokio::runtime::Runtime::new().unwrap();
175        let database = Arc::new(
176            rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
177        );
178
179        let observability = Arc::new(
180            things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
181                .unwrap(),
182        );
183        let server = HealthServer::new(8080, observability, database);
184        assert_eq!(server.port, 8080);
185    }
186
187    #[test]
188    fn test_health_response() {
189        let response = HealthResponse {
190            status: "healthy".to_string(),
191            timestamp: "2024-01-01T00:00:00Z".to_string(),
192            uptime: std::time::Duration::from_secs(3600),
193            version: "1.0.0".to_string(),
194            environment: "test".to_string(),
195            checks: std::collections::HashMap::new(),
196        };
197
198        assert_eq!(response.status, "healthy");
199        assert_eq!(response.version, "1.0.0");
200    }
201
202    #[test]
203    fn test_health_response_with_checks() {
204        let mut checks = std::collections::HashMap::new();
205        checks.insert(
206            "database".to_string(),
207            CheckResponse {
208                status: "healthy".to_string(),
209                message: Some("Connection successful".to_string()),
210                duration_ms: 5,
211            },
212        );
213        checks.insert(
214            "cache".to_string(),
215            CheckResponse {
216                status: "unhealthy".to_string(),
217                message: Some("Connection failed".to_string()),
218                duration_ms: 100,
219            },
220        );
221
222        let response = HealthResponse {
223            status: "degraded".to_string(),
224            timestamp: "2024-01-01T00:00:00Z".to_string(),
225            uptime: std::time::Duration::from_secs(7200),
226            version: "2.0.0".to_string(),
227            environment: "staging".to_string(),
228            checks,
229        };
230
231        assert_eq!(response.status, "degraded");
232        assert_eq!(response.version, "2.0.0");
233        assert_eq!(response.environment, "staging");
234        assert_eq!(response.checks.len(), 2);
235        assert_eq!(response.uptime.as_secs(), 7200);
236    }
237
238    #[test]
239    fn test_check_response() {
240        let check = CheckResponse {
241            status: "healthy".to_string(),
242            message: Some("All systems operational".to_string()),
243            duration_ms: 10,
244        };
245
246        assert_eq!(check.status, "healthy");
247        assert_eq!(check.message, Some("All systems operational".to_string()));
248        assert_eq!(check.duration_ms, 10);
249    }
250
251    #[test]
252    fn test_check_response_without_message() {
253        let check = CheckResponse {
254            status: "unhealthy".to_string(),
255            message: None,
256            duration_ms: 500,
257        };
258
259        assert_eq!(check.status, "unhealthy");
260        assert_eq!(check.message, None);
261        assert_eq!(check.duration_ms, 500);
262    }
263
264    #[test]
265    fn test_app_state_creation() {
266        let temp_file = NamedTempFile::new().unwrap();
267        let db_path = temp_file.path();
268
269        let config = things3_core::ThingsConfig::new(db_path, false);
270        let rt = tokio::runtime::Runtime::new().unwrap();
271        let database = Arc::new(
272            rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
273        );
274
275        let observability = Arc::new(
276            things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
277                .unwrap(),
278        );
279
280        let state = AppState {
281            observability: Arc::clone(&observability),
282            database: Arc::clone(&database),
283        };
284
285        // Test that state can be created and cloned
286        let _cloned_state = state.clone();
287    }
288
289    #[test]
290    fn test_health_server_with_different_ports() {
291        let temp_file = NamedTempFile::new().unwrap();
292        let db_path = temp_file.path();
293
294        let config = things3_core::ThingsConfig::new(db_path, false);
295        let rt = tokio::runtime::Runtime::new().unwrap();
296        let database = Arc::new(
297            rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
298        );
299
300        let observability = Arc::new(
301            things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
302                .unwrap(),
303        );
304
305        // Test with different ports
306        let server1 = HealthServer::new(8080, Arc::clone(&observability), Arc::clone(&database));
307        let server2 = HealthServer::new(9090, Arc::clone(&observability), Arc::clone(&database));
308        let server3 = HealthServer::new(3000, Arc::clone(&observability), Arc::clone(&database));
309
310        assert_eq!(server1.port, 8080);
311        assert_eq!(server2.port, 9090);
312        assert_eq!(server3.port, 3000);
313    }
314
315    #[test]
316    fn test_health_response_serialization() {
317        let response = HealthResponse {
318            status: "healthy".to_string(),
319            timestamp: "2024-01-01T00:00:00Z".to_string(),
320            uptime: std::time::Duration::from_secs(3600),
321            version: "1.0.0".to_string(),
322            environment: "test".to_string(),
323            checks: std::collections::HashMap::new(),
324        };
325
326        // Test serialization
327        let json = serde_json::to_string(&response).unwrap();
328        assert!(json.contains("healthy"));
329        assert!(json.contains("1.0.0"));
330
331        // Test deserialization
332        let deserialized: HealthResponse = serde_json::from_str(&json).unwrap();
333        assert_eq!(deserialized.status, response.status);
334        assert_eq!(deserialized.version, response.version);
335    }
336
337    #[test]
338    fn test_check_response_serialization() {
339        let check = CheckResponse {
340            status: "healthy".to_string(),
341            message: Some("All systems operational".to_string()),
342            duration_ms: 10,
343        };
344
345        // Test serialization
346        let json = serde_json::to_string(&check).unwrap();
347        assert!(json.contains("healthy"));
348        assert!(json.contains("All systems operational"));
349
350        // Test deserialization
351        let deserialized: CheckResponse = serde_json::from_str(&json).unwrap();
352        assert_eq!(deserialized.status, check.status);
353        assert_eq!(deserialized.message, check.message);
354        assert_eq!(deserialized.duration_ms, check.duration_ms);
355    }
356
357    #[test]
358    fn test_health_response_debug_formatting() {
359        let response = HealthResponse {
360            status: "healthy".to_string(),
361            timestamp: "2024-01-01T00:00:00Z".to_string(),
362            uptime: std::time::Duration::from_secs(3600),
363            version: "1.0.0".to_string(),
364            environment: "test".to_string(),
365            checks: std::collections::HashMap::new(),
366        };
367
368        let debug_str = format!("{response:?}");
369        assert!(debug_str.contains("healthy"));
370        assert!(debug_str.contains("1.0.0"));
371    }
372
373    #[test]
374    fn test_check_response_debug_formatting() {
375        let check = CheckResponse {
376            status: "unhealthy".to_string(),
377            message: Some("Connection failed".to_string()),
378            duration_ms: 100,
379        };
380
381        let debug_str = format!("{check:?}");
382        assert!(debug_str.contains("unhealthy"));
383        assert!(debug_str.contains("Connection failed"));
384    }
385
386    #[test]
387    fn test_health_response_clone() {
388        let mut checks = std::collections::HashMap::new();
389        checks.insert(
390            "database".to_string(),
391            CheckResponse {
392                status: "healthy".to_string(),
393                message: Some("OK".to_string()),
394                duration_ms: 5,
395            },
396        );
397
398        let response = HealthResponse {
399            status: "healthy".to_string(),
400            timestamp: "2024-01-01T00:00:00Z".to_string(),
401            uptime: std::time::Duration::from_secs(3600),
402            version: "1.0.0".to_string(),
403            environment: "test".to_string(),
404            checks,
405        };
406
407        let cloned = response.clone();
408        assert_eq!(cloned.status, response.status);
409        assert_eq!(cloned.version, response.version);
410        assert_eq!(cloned.checks.len(), response.checks.len());
411    }
412
413    #[test]
414    fn test_check_response_clone() {
415        let check = CheckResponse {
416            status: "healthy".to_string(),
417            message: Some("OK".to_string()),
418            duration_ms: 5,
419        };
420
421        let cloned = check.clone();
422        assert_eq!(cloned.status, check.status);
423        assert_eq!(cloned.message, check.message);
424        assert_eq!(cloned.duration_ms, check.duration_ms);
425    }
426
427    #[test]
428    fn test_health_response_with_empty_checks() {
429        let response = HealthResponse {
430            status: "healthy".to_string(),
431            timestamp: "2024-01-01T00:00:00Z".to_string(),
432            uptime: std::time::Duration::from_secs(0),
433            version: "0.1.0".to_string(),
434            environment: "development".to_string(),
435            checks: std::collections::HashMap::new(),
436        };
437
438        assert_eq!(response.status, "healthy");
439        assert_eq!(response.uptime.as_secs(), 0);
440        assert_eq!(response.checks.len(), 0);
441    }
442
443    #[test]
444    fn test_health_response_with_multiple_checks() {
445        let mut checks = std::collections::HashMap::new();
446        checks.insert(
447            "database".to_string(),
448            CheckResponse {
449                status: "healthy".to_string(),
450                message: Some("Connection OK".to_string()),
451                duration_ms: 2,
452            },
453        );
454        checks.insert(
455            "redis".to_string(),
456            CheckResponse {
457                status: "healthy".to_string(),
458                message: Some("Cache OK".to_string()),
459                duration_ms: 1,
460            },
461        );
462        checks.insert(
463            "api".to_string(),
464            CheckResponse {
465                status: "unhealthy".to_string(),
466                message: Some("Service down".to_string()),
467                duration_ms: 1000,
468            },
469        );
470
471        let response = HealthResponse {
472            status: "degraded".to_string(),
473            timestamp: "2024-01-01T00:00:00Z".to_string(),
474            uptime: std::time::Duration::from_secs(86400), // 24 hours
475            version: "3.0.0".to_string(),
476            environment: "production".to_string(),
477            checks,
478        };
479
480        assert_eq!(response.status, "degraded");
481        assert_eq!(response.checks.len(), 3);
482        assert_eq!(response.uptime.as_secs(), 86400);
483        assert_eq!(response.environment, "production");
484    }
485}