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#[derive(Clone)]
77pub struct AppState {
78 pub observability: Arc<ObservabilityManager>,
79 pub database: Arc<ThingsDatabase>,
80}
81
82#[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 #[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 #[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
142pub struct HealthServer {
144 port: u16,
145 observability: Arc<ObservabilityManager>,
146 database: Arc<ThingsDatabase>,
147}
148
149#[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)]
164#[allow(deprecated)]
165mod tests {
166 use super::*;
167 use tempfile::NamedTempFile;
168
169 #[test]
170 fn test_health_server_creation() {
171 let temp_file = NamedTempFile::new().unwrap();
172 let db_path = temp_file.path();
173
174 let config = things3_core::ThingsConfig::new(db_path, false);
175 let rt = tokio::runtime::Runtime::new().unwrap();
176 let database = Arc::new(
177 rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
178 );
179
180 let observability = Arc::new(
181 things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
182 .unwrap(),
183 );
184 let server = HealthServer::new(8080, observability, database);
185 assert_eq!(server.port, 8080);
186 }
187
188 #[test]
189 fn test_health_response() {
190 let response = HealthResponse {
191 status: "healthy".to_string(),
192 timestamp: "2024-01-01T00:00:00Z".to_string(),
193 uptime: std::time::Duration::from_secs(3600),
194 version: "1.0.0".to_string(),
195 environment: "test".to_string(),
196 checks: std::collections::HashMap::new(),
197 };
198
199 assert_eq!(response.status, "healthy");
200 assert_eq!(response.version, "1.0.0");
201 }
202
203 #[test]
204 fn test_health_response_with_checks() {
205 let mut checks = std::collections::HashMap::new();
206 checks.insert(
207 "database".to_string(),
208 CheckResponse {
209 status: "healthy".to_string(),
210 message: Some("Connection successful".to_string()),
211 duration_ms: 5,
212 },
213 );
214 checks.insert(
215 "cache".to_string(),
216 CheckResponse {
217 status: "unhealthy".to_string(),
218 message: Some("Connection failed".to_string()),
219 duration_ms: 100,
220 },
221 );
222
223 let response = HealthResponse {
224 status: "degraded".to_string(),
225 timestamp: "2024-01-01T00:00:00Z".to_string(),
226 uptime: std::time::Duration::from_secs(7200),
227 version: "2.0.0".to_string(),
228 environment: "staging".to_string(),
229 checks,
230 };
231
232 assert_eq!(response.status, "degraded");
233 assert_eq!(response.version, "2.0.0");
234 assert_eq!(response.environment, "staging");
235 assert_eq!(response.checks.len(), 2);
236 assert_eq!(response.uptime.as_secs(), 7200);
237 }
238
239 #[test]
240 fn test_check_response() {
241 let check = CheckResponse {
242 status: "healthy".to_string(),
243 message: Some("All systems operational".to_string()),
244 duration_ms: 10,
245 };
246
247 assert_eq!(check.status, "healthy");
248 assert_eq!(check.message, Some("All systems operational".to_string()));
249 assert_eq!(check.duration_ms, 10);
250 }
251
252 #[test]
253 fn test_check_response_without_message() {
254 let check = CheckResponse {
255 status: "unhealthy".to_string(),
256 message: None,
257 duration_ms: 500,
258 };
259
260 assert_eq!(check.status, "unhealthy");
261 assert_eq!(check.message, None);
262 assert_eq!(check.duration_ms, 500);
263 }
264
265 #[test]
266 fn test_app_state_creation() {
267 let temp_file = NamedTempFile::new().unwrap();
268 let db_path = temp_file.path();
269
270 let config = things3_core::ThingsConfig::new(db_path, false);
271 let rt = tokio::runtime::Runtime::new().unwrap();
272 let database = Arc::new(
273 rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
274 );
275
276 let observability = Arc::new(
277 things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
278 .unwrap(),
279 );
280
281 let state = AppState {
282 observability: Arc::clone(&observability),
283 database: Arc::clone(&database),
284 };
285
286 let _cloned_state = state.clone();
288 }
289
290 #[test]
291 fn test_health_server_with_different_ports() {
292 let temp_file = NamedTempFile::new().unwrap();
293 let db_path = temp_file.path();
294
295 let config = things3_core::ThingsConfig::new(db_path, false);
296 let rt = tokio::runtime::Runtime::new().unwrap();
297 let database = Arc::new(
298 rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
299 );
300
301 let observability = Arc::new(
302 things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
303 .unwrap(),
304 );
305
306 let server1 = HealthServer::new(8080, Arc::clone(&observability), Arc::clone(&database));
308 let server2 = HealthServer::new(9090, Arc::clone(&observability), Arc::clone(&database));
309 let server3 = HealthServer::new(3000, Arc::clone(&observability), Arc::clone(&database));
310
311 assert_eq!(server1.port, 8080);
312 assert_eq!(server2.port, 9090);
313 assert_eq!(server3.port, 3000);
314 }
315
316 #[test]
317 fn test_health_response_serialization() {
318 let response = HealthResponse {
319 status: "healthy".to_string(),
320 timestamp: "2024-01-01T00:00:00Z".to_string(),
321 uptime: std::time::Duration::from_secs(3600),
322 version: "1.0.0".to_string(),
323 environment: "test".to_string(),
324 checks: std::collections::HashMap::new(),
325 };
326
327 let json = serde_json::to_string(&response).unwrap();
329 assert!(json.contains("healthy"));
330 assert!(json.contains("1.0.0"));
331
332 let deserialized: HealthResponse = serde_json::from_str(&json).unwrap();
334 assert_eq!(deserialized.status, response.status);
335 assert_eq!(deserialized.version, response.version);
336 }
337
338 #[test]
339 fn test_check_response_serialization() {
340 let check = CheckResponse {
341 status: "healthy".to_string(),
342 message: Some("All systems operational".to_string()),
343 duration_ms: 10,
344 };
345
346 let json = serde_json::to_string(&check).unwrap();
348 assert!(json.contains("healthy"));
349 assert!(json.contains("All systems operational"));
350
351 let deserialized: CheckResponse = serde_json::from_str(&json).unwrap();
353 assert_eq!(deserialized.status, check.status);
354 assert_eq!(deserialized.message, check.message);
355 assert_eq!(deserialized.duration_ms, check.duration_ms);
356 }
357
358 #[test]
359 fn test_health_response_debug_formatting() {
360 let response = HealthResponse {
361 status: "healthy".to_string(),
362 timestamp: "2024-01-01T00:00:00Z".to_string(),
363 uptime: std::time::Duration::from_secs(3600),
364 version: "1.0.0".to_string(),
365 environment: "test".to_string(),
366 checks: std::collections::HashMap::new(),
367 };
368
369 let debug_str = format!("{response:?}");
370 assert!(debug_str.contains("healthy"));
371 assert!(debug_str.contains("1.0.0"));
372 }
373
374 #[test]
375 fn test_check_response_debug_formatting() {
376 let check = CheckResponse {
377 status: "unhealthy".to_string(),
378 message: Some("Connection failed".to_string()),
379 duration_ms: 100,
380 };
381
382 let debug_str = format!("{check:?}");
383 assert!(debug_str.contains("unhealthy"));
384 assert!(debug_str.contains("Connection failed"));
385 }
386
387 #[test]
388 fn test_health_response_clone() {
389 let mut checks = std::collections::HashMap::new();
390 checks.insert(
391 "database".to_string(),
392 CheckResponse {
393 status: "healthy".to_string(),
394 message: Some("OK".to_string()),
395 duration_ms: 5,
396 },
397 );
398
399 let response = HealthResponse {
400 status: "healthy".to_string(),
401 timestamp: "2024-01-01T00:00:00Z".to_string(),
402 uptime: std::time::Duration::from_secs(3600),
403 version: "1.0.0".to_string(),
404 environment: "test".to_string(),
405 checks,
406 };
407
408 let cloned = response.clone();
409 assert_eq!(cloned.status, response.status);
410 assert_eq!(cloned.version, response.version);
411 assert_eq!(cloned.checks.len(), response.checks.len());
412 }
413
414 #[test]
415 fn test_check_response_clone() {
416 let check = CheckResponse {
417 status: "healthy".to_string(),
418 message: Some("OK".to_string()),
419 duration_ms: 5,
420 };
421
422 let cloned = check.clone();
423 assert_eq!(cloned.status, check.status);
424 assert_eq!(cloned.message, check.message);
425 assert_eq!(cloned.duration_ms, check.duration_ms);
426 }
427
428 #[test]
429 fn test_health_response_with_empty_checks() {
430 let response = HealthResponse {
431 status: "healthy".to_string(),
432 timestamp: "2024-01-01T00:00:00Z".to_string(),
433 uptime: std::time::Duration::from_secs(0),
434 version: "0.1.0".to_string(),
435 environment: "development".to_string(),
436 checks: std::collections::HashMap::new(),
437 };
438
439 assert_eq!(response.status, "healthy");
440 assert_eq!(response.uptime.as_secs(), 0);
441 assert_eq!(response.checks.len(), 0);
442 }
443
444 #[test]
445 fn test_health_response_with_multiple_checks() {
446 let mut checks = std::collections::HashMap::new();
447 checks.insert(
448 "database".to_string(),
449 CheckResponse {
450 status: "healthy".to_string(),
451 message: Some("Connection OK".to_string()),
452 duration_ms: 2,
453 },
454 );
455 checks.insert(
456 "redis".to_string(),
457 CheckResponse {
458 status: "healthy".to_string(),
459 message: Some("Cache OK".to_string()),
460 duration_ms: 1,
461 },
462 );
463 checks.insert(
464 "api".to_string(),
465 CheckResponse {
466 status: "unhealthy".to_string(),
467 message: Some("Service down".to_string()),
468 duration_ms: 1000,
469 },
470 );
471
472 let response = HealthResponse {
473 status: "degraded".to_string(),
474 timestamp: "2024-01-01T00:00:00Z".to_string(),
475 uptime: std::time::Duration::from_secs(86400), version: "3.0.0".to_string(),
477 environment: "production".to_string(),
478 checks,
479 };
480
481 assert_eq!(response.status, "degraded");
482 assert_eq!(response.checks.len(), 3);
483 assert_eq!(response.uptime.as_secs(), 86400);
484 assert_eq!(response.environment, "production");
485 }
486}