use std::time::Instant;
use axum::{
extract::Request,
http::HeaderValue,
middleware::Next,
response::Response,
};
use tracing::field;
use uuid::Uuid;
pub async fn track_latency(req: Request, next: Next) -> Response {
let method = req.method().clone();
let path = req.uri().path().to_owned();
let request_id = Uuid::new_v4();
let span = tracing::info_span!(
"http_request",
%method,
%path,
request_id = %request_id,
status = field::Empty,
latency_ms = field::Empty,
);
let _enter = span.enter();
let started = Instant::now();
let mut response = next.run(req).await;
let latency_ms = started.elapsed().as_secs_f64() * 1000.0;
let status = response.status();
span.record("status", status.as_u16());
span.record("latency_ms", latency_ms);
if let Ok(value) = HeaderValue::from_str(&request_id.to_string()) {
response.headers_mut().insert("x-request-id", value);
}
if status.is_server_error() {
tracing::error!(status = %status, latency_ms, "request failed");
} else {
tracing::info!(status = %status, latency_ms, "request completed");
}
response
}
mod latency;
use std::net::SocketAddr;
use axum::{
middleware,
routing::get,
Json, Router,
};
use serde_json::json;
use tracing_subscriber::{fmt, EnvFilter};
async fn health() -> Json<serde_json::Value> {
Json(json!({ "status": "ok" }))
}
#[tokio::main]
async fn main() {
fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.json()
.init();
let app = Router::new()
.route("/health", get(health))
.route("/slow", get(|| async {
tokio::time::sleep(std::time::Duration::from_millis(120)).await;
"done"
}))
.layer(middleware::from_fn(latency::track_latency));
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
tracing::info!(%addr, "starting server");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
This snippet shows how request latency logging is wired into an axum service using a tower-style middleware function and structured tracing spans. The middleware in latency.rs is written with axum::middleware::from_fn, which adapts a plain async function into a layer that sits between the router and the handlers. The pattern is common in production HTTP services because it centralizes cross-cutting concerns — timing, request identifiers, and status logging — so individual handlers stay focused on business logic.
In latency.rs, track_latency receives the incoming Request and a Next, records Instant::now() before calling next.run(req).await, then computes elapsed() after the response comes back. Wrapping the downstream call this way is important: the timer must straddle the entire handler chain, including any inner middleware, so the elapsed value reflects real end-to-end latency rather than just handler execution. A per-request Uuid is generated and injected into the response headers via x-request-id, giving clients and log aggregators a correlation key. The tracing::info_span! creates a span carrying the method, path, and request id, and record fills in status and latency_ms once known, so a single structured log line captures the full picture. The % and field::Empty sigils are tracing conventions for display-formatting and deferred fields.
Because from_fn closures must be Clone and Send, the function signature avoids capturing non-Send state and instead derives everything from the request. Requests that panic or return early still flow through because next.run yields a Response in all normal cases; genuinely panicking handlers are a separate concern handled by a catch layer.
In main.rs, the middleware is attached with .layer(middleware::from_fn(track_latency)) after tracing_subscriber is initialized, since spans need a subscriber to be recorded. Layer ordering matters — outer layers wrap inner ones — so the latency layer is placed to encompass the routes it should measure. A pitfall worth noting is that adding this layer above a large ServiceBuilder stack changes what the timer includes; developers should place it deliberately depending on whether they want to measure gzip, auth, and other middleware. This approach is reached for whenever an operator needs cheap, always-on visibility into slow endpoints without pulling in a full APM agent.
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
// 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
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
Share this code
Here's the card — post it anywhere.