feat(supervisor): rotating log writer

This commit is contained in:
2026-05-25 11:36:23 +02:00
parent d237e980e9
commit d51f25350c
2 changed files with 106 additions and 0 deletions
+2
View File
@@ -2,10 +2,12 @@
pub mod backoff;
pub mod child;
pub mod logs;
pub mod policy;
pub mod retry_window;
pub use backoff::Backoff;
pub use child::{ChildHandle, MockChild, MockChildController};
pub use logs::RotatingLogWriter;
pub use policy::{RestartDecision, decide};
pub use retry_window::RetryWindow;
+104
View File
@@ -0,0 +1,104 @@
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
pub struct RotatingLogWriter {
base: PathBuf,
max_bytes: u64,
keep: usize,
file: File,
written: u64,
}
impl RotatingLogWriter {
pub fn open(base: &Path, max_bytes: u64, keep: usize) -> std::io::Result<Self> {
if let Some(parent) = base.parent() {
std::fs::create_dir_all(parent)?;
}
let file = OpenOptions::new().create(true).append(true).open(base)?;
let written = file.metadata()?.len();
Ok(Self {
base: base.to_path_buf(),
max_bytes,
keep,
file,
written,
})
}
pub fn write_line(&mut self, tag: &str, line: &str) -> std::io::Result<()> {
let bytes = format!("{tag} {line}\n");
self.file.write_all(bytes.as_bytes())?;
self.written += bytes.len() as u64;
if self.written >= self.max_bytes {
self.rotate()?;
}
Ok(())
}
fn rotate(&mut self) -> std::io::Result<()> {
// Drop the current handle by replacing with /dev/null briefly.
self.file = OpenOptions::new().read(true).open("/dev/null")?;
for i in (1..self.keep).rev() {
let src = self.gen_path(i);
let dst = self.gen_path(i + 1);
if src.exists() {
let _ = std::fs::rename(&src, &dst);
}
}
if self.base.exists() {
let _ = std::fs::rename(&self.base, self.gen_path(1));
}
self.file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.base)?;
self.written = 0;
Ok(())
}
fn gen_path(&self, n: usize) -> PathBuf {
let mut s = self.base.as_os_str().to_os_string();
s.push(format!(".{n}"));
PathBuf::from(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::io::Read;
#[test]
fn writes_lines_with_tags() {
let dir = tempdir().unwrap();
let base = dir.path().join("x.log");
let mut w = RotatingLogWriter::open(&base, 1024, 3).unwrap();
w.write_line("[out]", "hello").unwrap();
w.write_line("[err]", "boom").unwrap();
let mut s = String::new();
File::open(&base)
.unwrap()
.read_to_string(&mut s)
.unwrap();
assert_eq!(s, "[out] hello\n[err] boom\n");
}
#[test]
fn rotates_at_threshold() {
let dir = tempdir().unwrap();
let base = dir.path().join("x.log");
let mut w = RotatingLogWriter::open(&base, 20, 3).unwrap();
for _ in 0..5 {
w.write_line("[out]", "0123456789").unwrap();
}
assert!(base.exists());
let rotated = dir.path().join("x.log.1");
assert!(
rotated.exists(),
"expected rotated file at {}",
rotated.display()
);
}
}