fix(lib): keep the log level in an atomic and let hosts change it

The level lived in a `static mut` read from every logging thread and
written by `init`, an unsynchronised access the compiler only tolerates
inside `unsafe`. It is an atomic now, read without `unsafe`, and a host
can raise or lower it while running through `set_log_level`.
This commit is contained in:
Michel de Graaf
2026-09-07 17:30:35 +02:00
parent 1e3412efcb
commit c08de74e72
+37 -14
View File
@@ -3,7 +3,7 @@ use std::fs::{File, OpenOptions};
use std::io::Write; use std::io::Write;
use std::sync::{ use std::sync::{
Mutex, Once, Mutex, Once,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, AtomicU8, Ordering},
}; };
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@@ -16,7 +16,23 @@ pub enum LogLevel {
} }
static INIT: Once = Once::new(); static INIT: Once = Once::new();
static mut LOG_LEVEL: LogLevel = LogLevel::Warn; static LOG_LEVEL: AtomicU8 = AtomicU8::new(LogLevel::Warn as u8);
/// Change the level at runtime, from any thread.
pub fn set_log_level(level: LogLevel) {
LOG_LEVEL.store(level as u8, Ordering::Relaxed);
}
#[must_use]
pub fn log_level() -> LogLevel {
match LOG_LEVEL.load(Ordering::Relaxed) {
0 => LogLevel::Error,
2 => LogLevel::Info,
3 => LogLevel::Debug,
4 => LogLevel::Trace,
_ => LogLevel::Warn,
}
}
static BUFFER: Mutex<Vec<String>> = Mutex::new(Vec::new()); static BUFFER: Mutex<Vec<String>> = Mutex::new(Vec::new());
static BUFFERING: AtomicBool = AtomicBool::new(false); static BUFFERING: AtomicBool = AtomicBool::new(false);
@@ -29,15 +45,13 @@ pub fn init() {
.unwrap_or_else(|_| "WARN".to_string()) .unwrap_or_else(|_| "WARN".to_string())
.to_uppercase(); .to_uppercase();
unsafe { set_log_level(match level.as_str() {
LOG_LEVEL = match level.as_str() { "ERROR" => LogLevel::Error,
"ERROR" => LogLevel::Error, "INFO" => LogLevel::Info,
"INFO" => LogLevel::Info, "DEBUG" | "VERBOSE" => LogLevel::Debug,
"DEBUG" | "VERBOSE" => LogLevel::Debug, // Map VERBOSE to DEBUG "TRACE" => LogLevel::Trace,
"TRACE" => LogLevel::Trace, _ => LogLevel::Warn,
_ => LogLevel::Warn, // "WARN" or default });
};
}
// Initialize log file if LOG_FILE env var is set // Initialize log file if LOG_FILE env var is set
if let Ok(log_file_path) = env::var("LOG_FILE") { if let Ok(log_file_path) = env::var("LOG_FILE") {
@@ -85,8 +99,8 @@ fn has_log_file() -> bool {
} }
pub fn log(level: LogLevel, message: &str) { pub fn log(level: LogLevel, message: &str) {
unsafe { if level <= log_level() {
if level <= LOG_LEVEL { {
let (name, colour) = match level { let (name, colour) = match level {
LogLevel::Error => ("ERROR", "\x1b[31m"), // Red LogLevel::Error => ("ERROR", "\x1b[31m"), // Red
LogLevel::Warn => ("WARN", "\x1b[33m"), // Yellow LogLevel::Warn => ("WARN", "\x1b[33m"), // Yellow
@@ -239,7 +253,16 @@ macro_rules! trace {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{LogSink, choose_sink}; use super::{LogLevel, LogSink, choose_sink, log_level, set_log_level};
#[test]
fn the_level_can_be_changed_while_running() {
let before = log_level();
set_log_level(LogLevel::Trace);
assert_eq!(log_level(), LogLevel::Trace);
set_log_level(before);
assert_eq!(log_level(), before);
}
#[test] #[test]
fn a_configured_file_bypasses_buffering_so_lines_are_not_duplicated() { fn a_configured_file_bypasses_buffering_so_lines_are_not_duplicated() {