warp/filters/
trace.rs

1//! [`tracing`] filters.
2//!
3//! [`tracing`] is a framework for instrumenting Rust programs to
4//! collect scoped, structured, and async-aware diagnostics. This module
5//! provides a set of filters for instrumenting Warp applications with `tracing`
6//! spans. [`Spans`] can be used to associate individual events  with a request,
7//! and track contexts through the application.
8//!
9//! [`tracing`]: https://crates.io/crates/tracing
10//! [`Spans`]: https://docs.rs/tracing/latest/tracing/#spans
11use tracing::Span;
12
13use http::header;
14
15use crate::filter::{Filter, WrapSealed};
16use crate::reject::IsReject;
17use crate::reply::Reply;
18use crate::route::Route;
19
20use self::internal::WithTrace;
21
22/// Create a wrapping filter that instruments every request with a `tracing`
23/// [`Span`] at the [`INFO`] level, containing a summary of the request.
24/// Additionally, if the [`DEBUG`] level is enabled, the span will contain an
25/// event recording the request's headers.
26///
27/// # Example
28///
29/// ```
30/// use warp::Filter;
31///
32/// let route = warp::any()
33///     .map(warp::reply)
34///     .with(warp::trace::request());
35/// ```
36///
37/// [`Span`]: https://docs.rs/tracing/latest/tracing/#spans
38/// [`INFO`]: https://docs.rs/tracing/0.1.16/tracing/struct.Level.html#associatedconstant.INFO
39/// [`DEBUG`]: https://docs.rs/tracing/0.1.16/tracing/struct.Level.html#associatedconstant.DEBUG
40pub fn request() -> Trace<impl Fn(Info<'_>) -> Span + Clone> {
41    use tracing::field::{display, Empty};
42    trace(|info: Info<'_>| {
43        let span = tracing::info_span!(
44            "request",
45            remote.addr = Empty,
46            method = %info.method(),
47            path = %info.path(),
48            version = ?info.route.version(),
49            referer = Empty,
50        );
51
52        // Record optional fields.
53        if let Some(referer) = info.referer() {
54            span.record("referer", &display(referer));
55        }
56
57        tracing::debug!(parent: &span, "received request");
58
59        span
60    })
61}
62
63/// Create a wrapping filter that instruments every request with a custom
64/// `tracing` [`Span`] provided by a function.
65///
66///
67/// # Example
68///
69/// ```
70/// use warp::Filter;
71///
72/// let route = warp::any()
73///     .map(warp::reply)
74///     .with(warp::trace(|info| {
75///         // Create a span using tracing macros
76///         tracing::info_span!(
77///             "request",
78///             method = %info.method(),
79///             path = %info.path(),
80///         )
81///     }));
82/// ```
83///
84/// [`Span`]: https://docs.rs/tracing/latest/tracing/#spans
85pub fn trace<F>(func: F) -> Trace<F>
86where
87    F: Fn(Info<'_>) -> Span + Clone,
88{
89    Trace { func }
90}
91
92/// Create a wrapping filter that instruments every request with a `tracing`
93/// [`Span`] at the [`DEBUG`] level representing a named context.
94///
95/// This can be used to instrument multiple routes with their own sub-spans in a
96/// per-request trace.
97///
98/// # Example
99///
100/// ```
101/// use warp::Filter;
102///
103/// let hello = warp::path("hello")
104///     .map(warp::reply)
105///     .with(warp::trace::named("hello"));
106///
107/// let goodbye = warp::path("goodbye")
108///     .map(warp::reply)
109///     .with(warp::trace::named("goodbye"));
110///
111/// let routes = hello.or(goodbye);
112/// ```
113///
114/// [`Span`]: https://docs.rs/tracing/latest/tracing/#spans
115/// [`DEBUG`]: https://docs.rs/tracing/0.1.16/tracing/struct.Level.html#associatedconstant.DEBUG
116pub fn named(name: &'static str) -> Trace<impl Fn(Info<'_>) -> Span + Copy> {
117    trace(move |_| tracing::debug_span!("context", "{}", name,))
118}
119
120/// Decorates a [`Filter`] to create a [`tracing`] [span] for
121/// requests and responses.
122///
123/// [`tracing`]: https://crates.io/crates/tracing
124/// [span]: https://docs.rs/tracing/latest/tracing/#spans
125#[derive(Clone, Copy, Debug)]
126pub struct Trace<F> {
127    func: F,
128}
129
130/// Information about the request/response that can be used to prepare log lines.
131#[allow(missing_debug_implementations)]
132pub struct Info<'a> {
133    route: &'a Route,
134}
135
136impl<FN, F> WrapSealed<F> for Trace<FN>
137where
138    FN: Fn(Info<'_>) -> Span + Clone + Send,
139    F: Filter + Clone + Send,
140    F::Extract: Reply,
141    F::Error: IsReject,
142{
143    type Wrapped = WithTrace<FN, F>;
144
145    fn wrap(&self, filter: F) -> Self::Wrapped {
146        WithTrace {
147            filter,
148            trace: self.clone(),
149        }
150    }
151}
152
153impl<'a> Info<'a> {
154    /// View the `http::Method` of the request.
155    pub fn method(&self) -> &http::Method {
156        self.route.method()
157    }
158
159    /// View the URI path of the request.
160    pub fn path(&self) -> &str {
161        self.route.full_path()
162    }
163
164    /// View the `http::Version` of the request.
165    pub fn version(&self) -> http::Version {
166        self.route.version()
167    }
168
169    /// View the referer of the request.
170    pub fn referer(&self) -> Option<&str> {
171        self.route
172            .headers()
173            .get(header::REFERER)
174            .and_then(|v| v.to_str().ok())
175    }
176
177    /// View the user agent of the request.
178    pub fn user_agent(&self) -> Option<&str> {
179        self.route
180            .headers()
181            .get(header::USER_AGENT)
182            .and_then(|v| v.to_str().ok())
183    }
184
185    /// View the host of the request
186    pub fn host(&self) -> Option<&str> {
187        self.route
188            .headers()
189            .get(header::HOST)
190            .and_then(|v| v.to_str().ok())
191    }
192
193    /// View the request headers.
194    pub fn request_headers(&self) -> &http::HeaderMap {
195        self.route.headers()
196    }
197}
198
199mod internal {
200    use futures_util::{future::Inspect, future::MapOk, FutureExt, TryFutureExt};
201
202    use super::{Info, Trace};
203    use crate::filter::{Filter, FilterBase, Internal};
204    use crate::reject::IsReject;
205    use crate::reply::Reply;
206    use crate::reply::Response;
207    use crate::route;
208
209    #[allow(missing_debug_implementations)]
210    pub struct Traced(pub(super) Response);
211
212    impl Reply for Traced {
213        #[inline]
214        fn into_response(self) -> Response {
215            self.0
216        }
217    }
218
219    #[allow(missing_debug_implementations)]
220    #[derive(Clone, Copy)]
221    pub struct WithTrace<FN, F> {
222        pub(super) filter: F,
223        pub(super) trace: Trace<FN>,
224    }
225
226    use tracing::instrument::{Instrument, Instrumented};
227    use tracing::Span;
228
229    fn finished_logger<E: IsReject>(reply: &Result<(Traced,), E>) {
230        let (status, error) = match reply {
231            Ok((Traced(resp),)) => (resp.status(), None),
232            Err(error) => (error.status(), Some(error)),
233        };
234
235        if status.is_success() {
236            tracing::info!(
237                target: "warp::filters::trace",
238                status = status.as_u16(),
239                "finished processing with success"
240            );
241        } else if status.is_server_error() {
242            tracing::error!(
243                target: "warp::filters::trace",
244                status = status.as_u16(),
245                error = ?error,
246                "unable to process request (internal error)"
247            );
248        } else if status.is_client_error() {
249            tracing::warn!(
250                target: "warp::filters::trace",
251                status = status.as_u16(),
252                error = ?error,
253                "unable to serve request (client error)"
254            );
255        } else {
256            // Either informational or redirect
257            tracing::info!(
258                target: "warp::filters::trace",
259                status = status.as_u16(),
260                error = ?error,
261                    "finished processing with status"
262            );
263        }
264    }
265
266    fn convert_reply<R: Reply>(reply: R) -> (Traced,) {
267        (Traced(reply.into_response()),)
268    }
269
270    impl<FN, F> FilterBase for WithTrace<FN, F>
271    where
272        FN: Fn(Info<'_>) -> Span + Clone + Send,
273        F: Filter + Clone + Send,
274        F::Extract: Reply,
275        F::Error: IsReject,
276    {
277        type Extract = (Traced,);
278        type Error = F::Error;
279        type Future = Instrumented<
280            Inspect<
281                MapOk<F::Future, fn(F::Extract) -> Self::Extract>,
282                fn(&Result<Self::Extract, F::Error>),
283            >,
284        >;
285
286        fn filter(&self, _: Internal) -> Self::Future {
287            let span = route::with(|route| (self.trace.func)(Info { route }));
288            let _entered = span.enter();
289
290            tracing::info!(target: "warp::filters::trace", "processing request");
291            self.filter
292                .filter(Internal)
293                .map_ok(convert_reply as fn(F::Extract) -> Self::Extract)
294                .inspect(finished_logger as fn(&Result<Self::Extract, F::Error>))
295                .instrument(span.clone())
296        }
297    }
298}