Skip to main content

modo/middleware/
tracing.rs

1use tower_http::classify::ServerErrorsAsFailures;
2use tower_http::classify::SharedClassifier;
3use tower_http::trace::{MakeSpan, TraceLayer};
4
5/// Span maker that creates an `http_request` tracing span for each request.
6///
7/// Includes a `tenant_id` field (initially empty) so that the tenant
8/// middleware can record it via `span.record("tenant_id", ...)` after
9/// resolving the tenant.
10#[derive(Clone, Debug)]
11pub struct ModoMakeSpan;
12
13impl<B> MakeSpan<B> for ModoMakeSpan {
14    fn make_span(&mut self, request: &http::Request<B>) -> tracing::Span {
15        tracing::info_span!(
16            "http_request",
17            method = %request.method(),
18            uri = %request.uri(),
19            version = ?request.version(),
20            tenant_id = tracing::field::Empty,
21        )
22    }
23}
24
25/// Returns a tracing layer configured for HTTP request/response lifecycle logging.
26///
27/// The span name is `http_request` and the initial fields are `method`,
28/// `uri`, `version`, and `tenant_id` (empty — filled in by the tenant
29/// middleware via [`tracing::Span::record`] after tenant resolution).
30/// Any middleware that needs to record a span field must add it to
31/// [`ModoMakeSpan`] first; fields not pre-declared are dropped by
32/// `tracing`.
33///
34/// # Layer ordering
35///
36/// Install `tracing()` **outermost** (the last `.layer(...)` call in your
37/// chain) so every inbound request — including those rejected by
38/// [`csrf`](super::csrf) / [`rate_limit`](super::rate_limit) — is
39/// observed inside the span.
40///
41/// # Example
42///
43/// ```rust,no_run
44/// use axum::{Router, routing::get};
45/// use modo::middleware::tracing;
46///
47/// let app: Router = Router::new()
48///     .route("/", get(|| async { "ok" }))
49///     .layer(tracing());
50/// ```
51pub fn tracing() -> TraceLayer<SharedClassifier<ServerErrorsAsFailures>, ModoMakeSpan> {
52    TraceLayer::new_for_http().make_span_with(ModoMakeSpan)
53}