use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use tracing_subscriber::fmt::MakeWriter;
pub const DAEMON_LOG_FILENAME: &str = "daemon.log";
const MAX_BYTES: u64 = 1 << 20;
pub const DAEMON_DIRECTIVES: &str = "warn,mati=info,mati_core=info";
struct Sink {
path: PathBuf,
file: File,
written: u64,
}
impl Sink {
fn write_line(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.written + buf.len() as u64 > MAX_BYTES {
self.rotate()?;
}
let n = self.file.write(buf)?;
self.written += n as u64;
Ok(n)
}
fn rotate(&mut self) -> io::Result<()> {
let previous = self.path.with_extension("log.1");
std::fs::rename(&self.path, &previous)?;
self.file = open_append(&self.path)?;
self.written = 0;
Ok(())
}
}
static SINK: OnceLock<Mutex<Sink>> = OnceLock::new();
fn open_append(path: &Path) -> io::Result<File> {
OpenOptions::new().create(true).append(true).open(path)
}
pub fn log_path(root: &Path) -> PathBuf {
root.join(DAEMON_LOG_FILENAME)
}
pub fn install(root: &Path) -> bool {
let path = log_path(root);
let Ok(file) = open_append(&path) else {
return false;
};
let written = file.metadata().map(|m| m.len()).unwrap_or(0);
SINK.set(Mutex::new(Sink {
path,
file,
written,
}))
.is_ok()
}
#[derive(Clone, Copy, Default)]
pub struct DaemonLogWriter;
pub enum Target {
Rotating,
Stderr(io::Stderr),
}
impl Write for Target {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Rotating => match SINK.get().and_then(|s| s.lock().ok()) {
Some(mut sink) => sink.write_line(buf),
None => Ok(buf.len()),
},
Self::Stderr(w) => w.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Rotating => Ok(()),
Self::Stderr(w) => w.flush(),
}
}
}
impl<'a> MakeWriter<'a> for DaemonLogWriter {
type Writer = Target;
fn make_writer(&'a self) -> Self::Writer {
if SINK.get().is_some() {
Target::Rotating
} else {
Target::Stderr(io::stderr())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sink_at(path: &Path) -> Sink {
Sink {
path: path.to_path_buf(),
file: open_append(path).unwrap(),
written: 0,
}
}
#[test]
fn growth_is_bounded_by_two_generations() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join(DAEMON_LOG_FILENAME);
let mut sink = sink_at(&path);
let line = vec![b'x'; 64 * 1024];
for _ in 0..64 {
sink.write_line(&line).unwrap();
}
let total: u64 = std::fs::read_dir(dir.path())
.unwrap()
.flatten()
.map(|e| e.metadata().unwrap().len())
.sum();
assert!(
total <= 2 * MAX_BYTES,
"daemon log grew past two generations: {total} bytes"
);
assert!(
path.with_extension("log.1").exists(),
"rotation must keep one previous generation"
);
}
#[test]
fn rotation_preserves_the_most_recent_lines() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join(DAEMON_LOG_FILENAME);
let mut sink = sink_at(&path);
sink.write_line(&vec![b'o'; MAX_BYTES as usize]).unwrap();
sink.write_line(b"newest\n").unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "newest\n");
}
#[test]
fn writer_falls_back_to_stderr_until_installed() {
if SINK.get().is_some() {
return;
}
assert!(matches!(DaemonLogWriter.make_writer(), Target::Stderr(_)));
}
}