4 Commits

Author SHA1 Message Date
efdb47a117 Merge pull request 'Add health check routes and state management' (#6) from feature/health-check into master
All checks were successful
Test / verify-generated-code (push) Successful in 53s
Test / test-frontend (push) Successful in 19s
Test / frontend-build (push) Successful in 23s
Test / test (push) Successful in 1m12s
Test / lint (push) Successful in 1m12s
Reviewed-on: #6
2025-12-05 15:15:29 +08:00
GW_MC
5210c64c5d 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
2025-12-05 15:08:21 +08:00
GW_MC
23c6bc4fd0 Add #[allow(dead_code)] annotation to HealthState::new for test usage
All checks were successful
Test / verify-generated-code (pull_request) Successful in 55s
Test / test-frontend (pull_request) Successful in 20s
Test / frontend-build (pull_request) Successful in 23s
Test / test (pull_request) Successful in 1m13s
Test / lint (pull_request) Successful in 1m15s
2025-12-05 14:20:56 +08:00
GW_MC
bbc6977e73 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
2025-12-05 14:05:09 +08:00
4 changed files with 112 additions and 0 deletions

View File

@@ -1,7 +1,10 @@
mod health;
use axum::{Router, response::IntoResponse, routing::any}; use axum::{Router, response::IntoResponse, routing::any};
pub fn get_api_router() -> Router { pub fn get_api_router() -> Router {
Router::new() Router::new()
.nest("/health", health::get_health_router())
// explicit fallback for unmatched API routes // explicit fallback for unmatched API routes
.route("/{*wildcard}", any(api_fallback_handler)) .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,78 @@
use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
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,
// RFC 3339 formatted timestamp
pub up_since: DateTime<Utc>,
pub errors: Option<Vec<String>>,
}
pub async fn get_health_info(
State(state): State<Arc<HealthState>>,
) -> (StatusCode, Json<HealthInfo>) {
#[allow(unused_mut)]
let mut errors = vec![];
let is_healthy = errors.is_empty();
(
if is_healthy {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
},
Json(HealthInfo {
status: if is_healthy {
STATUS_HEALTHY.into()
} else {
STATUS_UNHEALTHY.into()
},
version: env!("CARGO_PKG_VERSION").into(),
up_since: *state.get_start_at(),
errors: if is_healthy { 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());
}
}

View File

@@ -0,0 +1,19 @@
use chrono::{DateTime, Utc};
pub struct HealthState {
start_at: DateTime<Utc>,
}
impl Default for HealthState {
fn default() -> Self {
Self {
start_at: Utc::now(),
}
}
}
impl HealthState {
pub fn get_start_at(&self) -> &DateTime<Utc> {
&self.start_at
}
}