Skip to main content

trillium_logger/
client.rs

1//! Request/response logging for [`trillium-client`][trillium_client].
2//!
3//! This module is gated behind the `client` cargo feature. It provides [`ClientLogger`], a
4//! [`ClientHandler`] that emits a log line per request, with a composable formatter system that
5//! mirrors the server-side [`Logger`][crate::Logger] in spirit.
6//!
7//! # Lifecycle
8//!
9//! `ClientLogger` records a request-start instant the first time it runs and emits the
10//! formatted line after the response is available. Its position in the handler chain
11//! determines the scope of [`response_time`][formatters::response_time] — only handlers running
12//! after `ClientLogger` are timed.
13//!
14//! A log line is emitted for every request, regardless of outcome: successful responses,
15//! responses synthesized by an upstream handler (cache hit, mock), and transport-layer
16//! failures (connection refused, TLS error, timeout) all land in the log. The [`dev_formatter`]
17//! renders the transport error inline when one is stashed on the conn; custom formatters can
18//! pick it up via the [`error`][formatters::error] component.
19//!
20//! # Formatters
21//!
22//! See [`ClientLogFormatter`] for the trait, and the [`formatters`] submodule for the building
23//! blocks. The default is [`dev_formatter`]. The [`client_log_format!`] macro builds a formatter
24//! from a [`format_args!`]-style string, mirroring [`log_format!`](crate::log_format) on the server
25//! side:
26//!
27//! ```
28//! use trillium_logger::client::{ClientLogger, client_log_format};
29//! ClientLogger::new().with_formatter(client_log_format!("{method} {url} -> {status}"));
30//! ```
31//!
32//! # Example
33//!
34//! ```no_run
35//! use trillium_client::Client;
36//! use trillium_logger::client::{ClientLogger, formatters};
37//! # use trillium_testing::client_config;
38//!
39//! let client = Client::new(client_config()).with_handler(ClientLogger::new().with_formatter((
40//!     formatters::method,
41//!     " ",
42//!     formatters::url,
43//!     " -> ",
44//!     formatters::status,
45//! )));
46//! ```
47
48use crate::{ColorMode, Target, Targetable};
49use std::{borrow::Cow, fmt::Display, sync::Arc, time::Instant};
50use trillium_client::{ClientHandler, Conn, Result};
51
52pub mod formatters;
53pub use formatters::dev_formatter;
54pub use trillium_logger_macros::client_log_format;
55
56/// The interface to format a [`client::Conn`][Conn] as a [`Display`]-able output.
57///
58/// Mirrors the server-side [`LogFormatter`][crate::LogFormatter] trait, but takes a
59/// [`trillium_client::Conn`] rather than a [`trillium::Conn`].
60///
61/// ## Implementations
62///
63/// `ClientLogFormatter` is implemented for:
64///
65/// - all 2-26-arity tuples of `ClientLogFormatter`s, output concatenated with no separator
66/// - `&'static str` and `Arc<str>`, for interspersing static text
67/// - `Fn(&Conn, bool) -> impl Display`, the most common way to write a custom formatter
68///
69/// ```rust
70/// use std::borrow::Cow;
71/// use trillium_client::Conn;
72/// use trillium_logger::client::{ClientLogger, formatters};
73///
74/// fn marker(_conn: &Conn, _color: bool) -> Cow<'static, str> {
75///     "[client] ".into()
76/// }
77///
78/// ClientLogger::new().with_formatter((marker, formatters::method, " ", formatters::url));
79/// ```
80pub trait ClientLogFormatter: Send + Sync + 'static {
81    /// The display type for this formatter.
82    ///
83    /// For a simple formatter, this will likely be a `String`, or even better, a lightweight type
84    /// that implements [`Display`].
85    type Output: Display + Send + Sync + 'static;
86
87    /// Extract `Output` from this `Conn`.
88    fn format(&self, conn: &Conn, color: bool) -> Self::Output;
89}
90
91/// Internal state inserted by [`ClientLogger::run`] and read by [`formatters::response_time`].
92#[derive(Copy, Clone, Debug)]
93pub(crate) struct RequestStart(pub(crate) Instant);
94
95/// The [`ClientHandler`] that emits one log line per request.
96pub struct ClientLogger<F> {
97    format: F,
98    color_mode: ColorMode,
99    target: Arc<dyn Targetable>,
100}
101
102impl<F> std::fmt::Debug for ClientLogger<F> {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("ClientLogger")
105            .field("color_mode", &self.color_mode)
106            .finish_non_exhaustive()
107    }
108}
109
110impl ClientLogger<()> {
111    /// Builds a new client logger.
112    ///
113    /// Defaults:
114    ///
115    /// * formatter: [`dev_formatter`]
116    /// * color mode: [`ColorMode::Auto`]
117    /// * target: [`Target::Stdout`]
118    pub fn new() -> ClientLogger<impl ClientLogFormatter> {
119        ClientLogger {
120            format: dev_formatter,
121            color_mode: ColorMode::Auto,
122            target: Arc::new(Target::Stdout),
123        }
124    }
125}
126
127impl<T> ClientLogger<T> {
128    /// Replace the formatter with any type that implements [`ClientLogFormatter`].
129    ///
130    /// ```
131    /// use trillium_logger::client::{ClientLogger, formatters};
132    /// ClientLogger::new().with_formatter((formatters::method, " ", formatters::url));
133    /// ```
134    pub fn with_formatter<Formatter: ClientLogFormatter>(
135        self,
136        formatter: Formatter,
137    ) -> ClientLogger<Formatter> {
138        ClientLogger {
139            format: formatter,
140            color_mode: self.color_mode,
141            target: self.target,
142        }
143    }
144}
145
146impl<F: ClientLogFormatter> ClientLogger<F> {
147    /// Specify the color mode for this logger. See [`ColorMode`] for details.
148    pub fn with_color_mode(mut self, color_mode: ColorMode) -> Self {
149        self.color_mode = color_mode;
150        self
151    }
152
153    /// Specify the logger target. See [`Target`] and [`Targetable`].
154    pub fn with_target(mut self, target: impl Targetable) -> Self {
155        self.target = Arc::new(target);
156        self
157    }
158}
159
160impl<F: ClientLogFormatter> ClientHandler for ClientLogger<F> {
161    async fn run(&self, conn: &mut Conn) -> Result<()> {
162        conn.insert_state(RequestStart(Instant::now()));
163        Ok(())
164    }
165
166    async fn after_response(&self, conn: &mut Conn) -> Result<()> {
167        let output = self.format.format(conn, self.color_mode.is_enabled());
168        self.target.write(output.to_string());
169        Ok(())
170    }
171
172    fn name(&self) -> Cow<'static, str> {
173        "trillium-logger ClientLogger".into()
174    }
175}
176
177/// Convenience alias for [`ClientLogger::new`].
178pub fn client_logger() -> ClientLogger<impl ClientLogFormatter> {
179    ClientLogger::new()
180}