Files
foxhunt/services/api/src/health.rs
jgrusewski e50ea55064 feat: create services/api/ — unified gRPC gateway with tonic-web
Copied from api_gateway, removed REST handlers (port 8080),
added tonic-web + CORS for grpc-web browser access.
Binary renamed: api-gateway → api

Changes:
- Package name: api-gateway → api
- Deleted src/handlers/ (REST ML endpoints on port 8080)
- Added tonic-web 0.13 + tower-http CORS layer
- Server::builder().accept_http1(true) for grpc-web
- CORS_ORIGINS env var (default http://localhost:5173)
- Metrics server on port 9091 (axum) preserved
- All 95 lib tests pass, 0 clippy warnings
- Added services/api to workspace members

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:32:46 +01:00

134 lines
3.5 KiB
Rust

//! Health check endpoints for API Gateway service
//!
//! Provides HTTP-based health and readiness endpoints for Kubernetes probes.
//! These endpoints don't require authentication to allow load balancers and
//! orchestrators to check service health.
use axum::{extract::State, http::StatusCode, routing::get, Json, Router};
use serde::Serialize;
use std::sync::Arc;
use sqlx::PgPool;
use redis::aio::ConnectionManager;
/// Health status response
#[derive(Serialize)]
pub struct HealthResponse {
/// Service status
pub status: &'static str,
/// Service name
pub service: &'static str,
/// Service version
pub version: &'static str,
}
/// Detailed health status with dependency checks
#[derive(Serialize)]
pub struct DetailedHealthResponse {
/// Service status
pub status: &'static str,
/// Service name
pub service: &'static str,
/// Service version
pub version: &'static str,
/// Database connectivity
pub database: bool,
/// Redis connectivity
pub redis: bool,
}
/// Shared health check state
#[derive(Clone)]
pub struct HealthState {
/// PostgreSQL connection pool
pub db_pool: Option<PgPool>,
/// Redis connection manager
pub redis: Option<ConnectionManager>,
}
/// Creates the health check router with /health and /ready endpoints
pub fn health_router(state: HealthState) -> Router {
Router::new()
.route("/health", get(health_check))
.route("/ready", get(readiness_check))
.with_state(Arc::new(state))
}
/// Liveness probe - indicates service is running
/// Returns 200 OK if the service process is alive
async fn health_check() -> Json<HealthResponse> {
Json(HealthResponse {
status: "healthy",
service: "api",
version: env!("CARGO_PKG_VERSION"),
})
}
/// Readiness probe - indicates service is ready to accept requests
/// Checks database and Redis connectivity
async fn readiness_check(
State(state): State<Arc<HealthState>>,
) -> Result<Json<DetailedHealthResponse>, StatusCode> {
let mut db_ok = false;
let mut redis_ok = false;
// Check database connectivity
if let Some(ref pool) = state.db_pool {
db_ok = sqlx::query("SELECT 1")
.fetch_one(pool)
.await
.is_ok();
}
// Check Redis connectivity
if let Some(ref mut redis_conn) = state.redis.clone() {
use redis::AsyncCommands;
redis_ok = redis_conn.ping::<()>().await.is_ok();
}
// Service is ready only if all dependencies are healthy
let is_ready = db_ok && redis_ok;
let status = if is_ready { "ready" } else { "not_ready" };
let http_status = if is_ready {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
};
let response = DetailedHealthResponse {
status,
service: "api",
version: env!("CARGO_PKG_VERSION"),
database: db_ok,
redis: redis_ok,
};
if is_ready {
Ok(Json(response))
} else {
Err(http_status)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_health_check() {
let response = health_check().await;
assert_eq!(response.status, "healthy");
assert_eq!(response.service, "api");
}
#[tokio::test]
async fn test_readiness_check_without_deps() {
let state = Arc::new(HealthState {
db_pool: None,
redis: None,
});
let result = readiness_check(State(state)).await;
assert!(result.is_err());
}
}