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/// formatter for the request authority (host), as resolved by [`Conn::host`]
61///
62/// This is the host the request was addressed to — drawn from the `Host` header, the HTTP/2 or
63/// HTTP/3 `:authority` pseudo-header, or an absolute-form request target, depending on protocol.
64/// Displays `"-"` if no host is available.
65pub fn host(conn: &Conn, _color: bool) -> Cow<'static, str> {
66    conn.host()
67        .map_or(Cow::Borrowed("-"), |host| Cow::Owned(host.to_string()))
68}
69
70mod dev_formatter_mod {
71    use super::*;
72    use response_time_mod::ResponseTimeOutput;
73    use status_mod::StatusOutput;
74
75    /// The default formatter: a simple colorized development-mode format
76    ///
77    /// Renders as `{version} {method} {url} {response_time} {status}`, with the same components
78    /// as the formatters of those names. [`dev_formatter`] is the same format as a free function;
79    /// this type exists so the default can be named, as in `Logger<DevFormatter>`.
80    #[derive(Clone, Copy, Debug, Default)]
81    pub struct DevFormatter;
82
83    /// The display type for [`DevFormatter`]
84    #[derive(Clone, Debug)]
85    pub struct DevOutput {
86        version: Version,
87        method: Method,
88        url: String,
89        response_time: ResponseTimeOutput,
90        status: StatusOutput,
91    }
92
93    impl Display for DevOutput {
94        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95            write!(
96                f,
97                "{} {} {} {} {}",
98                self.version, self.method, self.url, self.response_time, self.status
99            )
100        }
101    }
102
103    impl LogFormatter for DevFormatter {
104        type Output = DevOutput;
105
106        fn format(&self, conn: &Conn, color: bool) -> Self::Output {
107            DevOutput {
108                version: conn.http_version(),
109                method: conn.method(),
110                url: url(conn, color),
111                response_time: response_time(conn, color),
112                status: status(conn, color),
113            }
114        }
115    }
116}
117
118pub use dev_formatter_mod::DevFormatter;
119
120/// simple development-mode formatter
121///
122/// composed of
123///
124/// `"`[`method`] [`url`](url()) [`response_time`] [`status`]`"`
125///
126/// This is the same format as [`DevFormatter`], as a free function.
127pub fn dev_formatter(conn: &Conn, color: bool) -> impl Display + Send + 'static + use<> {
128    DevFormatter.format(conn, color)
129}
130
131mod start_formatter_mod {
132    use super::*;
133
134    /// The default formatter for the request-start line
135    ///
136    /// [`Logger::with_start_logging`](crate::Logger::with_start_logging) emits this line when a
137    /// request is received. It renders as `Started {version} {method} {url}`, with the same
138    /// components as [`version`], [`method`], and [`url`](url()).
139    #[derive(Clone, Copy, Debug, Default)]
140    pub struct StartFormatter;
141
142    /// The display type for [`StartFormatter`]
143    #[derive(Clone, Debug)]
144    pub struct StartOutput {
145        version: Version,
146        method: Method,
147        url: String,
148    }
149
150    impl Display for StartOutput {
151        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152            write!(f, "Started {} {} {}", self.version, self.method, self.url)
153        }
154    }
155
156    impl LogFormatter for StartFormatter {
157        type Output = StartOutput;
158
159        fn format(&self, conn: &Conn, color: bool) -> Self::Output {
160            StartOutput {
161                version: conn.http_version(),
162                method: conn.method(),
163                url: url(conn, color),
164            }
165        }
166    }
167}
168
169pub use start_formatter_mod::StartFormatter;
170
171/// formatter for the request-start line
172///
173/// composed of
174///
175/// `"Started "` [`version`] [`method`] [`url`](url())
176///
177/// This is the same format as [`StartFormatter`], as a free function.
178pub fn start_formatter(conn: &Conn, color: bool) -> impl Display + Send + 'static + use<> {
179    StartFormatter.format(conn, color)
180}
181
182/// formatter for the peer ip address of the connection
183///
184/// **note**: this can be modified by handlers prior to logging, such as
185/// when running a trillium application behind a reverse proxy or load
186/// balancer that sets a `forwarded` or `x-forwarded-for` header. this
187/// will display `"-"` if there is no available peer ip address, such as
188/// when running on a runtime adapter that does not have access to this
189/// information
190pub fn ip(conn: &Conn, _color: bool) -> Cow<'static, str> {
191    match conn.peer_ip() {
192        Some(peer) => format!("{peer:?}").into(),
193        None => "-".into(),
194    }
195}
196
197mod status_mod {
198    use super::*;
199    /// The display type for [`status`]
200    #[derive(Copy, Clone, Debug)]
201    pub struct StatusOutput(Status, bool);
202    impl Display for StatusOutput {
203        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204            let StatusOutput(status, color) = *self;
205            let status_string = (status as u16).to_string();
206            if color {
207                f.write_fmt(format_args!(
208                    "{}",
209                    status_string.color(match status as u16 {
210                        200..=299 => "green",
211                        300..=399 => "cyan",
212                        400..=499 => "yellow",
213                        500..=599 => "red",
214                        _ => "white",
215                    })
216                ))
217            } else {
218                f.write_str(&status_string)
219            }
220        }
221    }
222
223    /// formatter for the http status
224    ///
225    /// displays just the numeric code of the
226    /// status. when color is enabled, it uses the following color encoding:
227    ///
228    /// | code | color  |
229    /// |------|--------|
230    /// | 2xx  | green  |
231    /// | 3xx  | cyan   |
232    /// | 4xx  | yellow |
233    /// | 5xx  | red    |
234    /// | ???  | white  |
235    pub fn status(conn: &Conn, color: bool) -> StatusOutput {
236        StatusOutput(conn.status().unwrap_or(Status::NotFound), color)
237    }
238}
239
240pub use status_mod::status;
241
242/// formatter-builder for a particular request header, formatted wrapped
243/// in quotes. `""` if the header is not present
244///
245/// usage:
246///
247/// ```rust
248/// # use trillium_logger::{Logger, formatters::request_header};
249/// Logger::new().with_formatter(("user-agent: ", request_header("user-agent")));
250///
251/// // or
252///
253/// Logger::new().with_formatter((
254///     "user-agent: ",
255///     request_header(trillium::KnownHeaderName::UserAgent),
256/// ));
257/// ```
258///
259/// **note**: this is not a formatter itself, but returns a formatter when
260/// called with a header name
261pub fn request_header(header_name: impl Into<HeaderName<'static>>) -> impl LogFormatter {
262    let header_name = header_name.into();
263    move |conn: &Conn, _color: bool| {
264        format!(
265            "{:?}",
266            conn.request_headers()
267                .get_str(header_name.clone())
268                .unwrap_or("")
269        )
270    }
271}
272
273/// formatter for the request `User-Agent` header, wrapped in quotes. `""` if absent
274///
275/// Equivalent to [`request_header`]`(`[`KnownHeaderName::UserAgent`]`)`, provided as a bare
276/// formatter so it can be named directly in a format.
277pub fn user_agent(conn: &Conn, _color: bool) -> String {
278    format!(
279        "{:?}",
280        conn.request_headers()
281            .get_str(KnownHeaderName::UserAgent)
282            .unwrap_or("")
283    )
284}
285
286/// formatter for the request `Referer` header, wrapped in quotes. `""` if absent
287///
288/// Equivalent to [`request_header`]`(`[`KnownHeaderName::Referer`]`)`, provided as a bare formatter
289/// so it can be named directly in a format.
290pub fn referer(conn: &Conn, _color: bool) -> String {
291    format!(
292        "{:?}",
293        conn.request_headers()
294            .get_str(KnownHeaderName::Referer)
295            .unwrap_or("")
296    )
297}
298
299/// formatter-builder for a particular response header, formatted wrapped
300/// in quotes. `""` if the header is not present
301///
302/// usage:
303///
304/// ```rust
305/// # use trillium_logger::{Logger, formatters::response_header};
306/// Logger::new().with_formatter((
307///     "location: ",
308///     response_header(trillium::KnownHeaderName::Location),
309/// ));
310/// // or
311/// Logger::new().with_formatter(("location: ", response_header("Location")));
312/// ```
313///
314/// **note**: this is not a formatter itself, but returns a formatter when
315/// called with a header name
316pub fn response_header(header_name: impl Into<HeaderName<'static>>) -> impl LogFormatter {
317    let header_name = header_name.into();
318    move |conn: &Conn, _color: bool| {
319        format!(
320            "{:?}",
321            conn.response_headers()
322                .get_str(header_name.clone())
323                .unwrap_or("")
324        )
325    }
326}
327
328mod timestamp_mod {
329    use super::*;
330    use time::{OffsetDateTime, macros::format_description};
331    /// Display output for [`timestamp`]
332    pub struct Now;
333
334    /// formatter for the current timestamp. this represents the time that the
335    /// log is written, not the beginning timestamp of the request
336    pub fn timestamp(_conn: &Conn, _color: bool) -> Now {
337        Now
338    }
339
340    // apache time format is 10/Oct/2000:13:55:36 -0700
341    impl Display for Now {
342        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343            let now = OffsetDateTime::now_local()
344                .unwrap_or_else(|_| OffsetDateTime::now_utc())
345                .format(format_description!(
346                    version = 2,
347                    "[day]/[month repr:short]/[year repr:full]:[hour repr:24]:[minute]:[second] \
348                     [offset_hour sign:mandatory][offset_minute]"
349                ))
350                .unwrap();
351            f.write_str(&now)
352        }
353    }
354}
355pub use timestamp_mod::timestamp;
356
357/// formatter for the response body length, represented as a
358/// human-readable string like `5 bytes` or `10.1mb`. prints `-` if there
359/// is no response body. see [`bytes`] for the raw number of bytes
360pub fn body_len_human(conn: &Conn, _color: bool) -> Cow<'static, str> {
361    conn.response_len()
362        .map(|l| {
363            Size::from_bytes(l)
364                .format()
365                .with_base(Base::Base10)
366                .to_string()
367                .into()
368        })
369        .unwrap_or_else(|| Cow::from("-"))
370}
371
372/// [apache common log format][apache]
373///
374/// [apache]: https://httpd.apache.org/docs/current/logs.html#common
375///
376/// This is defined as follows:
377///
378/// [`ip`] `request_id` `user_id` `\[`[`timestamp`]`\]` "[`method`] [`url()`] [`version`]"
379/// [`status`] [`bytes`]
380///
381/// where `request_id` and `user_id` are mandatory formatters provided at time of usage.
382///
383/// ## usage without `request_id` or `user_id`
384/// ```
385/// # use trillium_logger::{Logger, apache_common};
386/// Logger::new().with_formatter(apache_common("-", "-"));
387/// ```
388///
389/// ## usage with app-specific `user_id`
390/// ```
391/// # use trillium_logger::{Logger, apache_common};
392/// # use trillium::Conn;
393/// # use std::borrow::Cow;
394/// # struct User(String); impl User { fn name(&self) -> &str { &self.0 } }
395/// fn user(conn: &Conn, color: bool) -> Cow<'static, str> {
396///     match conn.state::<User>() {
397///         Some(user) => String::from(user.name()).into(),
398///         None => "guest".into(),
399///     }
400/// }
401///
402/// Logger::new().with_formatter(apache_common("-", user));
403/// ```
404pub fn apache_common(
405    request_id: impl LogFormatter,
406    user_id: impl LogFormatter,
407) -> impl LogFormatter {
408    (
409        ip, " ", request_id, " ", user_id, " [", timestamp, "] \"", method, " ", url, " ", version,
410        "\" ", status, " ", bytes,
411    )
412}
413
414/// formatter that prints the number of response body bytes as a
415/// number. see [`body_len_human`] for a human-readable response body
416/// length with units
417pub fn bytes(conn: &Conn, _color: bool) -> u64 {
418    conn.response_len().unwrap_or_default()
419}
420
421/// formatter that prints an emoji if the request is secure as determined
422/// by [`Conn::is_secure`]
423pub fn secure(conn: &Conn, _: bool) -> &'static str {
424    if conn.is_secure() { "🔒" } else { "  " }
425}
426
427/// formatter for the current url or path of the request, including query
428pub fn url(conn: &Conn, _color: bool) -> String {
429    match conn.querystring() {
430        "" => conn.path().into(),
431        query => format!("{}?{}", conn.path(), query),
432    }
433}
434
435mod response_time_mod {
436    use super::*;
437    /// display output type for the [`response_time`] formatter
438    #[derive(Clone, Copy, Debug)]
439    pub struct ResponseTimeOutput(Instant);
440    impl Display for ResponseTimeOutput {
441        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442            f.write_fmt(format_args!("{:?}", Instant::now() - self.0))
443        }
444    }
445
446    /// formatter for the wall-time duration with units that this http
447    /// request-response cycle took, from the first bytes read to the
448    /// completion of the response.
449    pub fn response_time(conn: &Conn, _color: bool) -> ResponseTimeOutput {
450        ResponseTimeOutput(conn.start_time())
451    }
452}
453
454pub use response_time_mod::response_time;
455
456/// formatter for the http version, as delegated to the display
457/// implementation of [`Version`]
458pub fn version(conn: &Conn, _color: bool) -> Version {
459    conn.http_version()
460}
461
462impl LogFormatter for &'static str {
463    type Output = Self;
464
465    fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
466        self
467    }
468}
469
470impl LogFormatter for Arc<str> {
471    type Output = Self;
472
473    fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
474        Arc::clone(self)
475    }
476}
477
478impl LogFormatter for ColoredString {
479    type Output = String;
480
481    fn format(&self, _conn: &Conn, color: bool) -> Self::Output {
482        if color {
483            self.to_string()
484        } else {
485            (**self).to_string()
486        }
487    }
488}
489
490impl<F, O> LogFormatter for F
491where
492    F: Fn(&Conn, bool) -> O + Send + Sync + 'static,
493    O: Display + Send + Sync + 'static,
494{
495    type Output = O;
496
497    fn format(&self, conn: &Conn, color: bool) -> Self::Output {
498        self(conn, color)
499    }
500}
501
502mod tuples {
503    use super::*;
504    /// display output for the tuple implementation
505    ///
506    /// The Display type of each tuple element is contained in this type, and
507    /// it implements [`Display`] for 2-26-arity tuples.
508    ///
509    /// Please open an issue if you find yourself needing to do something with
510    /// this other than [`Display`] it.
511    pub struct TupleOutput<O>(O);
512    macro_rules! impl_formatter_tuple {
513        ($($name:ident)+) => (
514            #[allow(non_snake_case)]
515            impl<$($name,)*> Display for TupleOutput<($($name,)*)> where $($name: Display + Send + Sync + 'static,)* {
516                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517                    let ($(ref $name,)*) = self.0;
518                    f.write_fmt(format_args!(
519                        concat!($(
520                            concat!("{",stringify!($name) ,":}")
521                        ),*),
522                        $($name = ($name)),*
523                    ))
524                }
525            }
526
527            #[allow(non_snake_case)]
528            impl<$($name),*> LogFormatter for ($($name,)*) where $($name: LogFormatter),* {
529                type Output = TupleOutput<($($name::Output,)*)>;
530                fn format(&self, conn: &Conn, color: bool) -> Self::Output {
531                    let ($(ref $name,)*) = *self;
532                    TupleOutput(($(($name).format(conn, color),)*))
533                }
534            }
535        )
536    }
537
538    impl_formatter_tuple! { A B }
539    impl_formatter_tuple! { A B C }
540    impl_formatter_tuple! { A B C D }
541    impl_formatter_tuple! { A B C D E }
542    impl_formatter_tuple! { A B C D E F }
543    impl_formatter_tuple! { A B C D E F G }
544    impl_formatter_tuple! { A B C D E F G H }
545    impl_formatter_tuple! { A B C D E F G H I }
546    impl_formatter_tuple! { A B C D E F G H I J }
547    impl_formatter_tuple! { A B C D E F G H I J K }
548    impl_formatter_tuple! { A B C D E F G H I J K L }
549    impl_formatter_tuple! { A B C D E F G H I J K L M }
550    impl_formatter_tuple! { A B C D E F G H I J K L M N }
551    impl_formatter_tuple! { A B C D E F G H I J K L M N O }
552    impl_formatter_tuple! { A B C D E F G H I J K L M N O P }
553    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q }
554    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R }
555    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S }
556    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T }
557    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U }
558    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 }
559    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 }
560    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 }
561    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 }
562    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 }
563}