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}