Cookbook / Production

Production setup

Logging with request IDs, metrics, health checks, limits, timeouts and graceful shutdown.

Everything you need to run behind a load balancer: structured logs with a request ID on every line, Prometheus metrics, liveness and readiness endpoints, and a shutdown sequence that never drops in-flight requests.

src/main.rs
use axeon::health::Health;
use axeon::metrics::Metrics;
use axeon::middleware::{Compression, RateLimiter, RequestLogger, SecurityHeaders, SetRequestId};
use axeon::{json, Json, Server};
use std::time::Duration;

fn main() {
    // Use .json() on the builder for JSON logs in production.
    tracing_subscriber::fmt().with_target(false).init();

    let metrics = Metrics::new();
    let mut app = Server::new();
    app.max_connections(10_000)
        .keep_alive(Duration::from_secs(15))
        .request_timeout(Duration::from_secs(10))
        .shutdown_delay(Duration::from_secs(5))    // report not-ready, keep serving
        .shutdown_timeout(Duration::from_secs(20)) // then finish in-flight requests
        .max_body_size(1024 * 1024);

    // Order matters: the request ID is set before the logger reads it.
    app.middleware(SetRequestId::new())
        .middleware(RequestLogger::new())
        .middleware(metrics.clone())
        .middleware(SecurityHeaders::new())
        .middleware(Compression::new())
        .middleware(RateLimiter::per_minute(600).burst(50));

    app.mount(
        "/",
        Health::new()
            .check("database", || async { Ok::<_, std::io::Error>(()) }) // ping it for real
            .router(),
    )
    .mount("/", metrics.router());

    app.get("/", |_req| async { Json(json!({ "service": "demo", "status": "ok" })) });
    app.get("/slow", |_req| async {
        tokio::time::sleep(Duration::from_secs(5)).await;
        "done"
    });

    app.listen("0.0.0.0:3000").expect("server failed to start");
}
Terminal
curl http://127.0.0.1:3000/health     # liveness
curl http://127.0.0.1:3000/ready      # readiness: fails during shutdown_delay
curl http://127.0.0.1:3000/metrics    # Prometheus format
curl -i http://127.0.0.1:3000/slow    # now press Ctrl+C: this request still completes

How shutdown works#

  1. SIGTERM or Ctrl+C arrives. For shutdown_delay, the server keeps serving but /ready fails, so the load balancer moves traffic away.
  2. It stops accepting connections and lets in-flight requests finish, for up to shutdown_timeout.
  3. Event streams end, WebSockets close, and the process exits.

Set Kubernetes' terminationGracePeriodSeconds above shutdown_delay + shutdown_timeout.

Notes#

  • Behind a proxy or load balancer, call .trust_proxy(true) on RateLimiter so limits apply per real client, not per proxy.
  • Inside your own Tokio runtime use app.serve_with_shutdown(listener, axeon::shutdown_signal()).