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}