use async_trait::async_trait;
use serde::Serialize;
#[derive(Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Up,
Degraded,
Down,
}
#[derive(Serialize)]
pub struct Component {
pub name: &'static str,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[async_trait]
pub trait HealthCheck: Send + Sync {
fn name(&self) -> &'static str;
async fn check(&self) -> (Status, Option<String>);
}
pub struct PgCheck {
pub pool: sqlx::PgPool,
}
#[async_trait]
impl HealthCheck for PgCheck {
fn name(&self) -> &'static str {
"postgres"
}
async fn check(&self) -> (Status, Option<String>) {
match sqlx::query("SELECT 1").execute(&self.pool).await {
Ok(_) => (Status::Up, None),
Err(e) => (Status::Down, Some(e.to_string())),
}
}
}
use std::sync::Arc;
use std::time::Duration;
use futures::future::join_all;
use serde::Serialize;
use tokio::time::timeout;
use crate::health::{Component, HealthCheck, Status};
#[derive(Clone)]
pub struct HealthRegistry {
checks: Vec<Arc<dyn HealthCheck>>,
budget: Duration,
}
#[derive(Serialize)]
pub struct HealthReport {
pub status: Status,
pub components: Vec<Component>,
}
impl HealthRegistry {
pub fn new(budget: Duration) -> Self {
Self { checks: Vec::new(), budget }
}
pub fn register(mut self, check: Arc<dyn HealthCheck>) -> Self {
self.checks.push(check);
self
}
pub async fn check_all(&self) -> HealthReport {
let probes = self.checks.iter().map(|c| {
let budget = self.budget;
async move {
let (status, detail) = timeout(budget, c.check())
.await
.unwrap_or((Status::Down, Some("timeout".into())));
Component { name: c.name(), status, detail }
}
});
let components = join_all(probes).await;
let status = components.iter().fold(Status::Up, |acc, c| match c.status {
Status::Down => Status::Down,
Status::Degraded if acc != Status::Down => Status::Degraded,
_ => acc,
});
HealthReport { status, components }
}
}
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json};
use axum::routing::get;
use axum::Router;
use crate::health::Status;
use crate::registry::HealthRegistry;
pub fn router(registry: HealthRegistry) -> Router {
Router::new()
.route("/healthz", get(health_handler))
.with_state(registry)
}
async fn health_handler(State(registry): State<HealthRegistry>) -> impl IntoResponse {
let report = registry.check_all().await;
let code = match report.status {
Status::Up | Status::Degraded => StatusCode::OK,
Status::Down => StatusCode::SERVICE_UNAVAILABLE,
};
(code, Json(report))
}
This snippet shows how to build a /healthz endpoint that reports the health of several independent subsystems by probing each one concurrently and merging the results into a single response. The pattern is common in microservices where an orchestrator (Kubernetes, a load balancer) needs one endpoint that answers "is this instance actually serving?" while the service itself may depend on a database, a cache, and downstream HTTP APIs.
The HealthCheck trait defines the contract every probe implements: a stable name for reporting and an async check that returns a Status. Modeling checks as trait objects (Arc<dyn HealthCheck>) keeps the aggregator agnostic to what each subsystem actually does — a Postgres ping and an HTTP HEAD request look identical to the caller. The Status enum distinguishes Up, Degraded, and Down so partial failures don't get flattened into a boolean.
PgCheck is a concrete probe. Notice it doesn't manage its own timeout; instead each probe is expected to be simple and let the aggregator enforce time bounds. This avoids every author reinventing timeout logic and keeps a slow dependency from stalling the whole endpoint.
In HealthRegistry, check_all is the core of the design. It wraps every probe in tokio::time::timeout and drives them with join_all, so all subsystems are queried in parallel and the total latency is bounded by the slowest survivor rather than the sum. A probe that exceeds the deadline is coerced into Status::Down with a timeout reason via unwrap_or, which means a hung dependency degrades the report instead of hanging the request. The overall status is derived by folding component statuses: any Down makes the aggregate Down, any Degraded makes it Degraded, otherwise Up.
health_handler in routes.rs maps that aggregate onto an HTTP status code — 200 for healthy or degraded, 503 when down — so a load balancer can act on the code alone while richer detail rides along in the JSON body. Returning Degraded as 200 is a deliberate trade-off: the instance is imperfect but still worth routing traffic to. This structure scales cleanly — adding a subsystem is just pushing another Arc<dyn HealthCheck> into the registry.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
Share this code
Here's the card — post it anywhere.