Skip to main content

datasynth_server/rest/
request_logging.rs

1//! Structured request logging middleware.
2//!
3//! Configures `tower_http::TraceLayer` for structured request/response spans
4//! with request_id, method, path, status, and latency.
5
6use axum::{body::Body, http::Request, middleware::Next, response::Response};
7use tracing::{info_span, Instrument};
8
9/// Request logging middleware that creates structured spans.
10///
11/// Logs method, path, status code, and duration for every request.
12pub async fn request_logging_middleware(request: Request<Body>, next: Next) -> Response {
13    let method = request.method().clone();
14    let path = request.uri().path().to_string();
15    let start = std::time::Instant::now();
16
17    // Extract request ID if present (set by request_id middleware)
18    let request_id = request
19        .extensions()
20        .get::<crate::rest::request_id::RequestId>()
21        .map(|r| r.0.clone())
22        .unwrap_or_default();
23
24    let span = info_span!(
25        "http_request",
26        method = %method,
27        path = %path,
28        request_id = %request_id,
29    );
30
31    let response = next.run(request).instrument(span).await;
32
33    let duration_ms = start.elapsed().as_millis();
34    let status = response.status().as_u16();
35
36    tracing::info!(
37        method = %method,
38        path = %path,
39        status = status,
40        latency_ms = duration_ms,
41        request_id = %request_id,
42        "Request completed"
43    );
44
45    response
46}
47
48#[cfg(test)]
49#[allow(clippy::unwrap_used)]
50mod tests {
51    use super::*;
52    use axum::{routing::get, Router};
53    use tower::ServiceExt;
54
55    async fn ok_handler() -> &'static str {
56        "ok"
57    }
58
59    #[tokio::test]
60    async fn test_request_logging_passes_through() {
61        let router = Router::new()
62            .route("/test", get(ok_handler))
63            .layer(axum::middleware::from_fn(request_logging_middleware));
64
65        let request = Request::builder().uri("/test").body(Body::empty()).unwrap();
66
67        let response = router.oneshot(request).await.unwrap();
68        assert_eq!(response.status(), 200);
69    }
70}