use std::{
fs::{File, OpenOptions},
io::Write,
path::PathBuf,
sync::{
Arc, Mutex, Once, OnceLock,
atomic::{AtomicBool, Ordering},
},
thread::JoinHandle,
time::Duration,
};
use arc_swap::{ArcSwap, ArcSwapOption};
use log::{Log, Metadata, Record};
use crate::{
FLUSH_FLAG, MinimalLoggerConfig, REOPEN_FLAG,
config::{ActiveConfig, ReloadConfig},
log_file::{LogFile, with_thread_writer, write_record},
native::platform,
shutdown,
};
pub(crate) struct MinimalLogger {
pub(crate) state: ArcSwap<ActiveConfig>,
pub(crate) file: ArcSwapOption<LogFile>,
pub(crate) flush_worker: Mutex<FlushWorker>,
}
pub(crate) static LOGGER: OnceLock<MinimalLogger> = OnceLock::new();
static RUNTIME_INIT: Once = Once::new();
pub(crate) struct FlushWorker {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl FlushWorker {
fn spawn(flush_ms: u64) -> Self {
let stop = Arc::new(AtomicBool::new(false));
let stop_for_thread = Arc::clone(&stop);
let handle = std::thread::Builder::new()
.name("minimal_logger-flush".into())
.stack_size(64 * 1024)
.spawn(move || {
let interval = Duration::from_millis(flush_ms);
loop {
if stop_for_thread.load(Ordering::Acquire) {
break;
}
std::thread::sleep(interval);
if stop_for_thread.load(Ordering::Acquire) {
break;
}
FLUSH_FLAG.store(true, Ordering::Relaxed);
}
})
.ok();
if handle.is_none() {
eprintln!("[minimal_logger] Failed to spawn flush thread — periodic flush disabled");
}
FlushWorker { stop, handle }
}
fn stop(&mut self) {
self.stop.store(true, Ordering::Release);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
pub(crate) enum FileTarget {
Stderr,
Path(PathBuf),
}
pub(crate) fn install_runtime_hooks_once() {
RUNTIME_INIT.call_once(|| {
platform::register_rotation_handler();
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
default_hook(info);
let _ = std::panic::catch_unwind(shutdown);
}));
});
}
fn load_file_for_config(cfg: &ReloadConfig) -> Option<Arc<LogFile>> {
cfg.file_path.as_deref().and_then(|path| match open_log_file(path) {
Ok(f) => {
eprintln!(
"[minimal_logger] \"{path}\" buf={}B/thread flush={}ms writer=BufWriter<FileWriter> drain={} os={}",
cfg.buf_capacity,
cfg.flush_ms,
if cfg!(windows) {
"Mutex<File>"
} else {
"O_APPEND"
},
std::env::consts::OS,
);
Some(Arc::new(LogFile::new(f, cfg.buf_capacity)))
}
Err(e) => {
eprintln!("[minimal_logger] Cannot open {path}: {e} — stderr fallback");
None
}
})
}
impl MinimalLogger {
pub(crate) fn from_config(config: MinimalLoggerConfig) -> Self {
let reload = config.into_reload(None);
let active = Arc::new(ActiveConfig::from_reload(reload.clone()));
MinimalLogger {
state: ArcSwap::from(active),
file: ArcSwapOption::new(load_file_for_config(&reload)),
flush_worker: Mutex::new(FlushWorker::spawn(reload.flush_ms)),
}
}
fn swap_file_handle(&self, replacement: Option<Arc<LogFile>>) {
let old = self.file.swap(replacement);
if let Some(old_arc) = old {
match Arc::try_unwrap(old_arc) {
Ok(old_log_file) => {
old_log_file.flush_and_sync();
drop(old_log_file);
eprintln!("[minimal_logger] Old log file flushed, synced, and closed");
}
Err(still_shared) => {
drop(still_shared);
eprintln!(
"[minimal_logger] Old log file has live BufWriters — will close when threads rotate"
);
}
}
}
}
fn maybe_replace_flush_worker(&self, old_ms: u64, new_ms: u64) {
if old_ms == new_ms {
return;
}
let mut worker = match self.flush_worker.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
worker.stop();
*worker = FlushWorker::spawn(new_ms);
}
pub(crate) fn apply_reload(&self, next_reload: ReloadConfig) {
let current = self.state.load();
if current.reload == next_reload {
return;
}
let old_reload = current.reload.clone();
drop(current);
if old_reload.file_path != next_reload.file_path {
self.swap_file_handle(load_file_for_config(&next_reload));
if let Some(path) = next_reload.file_path.as_deref() {
eprintln!("[minimal_logger] Reconfigured output file: {path}");
} else {
eprintln!("[minimal_logger] Reconfigured output to stderr");
}
}
self.maybe_replace_flush_worker(old_reload.flush_ms, next_reload.flush_ms);
let next_active = Arc::new(ActiveConfig::from_reload(next_reload));
log::set_max_level(next_active.max_level);
self.state.store(next_active);
}
}
impl MinimalLogger {
pub(crate) fn reopen(&self) {
let state = self.state.load();
let Some(path) = state.reload.file_path.clone() else {
return;
};
let new_log_file = match open_log_file(&path) {
Ok(f) => Arc::new(LogFile::new(f, state.reload.buf_capacity)),
Err(e) => {
eprintln!("[minimal_logger] reopen failed ({path}): {e} — keeping old file");
return;
}
};
drop(state);
let old = self.file.swap(Some(new_log_file));
if let Some(old_arc) = old {
match Arc::try_unwrap(old_arc) {
Ok(old_log_file) => {
old_log_file.flush_and_sync();
drop(old_log_file); eprintln!("[minimal_logger] Old log file flushed, synced, and closed");
}
Err(still_shared) => {
drop(still_shared);
eprintln!(
"[minimal_logger] Old log file has live BufWriters — will close when threads rotate"
);
}
}
}
eprintln!("[minimal_logger] Reopened: {path}");
}
}
fn open_log_file(path: &str) -> std::io::Result<File> {
OpenOptions::new()
.create(true)
.append(true) .open(path)
}
impl Log for MinimalLogger {
#[inline]
fn enabled(&self, metadata: &Metadata) -> bool {
let state = self.state.load();
metadata.level() <= state.level_for(metadata.target())
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
if REOPEN_FLAG.swap(false, Ordering::Acquire) {
self.reopen();
}
let state = self.state.load();
let buf_capacity = state.reload.buf_capacity;
let format = state.format.clone();
drop(state);
match self.file.load_full() {
Some(arc) => write_record(record, arc, buf_capacity, &format),
None => {
let stderr = std::io::stderr();
let mut out = stderr.lock();
let _ = out.write_all(format.render(record).as_bytes());
}
}
}
fn flush(&self) {
with_thread_writer(|bw| {
let _ = bw.flush();
});
let _ = std::io::stderr().lock().flush();
}
}