rust 48 lines · 7 steps

Structured request tracing in Axum

Wire a TraceLayer into an Axum router to emit a structured span for every HTTP request.

Explained by highlit
1use std::time::Duration;
2 
3use axum::{
4 body::Body,
5 extract::MatchedPath,
6 http::{Request, Response},
7 routing::get,
8 Router,
9};
10use tower_http::trace::TraceLayer;
11use tracing::{info_span, Span};
12 
13pub fn app() -> Router {
14 Router::new()
15 .route("/health", get(|| async { "ok" }))
16 .route("/users/:id", get(get_user))
17 .layer(
18 TraceLayer::new_for_http()
19 .make_span_with(|request: &Request<Body>| {
20 let matched_path = request
21 .extensions()
22 .get::<MatchedPath>()
23 .map(MatchedPath::as_str);
24 
25 info_span!(
26 "http_request",
27 method = %request.method(),
28 uri = %request.uri(),
29 matched_path,
30 request_id = %uuid::Uuid::new_v4(),
31 )
32 })
33 .on_response(|response: &Response<Body>, latency: Duration, _span: &Span| {
34 tracing::info!(
35 status = response.status().as_u16(),
36 latency_ms = latency.as_millis(),
37 "request completed"
38 );
39 })
40 .on_failure(|error, latency: Duration, _span: &Span| {
41 tracing::error!(
42 %error,
43 latency_ms = latency.as_millis(),
44 "request failed"
45 );
46 }),
47 )
48}
01 / 01
STEP 01

Walkthrough

Space play ←→ step click any line
Three takeaways
  1. 1A single tower layer can instrument every route without touching individual handlers.
  2. 2Reading MatchedPath keeps span labels low-cardinality by grouping requests under their route template.
  3. 3Separate hooks for span creation, success, and failure give you full-lifecycle observability per request.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Structured request tracing in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code