foxy/logging/
middleware.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! HTTP middleware for request/response logging with trace context.
6
7use hyper::{Request, Response, StatusCode};
8use std::task::{Context, Poll};
9use std::pin::Pin;
10use std::future::Future;
11use futures_util::ready;
12use std::time::{Instant, Duration};
13use crate::logging::structured::{RequestInfo, generate_trace_id};
14use crate::logging::config::LoggingConfig;
15use slog_scope;
16use std::sync::Arc;
17use std::net::SocketAddr;
18
19/// Middleware for request/response logging with trace context
20#[derive(Debug, Clone)]
21pub struct LoggingMiddleware {
22    config: Arc<LoggingConfig>,
23}
24
25impl LoggingMiddleware {
26    /// Create a new logging middleware
27    pub fn new(config: LoggingConfig) -> Self {
28        Self {
29            config: Arc::new(config),
30        }
31    }
32    
33    /// Process a request and add trace context
34    pub async fn process<B>(
35        &self,
36        req: Request<B>,
37        remote_addr: Option<SocketAddr>,
38    ) -> (Request<B>, RequestInfo) {
39        let method = req.method().to_string();
40        let path = req.uri().path().to_string();
41        let remote_addr_str = remote_addr
42            .map(|addr| addr.to_string())
43            .unwrap_or_else(|| "unknown".to_string());
44        
45        let user_agent = req
46            .headers()
47            .get(hyper::header::USER_AGENT)
48            .and_then(|h| h.to_str().ok())
49            .unwrap_or("unknown")
50            .to_string();
51        
52        // Check for existing trace ID in headers if propagation is enabled
53        let trace_id = if self.config.propagate_trace_id {
54            req.headers()
55                .get(&self.config.trace_id_header)
56                .and_then(|h| h.to_str().ok())
57                .map(|s| s.to_string())
58                .unwrap_or_else(generate_trace_id)
59        } else {
60            generate_trace_id()
61        };
62        
63        let request_info = RequestInfo {
64            trace_id,
65            method,
66            path,
67            remote_addr: remote_addr_str,
68            user_agent,
69            start_time_ms: std::time::SystemTime::now()
70                .duration_since(std::time::UNIX_EPOCH)
71                .unwrap_or_default()
72                .as_millis(),
73        };
74        
75        // Log the incoming request with trace context
76        if self.config.structured {
77            let logger = slog_scope::logger();
78            slog::info!(logger, "Request received";
79                "trace_id" => &request_info.trace_id,
80                "method" => &request_info.method,
81                "path" => &request_info.path,
82                "remote_addr" => &request_info.remote_addr,
83                "user_agent" => &request_info.user_agent
84            );
85        } else {
86            log::info!(
87                "Request received: {} {} from {} (trace_id: {})",
88                request_info.method,
89                request_info.path,
90                request_info.remote_addr,
91                request_info.trace_id
92            );
93        }
94        
95        (req, request_info)
96    }
97    
98    /// Log the response with timing information
99    pub fn log_response<B>(
100        &self,
101        response: &Response<B>,
102        request_info: &RequestInfo,
103        upstream_duration: Option<Duration>,
104    ) {
105        let status = response.status().as_u16();
106        let elapsed_ms = request_info.elapsed_ms();
107        let upstream_ms = upstream_duration
108            .map(|d| d.as_millis())
109            .unwrap_or(0);
110        let internal_ms = elapsed_ms.saturating_sub(upstream_ms);
111        
112        if self.config.structured {
113            let logger = slog_scope::logger();
114            slog::info!(logger, "Response completed";
115                "trace_id" => &request_info.trace_id,
116                "method" => &request_info.method,
117                "path" => &request_info.path,
118                "status" => status,
119                "elapsed_ms" => elapsed_ms,
120                "upstream_ms" => upstream_ms,
121                "internal_ms" => internal_ms
122            );
123        } else {
124            log::info!(
125                "[timing] {} {} -> {} | total={}ms upstream={}ms internal={}ms (trace_id: {})",
126                request_info.method,
127                request_info.path,
128                status,
129                elapsed_ms,
130                upstream_ms,
131                internal_ms,
132                request_info.trace_id
133            );
134        }
135    }
136}
137
138/// Future that wraps a response future and adds trace ID header
139pub struct TracedResponseFuture<F> {
140    inner: F,
141    trace_id: String,
142    trace_header: String,
143    include_trace_id: bool,
144}
145
146impl<F, B, E> Future for TracedResponseFuture<F>
147where
148    F: Future<Output = Result<Response<B>, E>> + Unpin,
149{
150    type Output = Result<Response<B>, E>;
151    
152    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
153        let result = ready!(Pin::new(&mut self.inner).poll(cx));
154        
155        Poll::Ready(match result {
156            Ok(mut response) => {
157                // Add trace ID header to response if enabled
158                if self.include_trace_id {
159                    let header_name = hyper::header::HeaderName::from_bytes(self.trace_header.as_bytes())
160                        .unwrap_or_else(|_| hyper::header::HeaderName::from_static("x-trace-id"));
161                    
162                    response.headers_mut().insert(
163                        header_name,
164                        hyper::header::HeaderValue::from_str(&self.trace_id)
165                            .unwrap_or_else(|_| hyper::header::HeaderValue::from_static("invalid-trace-id")),
166                    );
167                }
168                Ok(response)
169            }
170            Err(e) => Err(e),
171        })
172    }
173}
174
175/// Extension trait for response futures to add trace context
176pub trait ResponseFutureExt: Sized {
177    /// Add trace ID header to the response
178    fn with_trace_id(
179        self,
180        trace_id: String,
181        trace_header: String,
182        include_trace_id: bool,
183    ) -> TracedResponseFuture<Self>;
184}
185
186impl<F, B, E> ResponseFutureExt for F
187where
188    F: Future<Output = Result<Response<B>, E>> + Unpin,
189{
190    fn with_trace_id(
191        self,
192        trace_id: String,
193        trace_header: String,
194        include_trace_id: bool,
195    ) -> TracedResponseFuture<Self> {
196        TracedResponseFuture {
197            inner: self,
198            trace_id,
199            trace_header,
200            include_trace_id,
201        }
202    }
203}