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}