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