use std::cell::RefCell;
use std::io::{self, Write};
use std::mem;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Off = 0,
Warn = 1,
Debug = 2,
}
const _: () = assert!(Level::Off as u8 == 0);
const _: () = assert!(Level::Warn as u8 == 1);
const _: () = assert!(Level::Debug as u8 == 2);
pub const ENV_VAR: &str = "LINESMITH_LOG";
const DEFAULT_LEVEL: Level = Level::Warn;
static LEVEL: AtomicU8 = AtomicU8::new(DEFAULT_LEVEL as u8);
pub fn apply(raw: Option<&str>, warn_sink: &mut dyn Write) {
match decide_init(raw) {
InitDecision::Keep => {}
InitDecision::Set(l) => set_level(l),
InitDecision::Warn(bad) => {
let _ = writeln!(
warn_sink,
"linesmith: {ENV_VAR}={bad:?} unrecognized; using default ({DEFAULT_LEVEL:?})"
);
set_level(DEFAULT_LEVEL);
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InitDecision<'a> {
Keep,
Set(Level),
Warn(&'a str),
}
pub(crate) fn decide_init(raw: Option<&str>) -> InitDecision<'_> {
match raw {
None => InitDecision::Keep,
Some(s) => match Level::parse(s) {
Some(l) => InitDecision::Set(l),
None => InitDecision::Warn(s),
},
}
}
pub fn set_level(l: Level) {
LEVEL.store(l as u8, Ordering::Relaxed);
}
#[must_use]
pub fn level() -> Level {
from_u8(LEVEL.load(Ordering::Relaxed))
}
fn from_u8(n: u8) -> Level {
match n {
0 => Level::Off,
1 => Level::Warn,
2 => Level::Debug,
_ => {
debug_assert!(false, "logging::LEVEL holds out-of-range byte {n}");
Level::Debug
}
}
}
#[must_use]
pub fn is_enabled(at_least: Level) -> bool {
level() >= at_least
}
pub trait LogSink: Send + Sync {
fn emit(&self, lvl: Level, msg: &str);
fn emit_error(&self, msg: &str);
}
#[derive(Debug, Default)]
pub struct StderrSink;
impl LogSink for StderrSink {
fn emit(&self, lvl: Level, msg: &str) {
let tag = match lvl {
Level::Off => return,
Level::Warn => "warn",
Level::Debug => "debug",
};
let _ = writeln!(io::stderr().lock(), "linesmith [{tag}]: {msg}");
}
fn emit_error(&self, msg: &str) {
let _ = writeln!(io::stderr().lock(), "linesmith [error]: {msg}");
}
}
#[derive(Debug, Default)]
pub struct CapturedSink {
entries: Mutex<Vec<String>>,
}
impl CapturedSink {
#[must_use]
pub fn drain(&self) -> Vec<String> {
let mut g = self.entries.lock().unwrap_or_else(|p| p.into_inner());
mem::take(&mut *g)
}
#[cfg(test)]
fn snapshot(&self) -> Vec<String> {
self.entries
.lock()
.unwrap_or_else(|p| p.into_inner())
.clone()
}
}
impl LogSink for CapturedSink {
fn emit(&self, lvl: Level, msg: &str) {
let tag = match lvl {
Level::Off => return,
Level::Warn => "warn",
Level::Debug => "debug",
};
self.entries
.lock()
.unwrap_or_else(|p| p.into_inner())
.push(format!("[{tag}] {msg}"));
}
fn emit_error(&self, msg: &str) {
self.entries
.lock()
.unwrap_or_else(|p| p.into_inner())
.push(format!("[error] {msg}"));
}
}
static SINK: OnceLock<Mutex<Arc<dyn LogSink>>> = OnceLock::new();
#[doc(hidden)]
pub fn _test_serial_lock() -> std::sync::MutexGuard<'static, ()> {
static M: OnceLock<Mutex<()>> = OnceLock::new();
M.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|p| p.into_inner())
}
#[must_use = "binding to `_` drops the guard immediately, which restores the prior thread-local sink before the helper's window opens; bind to a real name to hold it"]
struct ThreadSinkGuard {
prior: Option<Arc<CapturedSink>>,
}
impl Drop for ThreadSinkGuard {
fn drop(&mut self) {
let prior = self.prior.take();
let _old = THREAD_SINK.with(|cell| cell.replace(prior));
}
}
#[doc(hidden)]
pub fn _test_capture_warns<F, T>(f: F) -> (T, Vec<String>)
where
F: FnOnce() -> T,
{
let _serial = _test_serial_lock();
let sink = Arc::new(CapturedSink::default());
let prior = THREAD_SINK.with(|cell| cell.replace(Some(sink.clone())));
let _restore = ThreadSinkGuard { prior };
let result = f();
let captured = sink.drain();
(result, captured)
}
fn sink_slot() -> &'static Mutex<Arc<dyn LogSink>> {
SINK.get_or_init(|| Mutex::new(Arc::new(StderrSink)))
}
fn current_sink() -> Arc<dyn LogSink> {
sink_slot()
.lock()
.unwrap_or_else(|p| p.into_inner())
.clone()
}
pub(crate) fn install_sink(new_sink: Arc<dyn LogSink>) -> Arc<dyn LogSink> {
let mut g = sink_slot().lock().unwrap_or_else(|p| p.into_inner());
mem::replace(&mut *g, new_sink)
}
#[must_use = "binding to `_` drops the guard immediately, which restores the prior sink right away; bind to `_g` (or any real name) to hold it for the scope"]
pub struct SinkGuard {
prior: Option<Arc<dyn LogSink>>,
}
impl SinkGuard {
pub fn install(new_sink: Arc<dyn LogSink>) -> Self {
Self {
prior: Some(install_sink(new_sink)),
}
}
}
impl Drop for SinkGuard {
fn drop(&mut self) {
if let Some(prior) = self.prior.take() {
install_sink(prior);
}
}
}
thread_local! {
static THREAD_SINK: RefCell<Option<Arc<CapturedSink>>> = const { RefCell::new(None) };
}
#[must_use = "callers must skip the global sink when the thread-local fired, or the emission double-routes"]
fn with_thread_sink<F: FnOnce(&CapturedSink)>(f: F) -> bool {
let sink = THREAD_SINK
.try_with(|cell| cell.borrow().clone())
.ok()
.flatten();
if let Some(sink) = sink {
f(&sink);
true
} else {
false
}
}
pub fn emit(lvl: Level, msg: &str) {
if with_thread_sink(|sink| sink.emit(lvl, msg)) {
return;
}
if !is_enabled(lvl) {
return;
}
current_sink().emit(lvl, msg);
}
pub fn emit_error(msg: &str) {
if with_thread_sink(|sink| sink.emit_error(msg)) {
return;
}
current_sink().emit_error(msg);
}
impl Level {
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"off" | "none" | "0" => Some(Level::Off),
"warn" | "warning" => Some(Level::Warn),
"debug" | "trace" | "all" => Some(Level::Debug),
_ => None,
}
}
}
#[macro_export]
macro_rules! lsm_warn {
($($arg:tt)*) => {
$crate::logging::emit($crate::logging::Level::Warn, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! lsm_debug {
($($arg:tt)*) => {
if $crate::logging::is_enabled($crate::logging::Level::Debug) {
$crate::logging::emit($crate::logging::Level::Debug, &format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! lsm_error {
($($arg:tt)*) => {
$crate::logging::emit_error(&format!($($arg)*))
};
}
#[cfg(test)]
mod tests {
use super::*;
fn lock() -> std::sync::MutexGuard<'static, ()> {
super::_test_serial_lock()
}
#[test]
fn default_level_is_warn() {
let _g = lock();
set_level(DEFAULT_LEVEL);
assert_eq!(level(), Level::Warn);
assert!(is_enabled(Level::Warn));
assert!(!is_enabled(Level::Debug));
}
#[test]
fn debug_enables_every_lower_level() {
let _g = lock();
set_level(Level::Debug);
assert!(is_enabled(Level::Warn));
assert!(is_enabled(Level::Debug));
set_level(DEFAULT_LEVEL);
}
#[test]
fn off_suppresses_every_level() {
let _g = lock();
set_level(Level::Off);
assert!(!is_enabled(Level::Warn));
assert!(!is_enabled(Level::Debug));
set_level(DEFAULT_LEVEL);
}
#[test]
fn parse_accepts_common_aliases() {
assert_eq!(Level::parse("warn"), Some(Level::Warn));
assert_eq!(Level::parse("WARN"), Some(Level::Warn));
assert_eq!(Level::parse(" warn "), Some(Level::Warn));
assert_eq!(Level::parse("warning"), Some(Level::Warn));
assert_eq!(Level::parse("debug"), Some(Level::Debug));
assert_eq!(Level::parse("trace"), Some(Level::Debug));
assert_eq!(Level::parse("all"), Some(Level::Debug));
assert_eq!(Level::parse("off"), Some(Level::Off));
assert_eq!(Level::parse("none"), Some(Level::Off));
assert_eq!(Level::parse("0"), Some(Level::Off));
}
#[test]
fn parse_rejects_error_and_info_aliases() {
assert_eq!(Level::parse("error"), None);
assert_eq!(Level::parse("info"), None);
}
#[test]
fn parse_rejects_garbage() {
assert_eq!(Level::parse("verbose"), None);
assert_eq!(Level::parse(""), None);
assert_eq!(Level::parse("debug2"), None);
}
#[test]
fn decide_init_keeps_default_when_env_unset() {
assert_eq!(decide_init(None), InitDecision::Keep);
}
#[test]
fn decide_init_parses_recognized_levels() {
assert_eq!(decide_init(Some("debug")), InitDecision::Set(Level::Debug));
assert_eq!(decide_init(Some("warn")), InitDecision::Set(Level::Warn));
assert_eq!(decide_init(Some("off")), InitDecision::Set(Level::Off));
}
#[test]
fn decide_init_warns_on_garbage() {
assert_eq!(decide_init(Some("loud")), InitDecision::Warn("loud"));
assert_eq!(decide_init(Some("")), InitDecision::Warn(""));
}
#[test]
fn apply_writes_warning_to_injected_sink_and_resets_to_default() {
let _g = lock();
set_level(Level::Off);
let mut sink = Vec::<u8>::new();
apply(Some("loud"), &mut sink);
let written = String::from_utf8(sink).expect("utf8");
assert!(
written.contains("LINESMITH_LOG=\"loud\""),
"expected the unrecognized value echoed, got {written:?}"
);
assert!(written.contains("unrecognized"));
assert_eq!(level(), DEFAULT_LEVEL);
}
#[test]
fn apply_keeps_level_when_env_unset() {
let _g = lock();
set_level(Level::Debug);
let mut sink = Vec::<u8>::new();
apply(None, &mut sink);
assert!(sink.is_empty(), "no-env must not write: {sink:?}");
assert_eq!(level(), Level::Debug);
set_level(DEFAULT_LEVEL);
}
#[test]
fn apply_sets_recognized_level_without_writing() {
let _g = lock();
set_level(Level::Off);
let mut sink = Vec::<u8>::new();
apply(Some("debug"), &mut sink);
assert!(sink.is_empty());
assert_eq!(level(), Level::Debug);
set_level(DEFAULT_LEVEL);
}
#[test]
fn lsm_debug_skips_format_when_suppressed() {
use std::cell::Cell;
use std::fmt;
struct CountingDisplay<'a>(&'a Cell<u32>);
impl fmt::Display for CountingDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.set(self.0.get() + 1);
f.write_str("x")
}
}
let _g = lock();
let counter = Cell::new(0u32);
set_level(Level::Warn);
lsm_debug!("{}", CountingDisplay(&counter));
assert_eq!(counter.get(), 0, "format! must not run when suppressed");
set_level(Level::Debug);
lsm_debug!("{}", CountingDisplay(&counter));
assert_eq!(counter.get(), 1, "format! must run when enabled");
set_level(DEFAULT_LEVEL);
}
#[test]
fn from_u8_roundtrips_known_bytes() {
assert_eq!(from_u8(0), Level::Off);
assert_eq!(from_u8(1), Level::Warn);
assert_eq!(from_u8(2), Level::Debug);
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "out-of-range byte")]
fn from_u8_debug_panics_on_out_of_range() {
let _ = from_u8(99);
}
#[test]
#[cfg(not(debug_assertions))]
fn from_u8_saturates_out_of_range_to_debug_in_release() {
assert_eq!(from_u8(3), Level::Debug);
assert_eq!(from_u8(99), Level::Debug);
assert_eq!(from_u8(u8::MAX), Level::Debug);
}
#[test]
fn captured_sink_records_warn_emit_with_compact_format() {
let _g = lock();
set_level(Level::Warn);
let captured = Arc::new(CapturedSink::default());
let _restore = SinkGuard::install(captured.clone());
emit(Level::Warn, "hello");
assert_eq!(captured.snapshot(), vec!["[warn] hello".to_string()]);
set_level(DEFAULT_LEVEL);
}
#[test]
fn captured_sink_records_error_bypassing_off_level() {
let _g = lock();
set_level(Level::Off);
let captured = Arc::new(CapturedSink::default());
let _restore = SinkGuard::install(captured.clone());
emit_error("render panic");
assert_eq!(
captured.snapshot(),
vec!["[error] render panic".to_string()]
);
set_level(DEFAULT_LEVEL);
}
#[test]
fn captured_sink_skips_debug_emit_when_level_warn() {
let _g = lock();
set_level(Level::Warn);
let captured = Arc::new(CapturedSink::default());
let _restore = SinkGuard::install(captured.clone());
emit(Level::Debug, "verbose");
assert!(captured.snapshot().is_empty());
set_level(DEFAULT_LEVEL);
}
#[test]
fn emit_error_fires_at_every_level() {
let _g = lock();
for l in [Level::Off, Level::Warn, Level::Debug] {
set_level(l);
let captured = Arc::new(CapturedSink::default());
let _restore = SinkGuard::install(captured.clone());
emit_error("structural failure");
assert_eq!(
captured.snapshot(),
vec!["[error] structural failure".to_string()],
"emit_error must fire at level {l:?}",
);
}
set_level(DEFAULT_LEVEL);
}
#[test]
fn captured_sink_drain_returns_entries_and_empties_buffer() {
let _g = lock();
set_level(Level::Warn);
let captured = Arc::new(CapturedSink::default());
let _restore = SinkGuard::install(captured.clone());
emit(Level::Warn, "first");
emit(Level::Warn, "second");
let drained = captured.drain();
assert_eq!(
drained,
vec!["[warn] first".to_string(), "[warn] second".to_string()]
);
assert!(captured.drain().is_empty());
set_level(DEFAULT_LEVEL);
}
#[test]
fn sink_guard_restores_prior_sink_on_drop_lifo_three_deep() {
let _g = lock();
set_level(Level::Warn);
let outer = Arc::new(CapturedSink::default());
let _outer_g = SinkGuard::install(outer.clone());
{
let middle = Arc::new(CapturedSink::default());
let _middle_g = SinkGuard::install(middle.clone());
{
let inner = Arc::new(CapturedSink::default());
let _inner_g = SinkGuard::install(inner.clone());
emit(Level::Warn, "inner");
assert_eq!(inner.snapshot(), vec!["[warn] inner".to_string()]);
assert!(middle.snapshot().is_empty());
assert!(outer.snapshot().is_empty());
}
emit(Level::Warn, "middle");
assert_eq!(middle.snapshot(), vec!["[warn] middle".to_string()]);
assert!(outer.snapshot().is_empty());
}
emit(Level::Warn, "outer");
assert_eq!(outer.snapshot(), vec!["[warn] outer".to_string()]);
set_level(DEFAULT_LEVEL);
}
#[test]
fn install_sink_returns_prior_for_manual_restore() {
let _g = lock();
set_level(Level::Warn);
let captured = Arc::new(CapturedSink::default());
let prior = install_sink(captured.clone());
emit(Level::Warn, "captured");
assert_eq!(captured.snapshot(), vec!["[warn] captured".to_string()]);
let _ = install_sink(prior);
}
#[test]
fn test_capture_warns_returns_function_result_and_captured_emissions() {
let (result, captured) = _test_capture_warns(|| {
emit(Level::Warn, "first");
emit(Level::Warn, "second");
42
});
assert_eq!(result, 42);
assert_eq!(
captured,
vec!["[warn] first".to_string(), "[warn] second".to_string()]
);
}
#[test]
fn test_capture_warns_captures_emit_error_regardless_of_level() {
let (_, captured) = _test_capture_warns(|| {
emit_error("structural failure");
});
assert_eq!(captured, vec!["[error] structural failure".to_string()]);
}
#[test]
fn emit_routes_to_thread_local_sink_even_when_global_level_is_off() {
let _g = lock();
set_level(Level::Off);
let sink = Arc::new(CapturedSink::default());
let prior = THREAD_SINK.with(|cell| cell.replace(Some(sink.clone())));
let _restore = ThreadSinkGuard { prior };
emit(Level::Warn, "still captured");
assert_eq!(sink.drain(), vec!["[warn] still captured".to_string()]);
set_level(DEFAULT_LEVEL);
}
#[test]
fn test_capture_warns_subsequent_call_starts_empty() {
let _ = _test_capture_warns(|| emit(Level::Warn, "first"));
let (_, second) = _test_capture_warns(|| {});
assert!(
second.is_empty(),
"second call must start with no captured entries, got {second:?}"
);
}
#[test]
fn concrete_sink_types_remain_thread_safe() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<StderrSink>();
assert_send_sync::<CapturedSink>();
}
}