From d0c025c316eb3e29ded56e72f22c8570a140295d Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Mon, 27 Apr 2026 00:50:12 +0200 Subject: [PATCH] add X-Request-Id for each req, log all 400 errors --- Cargo.toml | 2 +- src/interfaces/middleware/trace_span.rs | 76 +++++++++++++++++++++---- src/main.rs | 21 ++++++- 3 files changed, 85 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9ef7b2b7..1eee2ea7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ serde_json = "1.0.149" futures = "0.3.32" async-stream = "0.3.6" mime_guess = "2.0.5" -uuid = { version = "1.23.0", features = ["v4", "serde"] } +uuid = { version = "1.23.0", features = ["v4", "v7", "serde"] } thiserror = "2.0.18" mockall = { version = "0.14.0", optional = true } diff --git a/src/interfaces/middleware/trace_span.rs b/src/interfaces/middleware/trace_span.rs index a193aa16..0241c7a1 100644 --- a/src/interfaces/middleware/trace_span.rs +++ b/src/interfaces/middleware/trace_span.rs @@ -1,25 +1,81 @@ -//! Custom [`MakeSpan`] that records the client IP in every request span. +//! Custom [`MakeSpan`], [`OnResponse`], and [`MakeRequestId`] for request tracing. //! -//! IP resolution delegates to [`super::trusted_proxy::client_ip`]: -//! 1. TCP peer in `OXICLOUD_TRUST_PROXY_CIDR` → `X-Forwarded-For` / `X-Real-Ip` -//! 2. Otherwise → raw TCP peer address (with port, e.g. `127.0.0.1:12345`) +//! [`UuidRequestId`] — generates a UUID v4 per request for `SetRequestIdLayer`. +//! +//! [`ClientIpMakeSpan`] — records `request_id`, `client_ip`, `method`, `uri`, +//! and a placeholder `user_id` (filled by auth middleware) on every request span. +//! +//! [`LogBadRequest`] — emits a WARN for every HTTP 400 response, inheriting +//! all span fields so the log line includes request ID, IP, user, method, URI. -use axum::http::Request; -use tower_http::trace::MakeSpan; +use axum::http::{HeaderValue, Request, Response, StatusCode}; +use std::time::Duration; +use tower_http::request_id::{MakeRequestId, RequestId}; +use tower_http::trace::{MakeSpan, OnResponse}; use tracing::Span; +use uuid::Uuid; -/// Implements [`MakeSpan`] so that every HTTP request span carries a -/// `client_ip` field visible in every log line produced inside that span. +// ─── Request ID generator ──────────────────────────────────────────────────── + +/// Generates a UUID v7 (fast, timed, sortable) for each request. +/// +/// Used with [`tower_http::request_id::SetRequestIdLayer`]: +/// ```ignore +/// .layer(SetRequestIdLayer::x_request_id(UuidRequestId)) +/// ``` +#[derive(Clone, Debug, Default)] +pub struct UuidRequestId; + +impl MakeRequestId for UuidRequestId { + fn make_request_id(&mut self, _request: &Request) -> Option { + let id = Uuid::now_v7().to_string(); + HeaderValue::from_str(&id).ok().map(RequestId::new) + } +} + +// ─── Span factory ──────────────────────────────────────────────────────────── + +/// Implements [`MakeSpan`] so that every HTTP request span carries +/// `request_id`, `client_ip`, `method`, `uri`, and a deferred `user_id`. +/// +/// `request_id` is read from the `x-request-id` header set by +/// [`tower_http::request_id::SetRequestIdLayer`] (which must wrap this layer). #[derive(Clone, Debug, Default)] pub struct ClientIpMakeSpan; impl MakeSpan for ClientIpMakeSpan { fn make_span(&mut self, request: &Request) -> Span { let ip = super::trusted_proxy::client_ip(request, true); + let request_id = request + .headers() + .get("x-request-id") + .and_then(|v| v.to_str().ok()) + .unwrap_or("-"); tracing::info_span!( "req", - client_ip = %ip, - user_id = tracing::field::Empty, + request_id = request_id, + client_ip = %ip, + method = %request.method(), + uri = %request.uri().path(), + user_id = tracing::field::Empty, ) } } + +// ─── Response observer ─────────────────────────────────────────────────────── + +/// Implements [`OnResponse`]: emits a WARN log for every HTTP 400 response. +#[derive(Clone, Debug, Default)] +pub struct LogBadRequest; + +impl OnResponse for LogBadRequest { + fn on_response(self, response: &Response, latency: Duration, _span: &Span) { + if response.status() == StatusCode::BAD_REQUEST { + tracing::warn!( + status = 400, + latency_ms = latency.as_millis(), + "bad request", + ); + } + } +} diff --git a/src/main.rs b/src/main.rs index 7406170f..457308ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,11 @@ use socket2::{Domain, Protocol, Socket, TcpKeepalive, Type}; use axum::Router; use axum::extract::DefaultBodyLimit; -use oxicloud::interfaces::middleware::trace_span::ClientIpMakeSpan; +use oxicloud::interfaces::middleware::trace_span::{ + ClientIpMakeSpan, LogBadRequest, UuidRequestId, +}; use tower_http::limit::RequestBodyLimitLayer; +use tower_http::request_id::{PropagateRequestIdLayer, SetRequestIdLayer}; use tower_http::set_header::SetResponseHeaderLayer; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -343,7 +346,13 @@ async fn main() -> Result<(), Box> { .merge(carddav_protected) .merge(webdav_protected) .merge(web_routes) - .layer(TraceLayer::new_for_http().make_span_with(ClientIpMakeSpan)); + .layer( + TraceLayer::new_for_http() + .make_span_with(ClientIpMakeSpan) + .on_response(LogBadRequest), + ) + .layer(PropagateRequestIdLayer::x_request_id()) + .layer(SetRequestIdLayer::x_request_id(UuidRequestId)); // Mount Nextcloud routes (uses its own Basic Auth middleware) if let Some(nc_router) = nextcloud_router { @@ -375,7 +384,13 @@ async fn main() -> Result<(), Box> { .merge(carddav_router) .merge(webdav_router) .merge(web_routes) - .layer(TraceLayer::new_for_http().make_span_with(ClientIpMakeSpan)); + .layer( + TraceLayer::new_for_http() + .make_span_with(ClientIpMakeSpan) + .on_response(LogBadRequest), + ) + .layer(PropagateRequestIdLayer::x_request_id()) + .layer(SetRequestIdLayer::x_request_id(UuidRequestId)); // Mount Nextcloud routes if let Some(nc_router) = nextcloud_router {