trillium_logger/formatters.rs
1use crate::LogFormatter;
2use colored::{ColoredString, Colorize};
3use size::{Base, Size};
4use std::{borrow::Cow, fmt::Display, sync::Arc, time::Instant};
5use trillium::{Conn, HeaderName, KnownHeaderName, Method, Status, Version};
6
7/// [apache combined log format][apache]
8///
9/// [apache]: https://httpd.apache.org/docs/current/logs.html#combined
10///
11/// This is defined as follows:
12///
13/// [`apache_combined`](`request_id`, `user_id`) [`request_header`]`("referrer")`
14/// [`request_header`]`("user-agent")`
15///
16/// where `request_id` and `user_id` are mandatory formatters provided at time of usage.
17///
18///
19/// ## usage with empty `request_id` and `user_id`
20/// ```
21/// # use trillium_logger::{Logger, apache_combined};
22/// Logger::new().with_formatter(apache_combined("-", "-"));
23/// ```
24///
25/// ## usage with an app-specific `user_id`
26///
27/// ```
28/// # use trillium_logger::{Logger, apache_combined};
29/// # use trillium::Conn;
30/// # use std::borrow::Cow;
31/// # struct User(String); impl User { fn name(&self) -> &str { &self.0 } }
32/// fn user(conn: &Conn, color: bool) -> Cow<'static, str> {
33/// match conn.state::<User>() {
34/// Some(user) => String::from(user.name()).into(),
35/// None => "guest".into(),
36/// }
37/// }
38///
39/// Logger::new().with_formatter(apache_combined("-", user));
40/// ```
41pub fn apache_combined(
42 request_id: impl LogFormatter,
43 user_id: impl LogFormatter,
44) -> impl LogFormatter {
45 (
46 apache_common(request_id, user_id),
47 " ",
48 request_header(KnownHeaderName::Referer),
49 " ",
50 request_header(KnownHeaderName::UserAgent),
51 )
52}
53
54/// formatter for the conn's http method that delegates to [`Method`]'s
55/// [`Display`] implementation
56pub fn method(conn: &Conn, _color: bool) -> Method {
57 conn.method()
58}
59
60/// formatter for the request authority (host), as resolved by [`Conn::host`]
61///
62/// This is the host the request was addressed to — drawn from the `Host` header, the HTTP/2 or
63/// HTTP/3 `:authority` pseudo-header, or an absolute-form request target, depending on protocol.
64/// Displays `"-"` if no host is available.
65pub fn host(conn: &Conn, _color: bool) -> Cow<'static, str> {
66 conn.host()
67 .map_or(Cow::Borrowed("-"), |host| Cow::Owned(host.to_string()))
68}
69
70mod dev_formatter_mod {
71 use super::*;
72 use response_time_mod::ResponseTimeOutput;
73 use status_mod::StatusOutput;
74
75 /// The default formatter: a simple colorized development-mode format
76 ///
77 /// Renders as `{version} {method} {url} {response_time} {status}`, with the same components
78 /// as the formatters of those names. [`dev_formatter`] is the same format as a free function;
79 /// this type exists so the default can be named, as in `Logger<DevFormatter>`.
80 #[derive(Clone, Copy, Debug, Default)]
81 pub struct DevFormatter;
82
83 /// The display type for [`DevFormatter`]
84 #[derive(Clone, Debug)]
85 pub struct DevOutput {
86 version: Version,
87 method: Method,
88 url: String,
89 response_time: ResponseTimeOutput,
90 status: StatusOutput,
91 }
92
93 impl Display for DevOutput {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 write!(
96 f,
97 "{} {} {} {} {}",
98 self.version, self.method, self.url, self.response_time, self.status
99 )
100 }
101 }
102
103 impl LogFormatter for DevFormatter {
104 type Output = DevOutput;
105
106 fn format(&self, conn: &Conn, color: bool) -> Self::Output {
107 DevOutput {
108 version: conn.http_version(),
109 method: conn.method(),
110 url: url(conn, color),
111 response_time: response_time(conn, color),
112 status: status(conn, color),
113 }
114 }
115 }
116}
117
118pub use dev_formatter_mod::DevFormatter;
119
120/// simple development-mode formatter
121///
122/// composed of
123///
124/// `"`[`method`] [`url`](url()) [`response_time`] [`status`]`"`
125///
126/// This is the same format as [`DevFormatter`], as a free function.
127pub fn dev_formatter(conn: &Conn, color: bool) -> impl Display + Send + 'static + use<> {
128 DevFormatter.format(conn, color)
129}
130
131mod start_formatter_mod {
132 use super::*;
133
134 /// The default formatter for the request-start line
135 ///
136 /// [`Logger::with_start_logging`](crate::Logger::with_start_logging) emits this line when a
137 /// request is received. It renders as `Started {version} {method} {url}`, with the same
138 /// components as [`version`], [`method`], and [`url`](url()).
139 #[derive(Clone, Copy, Debug, Default)]
140 pub struct StartFormatter;
141
142 /// The display type for [`StartFormatter`]
143 #[derive(Clone, Debug)]
144 pub struct StartOutput {
145 version: Version,
146 method: Method,
147 url: String,
148 }
149
150 impl Display for StartOutput {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 write!(f, "Started {} {} {}", self.version, self.method, self.url)
153 }
154 }
155
156 impl LogFormatter for StartFormatter {
157 type Output = StartOutput;
158
159 fn format(&self, conn: &Conn, color: bool) -> Self::Output {
160 StartOutput {
161 version: conn.http_version(),
162 method: conn.method(),
163 url: url(conn, color),
164 }
165 }
166 }
167}
168
169pub use start_formatter_mod::StartFormatter;
170
171/// formatter for the request-start line
172///
173/// composed of
174///
175/// `"Started "` [`version`] [`method`] [`url`](url())
176///
177/// This is the same format as [`StartFormatter`], as a free function.
178pub fn start_formatter(conn: &Conn, color: bool) -> impl Display + Send + 'static + use<> {
179 StartFormatter.format(conn, color)
180}
181
182/// formatter for the peer ip address of the connection
183///
184/// **note**: this can be modified by handlers prior to logging, such as
185/// when running a trillium application behind a reverse proxy or load
186/// balancer that sets a `forwarded` or `x-forwarded-for` header. this
187/// will display `"-"` if there is no available peer ip address, such as
188/// when running on a runtime adapter that does not have access to this
189/// information
190pub fn ip(conn: &Conn, _color: bool) -> Cow<'static, str> {
191 match conn.peer_ip() {
192 Some(peer) => format!("{peer:?}").into(),
193 None => "-".into(),
194 }
195}
196
197mod status_mod {
198 use super::*;
199 /// The display type for [`status`]
200 #[derive(Copy, Clone, Debug)]
201 pub struct StatusOutput(Status, bool);
202 impl Display for StatusOutput {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 let StatusOutput(status, color) = *self;
205 let status_string = (status as u16).to_string();
206 if color {
207 f.write_fmt(format_args!(
208 "{}",
209 status_string.color(match status as u16 {
210 200..=299 => "green",
211 300..=399 => "cyan",
212 400..=499 => "yellow",
213 500..=599 => "red",
214 _ => "white",
215 })
216 ))
217 } else {
218 f.write_str(&status_string)
219 }
220 }
221 }
222
223 /// formatter for the http status
224 ///
225 /// displays just the numeric code of the
226 /// status. when color is enabled, it uses the following color encoding:
227 ///
228 /// | code | color |
229 /// |------|--------|
230 /// | 2xx | green |
231 /// | 3xx | cyan |
232 /// | 4xx | yellow |
233 /// | 5xx | red |
234 /// | ??? | white |
235 pub fn status(conn: &Conn, color: bool) -> StatusOutput {
236 StatusOutput(conn.status().unwrap_or(Status::NotFound), color)
237 }
238}
239
240pub use status_mod::status;
241
242/// formatter-builder for a particular request header, formatted wrapped
243/// in quotes. `""` if the header is not present
244///
245/// usage:
246///
247/// ```rust
248/// # use trillium_logger::{Logger, formatters::request_header};
249/// Logger::new().with_formatter(("user-agent: ", request_header("user-agent")));
250///
251/// // or
252///
253/// Logger::new().with_formatter((
254/// "user-agent: ",
255/// request_header(trillium::KnownHeaderName::UserAgent),
256/// ));
257/// ```
258///
259/// **note**: this is not a formatter itself, but returns a formatter when
260/// called with a header name
261pub fn request_header(header_name: impl Into<HeaderName<'static>>) -> impl LogFormatter {
262 let header_name = header_name.into();
263 move |conn: &Conn, _color: bool| {
264 format!(
265 "{:?}",
266 conn.request_headers()
267 .get_str(header_name.clone())
268 .unwrap_or("")
269 )
270 }
271}
272
273/// formatter for the request `User-Agent` header, wrapped in quotes. `""` if absent
274///
275/// Equivalent to [`request_header`]`(`[`KnownHeaderName::UserAgent`]`)`, provided as a bare
276/// formatter so it can be named directly in a format.
277pub fn user_agent(conn: &Conn, _color: bool) -> String {
278 format!(
279 "{:?}",
280 conn.request_headers()
281 .get_str(KnownHeaderName::UserAgent)
282 .unwrap_or("")
283 )
284}
285
286/// formatter for the request `Referer` header, wrapped in quotes. `""` if absent
287///
288/// Equivalent to [`request_header`]`(`[`KnownHeaderName::Referer`]`)`, provided as a bare formatter
289/// so it can be named directly in a format.
290pub fn referer(conn: &Conn, _color: bool) -> String {
291 format!(
292 "{:?}",
293 conn.request_headers()
294 .get_str(KnownHeaderName::Referer)
295 .unwrap_or("")
296 )
297}
298
299/// formatter-builder for a particular response header, formatted wrapped
300/// in quotes. `""` if the header is not present
301///
302/// usage:
303///
304/// ```rust
305/// # use trillium_logger::{Logger, formatters::response_header};
306/// Logger::new().with_formatter((
307/// "location: ",
308/// response_header(trillium::KnownHeaderName::Location),
309/// ));
310/// // or
311/// Logger::new().with_formatter(("location: ", response_header("Location")));
312/// ```
313///
314/// **note**: this is not a formatter itself, but returns a formatter when
315/// called with a header name
316pub fn response_header(header_name: impl Into<HeaderName<'static>>) -> impl LogFormatter {
317 let header_name = header_name.into();
318 move |conn: &Conn, _color: bool| {
319 format!(
320 "{:?}",
321 conn.response_headers()
322 .get_str(header_name.clone())
323 .unwrap_or("")
324 )
325 }
326}
327
328mod timestamp_mod {
329 use super::*;
330 use time::{OffsetDateTime, macros::format_description};
331 /// Display output for [`timestamp`]
332 pub struct Now;
333
334 /// formatter for the current timestamp. this represents the time that the
335 /// log is written, not the beginning timestamp of the request
336 pub fn timestamp(_conn: &Conn, _color: bool) -> Now {
337 Now
338 }
339
340 // apache time format is 10/Oct/2000:13:55:36 -0700
341 impl Display for Now {
342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343 let now = OffsetDateTime::now_local()
344 .unwrap_or_else(|_| OffsetDateTime::now_utc())
345 .format(format_description!(
346 version = 2,
347 "[day]/[month repr:short]/[year repr:full]:[hour repr:24]:[minute]:[second] \
348 [offset_hour sign:mandatory][offset_minute]"
349 ))
350 .unwrap();
351 f.write_str(&now)
352 }
353 }
354}
355pub use timestamp_mod::timestamp;
356
357/// formatter for the response body length, represented as a
358/// human-readable string like `5 bytes` or `10.1mb`. prints `-` if there
359/// is no response body. see [`bytes`] for the raw number of bytes
360pub fn body_len_human(conn: &Conn, _color: bool) -> Cow<'static, str> {
361 conn.response_len()
362 .map(|l| {
363 Size::from_bytes(l)
364 .format()
365 .with_base(Base::Base10)
366 .to_string()
367 .into()
368 })
369 .unwrap_or_else(|| Cow::from("-"))
370}
371
372/// [apache common log format][apache]
373///
374/// [apache]: https://httpd.apache.org/docs/current/logs.html#common
375///
376/// This is defined as follows:
377///
378/// [`ip`] `request_id` `user_id` `\[`[`timestamp`]`\]` "[`method`] [`url()`] [`version`]"
379/// [`status`] [`bytes`]
380///
381/// where `request_id` and `user_id` are mandatory formatters provided at time of usage.
382///
383/// ## usage without `request_id` or `user_id`
384/// ```
385/// # use trillium_logger::{Logger, apache_common};
386/// Logger::new().with_formatter(apache_common("-", "-"));
387/// ```
388///
389/// ## usage with app-specific `user_id`
390/// ```
391/// # use trillium_logger::{Logger, apache_common};
392/// # use trillium::Conn;
393/// # use std::borrow::Cow;
394/// # struct User(String); impl User { fn name(&self) -> &str { &self.0 } }
395/// fn user(conn: &Conn, color: bool) -> Cow<'static, str> {
396/// match conn.state::<User>() {
397/// Some(user) => String::from(user.name()).into(),
398/// None => "guest".into(),
399/// }
400/// }
401///
402/// Logger::new().with_formatter(apache_common("-", user));
403/// ```
404pub fn apache_common(
405 request_id: impl LogFormatter,
406 user_id: impl LogFormatter,
407) -> impl LogFormatter {
408 (
409 ip, " ", request_id, " ", user_id, " [", timestamp, "] \"", method, " ", url, " ", version,
410 "\" ", status, " ", bytes,
411 )
412}
413
414/// formatter that prints the number of response body bytes as a
415/// number. see [`body_len_human`] for a human-readable response body
416/// length with units
417pub fn bytes(conn: &Conn, _color: bool) -> u64 {
418 conn.response_len().unwrap_or_default()
419}
420
421/// formatter that prints an emoji if the request is secure as determined
422/// by [`Conn::is_secure`]
423pub fn secure(conn: &Conn, _: bool) -> &'static str {
424 if conn.is_secure() { "🔒" } else { " " }
425}
426
427/// formatter for the current url or path of the request, including query
428pub fn url(conn: &Conn, _color: bool) -> String {
429 match conn.querystring() {
430 "" => conn.path().into(),
431 query => format!("{}?{}", conn.path(), query),
432 }
433}
434
435mod response_time_mod {
436 use super::*;
437 /// display output type for the [`response_time`] formatter
438 #[derive(Clone, Copy, Debug)]
439 pub struct ResponseTimeOutput(Instant);
440 impl Display for ResponseTimeOutput {
441 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442 f.write_fmt(format_args!("{:?}", Instant::now() - self.0))
443 }
444 }
445
446 /// formatter for the wall-time duration with units that this http
447 /// request-response cycle took, from the first bytes read to the
448 /// completion of the response.
449 pub fn response_time(conn: &Conn, _color: bool) -> ResponseTimeOutput {
450 ResponseTimeOutput(conn.start_time())
451 }
452}
453
454pub use response_time_mod::response_time;
455
456/// formatter for the http version, as delegated to the display
457/// implementation of [`Version`]
458pub fn version(conn: &Conn, _color: bool) -> Version {
459 conn.http_version()
460}
461
462impl LogFormatter for &'static str {
463 type Output = Self;
464
465 fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
466 self
467 }
468}
469
470impl LogFormatter for Arc<str> {
471 type Output = Self;
472
473 fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
474 Arc::clone(self)
475 }
476}
477
478impl LogFormatter for ColoredString {
479 type Output = String;
480
481 fn format(&self, _conn: &Conn, color: bool) -> Self::Output {
482 if color {
483 self.to_string()
484 } else {
485 (**self).to_string()
486 }
487 }
488}
489
490impl<F, O> LogFormatter for F
491where
492 F: Fn(&Conn, bool) -> O + Send + Sync + 'static,
493 O: Display + Send + Sync + 'static,
494{
495 type Output = O;
496
497 fn format(&self, conn: &Conn, color: bool) -> Self::Output {
498 self(conn, color)
499 }
500}
501
502mod tuples {
503 use super::*;
504 /// display output for the tuple implementation
505 ///
506 /// The Display type of each tuple element is contained in this type, and
507 /// it implements [`Display`] for 2-26-arity tuples.
508 ///
509 /// Please open an issue if you find yourself needing to do something with
510 /// this other than [`Display`] it.
511 pub struct TupleOutput<O>(O);
512 macro_rules! impl_formatter_tuple {
513 ($($name:ident)+) => (
514 #[allow(non_snake_case)]
515 impl<$($name,)*> Display for TupleOutput<($($name,)*)> where $($name: Display + Send + Sync + 'static,)* {
516 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517 let ($(ref $name,)*) = self.0;
518 f.write_fmt(format_args!(
519 concat!($(
520 concat!("{",stringify!($name) ,":}")
521 ),*),
522 $($name = ($name)),*
523 ))
524 }
525 }
526
527 #[allow(non_snake_case)]
528 impl<$($name),*> LogFormatter for ($($name,)*) where $($name: LogFormatter),* {
529 type Output = TupleOutput<($($name::Output,)*)>;
530 fn format(&self, conn: &Conn, color: bool) -> Self::Output {
531 let ($(ref $name,)*) = *self;
532 TupleOutput(($(($name).format(conn, color),)*))
533 }
534 }
535 )
536 }
537
538 impl_formatter_tuple! { A B }
539 impl_formatter_tuple! { A B C }
540 impl_formatter_tuple! { A B C D }
541 impl_formatter_tuple! { A B C D E }
542 impl_formatter_tuple! { A B C D E F }
543 impl_formatter_tuple! { A B C D E F G }
544 impl_formatter_tuple! { A B C D E F G H }
545 impl_formatter_tuple! { A B C D E F G H I }
546 impl_formatter_tuple! { A B C D E F G H I J }
547 impl_formatter_tuple! { A B C D E F G H I J K }
548 impl_formatter_tuple! { A B C D E F G H I J K L }
549 impl_formatter_tuple! { A B C D E F G H I J K L M }
550 impl_formatter_tuple! { A B C D E F G H I J K L M N }
551 impl_formatter_tuple! { A B C D E F G H I J K L M N O }
552 impl_formatter_tuple! { A B C D E F G H I J K L M N O P }
553 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q }
554 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R }
555 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S }
556 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T }
557 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U }
558 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V }
559 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V W }
560 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V W X }
561 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V W X Y }
562 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U V W X Y Z }
563}