Skip to main content

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}