Skip to main content

webx_host/
request_tracing.rs

1//! Structured request tracing middleware with trace_id injection.
2
3use std::ops::ControlFlow;
4use std::sync::atomic::{AtomicU64, Ordering};
5use webx_core::error::Result;
6use webx_core::http::IHttpContext;
7use webx_core::middleware::IMiddleware;
8
9static COUNTER: AtomicU64 = AtomicU64::new(0);
10static TRACE_SEQ: AtomicU64 = AtomicU64::new(0);
11
12pub struct RequestTracing {
13    pub log_all: bool,
14}
15
16impl RequestTracing {
17    pub fn new() -> Self {
18        Self { log_all: true }
19    }
20    pub fn errors_only() -> Self {
21        Self { log_all: false }
22    }
23}
24
25impl Default for RequestTracing {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31#[async_trait::async_trait]
32impl IMiddleware for RequestTracing {
33    async fn invoke(&self, ctx: &mut dyn IHttpContext) -> Result<ControlFlow<()>> {
34        // Propagate existing request ID from upstream, or generate a new one.
35        let tid = ctx
36            .request()
37            .header("x-request-id")
38            .map(|s| s.to_string())
39            .unwrap_or_else(next_trace_id);
40        // Store on the context *and* echo in the header: handlers read the same
41        // value through `RequestContext::trace_id()`, so an audit row can be
42        // joined with the access log line for this request.
43        ctx.set_trace_id(&tid);
44        ctx.response_mut().set_header("x-trace-id", &tid);
45        Ok(ControlFlow::Continue(()))
46    }
47
48    async fn after(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
49        let status = ctx.response().status();
50        let is_err = status >= 400;
51
52        if self.log_all || is_err {
53            let count = COUNTER.fetch_add(1, Ordering::Relaxed);
54            let method = ctx.request().method().to_string();
55            let path = ctx.request().path().to_string();
56            let user = ctx.claims().map(|c| c.subject().to_string());
57
58            if is_err {
59                tracing::warn!(
60                    count = count, method = %method, path = %path,
61                    status = status, user = %user.as_deref().unwrap_or("anon"),
62                    "request error"
63                );
64            } else {
65                tracing::info!(
66                    count = count, method = %method, path = %path,
67                    status = status, user = %user.as_deref().unwrap_or("anon"),
68                    "request ok"
69                );
70            }
71        }
72        Ok(())
73    }
74}
75
76fn next_trace_id() -> String {
77    let id = TRACE_SEQ.fetch_add(1, Ordering::Relaxed);
78    format!("{:016x}", id)
79}