feat(server): 健康检查增强 — 新增 /health/ready 就绪检查
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 保留 /health 轻量存活检查
- 新增 /health/ready 含 DB ping + Redis ping 并行检测
- 返回 status(ok/degraded/unavailable) + 各组件延迟和错误信息
This commit is contained in:
iven
2026-04-27 12:54:16 +08:00
parent a2c1b5ece8
commit a4daa8f49c

View File

@@ -13,9 +13,7 @@ pub struct HealthResponse {
pub modules: Vec<String>,
}
/// GET /health
///
/// 服务健康检查,返回运行状态和已注册模块列表
/// GET /health — 轻量存活检查
pub async fn health_check(State(state): State<AppState>) -> Json<HealthResponse> {
let modules = state
.module_registry
@@ -31,6 +29,106 @@ pub async fn health_check(State(state): State<AppState>) -> Json<HealthResponse>
})
}
pub fn health_check_router() -> Router<AppState> {
Router::new().route("/health", get(health_check))
#[derive(Debug, Serialize)]
pub struct ReadyResponse {
pub status: String,
pub version: String,
pub database: ComponentStatus,
pub redis: ComponentStatus,
pub modules: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct ComponentStatus {
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub latency_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
/// GET /health/ready — 就绪检查(含 DB + Redis 连通性)
pub async fn readiness_check(State(state): State<AppState>) -> Json<ReadyResponse> {
let modules = state
.module_registry
.modules()
.iter()
.map(|m| m.name().to_string())
.collect();
let (db_status, redis_status) = tokio::join!(
check_database(&state.db),
check_redis(&state.redis),
);
let overall = if db_status.status == "ok" && redis_status.status == "ok" {
"ok"
} else if db_status.status == "ok" {
"degraded"
} else {
"unavailable"
};
Json(ReadyResponse {
status: overall.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
database: db_status,
redis: redis_status,
modules,
})
}
async fn check_database(db: &sea_orm::DatabaseConnection) -> ComponentStatus {
use sea_orm::ConnectionTrait;
let start = std::time::Instant::now();
let stmt = sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"SELECT 1".to_string(),
);
match db.query_one(stmt).await {
Ok(_) => ComponentStatus {
status: "ok".to_string(),
latency_ms: Some(start.elapsed().as_millis() as u64),
error: None,
},
Err(e) => ComponentStatus {
status: "error".to_string(),
latency_ms: Some(start.elapsed().as_millis() as u64),
error: Some(e.to_string()),
},
}
}
async fn check_redis(client: &redis::Client) -> ComponentStatus {
let start = std::time::Instant::now();
match client.get_multiplexed_async_connection().await {
Ok(mut conn) => {
match redis::cmd("PING")
.query_async::<String>(&mut conn)
.await
{
Ok(_) => ComponentStatus {
status: "ok".to_string(),
latency_ms: Some(start.elapsed().as_millis() as u64),
error: None,
},
Err(e) => ComponentStatus {
status: "error".to_string(),
latency_ms: Some(start.elapsed().as_millis() as u64),
error: Some(e.to_string()),
},
}
}
Err(e) => ComponentStatus {
status: "error".to_string(),
latency_ms: Some(start.elapsed().as_millis() as u64),
error: Some(e.to_string()),
},
}
}
pub fn health_check_router() -> Router<AppState> {
Router::new()
.route("/health", get(health_check))
.route("/health/ready", get(readiness_check))
}