use std::time::Instant; use actix_web::dev::Service; use actix_web::{App, HttpResponse, HttpServer, dev::Server, web}; use cache::AppCache; use db::database::AppDatabase; const REQUEST_LOG_EXCLUDED_PATHS: &[&str] = &[ "/health", "/live", "/ready", "/metrics", "/favicon.ico", "/robots.txt", ]; fn should_log_request(path: &str) -> bool { !REQUEST_LOG_EXCLUDED_PATHS.contains(&path) } async fn health( db: web::Data, cache: web::Data, ) -> HttpResponse { let db_ok = sqlx::query("SELECT 1").execute(db.reader()).await.is_ok(); let cache_ok = cache.ping_cluster().await.is_ok(); if db_ok && cache_ok { HttpResponse::Ok().json(serde_json::json!({ "status": "ok", "db": "ok", "cache": "ok", })) } else { HttpResponse::ServiceUnavailable().json(serde_json::json!({ "status": "unhealthy", "db": if db_ok { "ok" } else { "error" }, "cache": if cache_ok { "ok" } else { "error" }, })) } } pub fn start_health( port: u16, db: AppDatabase, cache: AppCache, ) -> anyhow::Result { tracing::info!("health endpoint starting on 0.0.0.0:{}", port); let srv = HttpServer::new(move || { App::new() .app_data(web::Data::new(db.clone())) .app_data(web::Data::new(cache.clone())) .wrap_fn(|req, srv| { let should_log = should_log_request(req.path()); let method = req.method().clone(); let path = req.path().to_owned(); let peer_addr = req.connection_info().peer_addr().map(str::to_owned); let started_at = Instant::now(); let fut = srv.call(req); async move { match fut.await { Ok(res) => { if should_log { tracing::info!( method = %method, path = %path, status = res.status().as_u16(), elapsed_ms = started_at.elapsed().as_millis(), peer_addr = peer_addr.as_deref().unwrap_or("-"), "http request" ); } Ok(res) } Err(err) => { if should_log { tracing::warn!( method = %method, path = %path, elapsed_ms = started_at.elapsed().as_millis(), peer_addr = peer_addr.as_deref().unwrap_or("-"), error = %err, "http request failed" ); } Err(err) } } } }) .route("/health", web::get().to(health)) }) .bind(format!("0.0.0.0:{}", port))?; Ok(srv.run()) }