Skip to main content

trillium_logger/
lib.rs

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