warp/filters/
log.rs

1//! Logger Filters
2
3use std::fmt;
4use std::time::{Duration, Instant};
5
6use http::{header, StatusCode};
7
8use crate::filter::{Filter, WrapSealed};
9use crate::reject::IsReject;
10use crate::reply::Reply;
11use crate::route::Route;
12
13use self::internal::WithLog;
14
15/// Create a wrapping [`Filter`] with the specified `name` as the `target`.
16///
17/// This uses the default access logging format, and log records produced
18/// will have their `target` set to `name`.
19///
20/// # Example
21///
22/// ```
23/// use warp::Filter;
24///
25/// // If using something like `pretty_env_logger`,
26/// // view logs by setting `RUST_LOG=example::api`.
27/// let log = warp::log("example::api");
28/// let route = warp::any()
29///     .map(warp::reply)
30///     .with(log);
31/// ```
32pub fn log(name: &'static str) -> Log<impl Fn(Info<'_>) + Copy> {
33    let func = move |info: Info<'_>| {
34        // TODO?
35        // - response content length?
36        log::info!(
37            target: name,
38            "\"{} {} {:?}\" {} \"{}\" \"{}\" {:?}",
39            info.method(),
40            info.path(),
41            info.route.version(),
42            info.status().as_u16(),
43            OptFmt(info.referer()),
44            OptFmt(info.user_agent()),
45            info.elapsed(),
46        );
47    };
48    Log { func }
49}
50
51/// Create a wrapping [`Filter`](crate::Filter) that receives `warp::log::Info`.
52///
53/// # Example
54///
55/// ```
56/// use warp::Filter;
57///
58/// let log = warp::log::custom(|info| {
59///     // Use a log macro, or slog, or println, or whatever!
60///     eprintln!(
61///         "{} {} {}",
62///         info.method(),
63///         info.path(),
64///         info.status(),
65///     );
66/// });
67/// let route = warp::any()
68///     .map(warp::reply)
69///     .with(log);
70/// ```
71pub fn custom<F>(func: F) -> Log<F>
72where
73    F: Fn(Info<'_>),
74{
75    Log { func }
76}
77
78/// Decorates a [`Filter`] to log requests and responses.
79#[derive(Clone, Copy, Debug)]
80pub struct Log<F> {
81    func: F,
82}
83
84/// Information about the request/response that can be used to prepare log lines.
85#[allow(missing_debug_implementations)]
86pub struct Info<'a> {
87    route: &'a Route,
88    start: Instant,
89    status: StatusCode,
90}
91
92impl<FN, F> WrapSealed<F> for Log<FN>
93where
94    FN: Fn(Info<'_>) + Clone + Send,
95    F: Filter + Clone + Send,
96    F::Extract: Reply,
97    F::Error: IsReject,
98{
99    type Wrapped = WithLog<FN, F>;
100
101    fn wrap(&self, filter: F) -> Self::Wrapped {
102        WithLog {
103            filter,
104            log: self.clone(),
105        }
106    }
107}
108
109impl<'a> Info<'a> {
110    /// View the `http::Method` of the request.
111    pub fn method(&self) -> &http::Method {
112        self.route.method()
113    }
114
115    /// View the URI path of the request.
116    pub fn path(&self) -> &str {
117        self.route.full_path()
118    }
119
120    /// View the `http::Version` of the request.
121    pub fn version(&self) -> http::Version {
122        self.route.version()
123    }
124
125    /// View the `http::StatusCode` of the response.
126    pub fn status(&self) -> http::StatusCode {
127        self.status
128    }
129
130    /// View the referer of the request.
131    pub fn referer(&self) -> Option<&str> {
132        self.route
133            .headers()
134            .get(header::REFERER)
135            .and_then(|v| v.to_str().ok())
136    }
137
138    /// View the user agent of the request.
139    pub fn user_agent(&self) -> Option<&str> {
140        self.route
141            .headers()
142            .get(header::USER_AGENT)
143            .and_then(|v| v.to_str().ok())
144    }
145
146    /// View the `Duration` that elapsed for the request.
147    pub fn elapsed(&self) -> Duration {
148        tokio::time::Instant::now().into_std() - self.start
149    }
150
151    /// View the host of the request
152    pub fn host(&self) -> Option<&str> {
153        self.route
154            .headers()
155            .get(header::HOST)
156            .and_then(|v| v.to_str().ok())
157    }
158
159    /// Access the full headers of the request
160    pub fn request_headers(&self) -> &http::HeaderMap {
161        self.route.headers()
162    }
163}
164
165struct OptFmt<T>(Option<T>);
166
167impl<T: fmt::Display> fmt::Display for OptFmt<T> {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        if let Some(ref t) = self.0 {
170            fmt::Display::fmt(t, f)
171        } else {
172            f.write_str("-")
173        }
174    }
175}
176
177mod internal {
178    use std::future::Future;
179    use std::pin::Pin;
180    use std::task::{Context, Poll};
181    use std::time::Instant;
182
183    use futures_util::{ready, TryFuture};
184    use pin_project::pin_project;
185
186    use super::{Info, Log};
187    use crate::filter::{Filter, FilterBase, Internal};
188    use crate::reject::IsReject;
189    use crate::reply::{Reply, Response};
190    use crate::route;
191
192    #[allow(missing_debug_implementations)]
193    pub struct Logged(pub(super) Response);
194
195    impl Reply for Logged {
196        #[inline]
197        fn into_response(self) -> Response {
198            self.0
199        }
200    }
201
202    #[allow(missing_debug_implementations)]
203    #[derive(Clone, Copy)]
204    pub struct WithLog<FN, F> {
205        pub(super) filter: F,
206        pub(super) log: Log<FN>,
207    }
208
209    impl<FN, F> FilterBase for WithLog<FN, F>
210    where
211        FN: Fn(Info<'_>) + Clone + Send,
212        F: Filter + Clone + Send,
213        F::Extract: Reply,
214        F::Error: IsReject,
215    {
216        type Extract = (Logged,);
217        type Error = F::Error;
218        type Future = WithLogFuture<FN, F::Future>;
219
220        fn filter(&self, _: Internal) -> Self::Future {
221            let started = tokio::time::Instant::now().into_std();
222            WithLogFuture {
223                log: self.log.clone(),
224                future: self.filter.filter(Internal),
225                started,
226            }
227        }
228    }
229
230    #[allow(missing_debug_implementations)]
231    #[pin_project]
232    pub struct WithLogFuture<FN, F> {
233        log: Log<FN>,
234        #[pin]
235        future: F,
236        started: Instant,
237    }
238
239    impl<FN, F> Future for WithLogFuture<FN, F>
240    where
241        FN: Fn(Info<'_>),
242        F: TryFuture,
243        F::Ok: Reply,
244        F::Error: IsReject,
245    {
246        type Output = Result<(Logged,), F::Error>;
247
248        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
249            let pin = self.as_mut().project();
250            let (result, status) = match ready!(pin.future.try_poll(cx)) {
251                Ok(reply) => {
252                    let resp = reply.into_response();
253                    let status = resp.status();
254                    (Poll::Ready(Ok((Logged(resp),))), status)
255                }
256                Err(reject) => {
257                    let status = reject.status();
258                    (Poll::Ready(Err(reject)), status)
259                }
260            };
261
262            route::with(|route| {
263                (self.log.func)(Info {
264                    route,
265                    start: self.started,
266                    status,
267                });
268            });
269
270            result
271        }
272    }
273}