siderite_core/health/mod.rs
1//! Opt-in health routes: process liveness
2//! (`App::routes([liveness("/livez")])`) and readiness with bounded
3//! application checks ([`readiness`]).
4//!
5//! The route answers `200` with `{"status":"alive"}` and
6//! `Cache-Control: no-store` whenever the server can answer at all, in
7//! every lifecycle phase, and reads nothing else: no database, no
8//! readiness, no version or host details. It is registered only where the
9//! application asks, so it never collides with an application route (a
10//! path used twice is rejected when the app is built, like any duplicate).
11//!
12//! Liveness says the process still serves requests; it is not readiness.
13//! The server binds only after startup completes and stops accepting new
14//! connections when shutdown begins, so a probe gets no connection before
15//! startup and after shutdown starts (requests already on a connection
16//! are still answered while it drains). Size the probe's failure
17//! threshold to cover the shutdown timeout so a draining process is not
18//! restarted.
19
20use crate::responses::WithHeaders;
21use crate::{Json, MethodRouter, Route, get};
22use serde::Serialize;
23use serde_json::json;
24use siderite_validation::{Dump, Schema, SchemaObject, SchemaRegistry};
25
26/// The liveness answer: always `{"status":"alive"}`.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
28pub struct Alive {
29 /// Always `alive`.
30 pub status: &'static str,
31}
32
33impl Alive {
34 /// The one value a liveness route answers with.
35 pub const ALIVE: Self = Self { status: "alive" };
36}
37
38impl Dump for Alive {}
39
40impl Schema for Alive {
41 fn schema_name() -> Option<&'static str> {
42 Some("Alive")
43 }
44
45 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
46 let properties = json!({
47 "status": { "type": "string", "const": "alive" },
48 });
49 SchemaObject::of_type("object")
50 .with("properties", properties)
51 .with("required", json!(["status"]))
52 }
53}
54
55async fn alive() -> WithHeaders<Json<Alive>> {
56 WithHeaders::new(Json(Alive::ALIVE)).header("cache-control", "no-store")
57}
58
59/// The liveness handler, documented under the `health` tag with the
60/// operation ID `liveness`; mount it with [`liveness`] or
61/// `App::route(path, liveness_router())`.
62pub fn liveness_router() -> MethodRouter {
63 get(alive)
64 .operation_id("liveness")
65 .tag("health")
66 .summary("Whether the process is serving requests.")
67 .response_model::<Alive>()
68}
69
70/// A `GET path` liveness route for `App::routes`.
71///
72/// Args:
73/// path: Where to answer, for example `/livez`.
74///
75/// Returns:
76/// The route; nothing is registered until the app adds it.
77pub fn liveness(path: &'static str) -> Route {
78 Route::new(path, liveness_router())
79}
80
81mod ready;
82
83pub use ready::{CheckError, CheckFuture, CheckStatus, ReadyReport};
84pub use ready::{ReadinessChecks, readiness, readiness_router, sql_check};
85
86#[cfg(test)]
87mod tests;
88#[cfg(test)]
89mod tests_ready;