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}