use std::fmt;
use std::io::{IsTerminal, Write};
use std::str::FromStr;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LogLevel {
Error = 0,
Warn = 1,
Info = 2,
Debug = 3,
Trace = 4,
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
};
f.write_str(s)
}
}
impl FromStr for LogLevel {
type Err = ParseLevelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"error" => Ok(LogLevel::Error),
"warn" => Ok(LogLevel::Warn),
"info" => Ok(LogLevel::Info),
"debug" => Ok(LogLevel::Debug),
"trace" => Ok(LogLevel::Trace),
_ => Err(ParseLevelError(s.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct ParseLevelError(String);
impl fmt::Display for ParseLevelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid log level: '{}'", self.0)
}
}
impl std::error::Error for ParseLevelError {}
impl LogLevel {
pub fn as_u8(self) -> u8 {
self as u8
}
pub fn from_u8(val: u8) -> Option<LogLevel> {
match val {
0 => Some(LogLevel::Error),
1 => Some(LogLevel::Warn),
2 => Some(LogLevel::Info),
3 => Some(LogLevel::Debug),
4 => Some(LogLevel::Trace),
_ => None,
}
}
pub fn tag(&self) -> String {
match self {
LogLevel::Error => "[ERROR]".to_string(),
LogLevel::Warn => "[WARN] ".to_string(),
LogLevel::Info => "[INFO] ".to_string(),
LogLevel::Debug => "[DEBUG]".to_string(),
LogLevel::Trace => "[TRACE]".to_string(),
}
}
}
pub use nanocolor::Colorize;
pub use nanocolor::{style, StyledString};
#[cfg(test)]
pub(crate) fn format_message(level: LogLevel, message: &str, use_color: bool) -> String {
format_message_full(level, message, use_color, None, None, None)
}
#[cfg(test)]
pub(crate) fn format_message_with_timestamp(
level: LogLevel,
message: &str,
use_color: bool,
timestamp: Option<&str>,
) -> String {
format_message_full(level, message, use_color, timestamp, None, None)
}
pub(crate) fn format_message_full(
level: LogLevel,
message: &str,
use_color: bool,
timestamp: Option<&str>,
source_loc: Option<(&str, u32)>,
thread_info: Option<&str>,
) -> String {
let tag = level.tag();
let ts_part = match timestamp {
Some(ts) => format!("{ts} "),
None => String::new(),
};
let thread_part = match thread_info {
Some(info) => format!("({info}) "),
None => String::new(),
};
let loc_part = match source_loc {
Some((file, line)) => format!("[{file}:{line}] "),
None => String::new(),
};
if use_color {
let styled = match level {
LogLevel::Error => tag.red().bold().to_string(),
LogLevel::Warn => tag.yellow().bold().to_string(),
LogLevel::Info => tag.green().bold().to_string(),
LogLevel::Debug => tag.blue().bold().to_string(),
LogLevel::Trace => tag.magenta().bold().to_string(),
};
format!("{ts_part}{thread_part}{styled} {loc_part}{message}\n")
} else {
format!("{ts_part}{thread_part}{tag} {loc_part}{message}\n")
}
}
pub fn matches_module_filter(module_path: &str, allow: &[String], deny: &[String]) -> bool {
let allowed = allow.is_empty() || allow.iter().any(|a| module_path.starts_with(a.as_str()));
if !allowed {
return false;
}
!deny.iter().any(|d| module_path.starts_with(d.as_str()))
}
pub enum LogOutput {
Term { level: LogLevel },
Writer {
level: LogLevel,
writer: std::sync::Mutex<Box<dyn Write + Send>>,
},
Test { level: LogLevel },
}
impl LogOutput {
pub fn term(level: LogLevel) -> Self {
LogOutput::Term { level }
}
pub fn writer(level: LogLevel, w: impl Write + Send + 'static) -> Self {
LogOutput::Writer {
level,
writer: std::sync::Mutex::new(Box::new(w)),
}
}
pub fn test(level: LogLevel) -> Self {
LogOutput::Test { level }
}
}
pub struct Logger {
level: AtomicU8,
timestamps: bool,
source_location: bool,
thread_info: bool,
module_allow: Vec<String>,
module_deny: Vec<String>,
outputs: Vec<LogOutput>,
}
static LOGGER: OnceLock<Logger> = OnceLock::new();
fn format_current_timestamp() -> String {
nanotime::NanoTime::now().to_string()
}
impl Logger {
pub fn level(&self) -> LogLevel {
LogLevel::from_u8(self.level.load(Ordering::Relaxed)).unwrap_or(LogLevel::Info)
}
}
pub struct LoggerBuilder {
level: LogLevel,
timestamps: bool,
source_location: bool,
thread_info: bool,
module_allow: Vec<String>,
module_deny: Vec<String>,
outputs: Vec<LogOutput>,
}
impl LoggerBuilder {
pub fn new() -> Self {
let default_level = std::env::var("NANOLOGGER_LEVEL")
.ok()
.and_then(|s| LogLevel::from_str(&s).ok())
.unwrap_or(LogLevel::Info);
Self {
level: default_level,
timestamps: false,
source_location: false,
thread_info: false,
module_allow: Vec::new(),
module_deny: Vec::new(),
outputs: Vec::new(),
}
}
pub fn level(mut self, level: LogLevel) -> Self {
self.level = level;
self
}
pub fn get_level(&self) -> LogLevel {
self.level
}
pub fn timestamps(mut self, enabled: bool) -> Self {
self.timestamps = enabled;
self
}
pub fn source_location(mut self, enabled: bool) -> Self {
self.source_location = enabled;
self
}
pub fn thread_info(mut self, enabled: bool) -> Self {
self.thread_info = enabled;
self
}
pub fn module_allow(mut self, modules: Vec<String>) -> Self {
self.module_allow = modules;
self
}
pub fn module_deny(mut self, modules: Vec<String>) -> Self {
self.module_deny = modules;
self
}
pub fn add_output(mut self, output: LogOutput) -> Self {
self.outputs.push(output);
self
}
pub fn init(self) -> Result<(), InitError> {
let outputs = if self.outputs.is_empty() {
vec![LogOutput::Term { level: self.level }]
} else {
self.outputs
};
let logger = Logger {
level: AtomicU8::new(self.level.as_u8()),
timestamps: self.timestamps,
source_location: self.source_location,
thread_info: self.thread_info,
module_allow: self.module_allow,
module_deny: self.module_deny,
outputs,
};
LOGGER.set(logger).map_err(|_| InitError)?;
#[cfg(feature = "log")]
{
let logger_ref: &'static Logger =
unsafe { &*(LOGGER.get().expect("just set") as *const Logger) };
log::set_logger(logger_ref).expect("log facade logger already set");
log::set_max_level(logger_ref.level().to_log_level_filter());
}
Ok(())
}
}
impl Default for LoggerBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct InitError;
impl fmt::Display for InitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "nanologger: logger already initialized")
}
}
impl std::error::Error for InitError {}
pub fn init() -> Result<(), InitError> {
LoggerBuilder::new().init()
}
pub fn set_level(level: LogLevel) {
if let Some(logger) = LOGGER.get() {
logger.level.store(level.as_u8(), Ordering::Relaxed);
#[cfg(feature = "log")]
log::set_max_level(level.to_log_level_filter());
}
}
#[doc(hidden)]
#[doc(hidden)]
pub fn __log_with_context(
level: LogLevel,
message: &str,
module_path: &str,
file: &str,
line: u32,
) {
let Some(logger) = LOGGER.get() else {
return;
};
if level > logger.level() {
return;
}
if !matches_module_filter(module_path, &logger.module_allow, &logger.module_deny) {
return;
}
let ts = if logger.timestamps {
Some(format_current_timestamp())
} else {
None
};
let source_loc = if logger.source_location {
Some((file, line))
} else {
None
};
let thread_info_str = if logger.thread_info {
let current = std::thread::current();
let info = match current.name() {
Some(name) => name.to_string(),
None => format!("{:?}", current.id()),
};
Some(info)
} else {
None
};
for output in &logger.outputs {
match output {
LogOutput::Term { level: out_level } => {
if level > *out_level {
continue;
}
let use_color = std::io::stderr().is_terminal();
let formatted = format_message_full(
level,
message,
use_color,
ts.as_deref(),
source_loc,
thread_info_str.as_deref(),
);
let mut stderr = std::io::stderr().lock();
let _ = stderr.write_all(formatted.as_bytes());
}
LogOutput::Writer {
level: out_level,
writer,
} => {
if level > *out_level {
continue;
}
let formatted = format_message_full(
level,
message,
false,
ts.as_deref(),
source_loc,
thread_info_str.as_deref(),
);
if let Ok(mut w) = writer.lock() {
let _ = w.write_all(formatted.as_bytes());
}
}
LogOutput::Test { level: out_level } => {
if level > *out_level {
continue;
}
let formatted = format_message_full(
level,
message,
false,
ts.as_deref(),
source_loc,
thread_info_str.as_deref(),
);
print!("{formatted}");
}
}
}
}
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {
$crate::__log_with_context($crate::LogLevel::Error, &format!($($arg)*), module_path!(), file!(), line!())
};
}
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {
$crate::__log_with_context($crate::LogLevel::Warn, &format!($($arg)*), module_path!(), file!(), line!())
};
}
#[macro_export]
macro_rules! info {
($($arg:tt)*) => {
$crate::__log_with_context($crate::LogLevel::Info, &format!($($arg)*), module_path!(), file!(), line!())
};
}
#[macro_export]
macro_rules! debug {
($($arg:tt)*) => {
$crate::__log_with_context($crate::LogLevel::Debug, &format!($($arg)*), module_path!(), file!(), line!())
};
}
#[macro_export]
macro_rules! trace {
($($arg:tt)*) => {
$crate::__log_with_context($crate::LogLevel::Trace, &format!($($arg)*), module_path!(), file!(), line!())
};
}
#[cfg(feature = "log")]
impl LogLevel {
fn from_log_level(level: log::Level) -> Self {
match level {
log::Level::Error => LogLevel::Error,
log::Level::Warn => LogLevel::Warn,
log::Level::Info => LogLevel::Info,
log::Level::Debug => LogLevel::Debug,
log::Level::Trace => LogLevel::Trace,
}
}
fn to_log_level_filter(self) -> log::LevelFilter {
match self {
LogLevel::Error => log::LevelFilter::Error,
LogLevel::Warn => log::LevelFilter::Warn,
LogLevel::Info => log::LevelFilter::Info,
LogLevel::Debug => log::LevelFilter::Debug,
LogLevel::Trace => log::LevelFilter::Trace,
}
}
}
#[cfg(feature = "log")]
impl log::Log for Logger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
let level = LogLevel::from_log_level(metadata.level());
if level > self.level() {
return false;
}
matches_module_filter(metadata.target(), &self.module_allow, &self.module_deny)
}
fn log(&self, record: &log::Record) {
if !self.enabled(record.metadata()) {
return;
}
let level = LogLevel::from_log_level(record.level());
let message = format!("{}", record.args());
let file = record.file().unwrap_or("");
let line = record.line().unwrap_or(0);
let ts = if self.timestamps {
Some(format_current_timestamp())
} else {
None
};
let source_loc = if self.source_location {
Some((file, line))
} else {
None
};
let thread_info_str = if self.thread_info {
let current = std::thread::current();
let info = match current.name() {
Some(name) => name.to_string(),
None => format!("{:?}", current.id()),
};
Some(info)
} else {
None
};
for output in &self.outputs {
match output {
LogOutput::Term { level: out_level } => {
if level > *out_level {
continue;
}
let use_color = std::io::stderr().is_terminal();
let formatted = format_message_full(
level,
&message,
use_color,
ts.as_deref(),
source_loc,
thread_info_str.as_deref(),
);
let mut stderr = std::io::stderr().lock();
let _ = stderr.write_all(formatted.as_bytes());
}
LogOutput::Writer {
level: out_level,
writer,
} => {
if level > *out_level {
continue;
}
let formatted = format_message_full(
level,
&message,
false,
ts.as_deref(),
source_loc,
thread_info_str.as_deref(),
);
if let Ok(mut w) = writer.lock() {
let _ = w.write_all(formatted.as_bytes());
}
}
LogOutput::Test { level: out_level } => {
if level > *out_level {
continue;
}
let formatted = format_message_full(
level,
&message,
false,
ts.as_deref(),
source_loc,
thread_info_str.as_deref(),
);
print!("{formatted}");
}
}
}
}
fn flush(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use serial_test::serial;
fn arb_log_level() -> impl Strategy<Value = LogLevel> {
prop_oneof![
Just(LogLevel::Error),
Just(LogLevel::Warn),
Just(LogLevel::Info),
Just(LogLevel::Debug),
Just(LogLevel::Trace),
]
}
#[test]
#[serial]
fn test_error_color_is_red_bold() {
nanocolor::set_colors_override(true);
let output = format_message(LogLevel::Error, "fail", true);
assert!(
output.contains("\x1b[1;31m"),
"Error prefix should be bold red, got: {:?}",
output
);
nanocolor::clear_colors_override();
}
#[test]
#[serial]
fn test_warn_color_is_yellow_bold() {
nanocolor::set_colors_override(true);
let output = format_message(LogLevel::Warn, "caution", true);
assert!(
output.contains("\x1b[1;33m"),
"Warn prefix should be bold yellow, got: {:?}",
output
);
nanocolor::clear_colors_override();
}
#[test]
#[serial]
fn test_info_color_is_green_bold() {
nanocolor::set_colors_override(true);
let output = format_message(LogLevel::Info, "ok", true);
assert!(
output.contains("\x1b[1;32m"),
"Info prefix should be bold green, got: {:?}",
output
);
nanocolor::clear_colors_override();
}
#[test]
#[serial]
fn test_debug_color_is_blue_bold() {
nanocolor::set_colors_override(true);
let output = format_message(LogLevel::Debug, "details", true);
assert!(
output.contains("\x1b[1;34m"),
"Debug prefix should be bold blue, got: {:?}",
output
);
nanocolor::clear_colors_override();
}
#[test]
#[serial]
fn test_trace_color_is_magenta_bold() {
nanocolor::set_colors_override(true);
let output = format_message(LogLevel::Trace, "verbose", true);
assert!(
output.contains("\x1b[1;35m"),
"Trace prefix should be bold magenta, got: {:?}",
output
);
nanocolor::clear_colors_override();
}
#[test]
fn test_plain_mode_no_ansi() {
let output = format_message(LogLevel::Error, "test", false);
assert!(
!output.contains("\x1b["),
"Plain mode must not contain ANSI sequences"
);
assert_eq!(output, "[ERROR] test\n");
}
#[test]
fn test_timestamp_prepended_plain() {
let output =
format_message_with_timestamp(LogLevel::Info, "hello", false, Some("14:30:05"));
assert_eq!(output, "14:30:05 [INFO] hello\n");
}
#[test]
#[serial]
fn test_timestamp_prepended_color() {
nanocolor::set_colors_override(true);
let output = format_message_with_timestamp(LogLevel::Error, "fail", true, Some("09:00:00"));
assert!(
output.starts_with("09:00:00 "),
"Timestamp should be at the start: {:?}",
output
);
assert!(
output.contains("\x1b[1;31m"),
"Should still have bold red: {:?}",
output
);
nanocolor::clear_colors_override();
}
#[test]
fn test_no_timestamp_same_as_format_message() {
let with = format_message_with_timestamp(LogLevel::Warn, "test", false, None);
let without = format_message(LogLevel::Warn, "test", false);
assert_eq!(with, without);
}
#[test]
fn test_plain_text_no_ansi_full() {
let output =
format_message_full(LogLevel::Info, "plain text check", false, None, None, None);
assert!(
!output.contains("\x1b["),
"Should have no ANSI codes: {output:?}"
);
assert!(
output.contains("[INFO]"),
"Should contain level tag: {output:?}"
);
assert!(
output.contains("plain text check"),
"Should contain message: {output:?}"
);
}
#[test]
fn test_thread_info_format() {
let output = format_message_full(
LogLevel::Warn,
"combined test",
false,
None,
None,
Some("my-thread"),
);
assert!(
output.contains("(my-thread)"),
"Should contain thread info: {output:?}"
);
assert!(
output.contains("[WARN]"),
"Should contain level tag: {output:?}"
);
assert!(
output.contains("combined test"),
"Should contain message: {output:?}"
);
assert!(
!output.contains("\x1b["),
"Should have no ANSI codes: {output:?}"
);
}
proptest! {
#[test]
fn prop_message_format_structure(level in arb_log_level(), msg in "[^\x00]{1,100}") {
let output = format_message(level, &msg, false);
let tag = level.tag();
prop_assert!(output.ends_with('\n'));
let expected = format!("{tag} {msg}\n");
prop_assert_eq!(output, expected);
}
#[test]
fn prop_plain_text_no_ansi(level in arb_log_level(), msg in "[^\x1b\x00]{0,100}") {
let output = format_message(level, &msg, false);
prop_assert!(!output.contains("\x1b["),
"Plain text output must not contain ANSI escape sequences: {:?}", output);
}
#[test]
fn prop_timestamp_format_structure(
level in arb_log_level(),
msg in "[^\x00]{1,100}",
h in 0u32..24,
m in 0u32..60,
s in 0u32..60,
) {
let ts = format!("{h:02}:{m:02}:{s:02}");
let output = format_message_with_timestamp(level, &msg, false, Some(&ts));
let tag = level.tag();
let expected = format!("{ts} {tag} {msg}\n");
prop_assert_eq!(output, expected);
}
#[test]
fn prop_no_timestamp_matches_format_message(
level in arb_log_level(),
msg in "[^\x00]{1,100}",
) {
let with_ts = format_message_with_timestamp(level, &msg, false, None);
let without = format_message(level, &msg, false);
prop_assert_eq!(with_ts, without);
}
}
proptest! {
#[test]
fn prop_thread_info_format_and_placement(
level in arb_log_level(),
msg in "[a-zA-Z0-9 ]{1,80}",
thread_name in "[a-zA-Z0-9_-]{1,30}",
use_ts in proptest::bool::ANY,
h in 0u32..24,
m in 0u32..60,
s in 0u32..60,
) {
let ts_str = format!("{h:02}:{m:02}:{s:02}");
let ts = if use_ts { Some(ts_str.as_str()) } else { None };
let output = format_message_full(level, &msg, false, ts, None, Some(&thread_name));
let tag = level.tag();
let wrapped = format!("({thread_name})");
prop_assert!(output.contains(&wrapped),
"Output should contain {wrapped}: {output:?}");
let thread_pos = output.find(&wrapped).unwrap();
let tag_pos = output.find(&tag).unwrap();
prop_assert!(thread_pos < tag_pos,
"Thread info should appear before level tag: {output:?}");
if let Some(ts) = ts {
let ts_pos = output.find(ts).unwrap();
prop_assert!(ts_pos < thread_pos,
"Timestamp should appear before thread info: {output:?}");
}
}
#[test]
fn prop_thread_info_disabled_preserves_format(
level in arb_log_level(),
msg in "[a-zA-Z0-9 ]{1,80}",
use_ts in proptest::bool::ANY,
h in 0u32..24,
m in 0u32..60,
s in 0u32..60,
use_loc in proptest::bool::ANY,
file in "[a-zA-Z0-9_/]{1,30}",
line in 1u32..100_000,
) {
let ts_str = format!("{h:02}:{m:02}:{s:02}");
let ts = if use_ts { Some(ts_str.as_str()) } else { None };
let loc = if use_loc { Some((file.as_str(), line)) } else { None };
let with_none = format_message_full(level, &msg, false, ts, loc, None);
let without = format_message_full(level, &msg, false, ts, loc, None);
prop_assert_eq!(&with_none, &without,
"Output with thread_info=None should be identical");
let tag = level.tag();
let tag_pos = with_none.find(&tag).unwrap();
let before_tag = &with_none[..tag_pos];
prop_assert!(!before_tag.contains('('),
"No parenthesized thread info should appear when disabled: {with_none:?}");
}
}
proptest! {
#[test]
fn prop_source_location_format(
level in arb_log_level(),
file in "[a-zA-Z0-9_/\\.]{1,50}",
line in 1u32..100_000,
msg in "[^\x00\x1b]{1,100}",
) {
let output = format_message_full(level, &msg, false, None, Some((&file, line)), None);
let tag = level.tag();
let loc_tag = format!("[{file}:{line}]");
prop_assert!(output.ends_with('\n'));
prop_assert!(output.contains(&loc_tag), "Output should contain {loc_tag}: {output:?}");
let tag_pos = output.find(&tag).expect("tag must be present");
let loc_pos = output.find(&loc_tag).expect("loc must be present");
let msg_pos = output.rfind(&*msg).expect("msg must be present");
prop_assert!(tag_pos < loc_pos, "Tag should come before location");
prop_assert!(loc_pos < msg_pos, "Location should come before message");
}
#[test]
fn prop_source_location_omitted(
level in arb_log_level(),
msg in "[a-zA-Z0-9 ]{1,100}",
) {
let output = format_message_full(level, &msg, false, None, None, None);
let tag = level.tag();
let after_tag = &output[output.find(&tag).unwrap() + tag.len()..];
let has_source_loc_after_tag = after_tag.contains('[');
prop_assert!(!has_source_loc_after_tag,
"Output should not contain source location brackets after tag when disabled: {output:?}");
}
}
proptest! {
#[test]
fn prop_test_output_contains_no_ansi(
level in arb_log_level(),
msg in "[a-zA-Z0-9 ]{1,80}",
use_ts in proptest::bool::ANY,
h in 0u32..24,
m in 0u32..60,
s in 0u32..60,
use_loc in proptest::bool::ANY,
file in "[a-zA-Z0-9_/]{1,30}",
line in 1u32..100_000,
use_thread in proptest::bool::ANY,
thread_name in "[a-zA-Z0-9_-]{1,30}",
) {
let ts_str = format!("{h:02}:{m:02}:{s:02}");
let ts = if use_ts { Some(ts_str.as_str()) } else { None };
let loc = if use_loc { Some((file.as_str(), line)) } else { None };
let thread = if use_thread { Some(thread_name.as_str()) } else { None };
let output = format_message_full(level, &msg, false, ts, loc, thread);
prop_assert!(!output.contains("\x1b["),
"Test output should contain no ANSI escape sequences: {output:?}");
}
}
fn arb_module_segment() -> impl Strategy<Value = String> {
"[a-z][a-z0-9_]{0,7}".prop_map(|s| s)
}
fn arb_module_path() -> impl Strategy<Value = String> {
prop::collection::vec(arb_module_segment(), 1..=4).prop_map(|segs| segs.join("::"))
}
fn arb_filter_list() -> impl Strategy<Value = Vec<String>> {
prop::collection::vec(arb_module_path(), 0..=3)
}
proptest! {
#[test]
fn prop_module_filter_correctness(
module_path in arb_module_path(),
allow in arb_filter_list(),
deny in arb_filter_list(),
) {
let result = matches_module_filter(&module_path, &allow, &deny);
let pass_allow = allow.is_empty()
|| allow.iter().any(|a| module_path.starts_with(a.as_str()));
let pass_deny = !deny.iter().any(|d| module_path.starts_with(d.as_str()));
let expected = pass_allow && pass_deny;
prop_assert_eq!(
result, expected,
"module_path={:?}, allow={:?}, deny={:?}: got {}, expected {}",
module_path, allow, deny, result, expected
);
}
}
proptest! {
#[test]
fn prop_set_level_then_level_consistency(level in arb_log_level()) {
let logger = Logger {
level: AtomicU8::new(LogLevel::Info.as_u8()),
timestamps: false,
source_location: false,
thread_info: false,
module_allow: Vec::new(),
module_deny: Vec::new(),
outputs: Vec::new(),
};
logger.level.store(level.as_u8(), Ordering::Relaxed);
prop_assert_eq!(logger.level(), level,
"After storing {:?}, level() should return it", level);
}
}
}