use std::{
ops::Deref,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use axum::{
error_handling::HandleErrorLayer,
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{self, Router},
};
use serde::{Deserialize, Serialize};
use tower::ServiceBuilder;
use tracing::{debug_span, info};
use crate::{
auth::{AuthExtractor, AuthLayer, AuthProvider},
builder::app::error_handler,
watchdog::{Watchdog, WatchdogConfig},
};
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[non_exhaustive]
pub struct ProbeConfig {
#[serde(default = "ProbeConfig::default_readiness_path")]
readiness_path: String,
#[serde(default = "ProbeConfig::default_liveness_path")]
liveness_path: String,
#[serde(default = "ProbeConfig::default_maintenance_on_path")]
maintenance_on_path: String,
#[serde(default = "ProbeConfig::default_maintenance_off_path")]
maintenance_off_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
watchdog: Option<WatchdogConfig>,
#[serde(default)]
start_in_maintenance: bool,
}
impl Default for ProbeConfig {
fn default() -> Self {
Self {
readiness_path: Self::default_readiness_path(),
liveness_path: Self::default_liveness_path(),
maintenance_on_path: Self::default_maintenance_on_path(),
maintenance_off_path: Self::default_maintenance_off_path(),
watchdog: Some(WatchdogConfig::default()),
start_in_maintenance: false,
}
}
}
impl ProbeConfig {
#[must_use]
#[inline]
fn default_readiness_path() -> String {
"/probe/ready".into()
}
#[must_use]
#[inline]
fn default_liveness_path() -> String {
"/probe/live".into()
}
#[must_use]
#[inline]
fn default_maintenance_on_path() -> String {
"/maintenance/on".into()
}
#[must_use]
#[inline]
fn default_maintenance_off_path() -> String {
"/maintenance/off".into()
}
pub fn build_router(
&self,
auth_provider: Box<dyn AuthProvider>,
auth_extractor: Box<dyn AuthExtractor>,
) -> Router {
let _span = debug_span!("build_probes").entered();
let state = ProbeState::new(self.start_in_maintenance, self.watchdog.as_ref());
Router::new()
.route(&self.readiness_path, routing::get(readiness_probe))
.route(&self.liveness_path, routing::get(liveness_probe))
.merge(
Router::new()
.route(&self.maintenance_on_path, routing::post(maintenance_on))
.route(&self.maintenance_off_path, routing::post(maintenance_off))
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(error_handler))
.layer(AuthLayer::new(
&["maintenance"],
auth_provider,
auth_extractor,
)),
),
)
.with_state(state)
}
}
#[derive(Clone)]
pub struct ProbeState(Arc<ProbeStateInner>);
impl Deref for ProbeState {
type Target = ProbeStateInner;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Default for ProbeState {
fn default() -> Self {
Self(Arc::new(ProbeStateInner {
in_maintenance: AtomicBool::new(true),
watchdog: None,
}))
}
}
impl ProbeState {
#[must_use]
pub fn new(in_maint: bool, watchdog: Option<&WatchdogConfig>) -> Self {
Self(Arc::new(ProbeStateInner {
in_maintenance: AtomicBool::new(in_maint),
watchdog: watchdog.map(|wc| {
let mut watchdog: Watchdog = wc.clone().into();
watchdog.start();
watchdog
}),
}))
}
}
pub struct ProbeStateInner {
in_maintenance: AtomicBool,
watchdog: Option<Watchdog>,
}
async fn readiness_probe(state: State<ProbeState>) -> impl IntoResponse {
match state.in_maintenance.load(Ordering::Relaxed) {
true => StatusCode::SERVICE_UNAVAILABLE,
false => StatusCode::OK,
}
}
async fn liveness_probe(state: State<ProbeState>) -> impl IntoResponse {
match &state.watchdog {
Some(watchdog) => match watchdog.is_alive() {
true => StatusCode::OK,
false => StatusCode::SERVICE_UNAVAILABLE,
},
None => StatusCode::OK,
}
}
async fn maintenance_on(state: State<ProbeState>) -> impl IntoResponse {
if !state.in_maintenance.swap(true, Ordering::Relaxed) {
info!("maintenance mode enabled");
}
StatusCode::OK
}
async fn maintenance_off(state: State<ProbeState>) -> impl IntoResponse {
if state.in_maintenance.swap(false, Ordering::Relaxed) {
info!("maintenance mode disabled");
}
StatusCode::OK
}