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