Compare commits
4 Commits
968911e489
...
efdb47a117
| Author | SHA1 | Date | |
|---|---|---|---|
| efdb47a117 | |||
|
|
5210c64c5d | ||
|
|
23c6bc4fd0 | ||
|
|
bbc6977e73 |
@@ -1,7 +1,10 @@
|
||||
mod health;
|
||||
|
||||
use axum::{Router, response::IntoResponse, routing::any};
|
||||
|
||||
pub fn get_api_router() -> Router {
|
||||
Router::new()
|
||||
.nest("/health", health::get_health_router())
|
||||
// explicit fallback for unmatched API routes
|
||||
.route("/{*wildcard}", any(api_fallback_handler))
|
||||
}
|
||||
|
||||
12
apps/api/src/routes/api/health.rs
Normal file
12
apps/api/src/routes/api/health.rs
Normal 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()))
|
||||
}
|
||||
78
apps/api/src/routes/api/health/info.rs
Normal file
78
apps/api/src/routes/api/health/info.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
19
apps/api/src/routes/api/health/state.rs
Normal file
19
apps/api/src/routes/api/health/state.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user