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