Skip to main content

log_io/
level.rs

1//! Severity levels.
2
3use core::cmp::Ordering;
4use core::fmt;
5use core::str::FromStr;
6
7/// Severity of a log record.
8///
9/// Levels are ordered from least to most severe: [`Self::Trace`] <
10/// [`Self::Debug`] < [`Self::Info`] < [`Self::Warn`] < [`Self::Error`].
11/// [`Self::Off`] is greater than all real severities and is used
12/// exclusively to disable a filter or logger entirely.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[repr(u8)]
15pub enum Level {
16    /// Fine-grained diagnostic information for tracing execution flow.
17    Trace = 0,
18    /// Diagnostic information useful while debugging.
19    Debug = 1,
20    /// Informational messages about normal operation.
21    Info = 2,
22    /// A condition that may require attention but is not an error.
23    Warn = 3,
24    /// An error condition that prevented an operation from completing.
25    Error = 4,
26    /// Disable logging entirely. Never attached to a real [`crate::Record`].
27    /// Used only as a filter threshold sentinel meaning "admit nothing".
28    Off = 255,
29}
30
31impl Level {
32    /// The lowercase canonical string for this level.
33    ///
34    /// Returns `"off"` for [`Self::Off`]. This is the form emitted by
35    /// formatters by default.
36    pub const fn as_str(self) -> &'static str {
37        match self {
38            Self::Off => "off",
39            Self::Trace => "trace",
40            Self::Debug => "debug",
41            Self::Info => "info",
42            Self::Warn => "warn",
43            Self::Error => "error",
44        }
45    }
46
47    /// The uppercase canonical string for this level.
48    ///
49    /// Useful for human-readable formats that conventionally upper-case
50    /// the severity column.
51    pub const fn as_str_upper(self) -> &'static str {
52        match self {
53            Self::Off => "OFF",
54            Self::Trace => "TRACE",
55            Self::Debug => "DEBUG",
56            Self::Info => "INFO",
57            Self::Warn => "WARN",
58            Self::Error => "ERROR",
59        }
60    }
61
62    /// Returns `true` if this level is at least as severe as `other`.
63    ///
64    /// Used by filters: a record at level `self` passes a threshold of
65    /// `other` when `self.is_enabled_at(other)` is `true`.
66    pub const fn is_enabled_at(self, threshold: Self) -> bool {
67        (self as u8) >= (threshold as u8)
68    }
69
70    /// All real severities, ordered from least to most severe.
71    ///
72    /// Excludes [`Self::Off`]. Useful for iteration in tests.
73    pub const ALL: [Self; 5] = [
74        Self::Trace,
75        Self::Debug,
76        Self::Info,
77        Self::Warn,
78        Self::Error,
79    ];
80}
81
82impl fmt::Display for Level {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        f.write_str(self.as_str())
85    }
86}
87
88impl PartialOrd for Level {
89    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
90        Some(self.cmp(other))
91    }
92}
93
94impl Ord for Level {
95    fn cmp(&self, other: &Self) -> Ordering {
96        (*self as u8).cmp(&(*other as u8))
97    }
98}
99
100/// Error returned when a string cannot be parsed as a [`Level`].
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ParseLevelError;
103
104impl fmt::Display for ParseLevelError {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        f.write_str("invalid log level (expected one of: off, trace, debug, info, warn, error)")
107    }
108}
109
110#[cfg(feature = "std")]
111impl std::error::Error for ParseLevelError {}
112
113impl FromStr for Level {
114    type Err = ParseLevelError;
115
116    fn from_str(s: &str) -> Result<Self, Self::Err> {
117        // Case-insensitive without allocating: compare byte-by-byte.
118        if s.eq_ignore_ascii_case("off") {
119            Ok(Self::Off)
120        } else if s.eq_ignore_ascii_case("trace") {
121            Ok(Self::Trace)
122        } else if s.eq_ignore_ascii_case("debug") {
123            Ok(Self::Debug)
124        } else if s.eq_ignore_ascii_case("info") {
125            Ok(Self::Info)
126        } else if s.eq_ignore_ascii_case("warn") || s.eq_ignore_ascii_case("warning") {
127            Ok(Self::Warn)
128        } else if s.eq_ignore_ascii_case("error") || s.eq_ignore_ascii_case("err") {
129            Ok(Self::Error)
130        } else {
131            Err(ParseLevelError)
132        }
133    }
134}
135
136#[cfg(all(test, feature = "std"))]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn ordering_is_total_and_severity_aligned() {
142        assert!(Level::Trace < Level::Debug);
143        assert!(Level::Debug < Level::Info);
144        assert!(Level::Info < Level::Warn);
145        assert!(Level::Warn < Level::Error);
146        assert!(Level::Error < Level::Off);
147    }
148
149    #[test]
150    fn is_enabled_at_respects_threshold() {
151        assert!(Level::Error.is_enabled_at(Level::Info));
152        assert!(Level::Info.is_enabled_at(Level::Info));
153        assert!(!Level::Debug.is_enabled_at(Level::Info));
154        assert!(!Level::Trace.is_enabled_at(Level::Warn));
155    }
156
157    #[test]
158    fn parse_round_trip_canonical_forms() {
159        for level in Level::ALL {
160            let parsed: Level = level.as_str().parse().unwrap();
161            assert_eq!(parsed, level);
162            let parsed_upper: Level = level.as_str_upper().parse().unwrap();
163            assert_eq!(parsed_upper, level);
164        }
165        assert_eq!("off".parse::<Level>().unwrap(), Level::Off);
166        assert_eq!("OFF".parse::<Level>().unwrap(), Level::Off);
167    }
168
169    #[test]
170    fn parse_aliases() {
171        assert_eq!("warning".parse::<Level>().unwrap(), Level::Warn);
172        assert_eq!("err".parse::<Level>().unwrap(), Level::Error);
173    }
174
175    #[test]
176    fn parse_rejects_garbage() {
177        assert!("verbose".parse::<Level>().is_err());
178        assert!("".parse::<Level>().is_err());
179    }
180
181    #[test]
182    fn display_lowercase() {
183        assert_eq!(Level::Info.to_string(), "info");
184    }
185
186    #[test]
187    fn upper_form() {
188        assert_eq!(Level::Warn.as_str_upper(), "WARN");
189    }
190}