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