Skip to main content

trillium_logger/
formatters.rs

1use crate::LogFormatter;
2use colored::{ColoredString, Colorize};
3use size::{Base, Size};
4use std::{borrow::Cow, fmt::Display, sync::Arc, time::Instant};
5use trillium::{Conn, HeaderName, KnownHeaderName, Method, Status, Version};
6
7/// [apache combined log format][apache]
8///
9/// [apache]: https://httpd.apache.org/docs/current/logs.html#combined
10///
11/// This is defined as follows:
12///
13/// [`apache_combined`](`request_id`, `user_id`) [`request_header`]`("referrer")`
14/// [`request_header`]`("user-agent")`
15///
16/// where `request_id` and `user_id` are mandatory formatters provided at time of usage.
17///
18///
19/// ## usage with empty `request_id` and `user_id`
20/// ```
21/// # use trillium_logger::{Logger, apache_combined};
22/// Logger::new().with_formatter(apache_combined("-", "-"));
23/// ```
24///
25/// ## usage with an app-specific `user_id`
26///
27/// ```
28/// # use trillium_logger::{Logger, apache_combined};
29/// # use trillium::Conn;
30/// # use std::borrow::Cow;
31/// # struct User(String); impl User { fn name(&self) -> &str { &self.0 } }
32/// fn user(conn: &Conn, color: bool) -> Cow<'static, str> {
33///     match conn.state::<User>() {
34///         Some(user) => String::from(user.name()).into(),
35///         None => "guest".into(),
36///     }
37/// }
38///
39/// Logger::new().with_formatter(apache_combined("-", user));
40/// ```
41pub fn apache_combined(
42    request_id: impl LogFormatter,
43    user_id: impl LogFormatter,
44) -> impl LogFormatter {
45    (
46        apache_common(request_id, user_id),
47        " ",
48        request_header(KnownHeaderName::Referer),
49        " ",
50        request_header(KnownHeaderName::UserAgent),
51    )
52}
53
54/// formatter for the conn's http method that delegates to [`Method`]'s
55/// [`Display`] implementation
56pub fn method(conn: &Conn, _color: bool) -> Method {
57    conn.method()
58}
59
60/// simple development-mode formatter
61///
62/// composed of
63///
64/// `"`[`method`] [`url()`] [`response_time`] [`status`]`"`
65pub fn dev_formatter(conn: &Conn, color: bool) -> impl Display + Send + 'static + use<> {
66    (
67        version,
68        " ",
69        method,
70        " ",
71        url,
72        " ",
73        response_time,
74        " ",
75        status,
76    )
77        .format(conn, color)
78}
79
80/// formatter for the peer ip address of the connection
81///
82/// **note**: this can be modified by handlers prior to logging, such as
83/// when running a trillium application behind a reverse proxy or load
84/// balancer that sets a `forwarded` or `x-forwarded-for` header. this
85/// will display `"-"` if there is no available peer ip address, such as
86/// when running on a runtime adapter that does not have access to this
87/// information
88pub fn ip(conn: &Conn, _color: bool) -> Cow<'static, str> {
89    match conn.peer_ip() {
90        Some(peer) => format!("{peer:?}").into(),
91        None => "-".into(),
92    }
93}
94
95mod status_mod {
96    use super::*;
97    /// The display type for [`status`]
98    #[derive(Copy, Clone)]
99    pub struct StatusOutput(Status, bool);
100    impl Display for StatusOutput {
101        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102            let StatusOutput(status, color) = *self;
103            let status_string = (status as u16).to_string();
104            if color {
105                f.write_fmt(format_args!(
106                    "{}",
107                    status_string.color(match status as u16 {
108                        200..=299 => "green",
109                        300..=399 => "cyan",
110                        400..=499 => "yellow",
111                        500..=599 => "red",
112                        _ => "white",
113                    })
114                ))
115            } else {
116                f.write_str(&status_string)
117            }
118        }
119    }
120
121    /// formatter for the http status
122    ///
123    /// displays just the numeric code of the
124    /// status. when color is enabled, it uses the following color encoding:
125    ///
126    /// | code | color  |
127    /// |------|--------|
128    /// | 2xx  | green  |
129    /// | 3xx  | cyan   |
130    /// | 4xx  | yellow |
131    /// | 5xx  | red    |
132    /// | ???  | white  |
133    pub fn status(conn: &Conn, color: bool) -> StatusOutput {
134        StatusOutput(conn.status().unwrap_or(Status::NotFound), color)
135    }
136}
137
138pub use status_mod::status;
139
140/// formatter-builder for a particular request header, formatted wrapped
141/// in quotes. `""` if the header is not present
142///
143/// usage:
144///
145/// ```rust
146/// # use trillium_logger::{Logger, formatters::request_header};
147/// Logger::new().with_formatter(("user-agent: ", request_header("user-agent")));
148///
149/// // or
150///
151/// Logger::new().with_formatter((
152///     "user-agent: ",
153///     request_header(trillium::KnownHeaderName::UserAgent),
154/// ));
155/// ```
156///
157/// **note**: this is not a formatter itself, but returns a formatter when
158/// called with a header name
159pub fn request_header(header_name: impl Into<HeaderName<'static>>) -> impl LogFormatter {
160    let header_name = header_name.into();
161    move |conn: &Conn, _color: bool| {
162        format!(
163            "{:?}",
164            conn.request_headers()
165                .get_str(header_name.clone())
166                .unwrap_or("")
167        )
168    }
169}
170
171/// formatter-builder for a particular response header, formatted wrapped
172/// in quotes. `""` if the header is not present
173///
174/// usage:
175///
176/// ```rust
177/// # use trillium_logger::{Logger, formatters::response_header};
178/// Logger::new().with_formatter((
179///     "location: ",
180///     response_header(trillium::KnownHeaderName::Location),
181/// ));
182/// // or
183/// Logger::new().with_formatter(("location: ", response_header("Location")));
184/// ```
185///
186/// **note**: this is not a formatter itself, but returns a formatter when
187/// called with a header name
188pub fn response_header(header_name: impl Into<HeaderName<'static>>) -> impl LogFormatter {
189    let header_name = header_name.into();
190    move |conn: &Conn, _color: bool| {
191        format!(
192            "{:?}",
193            conn.response_headers()
194                .get_str(header_name.clone())
195                .unwrap_or("")
196        )
197    }
198}
199
200mod timestamp_mod {
201    use super::*;
202    use time::{OffsetDateTime, macros::format_description};
203    /// Display output for [`timestamp`]
204    pub struct Now;
205
206    /// formatter for the current timestamp. this represents the time that the
207    /// log is written, not the beginning timestamp of the request
208    pub fn timestamp(_conn: &Conn, _color: bool) -> Now {
209        Now
210    }
211
212    // apache time format is 10/Oct/2000:13:55:36 -0700
213    impl Display for Now {
214        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215            let now = OffsetDateTime::now_local()
216                .unwrap_or_else(|_| OffsetDateTime::now_utc())
217                .format(format_description!(
218                    version = 2,
219                    "[day]/[month repr:short]/[year repr:full]:[hour repr:24]:[minute]:[second] \
220                     [offset_hour sign:mandatory][offset_minute]"
221                ))
222                .unwrap();
223            f.write_str(&now)
224        }
225    }
226}
227pub use timestamp_mod::timestamp;
228
229/// formatter for the response body length, represented as a
230/// human-readable string like `5 bytes` or `10.1mb`. prints `-` if there
231/// is no response body. see [`bytes`] for the raw number of bytes
232pub fn body_len_human(conn: &Conn, _color: bool) -> Cow<'static, str> {
233    conn.response_len()
234        .map(|l| {
235            Size::from_bytes(l)
236                .format()
237                .with_base(Base::Base10)
238                .to_string()
239                .into()
240        })
241        .unwrap_or_else(|| Cow::from("-"))
242}
243
244/// [apache common log format][apache]
245///
246/// [apache]: https://httpd.apache.org/docs/current/logs.html#common
247///
248/// This is defined as follows:
249///
250/// [`ip`] `request_id` `user_id` `\[`[`timestamp`]`\]` "[`method`] [`url()`] [`version`]"
251/// [`status`] [`bytes`]
252///
253/// where `request_id` and `user_id` are mandatory formatters provided at time of usage.
254///
255/// ## usage without `request_id` or `user_id`
256/// ```
257/// # use trillium_logger::{Logger, apache_common};
258/// Logger::new().with_formatter(apache_common("-", "-"));
259/// ```
260///
261/// ## usage with app-specific `user_id`
262/// ```
263/// # use trillium_logger::{Logger, apache_common};
264/// # use trillium::Conn;
265/// # use std::borrow::Cow;
266/// # struct User(String); impl User { fn name(&self) -> &str { &self.0 } }
267/// fn user(conn: &Conn, color: bool) -> Cow<'static, str> {
268///     match conn.state::<User>() {
269///         Some(user) => String::from(user.name()).into(),
270///         None => "guest".into(),
271///     }
272/// }
273///
274/// Logger::new().with_formatter(apache_common("-", user));
275/// ```
276pub fn apache_common(
277    request_id: impl LogFormatter,
278    user_id: impl LogFormatter,
279) -> impl LogFormatter {
280    (
281        ip, " ", request_id, " ", user_id, " [", timestamp, "] \"", method, " ", url, " ", version,
282        "\" ", status, " ", bytes,
283    )
284}
285
286/// formatter that prints the number of response body bytes as a
287/// number. see [`body_len_human`] for a human-readable response body
288/// length with units
289pub fn bytes(conn: &Conn, _color: bool) -> u64 {
290    conn.response_len().unwrap_or_default()
291}
292
293/// formatter that prints an emoji if the request is secure as determined
294/// by [`Conn::is_secure`]
295pub fn secure(conn: &Conn, _: bool) -> &'static str {
296    if conn.is_secure() { "🔒" } else { "  " }
297}
298
299/// formatter for the current url or path of the request, including query
300pub fn url(conn: &Conn, _color: bool) -> String {
301    match conn.querystring() {
302        "" => conn.path().into(),
303        query => format!("{}?{}", conn.path(), query),
304    }
305}
306
307mod response_time_mod {
308    use super::*;
309    /// display output type for the [`response_time`] formatter
310    pub struct ResponseTimeOutput(Instant);
311    impl Display for ResponseTimeOutput {
312        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313            f.write_fmt(format_args!("{:?}", Instant::now() - self.0))
314        }
315    }
316
317    /// formatter for the wall-time duration with units that this http
318    /// request-response cycle took, from the first bytes read to the
319    /// completion of the response.
320    pub fn response_time(conn: &Conn, _color: bool) -> ResponseTimeOutput {
321        ResponseTimeOutput(conn.start_time())
322    }
323}
324
325pub use response_time_mod::response_time;
326
327/// formatter for the http version, as delegated to the display
328/// implementation of [`Version`]
329pub fn version(conn: &Conn, _color: bool) -> Version {
330    conn.http_version()
331}
332
333impl LogFormatter for &'static str {
334    type Output = Self;
335
336    fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
337        self
338    }
339}
340
341impl LogFormatter for Arc<str> {
342    type Output = Self;
343
344    fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
345        Arc::clone(self)
346    }
347}
348
349impl LogFormatter for ColoredString {
350    type Output = String;
351
352    fn format(&self, _conn: &Conn, color: bool) -> Self::Output {
353        if color {
354            self.to_string()
355        } else {
356            (**self).to_string()
357        }
358    }
359}
360
361impl<F, O> LogFormatter for F
362where
363    F: Fn(&Conn, bool) -> O + Send + Sync + 'static,
364    O: Display + Send + Sync + 'static,
365{
366    type Output = O;
367
368    fn format(&self, conn: &Conn, color: bool) -> Self::Output {
369        self(conn, color)
370    }
371}
372
373mod tuples {
374    use super::*;
375    /// display output for the tuple implementation
376    ///
377    /// The Display type of each tuple element is contained in this type, and
378    /// it implements [`Display`] for 2-26-arity tuples.
379    ///
380    /// Please open an issue if you find yourself needing to do something with
381    /// this other than [`Display`] it.
382    pub struct TupleOutput<O>(O);
383    macro_rules! impl_formatter_tuple {
384        ($($name:ident)+) => (
385            #[allow(non_snake_case)]
386            impl<$($name,)*> Display for TupleOutput<($($name,)*)> where $($name: Display + Send + Sync + 'static,)* {
387                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388                    let ($(ref $name,)*) = self.0;
389                    f.write_fmt(format_args!(
390                        concat!($(
391                            concat!("{",stringify!($name) ,":}")
392                        ),*),
393                        $($name = ($name)),*
394                    ))
395                }
396            }
397
398            #[allow(non_snake_case)]
399            impl<$($name),*> LogFormatter for ($($name,)*) where $($name: LogFormatter),* {
400                type Output = TupleOutput<($($name::Output,)*)>;
401                fn format(&self, conn: &Conn, color: bool) -> Self::Output {
402                    let ($(ref $name,)*) = *self;
403                    TupleOutput(($(($name).format(conn, color),)*))
404                }
405            }
406        )
407    }
408
409    impl_formatter_tuple! { A B }
410    impl_formatter_tuple! { A B C }
411    impl_formatter_tuple! { A B C D }
412    impl_formatter_tuple! { A B C D E }
413    impl_formatter_tuple! { A B C D E F }
414    impl_formatter_tuple! { A B C D E F G }
415    impl_formatter_tuple! { A B C D E F G H }
416    impl_formatter_tuple! { A B C D E F G H I }
417    impl_formatter_tuple! { A B C D E F G H I J }
418    impl_formatter_tuple! { A B C D E F G H I J K }
419    impl_formatter_tuple! { A B C D E F G H I J K L }
420    impl_formatter_tuple! { A B C D E F G H I J K L M }
421    impl_formatter_tuple! { A B C D E F G H I J K L M N }
422    impl_formatter_tuple! { A B C D E F G H I J K L M N O }
423    impl_formatter_tuple! { A B C D E F G H I J K L M N O P }
424    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q }
425    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R }
426    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S }
427    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T }
428    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U }
429    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V }
430    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V W }
431    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V W X }
432    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V W X Y }
433    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V W X Y Z }
434}