Add health check routes and state management #6

Merged
GW_MC merged 3 commits from feature/health-check into master 2025-12-05 15:15:33 +08:00
4 changed files with 112 additions and 0 deletions

View File

@@ -1,7 +1,10 @@
mod health;
use axum::{Router, response::IntoResponse, routing::any}; use axum::{Router, response::IntoResponse, routing::any};
pub fn get_api_router() -> Router { pub fn get_api_router() -> Router {
Router::new() Router::new()
.nest("/health", health::get_health_router())
// explicit fallback for unmatched API routes // explicit fallback for unmatched API routes
.route("/{*wildcard}", any(api_fallback_handler)) .route("/{*wildcard}", any(api_fallback_handler))
} }

View File

@@ -0,0 +1,12 @@
mod info;
mod state;
use std::sync::Arc;
use axum::{Router, routing::get};
pub fn get_health_router() -> Router {
Router::new()
.route("/info", get(info::get_health_info))
.with_state(Arc::new(state::HealthState::default()))
}

View File

@@ -0,0 +1,78 @@
use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::routes::api::health::state::HealthState;
const STATUS_HEALTHY: &str = "healthy";
const STATUS_UNHEALTHY: &str = "unhealthy";
#[derive(Serialize, Deserialize)]
pub struct HealthInfo {
pub status: String,
pub version: String,
// RFC 3339 formatted timestamp
pub up_since: DateTime<Utc>,
pub errors: Option<Vec<String>>,
}
pub async fn get_health_info(
State(state): State<Arc<HealthState>>,
) -> (StatusCode, Json<HealthInfo>) {
#[allow(unused_mut)]
let mut errors = vec![];
let is_healthy = errors.is_empty();
(
if is_healthy {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
},
Json(HealthInfo {
status: if is_healthy {
STATUS_HEALTHY.into()
} else {
STATUS_UNHEALTHY.into()
},
version: env!("CARGO_PKG_VERSION").into(),
up_since: *state.get_start_at(),
errors: if is_healthy { None } else { Some(errors) },
}),
)
}
#[cfg(test)]
mod test {
use super::*;
use axum::body::to_bytes;
use axum::{
Router,
body::Body,
http::{Request, StatusCode},
};
use tower::ServiceExt;
#[tokio::test]
async fn test_get_health_info() {
let health_state = Arc::new(HealthState::default());
let app = Router::new()
.route("/info", axum::routing::get(get_health_info))
.with_state(health_state);
let response = app
.oneshot(Request::builder().uri("/info").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap(); // Set limit to 1 MB
let health_info: HealthInfo = serde_json::from_slice(&body).unwrap();
assert_eq!(health_info.status, STATUS_HEALTHY);
assert_eq!(health_info.version, env!("CARGO_PKG_VERSION"));
assert!(health_info.errors.is_none());
}
}

View File

@@ -0,0 +1,19 @@
use chrono::{DateTime, Utc};
pub struct HealthState {
start_at: DateTime<Utc>,
}
impl Default for HealthState {
fn default() -> Self {
Self {
start_at: Utc::now(),
}
}
}
impl HealthState {
pub fn get_start_at(&self) -> &DateTime<Utc> {
&self.start_at
}
}