fabro/lib/crates/fabro-util/src/run_log.rs
Bryan Helmkamp 1d304b771a Enable clippy pedantic lints and restriction lints workspace-wide
Adopts uv's clippy lint configuration: pedantic group at warn priority,
with noisy lints allowed, plus restriction lints for print/dbg/exit/use_self.
Fixes all violations across the workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:08 -04:00

208 lines
6 KiB
Rust

use std::io::{self, BufWriter, Write};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use tracing_subscriber::fmt::MakeWriter;
static RUN_LOG: OnceLock<RunLogWriter> = OnceLock::new();
/// A switchable `MakeWriter` that can be activated/deactivated at runtime.
///
/// When inactive, writes are silently discarded with no allocation.
/// When active, each event is buffered per-guard and flushed atomically on drop.
#[derive(Clone, Debug)]
pub struct RunLogWriter {
active: Arc<AtomicBool>,
file: Arc<Mutex<Option<BufWriter<std::fs::File>>>>,
}
impl RunLogWriter {
fn new() -> Self {
Self {
active: Arc::new(AtomicBool::new(false)),
file: Arc::new(Mutex::new(None)),
}
}
}
impl<'a> MakeWriter<'a> for RunLogWriter {
type Writer = RunLogGuard;
fn make_writer(&'a self) -> Self::Writer {
if self.active.load(Ordering::Relaxed) {
RunLogGuard::Active {
buf: Vec::new(),
file: self.file.clone(),
}
} else {
RunLogGuard::Inactive
}
}
}
/// Per-event write guard. Buffers writes and flushes atomically on drop.
pub enum RunLogGuard {
Inactive,
Active {
buf: Vec<u8>,
file: Arc<Mutex<Option<BufWriter<std::fs::File>>>>,
},
}
impl Write for RunLogGuard {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
match self {
Self::Inactive => Ok(data.len()),
Self::Active { buf, .. } => buf.write(data),
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Drop for RunLogGuard {
fn drop(&mut self) {
if let Self::Active { buf, file } = self {
if buf.is_empty() {
return;
}
if let Ok(mut guard) = file.lock() {
if let Some(writer) = guard.as_mut() {
let _ = writer.write_all(buf);
let _ = writer.flush();
}
}
}
}
}
/// Initialize the global run log writer. Returns a clone for use as a tracing layer writer.
///
/// Must be called exactly once (typically from logging init). Panics on second call.
pub fn init() -> RunLogWriter {
let writer = RunLogWriter::new();
let clone = writer.clone();
RUN_LOG
.set(writer)
.expect("run_log::init() called more than once");
clone
}
/// Activate per-run logging, directing tracing output to `path`.
pub fn activate(path: &Path) -> io::Result<()> {
let writer = RUN_LOG
.get()
.expect("run_log::activate() called before init()");
let file = std::fs::File::create(path)?;
let mut guard = writer.file.lock().expect("run log lock poisoned");
*guard = Some(BufWriter::new(file));
writer.active.store(true, Ordering::Release);
Ok(())
}
/// Deactivate per-run logging. Flushes and closes the current log file.
pub fn deactivate() {
let Some(writer) = RUN_LOG.get() else {
return;
};
writer.active.store(false, Ordering::Release);
let mut guard = writer.file.lock().expect("run log lock poisoned");
if let Some(mut w) = guard.take() {
let _ = w.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
/// Helper: create a standalone `RunLogWriter` (not the global singleton) for isolated tests.
fn test_writer() -> RunLogWriter {
RunLogWriter::new()
}
#[test]
fn inactive_writer_discards_writes() {
let w = test_writer();
let mut guard = w.make_writer();
guard.write_all(b"should be discarded").unwrap();
drop(guard);
// No file, no panic — writes silently dropped
}
#[test]
fn activate_writes_to_file() {
let w = test_writer();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
// Activate
let file = std::fs::File::create(&path).unwrap();
*w.file.lock().unwrap() = Some(BufWriter::new(file));
w.active.store(true, Ordering::Release);
// Write via guard
let mut guard = w.make_writer();
guard.write_all(b"hello world").unwrap();
drop(guard);
let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents, "hello world");
}
#[test]
fn deactivate_stops_writing() {
let w = test_writer();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
// Activate
let file = std::fs::File::create(&path).unwrap();
*w.file.lock().unwrap() = Some(BufWriter::new(file));
w.active.store(true, Ordering::Release);
// Write while active
let mut guard = w.make_writer();
guard.write_all(b"before").unwrap();
drop(guard);
// Deactivate
w.active.store(false, Ordering::Release);
if let Some(mut bw) = w.file.lock().unwrap().take() {
let _ = bw.flush();
}
// Write while inactive — should be discarded
let mut guard = w.make_writer();
guard.write_all(b"after").unwrap();
drop(guard);
let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents, "before");
}
#[test]
fn atomic_writes_no_interleaving() {
let w = test_writer();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
// Activate
let file = std::fs::File::create(&path).unwrap();
*w.file.lock().unwrap() = Some(BufWriter::new(file));
w.active.store(true, Ordering::Release);
// Multiple write() calls on the same guard should produce one contiguous block
let mut guard = w.make_writer();
guard.write_all(b"part1").unwrap();
guard.write_all(b"part2").unwrap();
drop(guard);
let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents, "part1part2");
}
}