foxy/logging/
middleware.rs1use 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#[derive(Debug, Clone)]
21pub struct LoggingMiddleware {
22 config: Arc<LoggingConfig>,
23}
24
25impl LoggingMiddleware {
26 pub fn new(config: LoggingConfig) -> Self {
28 Self {
29 config: Arc::new(config),
30 }
31 }
32
33 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 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 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 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
138pub 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 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
175pub trait ResponseFutureExt: Sized {
177 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}