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