Skip to main content

sozu_command_lib/logging/
display.rs

1use std::fmt;
2
3use crate::{
4    AsStr,
5    logging::{
6        EndpointRecord, FullTags, LogContext, LogDuration, LogLevel, LogMessage, LoggerBackend,
7        Rfc3339Time,
8    },
9};
10
11impl LogLevel {
12    pub const fn as_str(&self, access: bool, colored: bool) -> &'static str {
13        match (self, access, colored) {
14            (LogLevel::Error, false, false) => "ERROR",
15            (LogLevel::Warn, false, false) => "WARN ",
16            (LogLevel::Info, false, false) => "INFO ",
17            (LogLevel::Debug, false, false) => "DEBUG",
18            (LogLevel::Trace, false, false) => "TRACE",
19
20            (LogLevel::Error, false, true) => "\x1b[;31;1mERROR",
21            (LogLevel::Warn, false, true) => "\x1b[;33;1mWARN ",
22            (LogLevel::Info, false, true) => "\x1b[;32;1mINFO ",
23            (LogLevel::Debug, false, true) => "\x1b[;36mDEBUG",
24            (LogLevel::Trace, false, true) => "\x1b[;90mTRACE",
25
26            (LogLevel::Error, true, false) => "ERROR-ACCESS",
27            (LogLevel::Info, true, false) => "INFO-ACCESS ",
28            (_, true, false) => "???",
29
30            (LogLevel::Error, true, true) => "\x1b[;35;1mERROR-ACCESS",
31            (LogLevel::Info, true, true) => "\x1b[;35;1mINFO-ACCESS ",
32            (_, true, true) => "\x1b[;35;1m???",
33        }
34    }
35}
36
37impl AsRef<str> for LoggerBackend {
38    fn as_ref(&self) -> &str {
39        match self {
40            LoggerBackend::Stdout(_) => "stdout",
41            LoggerBackend::Unix(_) => "UNIX socket",
42            LoggerBackend::Udp(_, _) => "UDP socket",
43            LoggerBackend::Tcp(_) => "TCP socket",
44            LoggerBackend::File(_) => "file",
45        }
46    }
47}
48
49impl fmt::Display for Rfc3339Time {
50    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
51        let t = self.inner;
52        write!(
53            f,
54            "{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
55            t.year(),
56            t.month() as u8,
57            t.day(),
58            t.hour(),
59            t.minute(),
60            t.second(),
61            t.microsecond()
62        )
63    }
64}
65
66impl fmt::Display for LogMessage<'_> {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self.0 {
69            Some(message) => write!(f, " | {message}"),
70            None => Ok(()),
71        }
72    }
73}
74
75impl fmt::Display for LogDuration {
76    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
77        match self.0 {
78            None => write!(f, "-"),
79            Some(duration) => {
80                let secs = duration.as_secs();
81                if secs >= 10 {
82                    return write!(f, "{secs}s");
83                }
84
85                let ms = duration.as_millis();
86                if ms < 10 {
87                    let us = duration.as_micros();
88                    if us >= 10 {
89                        return write!(f, "{us}μs");
90                    }
91
92                    let ns = duration.as_nanos();
93                    return write!(f, "{ns}ns");
94                }
95
96                write!(f, "{ms}ms")
97            }
98        }
99    }
100}
101
102impl fmt::Display for LogContext<'_> {
103    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
104        write!(f, "[{} ", self.session_id)?;
105        match self.request_id {
106            Some(id) => write!(f, "{id}")?,
107            None => f.write_str("-")?,
108        }
109        write!(
110            f,
111            " {} {}]",
112            self.cluster_id.unwrap_or("-"),
113            self.backend_id.unwrap_or("-")
114        )
115    }
116}
117
118impl fmt::Display for EndpointRecord<'_> {
119    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120        match self {
121            Self::Http {
122                method,
123                authority,
124                path,
125                status,
126                ..
127            } => write!(
128                f,
129                "{} {} {} {}",
130                authority.as_str_or("-"),
131                method.as_str_or("-"),
132                path.as_str_or("-"),
133                display_status(*status, f.alternate()),
134            ),
135            Self::Tcp => {
136                write!(f, "-")
137            }
138        }
139    }
140}
141
142fn display_status(status: Option<u16>, pretty: bool) -> String {
143    match (status, pretty) {
144        (Some(s @ 200..=299), true) => format!("\x1b[32m{s}"),
145        (Some(s @ 300..=399), true) => format!("\x1b[34m{s}"),
146        (Some(s @ 400..=499), true) => format!("\x1b[33m{s}"),
147        (Some(s @ 500..=599), true) => format!("\x1b[31m{s}"),
148        (Some(s), _) => s.to_string(),
149        (None, _) => "-".to_string(),
150    }
151}
152
153impl fmt::Display for FullTags<'_> {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        match (self.concatenated, self.user_agent) {
156            (None, None) => Ok(()),
157            (Some(tags), None) => write!(f, "{tags}"),
158            (Some(tags), Some(ua)) if !tags.is_empty() => {
159                write!(f, "{tags}, user-agent={}", prepare_user_agent(ua))
160            }
161            (_, Some(ua)) => write!(f, "user-agent={}", prepare_user_agent(ua)),
162        }
163    }
164}
165
166fn prepare_user_agent(user_agent: &str) -> String {
167    user_agent
168        .replace(' ', "_")
169        .replace('[', "{")
170        .replace(']', "}")
171}
172
173#[cfg(test)]
174mod tests {
175    use rusty_ulid::Ulid;
176
177    use crate::logging::LogContext;
178
179    #[test]
180    fn log_context_display_all_fields_present() {
181        let session = Ulid::from(0x01_23_45_67_89_AB_CD_EF_FE_DC_BA_98_76_54_32_10_u128);
182        let request = Ulid::from(0x01_23_45_67_89_AB_CD_EF_FE_DC_BA_98_76_54_32_11_u128);
183        let ctx = LogContext {
184            session_id: session,
185            request_id: Some(request),
186            cluster_id: Some("cluster-abc"),
187            backend_id: Some("backend-1"),
188        };
189        let rendered = format!("{ctx}");
190        assert_eq!(
191            rendered,
192            format!("[{session} {request} cluster-abc backend-1]")
193        );
194    }
195
196    #[test]
197    fn log_context_display_dashes_when_missing() {
198        let session = Ulid::from(0xABCDu128);
199        let ctx = LogContext {
200            session_id: session,
201            request_id: None,
202            cluster_id: None,
203            backend_id: None,
204        };
205        assert_eq!(format!("{ctx}"), format!("[{session} - - -]"));
206    }
207
208    #[test]
209    fn log_context_display_partial() {
210        let session = Ulid::from(0x42u128);
211        let request = Ulid::from(0x43u128);
212        let ctx = LogContext {
213            session_id: session,
214            request_id: Some(request),
215            cluster_id: None,
216            backend_id: Some("backend-2"),
217        };
218        assert_eq!(
219            format!("{ctx}"),
220            format!("[{session} {request} - backend-2]")
221        );
222    }
223}