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