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)]
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 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 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 let json = serde_json::to_string(&response).unwrap();
328 assert!(json.contains("healthy"));
329 assert!(json.contains("1.0.0"));
330
331 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 let json = serde_json::to_string(&check).unwrap();
347 assert!(json.contains("healthy"));
348 assert!(json.contains("All systems operational"));
349
350 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), 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}