use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use axum::extract::Request;
use axum::http::{HeaderName, HeaderValue};
use axum::middleware::Next;
use axum::response::Response;
static REQ_SEQ: AtomicU64 = AtomicU64::new(1);
pub const SLO_BUDGET_MS: u128 = 1_000;
const REQUEST_ID_HEADER: &str = "x-request-id";
const RESPONSE_TIME_HEADER: &str = "x-response-time-ms";
#[derive(Debug, Clone)]
pub struct RequestId(pub String);
pub async fn request_id(mut req: Request, next: Next) -> Response {
let id = req
.headers()
.get(REQUEST_ID_HEADER)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_else(|| format!("req_{:016x}", REQ_SEQ.fetch_add(1, Ordering::Relaxed)));
req.extensions_mut().insert(RequestId(id.clone()));
let mut resp = next.run(req).await;
if let Ok(value) = HeaderValue::from_str(&id) {
resp.headers_mut()
.insert(HeaderName::from_static(REQUEST_ID_HEADER), value);
}
resp
}
pub async fn latency_slo(req: Request, next: Next) -> Response {
let method = req.method().clone();
let path = req.uri().path().to_string();
let request_id = req
.extensions()
.get::<RequestId>()
.map(|r| r.0.clone())
.unwrap_or_default();
let started = Instant::now();
let mut resp = next.run(req).await;
let elapsed = started.elapsed();
let ms = elapsed.as_millis();
if let Ok(value) = HeaderValue::from_str(&ms.to_string()) {
resp.headers_mut()
.insert(HeaderName::from_static(RESPONSE_TIME_HEADER), value);
}
if ms > SLO_BUDGET_MS {
tracing::warn!(
target: "web.slo",
%method, path, request_id, elapsed_ms = ms, status = resp.status().as_u16(),
"api_slo_violation"
);
} else {
tracing::debug!(
target: "web.request",
%method, path, request_id, elapsed_ms = ms, status = resp.status().as_u16(),
"request"
);
}
resp
}