use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
const MAX_SIZE: u64 = 5 * 1024 * 1024; const TARGET_SIZE: u64 = 2_500_000; const MAX_AGE_SECS: u64 = 24 * 60 * 60;
pub struct RotatingLogWriter {
path: PathBuf,
file: File,
}
impl RotatingLogWriter {
pub fn new(path: &Path) -> std::io::Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = OpenOptions::new().create(true).append(true).open(path)?;
Ok(Self {
path: path.to_owned(),
file,
})
}
fn maybe_rotate(&mut self) -> std::io::Result<()> {
let metadata = self.file.metadata()?;
if metadata.len() < MAX_SIZE {
return Ok(());
}
let content = std::fs::read_to_string(&self.path)?;
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
return Ok(());
}
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut surviving: Vec<&str> = lines
.iter()
.filter(|line| {
if let Some(ts) = parse_timestamp_secs(line) {
now_secs.saturating_sub(ts) < MAX_AGE_SECS
} else {
true }
})
.copied()
.collect();
let mut total_bytes: u64 = surviving.iter().map(|l| l.len() as u64 + 1).sum();
if total_bytes > TARGET_SIZE {
while total_bytes > TARGET_SIZE && !surviving.is_empty() {
let removed = surviving.remove(0);
total_bytes -= removed.len() as u64 + 1;
}
}
let mut new_content = String::with_capacity(total_bytes as usize);
for line in &surviving {
new_content.push_str(line);
new_content.push('\n');
}
std::fs::write(&self.path, &new_content)?;
self.file = OpenOptions::new().append(true).open(&self.path)?;
Ok(())
}
}
impl Write for RotatingLogWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let _ = self.maybe_rotate();
self.file.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.flush()
}
}
fn parse_timestamp_secs(line: &str) -> Option<u64> {
if line.len() < 19 {
return None;
}
let ts_part = &line[..19];
if ts_part.as_bytes()[4] != b'-' || ts_part.as_bytes()[10] != b'T' {
return None;
}
let year: u64 = ts_part[0..4].parse().ok()?;
let month: u64 = ts_part[5..7].parse().ok()?;
let day: u64 = ts_part[8..10].parse().ok()?;
let hour: u64 = ts_part[11..13].parse().ok()?;
let min: u64 = ts_part[14..16].parse().ok()?;
let sec: u64 = ts_part[17..19].parse().ok()?;
let mut days: u64 = 0;
for y in 1970..year {
days += if is_leap(y) { 366 } else { 365 };
}
let month_days = if is_leap(year) {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
for m in 0..(month.saturating_sub(1) as usize) {
days += month_days.get(m).copied().unwrap_or(30) as u64;
}
days += day.saturating_sub(1);
Some(days * 86400 + hour * 3600 + min * 60 + sec)
}
fn is_leap(y: u64) -> bool {
y.is_multiple_of(4) && (!y.is_multiple_of(100) || y.is_multiple_of(400))
}