use crate::axum::{extract::Request, middleware::Next, response::Response};
use doido_core::tracing::Instrument;
use http::{HeaderMap, HeaderName, HeaderValue};
use std::time::Instant;
use uuid::Uuid;
const REQUEST_ID_HEADER: &str = "x-request-id";
pub async fn log_requests(request: Request, next: Next) -> Response {
let request_id = resolve_request_id(request.headers());
let method = request.method().clone();
let path = request.uri().path().to_owned();
let query = request.uri().query().map(str::to_owned);
let request_headers = format_headers(request.headers());
let span = doido_core::tracing::info_span!(
"request",
request_id = %request_id,
method = %method,
path = %path,
);
doido_core::tracing::info!(
target: doido_core::logger::REQUEST_TARGET,
parent: &span,
request_id = %request_id,
method = %method,
path = %path,
query = query.as_deref().unwrap_or(""),
headers = %request_headers,
"request"
);
let start = Instant::now();
let mut response = next.run(request).instrument(span.clone()).await;
let latency_ms = start.elapsed().as_millis() as u64;
if let Ok(value) = HeaderValue::from_str(&request_id) {
response
.headers_mut()
.insert(HeaderName::from_static(REQUEST_ID_HEADER), value);
}
let status = response.status().as_u16();
let response_headers = format_headers(response.headers());
{
let _guard = span.enter();
doido_core::tracing::info!(
target: doido_core::logger::RESPONSE_TARGET,
request_id = %request_id,
method = %method,
path = %path,
status = status,
latency_ms = latency_ms,
headers = %response_headers,
"response"
);
}
response
}
fn resolve_request_id(headers: &HeaderMap) -> String {
headers
.get(REQUEST_ID_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|id| !id.is_empty())
.map(str::to_owned)
.unwrap_or_else(|| Uuid::new_v4().to_string())
}
fn format_headers(headers: &HeaderMap) -> String {
headers
.iter()
.map(|(name, value)| {
let rendered = if is_sensitive(name.as_str()) {
"[redacted]"
} else {
value.to_str().unwrap_or("[non-utf8]")
};
format!("{}: {}", name.as_str(), rendered)
})
.collect::<Vec<_>>()
.join(", ")
}
fn is_sensitive(name: &str) -> bool {
matches!(
name,
"authorization" | "proxy-authorization" | "cookie" | "set-cookie"
)
}