trillium_logger/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![forbid(unsafe_code)]
3#![warn(
4 rustdoc::missing_crate_level_docs,
5 missing_docs,
6 nonstandard_style,
7 unused_qualifications
8)]
9
10//! Request logging for [`trillium`].
11//!
12//! [`Logger`] is a [`Handler`] that emits one line per request. Add it to a handler tuple ahead of
13//! the handlers whose work you want to time and observe:
14//!
15//! ```
16//! use trillium_logger::logger;
17//! let handler = (logger(), "hello");
18//! ```
19//!
20//! Out of the box it uses [`dev_formatter`], a compact colorized development format. To customize
21//! the line, hand [`Logger::with_formatter`] a format built from the components in [`formatters`].
22//!
23//! # The `log_format!` macro
24//!
25//! [`log_format!`] builds a formatter from a [`format_args!`]-style string. Bare `{name}`
26//! placeholders refer to the building blocks in [`formatters`]; literal text between them is
27//! emitted verbatim:
28//!
29//! ```
30//! use trillium_logger::{Logger, log_format};
31//! Logger::new().with_formatter(log_format!("{method} {url} -> {status}"));
32//! ```
33//!
34//! Anything that isn't a bare built-in — a formatter that takes arguments, a closure, a value from
35//! your own crate — is supplied as a named or positional argument, exactly as in [`format_args!`]:
36//!
37//! ```
38//! use trillium::KnownHeaderName::UserAgent;
39//! use trillium_logger::{Logger, formatters::request_header, log_format};
40//! Logger::new().with_formatter(log_format!(
41//! "{ip} \"{method} {url}\" {status} {ua}",
42//! ua = request_header(UserAgent),
43//! ));
44//! ```
45//!
46//! The macro expands to the same composable [`LogFormatter`] tuples you can also build by hand; see
47//! that trait for the lower-level interface and for writing your own components.
48
49#[cfg(test)]
50#[doc = include_str!("../README.md")]
51mod readme {}
52pub use crate::formatters::{apache_combined, apache_common, dev_formatter};
53use std::{
54 convert::AsMut,
55 fmt::{Display, Write},
56 io::IsTerminal,
57 sync::Arc,
58};
59use trillium::{Conn, Handler, Info, ListenerKind, Transport};
60pub use trillium_logger_macros::log_format;
61/// Components with which common log formats can be constructed
62pub mod formatters;
63
64#[cfg(feature = "client")]
65#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
66pub mod client;
67
68/// A configuration option that determines if format will be colorful.
69///
70/// The default is [`ColorMode::Auto`], which only enables color if stdout
71/// is detected to be a shell terminal (tty). If this detection is
72/// incorrect, you can explicitly set it to [`ColorMode::On`] or
73/// [`ColorMode::Off`]
74///
75/// **Note**: The actual colorization of output is determined by the log
76/// formatters, so it is possible for this to be correctly enabled but for
77/// the output to have no colored components.
78
79#[derive(Clone, Copy, Debug)]
80#[non_exhaustive]
81#[derive(Default)]
82pub enum ColorMode {
83 /// detect if stdout is a tty
84 #[default]
85 Auto,
86 /// always enable colorful output
87 On,
88 /// always disable colorful output
89 Off,
90}
91
92impl ColorMode {
93 pub(crate) fn is_enabled(&self) -> bool {
94 match self {
95 ColorMode::Auto => std::io::stdout().is_terminal(),
96 ColorMode::On => true,
97 ColorMode::Off => false,
98 }
99 }
100}
101
102/// Specifies where the logger output should be sent
103///
104/// The default is [`Target::Stdout`].
105#[derive(Clone, Copy, Debug)]
106#[non_exhaustive]
107#[derive(Default)]
108pub enum Target {
109 /// Send trillium logger output to a log crate backend. See
110 /// [`log`] for output options
111 Logger(log::Level),
112
113 /// Send trillium logger output to stdout
114 #[default]
115 Stdout,
116}
117
118/// A trait for log targets. Implemented for [`Target`] and for all
119/// `Fn(String) + Send + Sync + 'static`.
120pub trait Targetable: Send + Sync + 'static {
121 /// write a log line
122 fn write(&self, data: String);
123}
124
125impl Targetable for Target {
126 fn write(&self, data: String) {
127 match self {
128 Target::Logger(level) => {
129 log::log!(*level, "{}", data);
130 }
131
132 Target::Stdout => {
133 println!("{data}");
134 }
135 }
136 }
137}
138
139impl<F> Targetable for F
140where
141 F: Fn(String) + Send + Sync + 'static,
142{
143 fn write(&self, data: String) {
144 self(data);
145 }
146}
147
148/// The interface to format a &[`Conn`] as a [`Display`]-able output
149///
150/// In general, the included loggers provide a mechanism for composing
151/// these, so top level formats like [`dev_formatter`], [`apache_common`]
152/// and [`apache_combined`] are composed in terms of component formatters
153/// like [`formatters::method`], [`formatters::ip`],
154/// [`formatters::timestamp`], and many others (see [`formatters`] for a
155/// full list)
156///
157/// When implementing this trait, note that [`Display::fmt`] is called on
158/// [`LogFormatter::Output`] _after_ the response has been fully sent, but
159/// that the [`LogFormatter::format`] is called _before_ the response has
160/// been sent. If you need to perform timing-sensitive calculations that
161/// represent the full http cycle, move whatever data is needed to make
162/// the calculation into a new type that implements Display, ensuring that
163/// it is calculated at the right time.
164///
165///
166/// Most formats are more conveniently expressed with the [`log_format!`](crate::log_format) macro,
167/// which expands to the tuple composition described below. Reach for the implementations here when
168/// you want a named, reusable formatter or are writing your own component.
169///
170/// ## Implementations
171///
172/// ### Tuples
173///
174/// LogFormatter is implemented for all tuples of other LogFormatter
175/// types, from 2-26 formatters long. The output of these formatters is
176/// concatenated with no space between.
177///
178/// ### `&'static str`
179///
180/// LogFormatter is implemented for &'static str, allowing for
181/// interspersing spaces and other static formatting details into tuples.
182///
183/// ```rust
184/// use trillium_logger::{Logger, formatters};
185/// let handler = Logger::new().with_formatter(("-> ", formatters::method, " ", formatters::url));
186/// ```
187///
188/// ### `Fn(&Conn, bool) -> impl Display`
189///
190/// LogFormatter is implemented for all functions that conform to this signature.
191///
192/// ```rust
193/// # use trillium_logger::{Logger, dev_formatter};
194/// # use trillium::Conn;
195/// # use std::borrow::Cow;
196/// # struct User(String); impl User { fn name(&self) -> &str { &self.0 } }
197/// fn user(conn: &Conn, color: bool) -> Cow<'static, str> {
198/// match conn.state::<User>() {
199/// Some(user) => String::from(user.name()).into(),
200/// None => "guest".into(),
201/// }
202/// }
203///
204/// let handler = Logger::new().with_formatter((dev_formatter, " ", user));
205/// ```
206pub trait LogFormatter: Send + Sync + 'static {
207 /// The display type for this formatter
208 ///
209 /// For a simple formatter, this will likely be a String, or even
210 /// better, a lightweight type that implements Display.
211 type Output: Display + Send + Sync + 'static;
212
213 /// Extract Output from this Conn
214 fn format(&self, conn: &Conn, color: bool) -> Self::Output;
215}
216
217/// The trillium handler for this crate, and the core type
218pub struct Logger<F> {
219 format: F,
220 color_mode: ColorMode,
221 target: Arc<dyn Targetable>,
222 init_message: bool,
223}
224
225impl Logger<()> {
226 /// Builds a new logger
227 ///
228 /// Defaults:
229 ///
230 /// * formatter: [`dev_formatter`]
231 /// * color mode: [`ColorMode::Auto`]
232 /// * target: [`Target::Stdout`]
233 /// * init message: true
234 pub fn new() -> Logger<impl LogFormatter> {
235 Logger {
236 format: dev_formatter,
237 color_mode: ColorMode::Auto,
238 target: Arc::new(Target::Stdout),
239 init_message: true,
240 }
241 }
242}
243
244impl<T> Logger<T> {
245 /// replace the formatter with any type that implements [`LogFormatter`]
246 ///
247 /// see the trait documentation for [`LogFormatter`] for more details. note that this can be
248 /// chained with [`Logger::with_target`] and [`Logger::with_color_mode`]
249 ///
250 /// ```
251 /// use trillium_logger::{Logger, apache_common};
252 /// Logger::new().with_formatter(apache_common("-", "-"));
253 /// ```
254 pub fn with_formatter<Formatter: LogFormatter>(
255 self,
256 formatter: Formatter,
257 ) -> Logger<Formatter> {
258 Logger {
259 format: formatter,
260 color_mode: self.color_mode,
261 target: self.target,
262 init_message: self.init_message,
263 }
264 }
265}
266
267impl<F: LogFormatter> Logger<F> {
268 /// specify the color mode for this logger.
269 ///
270 /// see [`ColorMode`] for more details. note that this can be chained
271 /// with [`Logger::with_target`] and [`Logger::with_formatter`]
272 /// ```
273 /// use trillium_logger::{ColorMode, Logger};
274 /// Logger::new().with_color_mode(ColorMode::On);
275 /// ```
276 pub fn with_color_mode(mut self, color_mode: ColorMode) -> Self {
277 self.color_mode = color_mode;
278 self
279 }
280
281 /// specify the logger target
282 ///
283 /// see [`Target`] for more details. note that this can be chained
284 /// with [`Logger::with_color_mode`] and [`Logger::with_formatter`]
285 ///
286 /// ```
287 /// use trillium_logger::{Logger, Target};
288 /// Logger::new().with_target(Target::Logger(log::Level::Info));
289 /// ```
290 pub fn with_target(mut self, target: impl Targetable) -> Self {
291 self.target = Arc::new(target);
292 self
293 }
294
295 /// Opt out of the init message
296 pub fn without_init_message(mut self) -> Self {
297 self.init_message = false;
298 self
299 }
300}
301
302/// An easily-named `Arc<dyn Targetable>` that is stored in trillium shared state
303#[derive(Clone)]
304pub struct LogTarget(Arc<dyn Targetable>);
305impl Targetable for LogTarget {
306 fn write(&self, data: String) {
307 self.0.write(data);
308 }
309}
310impl LogTarget {
311 /// Emit a log message to the logging backend
312 pub fn write(&self, data: String) {
313 self.0.write(data);
314 }
315}
316
317struct LoggerWasRun;
318
319impl<F> Handler for Logger<F>
320where
321 F: LogFormatter,
322{
323 async fn init(&mut self, info: &mut Info) {
324 if self.init_message {
325 let mut string = "\nTrillium started\n".to_string();
326
327 // The canonical URL, when known, can differ from any bound address (e.g. a configured
328 // DNS name behind a load balancer), so it is reported separately from the sockets.
329 if let Some(url) = info.shared_state::<url::Url>() {
330 writeln!(string, "✾ Listening at {}", url.as_str()).unwrap();
331 }
332
333 // A TCP-TLS listener and a QUIC listener on the same address render to the same URL;
334 // collapse them onto one line, marking `h3` where a QUIC listener is part of the group.
335 let mut bound: Vec<(String, bool)> = Vec::new();
336 for listener in info.listeners() {
337 let rendered = listener.to_string();
338 let is_h3 = matches!(listener.kind(), ListenerKind::Quic(_));
339 if let Some((_, h3)) = bound.iter_mut().find(|(r, _)| *r == rendered) {
340 *h3 |= is_h3;
341 } else {
342 bound.push((rendered, is_h3));
343 }
344 }
345 for (rendered, is_h3) in &bound {
346 let h3 = if *is_h3 { " (h3)" } else { "" };
347 writeln!(string, "✾ Bound to {rendered}{h3}").unwrap();
348 }
349
350 writeln!(string, "Control-c to quit").unwrap();
351 self.target.write(string);
352 }
353
354 info.insert_shared_state(LogTarget(Arc::clone(&self.target)));
355 }
356
357 async fn run(&self, conn: Conn) -> Conn {
358 conn.with_state(LoggerWasRun)
359 }
360
361 async fn before_send(&self, mut conn: Conn) -> Conn {
362 if conn.state::<LoggerWasRun>().is_some() {
363 let target = self.target.clone();
364 let output = self.format.format(&conn, self.color_mode.is_enabled());
365 let inner: &mut trillium_http::Conn<Box<dyn Transport>> = conn.as_mut();
366 inner.after_send(move |_| target.write(output.to_string()));
367 }
368
369 conn
370 }
371}
372
373/// Convenience alias for [`Logger::new`]
374pub fn logger() -> Logger<impl LogFormatter> {
375 Logger::new()
376}