feat: enhance health check with application state and initialization status

This commit is contained in:
GW_MC
2025-12-19 10:24:47 +08:00
parent 66b29b96ee
commit 8111aaf672
10 changed files with 122 additions and 10 deletions

View File

@@ -13,7 +13,7 @@ use axum::{Router, response::IntoResponse, routing::any};
pub fn get_api_router(state: Arc<AppState>) -> Router {
Router::new()
.nest("/health", health::get_health_router())
.nest("/health", health::get_health_router(state.clone()))
.merge(auth::get_basic_auth_router(state.clone()))
.merge(restricted::get_restricted_router(state.clone()))
// explicit fallback for unmatched API routes

View File

@@ -5,8 +5,13 @@ use std::sync::Arc;
use axum::{Router, routing::get};
pub fn get_health_router() -> Router {
use crate::routes::{AppState, api::health::state::AppStateWithHealth};
pub fn get_health_router(app_state: Arc<AppState>) -> Router {
Router::new()
.route("/info", get(info::get_health_info))
.with_state(Arc::new(state::HealthState::default()))
.with_state(Arc::new(AppStateWithHealth {
app_state: app_state.clone(),
health_state: Arc::new(state::HealthState::default()),
}))
}

View File

@@ -3,8 +3,9 @@ use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tracing::error;
use crate::routes::api::{health::state::HealthState, openapi::tag::HEALTH_TAG};
use crate::routes::api::{health::state::AppStateWithHealth, openapi::tag::HEALTH_TAG};
const STATUS_HEALTHY: &str = "healthy";
const STATUS_UNHEALTHY: &str = "unhealthy";
@@ -20,6 +21,8 @@ pub struct HealthInfo {
pub up_since: DateTime<Utc>,
/// List of error messages if unhealthy
pub errors: Option<Vec<String>>,
/// Is initialized
pub is_initialized: bool,
}
/// Health check endpoint
@@ -35,12 +38,23 @@ pub struct HealthInfo {
tag = HEALTH_TAG,
)]
pub async fn get_health_info(
State(state): State<Arc<HealthState>>,
State(app_state_with_health): State<Arc<AppStateWithHealth>>,
) -> (StatusCode, Json<HealthInfo>) {
#[allow(unused_mut)]
let mut errors = vec![];
let is_healthy = errors.is_empty();
let health_state = &app_state_with_health.health_state;
let app_state = &app_state_with_health.app_state;
let is_initialized = match app_state.service.server_state.is_server_initialized().await {
Ok(initialized) => initialized,
Err(err) => {
errors.push("Failed to determine if server is initialized".to_string());
error!("Error checking server initialization status: {}", err);
false
}
};
(
if is_healthy {
@@ -55,14 +69,29 @@ pub async fn get_health_info(
STATUS_UNHEALTHY.into()
},
version: env!("CARGO_PKG_VERSION").into(),
up_since: *state.get_start_at(),
up_since: *health_state.get_start_at(),
errors: if is_healthy { None } else { Some(errors) },
is_initialized,
}),
)
}
#[cfg(test)]
mod test {
use crate::{
routes::{AppState, api::health::state::HealthState},
services::{
auth::{
authentication::{
AuthenticationServiceImpl, strategies::password::PasswordStrategy,
},
user::UserServiceImpl,
},
server_state::ServerStateService,
settings::SettingsService,
},
};
use super::*;
use axum::body::to_bytes;
use axum::{
@@ -70,14 +99,38 @@ mod test {
body::Body,
http::{Request, StatusCode},
};
use sea_orm::MockDatabase;
use tower::ServiceExt;
#[tokio::test]
async fn test_get_health_info() {
let health_state = Arc::new(HealthState::default());
let db = MockDatabase::new(sea_orm::DatabaseBackend::Sqlite)
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
.into_connection();
let db = Arc::new(db);
let app_state = Arc::new(AppState {
database_connection: db.clone(),
service: Arc::new(crate::routes::AppService {
settings: Arc::new(SettingsService::new(db.clone())),
auth_state: crate::routes::AuthState {
strategy: crate::routes::AuthStrategy {
password: Arc::new(PasswordStrategy::new(db.clone())),
},
authentication: Arc::new(AuthenticationServiceImpl::new(None)),
},
user: Arc::new(UserServiceImpl::new(db.clone())),
server_state: Arc::new(ServerStateService::new(db.clone())),
}),
});
let app = Router::new()
.route("/info", axum::routing::get(get_health_info))
.with_state(health_state);
.with_state(Arc::new(AppStateWithHealth {
app_state: app_state.clone(),
health_state: health_state.clone(),
}));
let response = app
.oneshot(Request::builder().uri("/info").body(Body::empty()).unwrap())

View File

@@ -1,5 +1,14 @@
use std::sync::Arc;
use chrono::{DateTime, Utc};
use crate::routes::AppState;
pub struct AppStateWithHealth {
pub app_state: Arc<AppState>,
pub health_state: Arc<HealthState>,
}
pub struct HealthState {
start_at: DateTime<Utc>,
}