add X-Request-Id for each req, log all 400 errors

This commit is contained in:
Edouard Vanbelle
2026-04-27 00:50:12 +02:00
parent 8e1a738056
commit d0c025c316
3 changed files with 85 additions and 14 deletions
+1 -1
View File
@@ -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 }
+64 -8
View File
@@ -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<B>(&mut self, _request: &Request<B>) -> Option<RequestId> {
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<B> MakeSpan<B> for ClientIpMakeSpan {
fn make_span(&mut self, request: &Request<B>) -> 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",
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<B> OnResponse<B> for LogBadRequest {
fn on_response(self, response: &Response<B>, latency: Duration, _span: &Span) {
if response.status() == StatusCode::BAD_REQUEST {
tracing::warn!(
status = 400,
latency_ms = latency.as_millis(),
"bad request",
);
}
}
}
+18 -3
View File
@@ -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<dyn std::error::Error>> {
.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<dyn std::error::Error>> {
.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 {