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