Add health check routes and state management
Some checks failed
Test / verify-generated-code (pull_request) Successful in 56s
Test / test-frontend (pull_request) Successful in 22s
Test / frontend-build (pull_request) Successful in 24s
Test / test (pull_request) Successful in 1m14s
Test / lint (pull_request) Failing after 1m13s

This commit is contained in:
GW_MC
2025-12-05 14:05:09 +08:00
parent 968911e489
commit bbc6977e73
4 changed files with 146 additions and 0 deletions

View File

@@ -1,7 +1,10 @@
mod health;
use axum::{Router, response::IntoResponse, routing::any};
pub fn get_api_router() -> Router {
Router::new()
.nest("/health", health::get_health_router())
// explicit fallback for unmatched API routes
.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,108 @@
use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode};
use chrono::{Duration, Utc};
use serde::{Deserialize, Serialize};
use tokio::time::Instant;
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,
// timestamp in milliseconds when the service started
pub up_since: String,
pub errors: Option<Vec<String>>,
}
pub async fn get_health_info(
State(state): State<Arc<HealthState>>,
) -> (StatusCode, Json<HealthInfo>) {
let mut errors = vec![];
let up_since = if *state.get_start_time() > Instant::now() {
errors.push("Invalid up time".into());
// Fall back to reporting 'now' as the up_since timestamp when invalid.
Utc::now()
} else {
let elapsed = state.get_start_time().elapsed();
Utc::now()
- Duration::from_std(elapsed).unwrap_or_else(|_| {
errors.push("Invalid up time".into());
Duration::zero()
})
};
(
StatusCode::OK,
Json(HealthInfo {
status: if errors.is_empty() {
STATUS_HEALTHY.into()
} else {
STATUS_UNHEALTHY.into()
},
version: env!("CARGO_PKG_VERSION").into(),
up_since: up_since.to_rfc3339(),
errors: if errors.is_empty() {
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());
}
#[tokio::test]
async fn test_invalid_up_time() {
let health_state = Arc::new(HealthState::new(
tokio::time::Instant::now() + std::time::Duration::from_secs(60000),
));
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_UNHEALTHY);
assert_eq!(health_info.version, env!("CARGO_PKG_VERSION"));
assert!(health_info.errors.as_ref().is_some_and(|e| !e.is_empty()));
}
}

View File

@@ -0,0 +1,23 @@
use tokio::time::Instant;
pub struct HealthState {
start_time: Instant,
}
impl Default for HealthState {
fn default() -> Self {
Self {
start_time: Instant::now(),
}
}
}
impl HealthState {
pub fn new(start_time: Instant) -> Self {
Self { start_time }
}
pub fn get_start_time(&self) -> &Instant {
&self.start_time
}
}