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