Skip to main content

logging/
types.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::str::FromStr;
4
5/// A single log record: a map of field name to value, both borrowed for the
6/// duration of the logging call (e.g. `"message"`, `"level"`, `"timestamp"`).
7pub type Record<'a> = HashMap<&'a str, &'a str>;
8
9/// Convenience methods on [`Record`].
10pub trait RecordExt {
11    /// Returns the value for `key`, or `""` if the key is absent.
12    fn get_or_default(&self, key: &str) -> &str;
13}
14
15impl RecordExt for Record<'_> {
16    fn get_or_default(&self, key: &str) -> &str {
17        self.get(key).map_or("", |v| v)
18    }
19}
20
21/// Severity level of a log message, ordered from least to most severe.
22///
23/// The numeric values are stable and used for cheap comparisons and for
24/// storing the level in an atomic (`as u8` / [`Level::from_u8`]).
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26pub enum Level {
27    /// Fine-grained detail for diagnosing problems.
28    Debug = 0,
29    /// Routine confirmation that things are working.
30    Info = 1,
31    /// Something unexpected, or a problem that may occur soon.
32    Warning = 2,
33    /// A failure that prevented some operation from completing.
34    Error = 3,
35    /// A severe failure; the program may be unable to continue.
36    Critical = 4,
37}
38
39impl Level {
40    /// The uppercase name of the level, e.g. `Level::Info` -> `"INFO"`.
41    pub fn to_str(&self) -> &str {
42        match self {
43            Level::Debug => "DEBUG",
44            Level::Info => "INFO",
45            Level::Warning => "WARNING",
46            Level::Error => "ERROR",
47            Level::Critical => "CRITICAL",
48        }
49    }
50    /// The level's stable numeric value.
51    pub fn to_u8(self) -> u8 {
52        self as u8
53    }
54    /// Inverse of [`Level::to_u8`]. Panics on a value outside `0..=4`.
55    pub fn from_u8(u8: u8) -> Level {
56        match u8 {
57            0 => Level::Debug,
58            1 => Level::Info,
59            2 => Level::Warning,
60            3 => Level::Error,
61            4 => Level::Critical,
62            _ => panic!("Invalid level"),
63        }
64    }
65}
66
67/// The error returned when a string cannot be parsed into a [`Level`].
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct ParseLevelError {
70    /// The unrecognized input that failed to parse.
71    pub input: String,
72}
73
74impl fmt::Display for ParseLevelError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "invalid level: {}", self.input)
77    }
78}
79
80impl std::error::Error for ParseLevelError {}
81
82impl FromStr for Level {
83    type Err = ParseLevelError;
84
85    /// Parses a level from its uppercase name (e.g. `"INFO"`), erroring on an
86    /// unknown string. Enables `"INFO".parse::<Level>()`.
87    fn from_str(s: &str) -> Result<Level, Self::Err> {
88        match s {
89            "DEBUG" => Ok(Level::Debug),
90            "INFO" => Ok(Level::Info),
91            "WARNING" => Ok(Level::Warning),
92            "ERROR" => Ok(Level::Error),
93            "CRITICAL" => Ok(Level::Critical),
94            _ => Err(ParseLevelError {
95                input: s.to_string(),
96            }),
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::collections::HashMap;
105
106    #[test]
107    fn record_get_or_default() {
108        let mut record: Record = HashMap::new();
109        record.insert("a", "1");
110        assert_eq!(record.get_or_default("a"), "1");
111        assert_eq!(record.get_or_default("missing"), "");
112    }
113
114    #[test]
115    fn level_ordering_is_by_severity() {
116        assert!(Level::Debug < Level::Info);
117        assert!(Level::Info < Level::Warning);
118        assert!(Level::Warning < Level::Error);
119        assert!(Level::Error < Level::Critical);
120    }
121
122    #[test]
123    fn level_u8_round_trips() {
124        for level in [
125            Level::Debug,
126            Level::Info,
127            Level::Warning,
128            Level::Error,
129            Level::Critical,
130        ] {
131            assert_eq!(Level::from_u8(level.to_u8()), level);
132        }
133    }
134
135    #[test]
136    fn level_str_round_trips() {
137        for level in [
138            Level::Debug,
139            Level::Info,
140            Level::Warning,
141            Level::Error,
142            Level::Critical,
143        ] {
144            // Exercise the idiomatic `.parse()` path (via the FromStr impl).
145            assert_eq!(level.to_str().parse::<Level>().unwrap(), level);
146        }
147    }
148
149    #[test]
150    fn level_from_str_rejects_unknown() {
151        assert!("NOPE".parse::<Level>().is_err());
152    }
153
154    #[test]
155    #[should_panic]
156    fn level_from_u8_panics_on_out_of_range() {
157        Level::from_u8(42);
158    }
159}