feat(server): 健康检查增强 — 新增 /health/ready 就绪检查
- 保留 /health 轻量存活检查 - 新增 /health/ready 含 DB ping + Redis ping 并行检测 - 返回 status(ok/degraded/unavailable) + 各组件延迟和错误信息
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user