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.
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");
}
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#
SIGTERMor Ctrl+C arrives. Forshutdown_delay, the server keeps serving but/readyfails, so the load balancer moves traffic away.- It stops accepting connections and lets in-flight requests finish, for up to
shutdown_timeout. - 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)onRateLimiterso limits apply per real client, not per proxy. - Inside your own Tokio runtime use
app.serve_with_shutdown(listener, axeon::shutdown_signal()).