warp/filters/
log.rs

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