Skip to main content

trillium_logger/client/
formatters.rs

1//! Building-block formatters for [`ClientLogger`][super::ClientLogger].
2//!
3//! Compose them into tuples to build a full log line. The default top-level format is
4//! [`dev_formatter`].
5
6use super::{ClientLogFormatter, RequestStart};
7use colored::{ColoredString, Colorize};
8use size::{Base, Size};
9use std::{borrow::Cow, fmt::Display, sync::Arc, time::Instant};
10use trillium_client::{Conn, ConnExt, HeaderName, Method, Status, Version};
11
12/// The default development-mode formatter.
13///
14/// Composed of:
15///
16/// `"`[`version`] [`method`] [`url()`] [`status`] [`response_time`][`error`]`"`
17///
18/// The [`error()`] component is empty on success. When the transport failed, it renders as
19/// ` <error message>` — the leading space is part of the formatter, so the format string is
20/// concatenation, not separator-joined.
21pub fn dev_formatter(conn: &Conn, color: bool) -> impl Display + Send + 'static + use<> {
22    (
23        version,
24        " ",
25        method,
26        " ",
27        url,
28        " ",
29        status,
30        " ",
31        response_time,
32        error,
33    )
34        .format(conn, color)
35}
36
37/// Formatter for the conn's HTTP method.
38pub fn method(conn: &Conn, _color: bool) -> Method {
39    conn.method()
40}
41
42/// Formatter for the full request URL (scheme, host, path, query).
43pub fn url(conn: &Conn, _color: bool) -> String {
44    conn.url().to_string()
45}
46
47/// Formatter for the remote socket address the request connected to, per
48/// [`Conn::peer_addr`][trillium_client::Conn::peer_addr].
49///
50/// Displays `"-"` when the address is unavailable, such as over a transport that does not expose
51/// one.
52pub fn peer_addr(conn: &Conn, _color: bool) -> Cow<'static, str> {
53    conn.peer_addr()
54        .map_or(Cow::Borrowed("-"), |addr| Cow::Owned(addr.to_string()))
55}
56
57/// Formatter for the HTTP version used on the wire.
58///
59/// Because log output renders after the request executes, this reflects the version actually
60/// negotiated — an h2→h3 upgrade via `Alt-Svc` shows up here, not the originally-requested
61/// version.
62pub fn version(conn: &Conn, _color: bool) -> Version {
63    conn.http_version()
64}
65
66mod status_mod {
67    use super::*;
68    /// Display output for [`status`].
69    #[derive(Copy, Clone)]
70    pub struct StatusOutput(Option<Status>, bool);
71
72    impl Display for StatusOutput {
73        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74            let StatusOutput(status, color) = *self;
75            let Some(status) = status else {
76                return f.write_str("---");
77            };
78            let s = (status as u16).to_string();
79            if color {
80                f.write_fmt(format_args!(
81                    "{}",
82                    s.color(match status as u16 {
83                        200..=299 => "green",
84                        300..=399 => "cyan",
85                        400..=499 => "yellow",
86                        500..=599 => "red",
87                        _ => "white",
88                    })
89                ))
90            } else {
91                f.write_str(&s)
92            }
93        }
94    }
95
96    /// Formatter for the HTTP response status.
97    ///
98    /// Displays the numeric status code, or `---` if no response was received. With color enabled,
99    /// 2xx is green, 3xx cyan, 4xx yellow, 5xx red.
100    pub fn status(conn: &Conn, color: bool) -> StatusOutput {
101        StatusOutput(conn.status(), color)
102    }
103}
104
105pub use status_mod::status;
106
107/// Formatter-builder for a particular request header, wrapped in quotes. Produces `""` if the
108/// header is not present.
109pub fn request_header(header_name: impl Into<HeaderName<'static>>) -> impl ClientLogFormatter {
110    let header_name = header_name.into();
111    move |conn: &Conn, _color: bool| {
112        format!(
113            "{:?}",
114            conn.request_headers()
115                .get_str(header_name.clone())
116                .unwrap_or("")
117        )
118    }
119}
120
121/// Formatter-builder for a particular response header, wrapped in quotes. Produces `""` if the
122/// header is not present.
123pub fn response_header(header_name: impl Into<HeaderName<'static>>) -> impl ClientLogFormatter {
124    let header_name = header_name.into();
125    move |conn: &Conn, _color: bool| {
126        format!(
127            "{:?}",
128            conn.response_headers()
129                .get_str(header_name.clone())
130                .unwrap_or("")
131        )
132    }
133}
134
135mod timestamp_mod {
136    use super::*;
137    use time::{OffsetDateTime, macros::format_description};
138
139    /// Display output for [`timestamp`].
140    pub struct Now;
141
142    /// Formatter for the current timestamp at log-write time (apache format,
143    /// `10/Oct/2000:13:55:36 -0700`).
144    pub fn timestamp(_conn: &Conn, _color: bool) -> Now {
145        Now
146    }
147
148    impl Display for Now {
149        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150            let now = OffsetDateTime::now_local()
151                .unwrap_or_else(|_| OffsetDateTime::now_utc())
152                .format(format_description!(
153                    version = 2,
154                    "[day]/[month repr:short]/[year repr:full]:[hour repr:24]:[minute]:[second] \
155                     [offset_hour sign:mandatory][offset_minute]"
156                ))
157                .unwrap();
158            f.write_str(&now)
159        }
160    }
161}
162
163pub use timestamp_mod::timestamp;
164
165/// Formatter for the response Content-Length as a human-readable string (`5 bytes`, `10.1 kb`).
166/// Produces `-` if no Content-Length is set.
167pub fn body_len_human(conn: &Conn, _color: bool) -> Cow<'static, str> {
168    conn.response_headers()
169        .content_length()
170        .map(|l| {
171            Size::from_bytes(l)
172                .format()
173                .with_base(Base::Base10)
174                .to_string()
175                .into()
176        })
177        .unwrap_or_else(|| Cow::from("-"))
178}
179
180/// Formatter for the response Content-Length as a raw byte count, `0` if unknown.
181pub fn bytes(conn: &Conn, _color: bool) -> u64 {
182    conn.response_headers().content_length().unwrap_or_default()
183}
184
185/// Formatter for whether the request used a TLS-bearing scheme (https/wss).
186pub fn secure(conn: &Conn, _color: bool) -> &'static str {
187    match conn.url().scheme() {
188        "https" | "wss" => "🔒",
189        _ => "  ",
190    }
191}
192
193mod response_time_mod {
194    use super::*;
195
196    /// Display output for [`response_time`].
197    pub struct ResponseTimeOutput(Option<Instant>);
198
199    impl Display for ResponseTimeOutput {
200        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201            match self.0 {
202                Some(start) => f.write_fmt(format_args!("{:?}", Instant::now() - start)),
203                None => f.write_str("-"),
204            }
205        }
206    }
207
208    /// Formatter for the wall-clock duration between when
209    /// [`ClientLogger`][super::super::ClientLogger] first ran and when the log line is rendered.
210    ///
211    /// If no [`ClientLogger`][super::super::ClientLogger] preceded this in the handler chain,
212    /// prints `-`.
213    pub fn response_time(conn: &Conn, _color: bool) -> ResponseTimeOutput {
214        ResponseTimeOutput(conn.state::<RequestStart>().map(|RequestStart(i)| *i))
215    }
216}
217
218pub use response_time_mod::response_time;
219
220mod error_mod {
221    use super::*;
222
223    /// Display output for [`error`].
224    pub struct ErrorOutput(Option<String>, bool);
225
226    impl Display for ErrorOutput {
227        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228            let Some(msg) = &self.0 else {
229                return Ok(());
230            };
231            f.write_str(" ")?;
232            if self.1 {
233                f.write_fmt(format_args!("{}", msg.as_str().red()))
234            } else {
235                f.write_str(msg)
236            }
237        }
238    }
239
240    /// Formatter for the transport-level error stashed on the conn, if any.
241    ///
242    /// Renders as ` <error message>` (with a leading space) when an error is present, empty
243    /// otherwise. The leading space is built in so composing into a tuple looks like
244    /// concatenation, not separator-joining; place this where you want the error to land
245    /// without inserting your own separator.
246    ///
247    /// With color enabled, the error message renders in red.
248    pub fn error(conn: &Conn, color: bool) -> ErrorOutput {
249        ErrorOutput(conn.error().map(ToString::to_string), color)
250    }
251}
252
253pub use error_mod::error;
254
255impl ClientLogFormatter for &'static str {
256    type Output = Self;
257
258    fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
259        self
260    }
261}
262
263impl ClientLogFormatter for Arc<str> {
264    type Output = Self;
265
266    fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
267        Arc::clone(self)
268    }
269}
270
271impl ClientLogFormatter for ColoredString {
272    type Output = String;
273
274    fn format(&self, _conn: &Conn, color: bool) -> Self::Output {
275        if color {
276            self.to_string()
277        } else {
278            (**self).to_string()
279        }
280    }
281}
282
283impl<F, O> ClientLogFormatter for F
284where
285    F: Fn(&Conn, bool) -> O + Send + Sync + 'static,
286    O: Display + Send + Sync + 'static,
287{
288    type Output = O;
289
290    fn format(&self, conn: &Conn, color: bool) -> Self::Output {
291        self(conn, color)
292    }
293}
294
295mod tuples {
296    use super::*;
297
298    /// Display output for the tuple implementation. Implements [`Display`] for 2-26-arity tuples
299    /// of `Display` types.
300    pub struct TupleOutput<O>(O);
301
302    macro_rules! impl_formatter_tuple {
303        ($($name:ident)+) => (
304            #[allow(non_snake_case)]
305            impl<$($name,)*> Display for TupleOutput<($($name,)*)>
306            where
307                $($name: Display + Send + Sync + 'static,)*
308            {
309                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310                    let ($(ref $name,)*) = self.0;
311                    f.write_fmt(format_args!(
312                        concat!($(concat!("{", stringify!($name), ":}")),*),
313                        $($name = ($name)),*
314                    ))
315                }
316            }
317
318            #[allow(non_snake_case)]
319            impl<$($name),*> ClientLogFormatter for ($($name,)*)
320            where
321                $($name: ClientLogFormatter),*
322            {
323                type Output = TupleOutput<($($name::Output,)*)>;
324                fn format(&self, conn: &Conn, color: bool) -> Self::Output {
325                    let ($(ref $name,)*) = *self;
326                    TupleOutput(($(($name).format(conn, color),)*))
327                }
328            }
329        )
330    }
331
332    impl_formatter_tuple! { A B }
333    impl_formatter_tuple! { A B C }
334    impl_formatter_tuple! { A B C D }
335    impl_formatter_tuple! { A B C D E }
336    impl_formatter_tuple! { A B C D E F }
337    impl_formatter_tuple! { A B C D E F G }
338    impl_formatter_tuple! { A B C D E F G H }
339    impl_formatter_tuple! { A B C D E F G H I }
340    impl_formatter_tuple! { A B C D E F G H I J }
341    impl_formatter_tuple! { A B C D E F G H I J K }
342    impl_formatter_tuple! { A B C D E F G H I J K L }
343    impl_formatter_tuple! { A B C D E F G H I J K L M }
344    impl_formatter_tuple! { A B C D E F G H I J K L M N }
345    impl_formatter_tuple! { A B C D E F G H I J K L M N O }
346    impl_formatter_tuple! { A B C D E F G H I J K L M N O P }
347    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q }
348    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R }
349    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S }
350    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T }
351    impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U }
352    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 }
353    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 }
354    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 }
355    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 }
356    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 }
357}