use std::{ sync::atomic::{AtomicU64, Ordering}, time::Instant, }; use axum::{ body::{Body, Bytes, to_bytes}, http::{HeaderMap, Method, Request}, middleware::Next, response::Response, }; use base64::{Engine, prelude::BASE64_STANDARD}; use serde_json::Value; static REQ_SEQ: AtomicU64 = AtomicU64::new(1); fn format_x_auth(headers: &HeaderMap) -> Option { let v = headers.get("x-auth")?; let s = v.to_str().ok()?.to_string(); const MAX: usize = 120; if s.len() > MAX { Some(format!("{}… (len={})", &s[..MAX], s.len())) } else { Some(s) } } fn status_emoji(status: axum::http::StatusCode) -> &'static str { if status.is_success() { "✅" } else if status.is_redirection() { "↪" } else if status.is_client_error() { "⚠" } else if status.is_server_error() { "❌" } else { "ℹ" } } /// Convert JSON into a "key: value" style display. /// - Objects: `key: value` (nested objects/arrays are indented) /// - Arrays: `- item` (nested indented) /// If not JSON: UTF-8 text, else base64. fn body_as_kv(bytes: &Bytes) -> String { if bytes.is_empty() { return "".to_string(); } if let Ok(v) = serde_json::from_slice::(bytes) { let mut out = String::new(); write_value(&mut out, &v, 0); return out.trim_end().to_string(); } match std::str::from_utf8(bytes) { Ok(s) => s.to_string(), Err(_) => format!("\n{}", BASE64_STANDARD.encode(bytes)), } } fn write_value(out: &mut String, v: &Value, indent: usize) { match v { Value::Object(map) => { for (k, val) in map { write_key_value(out, k, val, indent); } } Value::Array(arr) => { for item in arr { write_array_item(out, item, indent); } } _ => { // Root primitive out.push_str(&indent_str(indent)); out.push_str(&format_primitive(v)); out.push('\n'); } } } fn write_key_value(out: &mut String, key: &str, val: &Value, indent: usize) { let pad = indent_str(indent); match val { Value::Object(_) | Value::Array(_) => { out.push_str(&pad); out.push_str(key); out.push_str(":\n"); write_value(out, val, indent + 2); } _ => { out.push_str(&pad); out.push_str(key); out.push_str(": "); out.push_str(&format_primitive(val)); out.push('\n'); } } } fn write_array_item(out: &mut String, item: &Value, indent: usize) { let pad = indent_str(indent); match item { Value::Object(_) | Value::Array(_) => { out.push_str(&pad); out.push_str("-\n"); write_value(out, item, indent + 2); } _ => { out.push_str(&pad); out.push_str("- "); out.push_str(&format_primitive(item)); out.push('\n'); } } } fn format_primitive(v: &Value) -> String { match v { Value::String(s) => s.clone(), Value::Number(n) => n.to_string(), Value::Bool(b) => b.to_string(), Value::Null => "null".to_string(), // Shouldn’t happen here (we route these elsewhere), but safe fallback Value::Object(_) | Value::Array(_) => "".to_string(), } } fn indent_str(spaces: usize) -> String { " ".repeat(spaces) } fn indent_block(s: &str, spaces: usize) -> String { let pad = " ".repeat(spaces); s.lines() .map(|line| format!("{pad}{line}\n")) .collect::() .trim_end_matches('\n') .to_string() } pub async fn log_req_res_bodies(req: Request, next: Next) -> Response { // Avoid noisy CORS preflight logs if req.method() == Method::OPTIONS { return next.run(req).await; } let id = REQ_SEQ.fetch_add(1, Ordering::Relaxed); let start = Instant::now(); let method = req.method().clone(); let uri = req.uri().clone(); let req_headers = req.headers().clone(); // Read + restore request body let (req_parts, req_body) = req.into_parts(); let req_bytes = to_bytes(req_body, usize::MAX).await.unwrap(); let req = Request::from_parts(req_parts, Body::from(req_bytes.clone())); // Run handler let res = next.run(req).await; let status = res.status(); let res_headers = res.headers().clone(); // Read + restore response body let (res_parts, res_body) = res.into_parts(); let res_bytes = to_bytes(res_body, usize::MAX).await.unwrap(); let res = Response::from_parts(res_parts, Body::from(res_bytes.clone())); let ms = start.elapsed().as_millis(); let sep = "════════════════════════════════════════════════════════════════"; let mut out = String::new(); out.push('\n'); out.push_str(sep); out.push('\n'); out.push_str(&format!( "🚦 #{id} {method} {uri} {} {status} ⏱ {ms}ms\n", status_emoji(status) )); out.push('\n'); out.push_str("📥 Request\n"); if let Some(xauth) = format_x_auth(&req_headers) { out.push_str(&format!(" 🔐 x-auth: {xauth}\n")); } out.push_str(&indent_block(&body_as_kv(&req_bytes), 2)); out.push('\n'); out.push_str("📤 Response\n"); out.push_str(&indent_block(&body_as_kv(&res_bytes), 2)); out.push('\n'); out.push_str(sep); out.push('\n'); tracing::info!("{out}"); let _ = res_headers; res }