Simplify health endpoint
All checks were successful
Test / verify-generated-code (pull_request) Successful in 54s
Test / test-frontend (pull_request) Successful in 22s
Test / frontend-build (pull_request) Successful in 25s
Test / test (pull_request) Successful in 1m12s
Test / lint (pull_request) Successful in 1m13s

This commit is contained in:
GW_MC
2025-12-05 15:08:21 +08:00
parent 23c6bc4fd0
commit 5210c64c5d
2 changed files with 19 additions and 54 deletions

View File

@@ -1,9 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode}; use axum::{Json, extract::State, http::StatusCode};
use chrono::{Duration, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::time::Instant;
use crate::routes::api::health::state::HealthState; use crate::routes::api::health::state::HealthState;
@@ -14,43 +13,34 @@ const STATUS_UNHEALTHY: &str = "unhealthy";
pub struct HealthInfo { pub struct HealthInfo {
pub status: String, pub status: String,
pub version: String, pub version: String,
// timestamp in milliseconds when the service started // RFC 3339 formatted timestamp
pub up_since: String, pub up_since: DateTime<Utc>,
pub errors: Option<Vec<String>>, pub errors: Option<Vec<String>>,
} }
pub async fn get_health_info( pub async fn get_health_info(
State(state): State<Arc<HealthState>>, State(state): State<Arc<HealthState>>,
) -> (StatusCode, Json<HealthInfo>) { ) -> (StatusCode, Json<HealthInfo>) {
#[allow(unused_mut)]
let mut errors = vec![]; let mut errors = vec![];
let up_since = if *state.get_start_time() > Instant::now() {
errors.push("Invalid up time".into()); let is_healthy = errors.is_empty();
// 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, if is_healthy {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
},
Json(HealthInfo { Json(HealthInfo {
status: if errors.is_empty() { status: if is_healthy {
STATUS_HEALTHY.into() STATUS_HEALTHY.into()
} else { } else {
STATUS_UNHEALTHY.into() STATUS_UNHEALTHY.into()
}, },
version: env!("CARGO_PKG_VERSION").into(), version: env!("CARGO_PKG_VERSION").into(),
up_since: up_since.to_rfc3339(), up_since: *state.get_start_at(),
errors: if errors.is_empty() { errors: if is_healthy { None } else { Some(errors) },
None
} else {
Some(errors)
},
}), }),
) )
} }
@@ -85,24 +75,4 @@ mod test {
assert_eq!(health_info.version, env!("CARGO_PKG_VERSION")); assert_eq!(health_info.version, env!("CARGO_PKG_VERSION"));
assert!(health_info.errors.is_none()); 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

@@ -1,24 +1,19 @@
use tokio::time::Instant; use chrono::{DateTime, Utc};
pub struct HealthState { pub struct HealthState {
start_time: Instant, start_at: DateTime<Utc>,
} }
impl Default for HealthState { impl Default for HealthState {
fn default() -> Self { fn default() -> Self {
Self { Self {
start_time: Instant::now(), start_at: Utc::now(),
} }
} }
} }
impl HealthState { impl HealthState {
#[allow(dead_code)] // used in tests pub fn get_start_at(&self) -> &DateTime<Utc> {
pub fn new(start_time: Instant) -> Self { &self.start_at
Self { start_time }
}
pub fn get_start_time(&self) -> &Instant {
&self.start_time
} }
} }