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`].
24//!
25//! # Example
26//!
27//! ```no_run
28//! use trillium_client::Client;
29//! use trillium_logger::client::{ClientLogger, formatters};
30//! # use trillium_testing::client_config;
31//!
32//! let client = Client::new(client_config()).with_handler(ClientLogger::new().with_formatter((
33//! formatters::method,
34//! " ",
35//! formatters::url,
36//! " -> ",
37//! formatters::status,
38//! )));
39//! ```
40
41use crate::{ColorMode, Target, Targetable};
42use std::{borrow::Cow, fmt::Display, sync::Arc, time::Instant};
43use trillium_client::{ClientHandler, Conn, Result};
44
45pub mod formatters;
46pub use formatters::dev_formatter;
47
48/// The interface to format a [`client::Conn`][Conn] as a [`Display`]-able output.
49///
50/// Mirrors the server-side [`LogFormatter`][crate::LogFormatter] trait, but takes a
51/// [`trillium_client::Conn`] rather than a [`trillium::Conn`].
52///
53/// ## Implementations
54///
55/// `ClientLogFormatter` is implemented for:
56///
57/// - all 2-26-arity tuples of `ClientLogFormatter`s, output concatenated with no separator
58/// - `&'static str` and `Arc<str>`, for interspersing static text
59/// - `Fn(&Conn, bool) -> impl Display`, the most common way to write a custom formatter
60///
61/// ```rust
62/// use std::borrow::Cow;
63/// use trillium_client::Conn;
64/// use trillium_logger::client::{ClientLogger, formatters};
65///
66/// fn marker(_conn: &Conn, _color: bool) -> Cow<'static, str> {
67/// "[client] ".into()
68/// }
69///
70/// ClientLogger::new().with_formatter((marker, formatters::method, " ", formatters::url));
71/// ```
72pub trait ClientLogFormatter: Send + Sync + 'static {
73 /// The display type for this formatter.
74 ///
75 /// For a simple formatter, this will likely be a `String`, or even better, a lightweight type
76 /// that implements [`Display`].
77 type Output: Display + Send + Sync + 'static;
78
79 /// Extract `Output` from this `Conn`.
80 fn format(&self, conn: &Conn, color: bool) -> Self::Output;
81}
82
83/// Internal state inserted by [`ClientLogger::run`] and read by [`formatters::response_time`].
84#[derive(Copy, Clone, Debug)]
85pub(crate) struct RequestStart(pub(crate) Instant);
86
87/// The [`ClientHandler`] that emits one log line per request.
88pub struct ClientLogger<F> {
89 format: F,
90 color_mode: ColorMode,
91 target: Arc<dyn Targetable>,
92}
93
94impl<F> std::fmt::Debug for ClientLogger<F> {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("ClientLogger")
97 .field("color_mode", &self.color_mode)
98 .finish_non_exhaustive()
99 }
100}
101
102impl ClientLogger<()> {
103 /// Builds a new client logger.
104 ///
105 /// Defaults:
106 ///
107 /// * formatter: [`dev_formatter`]
108 /// * color mode: [`ColorMode::Auto`]
109 /// * target: [`Target::Stdout`]
110 pub fn new() -> ClientLogger<impl ClientLogFormatter> {
111 ClientLogger {
112 format: dev_formatter,
113 color_mode: ColorMode::Auto,
114 target: Arc::new(Target::Stdout),
115 }
116 }
117}
118
119impl<T> ClientLogger<T> {
120 /// Replace the formatter with any type that implements [`ClientLogFormatter`].
121 ///
122 /// ```
123 /// use trillium_logger::client::{ClientLogger, formatters};
124 /// ClientLogger::new().with_formatter((formatters::method, " ", formatters::url));
125 /// ```
126 pub fn with_formatter<Formatter: ClientLogFormatter>(
127 self,
128 formatter: Formatter,
129 ) -> ClientLogger<Formatter> {
130 ClientLogger {
131 format: formatter,
132 color_mode: self.color_mode,
133 target: self.target,
134 }
135 }
136}
137
138impl<F: ClientLogFormatter> ClientLogger<F> {
139 /// Specify the color mode for this logger. See [`ColorMode`] for details.
140 pub fn with_color_mode(mut self, color_mode: ColorMode) -> Self {
141 self.color_mode = color_mode;
142 self
143 }
144
145 /// Specify the logger target. See [`Target`] and [`Targetable`].
146 pub fn with_target(mut self, target: impl Targetable) -> Self {
147 self.target = Arc::new(target);
148 self
149 }
150}
151
152impl<F: ClientLogFormatter> ClientHandler for ClientLogger<F> {
153 async fn run(&self, conn: &mut Conn) -> Result<()> {
154 conn.insert_state(RequestStart(Instant::now()));
155 Ok(())
156 }
157
158 async fn after_response(&self, conn: &mut Conn) -> Result<()> {
159 let output = self.format.format(conn, self.color_mode.is_enabled());
160 self.target.write(output.to_string());
161 Ok(())
162 }
163
164 fn name(&self) -> Cow<'static, str> {
165 "trillium-logger ClientLogger".into()
166 }
167}
168
169/// Convenience alias for [`ClientLogger::new`].
170pub fn client_logger() -> ClientLogger<impl ClientLogFormatter> {
171 ClientLogger::new()
172}