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
70/// simple development-mode formatter
71///
72/// composed of
73///
74/// `"`[`method`] [`url()`] [`response_time`] [`status`]`"`
75pub fn dev_formatter(conn: &Conn, color: bool) -> impl Display + Send + 'static + use<> {
76 (
77 version,
78 " ",
79 method,
80 " ",
81 url,
82 " ",
83 response_time,
84 " ",
85 status,
86 )
87 .format(conn, color)
88}
89
90/// formatter for the peer ip address of the connection
91///
92/// **note**: this can be modified by handlers prior to logging, such as
93/// when running a trillium application behind a reverse proxy or load
94/// balancer that sets a `forwarded` or `x-forwarded-for` header. this
95/// will display `"-"` if there is no available peer ip address, such as
96/// when running on a runtime adapter that does not have access to this
97/// information
98pub fn ip(conn: &Conn, _color: bool) -> Cow<'static, str> {
99 match conn.peer_ip() {
100 Some(peer) => format!("{peer:?}").into(),
101 None => "-".into(),
102 }
103}
104
105mod status_mod {
106 use super::*;
107 /// The display type for [`status`]
108 #[derive(Copy, Clone)]
109 pub struct StatusOutput(Status, bool);
110 impl Display for StatusOutput {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 let StatusOutput(status, color) = *self;
113 let status_string = (status as u16).to_string();
114 if color {
115 f.write_fmt(format_args!(
116 "{}",
117 status_string.color(match status as u16 {
118 200..=299 => "green",
119 300..=399 => "cyan",
120 400..=499 => "yellow",
121 500..=599 => "red",
122 _ => "white",
123 })
124 ))
125 } else {
126 f.write_str(&status_string)
127 }
128 }
129 }
130
131 /// formatter for the http status
132 ///
133 /// displays just the numeric code of the
134 /// status. when color is enabled, it uses the following color encoding:
135 ///
136 /// | code | color |
137 /// |------|--------|
138 /// | 2xx | green |
139 /// | 3xx | cyan |
140 /// | 4xx | yellow |
141 /// | 5xx | red |
142 /// | ??? | white |
143 pub fn status(conn: &Conn, color: bool) -> StatusOutput {
144 StatusOutput(conn.status().unwrap_or(Status::NotFound), color)
145 }
146}
147
148pub use status_mod::status;
149
150/// formatter-builder for a particular request header, formatted wrapped
151/// in quotes. `""` if the header is not present
152///
153/// usage:
154///
155/// ```rust
156/// # use trillium_logger::{Logger, formatters::request_header};
157/// Logger::new().with_formatter(("user-agent: ", request_header("user-agent")));
158///
159/// // or
160///
161/// Logger::new().with_formatter((
162/// "user-agent: ",
163/// request_header(trillium::KnownHeaderName::UserAgent),
164/// ));
165/// ```
166///
167/// **note**: this is not a formatter itself, but returns a formatter when
168/// called with a header name
169pub fn request_header(header_name: impl Into<HeaderName<'static>>) -> impl LogFormatter {
170 let header_name = header_name.into();
171 move |conn: &Conn, _color: bool| {
172 format!(
173 "{:?}",
174 conn.request_headers()
175 .get_str(header_name.clone())
176 .unwrap_or("")
177 )
178 }
179}
180
181/// formatter for the request `User-Agent` header, wrapped in quotes. `""` if absent
182///
183/// Equivalent to [`request_header`]`(`[`KnownHeaderName::UserAgent`]`)`, provided as a bare
184/// formatter so it can be named directly in a format.
185pub fn user_agent(conn: &Conn, _color: bool) -> String {
186 format!(
187 "{:?}",
188 conn.request_headers()
189 .get_str(KnownHeaderName::UserAgent)
190 .unwrap_or("")
191 )
192}
193
194/// formatter for the request `Referer` header, wrapped in quotes. `""` if absent
195///
196/// Equivalent to [`request_header`]`(`[`KnownHeaderName::Referer`]`)`, provided as a bare formatter
197/// so it can be named directly in a format.
198pub fn referer(conn: &Conn, _color: bool) -> String {
199 format!(
200 "{:?}",
201 conn.request_headers()
202 .get_str(KnownHeaderName::Referer)
203 .unwrap_or("")
204 )
205}
206
207/// formatter-builder for a particular response header, formatted wrapped
208/// in quotes. `""` if the header is not present
209///
210/// usage:
211///
212/// ```rust
213/// # use trillium_logger::{Logger, formatters::response_header};
214/// Logger::new().with_formatter((
215/// "location: ",
216/// response_header(trillium::KnownHeaderName::Location),
217/// ));
218/// // or
219/// Logger::new().with_formatter(("location: ", response_header("Location")));
220/// ```
221///
222/// **note**: this is not a formatter itself, but returns a formatter when
223/// called with a header name
224pub fn response_header(header_name: impl Into<HeaderName<'static>>) -> impl LogFormatter {
225 let header_name = header_name.into();
226 move |conn: &Conn, _color: bool| {
227 format!(
228 "{:?}",
229 conn.response_headers()
230 .get_str(header_name.clone())
231 .unwrap_or("")
232 )
233 }
234}
235
236mod timestamp_mod {
237 use super::*;
238 use time::{OffsetDateTime, macros::format_description};
239 /// Display output for [`timestamp`]
240 pub struct Now;
241
242 /// formatter for the current timestamp. this represents the time that the
243 /// log is written, not the beginning timestamp of the request
244 pub fn timestamp(_conn: &Conn, _color: bool) -> Now {
245 Now
246 }
247
248 // apache time format is 10/Oct/2000:13:55:36 -0700
249 impl Display for Now {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 let now = OffsetDateTime::now_local()
252 .unwrap_or_else(|_| OffsetDateTime::now_utc())
253 .format(format_description!(
254 version = 2,
255 "[day]/[month repr:short]/[year repr:full]:[hour repr:24]:[minute]:[second] \
256 [offset_hour sign:mandatory][offset_minute]"
257 ))
258 .unwrap();
259 f.write_str(&now)
260 }
261 }
262}
263pub use timestamp_mod::timestamp;
264
265/// formatter for the response body length, represented as a
266/// human-readable string like `5 bytes` or `10.1mb`. prints `-` if there
267/// is no response body. see [`bytes`] for the raw number of bytes
268pub fn body_len_human(conn: &Conn, _color: bool) -> Cow<'static, str> {
269 conn.response_len()
270 .map(|l| {
271 Size::from_bytes(l)
272 .format()
273 .with_base(Base::Base10)
274 .to_string()
275 .into()
276 })
277 .unwrap_or_else(|| Cow::from("-"))
278}
279
280/// [apache common log format][apache]
281///
282/// [apache]: https://httpd.apache.org/docs/current/logs.html#common
283///
284/// This is defined as follows:
285///
286/// [`ip`] `request_id` `user_id` `\[`[`timestamp`]`\]` "[`method`] [`url()`] [`version`]"
287/// [`status`] [`bytes`]
288///
289/// where `request_id` and `user_id` are mandatory formatters provided at time of usage.
290///
291/// ## usage without `request_id` or `user_id`
292/// ```
293/// # use trillium_logger::{Logger, apache_common};
294/// Logger::new().with_formatter(apache_common("-", "-"));
295/// ```
296///
297/// ## usage with app-specific `user_id`
298/// ```
299/// # use trillium_logger::{Logger, apache_common};
300/// # use trillium::Conn;
301/// # use std::borrow::Cow;
302/// # struct User(String); impl User { fn name(&self) -> &str { &self.0 } }
303/// fn user(conn: &Conn, color: bool) -> Cow<'static, str> {
304/// match conn.state::<User>() {
305/// Some(user) => String::from(user.name()).into(),
306/// None => "guest".into(),
307/// }
308/// }
309///
310/// Logger::new().with_formatter(apache_common("-", user));
311/// ```
312pub fn apache_common(
313 request_id: impl LogFormatter,
314 user_id: impl LogFormatter,
315) -> impl LogFormatter {
316 (
317 ip, " ", request_id, " ", user_id, " [", timestamp, "] \"", method, " ", url, " ", version,
318 "\" ", status, " ", bytes,
319 )
320}
321
322/// formatter that prints the number of response body bytes as a
323/// number. see [`body_len_human`] for a human-readable response body
324/// length with units
325pub fn bytes(conn: &Conn, _color: bool) -> u64 {
326 conn.response_len().unwrap_or_default()
327}
328
329/// formatter that prints an emoji if the request is secure as determined
330/// by [`Conn::is_secure`]
331pub fn secure(conn: &Conn, _: bool) -> &'static str {
332 if conn.is_secure() { "🔒" } else { " " }
333}
334
335/// formatter for the current url or path of the request, including query
336pub fn url(conn: &Conn, _color: bool) -> String {
337 match conn.querystring() {
338 "" => conn.path().into(),
339 query => format!("{}?{}", conn.path(), query),
340 }
341}
342
343mod response_time_mod {
344 use super::*;
345 /// display output type for the [`response_time`] formatter
346 pub struct ResponseTimeOutput(Instant);
347 impl Display for ResponseTimeOutput {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 f.write_fmt(format_args!("{:?}", Instant::now() - self.0))
350 }
351 }
352
353 /// formatter for the wall-time duration with units that this http
354 /// request-response cycle took, from the first bytes read to the
355 /// completion of the response.
356 pub fn response_time(conn: &Conn, _color: bool) -> ResponseTimeOutput {
357 ResponseTimeOutput(conn.start_time())
358 }
359}
360
361pub use response_time_mod::response_time;
362
363/// formatter for the http version, as delegated to the display
364/// implementation of [`Version`]
365pub fn version(conn: &Conn, _color: bool) -> Version {
366 conn.http_version()
367}
368
369impl LogFormatter for &'static str {
370 type Output = Self;
371
372 fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
373 self
374 }
375}
376
377impl LogFormatter for Arc<str> {
378 type Output = Self;
379
380 fn format(&self, _conn: &Conn, _color: bool) -> Self::Output {
381 Arc::clone(self)
382 }
383}
384
385impl LogFormatter for ColoredString {
386 type Output = String;
387
388 fn format(&self, _conn: &Conn, color: bool) -> Self::Output {
389 if color {
390 self.to_string()
391 } else {
392 (**self).to_string()
393 }
394 }
395}
396
397impl<F, O> LogFormatter for F
398where
399 F: Fn(&Conn, bool) -> O + Send + Sync + 'static,
400 O: Display + Send + Sync + 'static,
401{
402 type Output = O;
403
404 fn format(&self, conn: &Conn, color: bool) -> Self::Output {
405 self(conn, color)
406 }
407}
408
409mod tuples {
410 use super::*;
411 /// display output for the tuple implementation
412 ///
413 /// The Display type of each tuple element is contained in this type, and
414 /// it implements [`Display`] for 2-26-arity tuples.
415 ///
416 /// Please open an issue if you find yourself needing to do something with
417 /// this other than [`Display`] it.
418 pub struct TupleOutput<O>(O);
419 macro_rules! impl_formatter_tuple {
420 ($($name:ident)+) => (
421 #[allow(non_snake_case)]
422 impl<$($name,)*> Display for TupleOutput<($($name,)*)> where $($name: Display + Send + Sync + 'static,)* {
423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424 let ($(ref $name,)*) = self.0;
425 f.write_fmt(format_args!(
426 concat!($(
427 concat!("{",stringify!($name) ,":}")
428 ),*),
429 $($name = ($name)),*
430 ))
431 }
432 }
433
434 #[allow(non_snake_case)]
435 impl<$($name),*> LogFormatter for ($($name,)*) where $($name: LogFormatter),* {
436 type Output = TupleOutput<($($name::Output,)*)>;
437 fn format(&self, conn: &Conn, color: bool) -> Self::Output {
438 let ($(ref $name,)*) = *self;
439 TupleOutput(($(($name).format(conn, color),)*))
440 }
441 }
442 )
443 }
444
445 impl_formatter_tuple! { A B }
446 impl_formatter_tuple! { A B C }
447 impl_formatter_tuple! { A B C D }
448 impl_formatter_tuple! { A B C D E }
449 impl_formatter_tuple! { A B C D E F }
450 impl_formatter_tuple! { A B C D E F G }
451 impl_formatter_tuple! { A B C D E F G H }
452 impl_formatter_tuple! { A B C D E F G H I }
453 impl_formatter_tuple! { A B C D E F G H I J }
454 impl_formatter_tuple! { A B C D E F G H I J K }
455 impl_formatter_tuple! { A B C D E F G H I J K L }
456 impl_formatter_tuple! { A B C D E F G H I J K L M }
457 impl_formatter_tuple! { A B C D E F G H I J K L M N }
458 impl_formatter_tuple! { A B C D E F G H I J K L M N O }
459 impl_formatter_tuple! { A B C D E F G H I J K L M N O P }
460 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q }
461 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R }
462 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S }
463 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T }
464 impl_formatter_tuple! { A B C D E F G H I J K L M N O P Q R S T U }
465 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 }
466 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 }
467 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 }
468 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 }
469 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 }
470}