use core::fmt;
use std::str::FromStr;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LogLevel {
Trace,
Debug,
#[default]
Info,
Warn,
Error,
}
impl LogLevel {
#[inline]
#[must_use]
pub const fn is_enabled_at(&self, threshold: Self) -> bool {
(*self as u8) >= (threshold as u8)
}
#[inline]
#[must_use]
pub const fn as_char(&self) -> char {
match self {
Self::Trace => 'T',
Self::Debug => 'D',
Self::Info => 'I',
Self::Warn => 'W',
Self::Error => 'E',
}
}
#[inline]
#[must_use]
pub const fn as_str_lower(&self) -> &'static str {
match self {
Self::Trace => "trace",
Self::Debug => "debug",
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
}
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Trace => write!(f, "TRACE"),
Self::Debug => write!(f, "DEBUG"),
Self::Info => write!(f, "INFO"),
Self::Warn => write!(f, "WARN"),
Self::Error => write!(f, "ERROR"),
}
}
}
impl FromStr for LogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"TRACE" => Ok(Self::Trace),
"DEBUG" => Ok(Self::Debug),
"INFO" => Ok(Self::Info),
"WARN" => Ok(Self::Warn),
"ERROR" => Ok(Self::Error),
_ => Err(format!("Invalid log level: {s}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process;
use std::time::{SystemTime, UNIX_EPOCH};
fn level_table() -> String {
[
LogLevel::Trace,
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warn,
LogLevel::Error,
]
.into_iter()
.map(|level| format!("{level}|{}|{}", level.as_char(), level.as_str_lower()))
.collect::<Vec<_>>()
.join("\n")
}
fn structured_filter_snapshot() -> (String, u32, u128) {
let timestamp_nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time after unix epoch")
.as_nanos();
let pid = process::id();
let records = [
(LogLevel::Trace, "trace-path"),
(LogLevel::Debug, "debug-path"),
(LogLevel::Info, "info-path"),
(LogLevel::Warn, "warn-path"),
(LogLevel::Error, "error-path"),
];
let rendered = [
LogLevel::Trace,
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warn,
LogLevel::Error,
]
.into_iter()
.map(|threshold| {
let rendered = records
.iter()
.filter(|(level, _)| level.is_enabled_at(threshold))
.map(|(level, message)| {
format!(
"{{\"pid\":{pid},\"threshold\":\"{threshold}\",\"timestamp\":\"ts-{timestamp_nanos}\",\"level\":\"{level}\",\"message\":\"{message}\"}}",
threshold = threshold.as_str_lower(),
)
})
.collect::<Vec<_>>()
.join("\n");
format!("[threshold={}]\n{rendered}", threshold.as_str_lower())
})
.collect::<Vec<_>>()
.join("\n\n");
(rendered, pid, timestamp_nanos)
}
fn scrub_structured_filter_snapshot(rendered: &str, pid: u32, timestamp_nanos: u128) -> String {
rendered
.replace(&format!("\"pid\":{pid}"), "\"pid\":[PID]")
.replace(
&format!("\"timestamp\":\"ts-{timestamp_nanos}\""),
"\"timestamp\":\"[TIMESTAMP]\"",
)
}
#[test]
fn test_level_ordering() {
assert!(LogLevel::Trace < LogLevel::Debug);
assert!(LogLevel::Debug < LogLevel::Info);
assert!(LogLevel::Info < LogLevel::Warn);
assert!(LogLevel::Warn < LogLevel::Error);
}
#[test]
fn test_level_enabled_at_threshold() {
let threshold = LogLevel::Info;
assert!(LogLevel::Error.is_enabled_at(threshold));
assert!(LogLevel::Warn.is_enabled_at(threshold));
assert!(LogLevel::Info.is_enabled_at(threshold));
assert!(!LogLevel::Debug.is_enabled_at(threshold));
assert!(!LogLevel::Trace.is_enabled_at(threshold));
}
#[test]
fn test_level_from_str() {
assert_eq!(LogLevel::from_str("info"), Ok(LogLevel::Info));
assert_eq!(LogLevel::from_str("INFO"), Ok(LogLevel::Info));
assert_eq!(LogLevel::from_str("Warn"), Ok(LogLevel::Warn));
assert!(LogLevel::from_str("invalid").is_err());
}
#[test]
fn test_level_display() {
assert_eq!(format!("{}", LogLevel::Info), "INFO");
assert_eq!(format!("{}", LogLevel::Error), "ERROR");
}
#[test]
fn test_as_char() {
assert_eq!(LogLevel::Trace.as_char(), 'T');
assert_eq!(LogLevel::Error.as_char(), 'E');
}
#[test]
fn log_level_debug_clone_copy_hash_default() {
use std::collections::HashSet;
let l = LogLevel::Warn;
let dbg = format!("{l:?}");
assert!(dbg.contains("Warn"), "{dbg}");
let copied = l;
let cloned = l;
assert_eq!(copied, cloned);
let def = LogLevel::default();
assert_eq!(def, LogLevel::Info);
let mut set = HashSet::new();
set.insert(l);
assert!(set.contains(&LogLevel::Warn));
}
#[test]
fn log_level_table_snapshot() {
insta::assert_snapshot!("log_level_table", level_table());
}
#[test]
fn log_level_structured_filter_snapshot_scrubs_timestamp_and_pid() {
let (rendered, pid, timestamp_nanos) = structured_filter_snapshot();
insta::assert_snapshot!(
"log_level_structured_filter_scrubbed",
scrub_structured_filter_snapshot(&rendered, pid, timestamp_nanos)
);
}
}