use crate::config::LogRotation;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::Instant;
#[derive(Debug)]
pub struct RotatingFile {
file: File,
path: PathBuf,
policy: LogRotation,
bytes_written: u64,
events_written: u32,
opened_at: Instant,
opened_date: String,
}
impl RotatingFile {
pub fn open(path: &Path, policy: LogRotation) -> io::Result<Self> {
let file =
OpenOptions::new().create(true).append(true).open(path)?;
let bytes_written =
file.metadata().map(|m| m.len()).unwrap_or(0);
Ok(Self {
file,
path: path.to_path_buf(),
policy,
bytes_written,
events_written: 0,
opened_at: Instant::now(),
opened_date: today_date_string(),
})
}
pub fn write_batch(
&mut self,
data: &[u8],
event_count: u32,
) -> io::Result<()> {
self.file.write_all(data)?;
self.bytes_written += data.len() as u64;
self.events_written += event_count;
if self.should_rotate() {
self.rotate()?;
}
Ok(())
}
fn should_rotate(&self) -> bool {
match self.policy {
LogRotation::Size(max_bytes) => {
self.bytes_written >= max_bytes.get()
}
LogRotation::Time(seconds) => {
self.opened_at.elapsed().as_secs() >= seconds.get()
}
LogRotation::Date => {
today_date_string() != self.opened_date
}
LogRotation::Count(max_events) => {
self.events_written >= max_events
}
}
}
fn rotate(&mut self) -> io::Result<()> {
self.file.flush()?;
let timestamp = chrono_like_timestamp();
let rotated_name = if let Some(ext) = self.path.extension() {
let stem = self.path.with_extension("");
PathBuf::from(format!(
"{}.{timestamp}.{}",
stem.display(),
ext.to_string_lossy()
))
} else {
PathBuf::from(format!(
"{}.{timestamp}",
self.path.display()
))
};
fs::rename(&self.path, &rotated_name)?;
self.file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
self.bytes_written = 0;
self.events_written = 0;
self.opened_at = Instant::now();
self.opened_date = today_date_string();
Ok(())
}
}
fn today_date_string() -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let days = secs / 86400;
let (year, month, day) = days_to_ymd(days);
format!("{year:04}-{month:02}-{day:02}")
}
fn chrono_like_timestamp() -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let days = secs / 86400;
let (year, month, day) = days_to_ymd(days);
let day_secs = secs % 86400;
let h = day_secs / 3600;
let m = (day_secs % 3600) / 60;
let s = day_secs % 60;
format!("{year:04}{month:02}{day:02}-{h:02}{m:02}{s:02}")
}
const fn days_to_ymd(days: u64) -> (u64, u64, u64) {
let z = days + 719_468;
let era = z / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
use std::num::NonZeroU64;
#[test]
fn test_today_date_string_format() {
let date = today_date_string();
assert_eq!(date.len(), 10);
assert_eq!(&date[4..5], "-");
assert_eq!(&date[7..8], "-");
}
#[test]
fn test_chrono_like_timestamp_format() {
let ts = chrono_like_timestamp();
assert_eq!(ts.len(), 15);
assert_eq!(&ts[8..9], "-");
}
#[test]
fn test_days_to_ymd_epoch() {
let (y, m, d) = days_to_ymd(0);
assert_eq!((y, m, d), (1970, 1, 1));
}
#[test]
fn test_rotating_file_size_based() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
let policy = LogRotation::Size(NonZeroU64::new(100).unwrap());
let mut rf = RotatingFile::open(&path, policy).unwrap();
rf.write_batch(&[b'A'; 50], 1).unwrap();
assert!(path.exists());
rf.write_batch(&[b'B'; 60], 1).unwrap();
assert!(path.exists());
let entries: Vec<_> = fs::read_dir(dir.path())
.unwrap()
.filter_map(Result::ok)
.collect();
assert!(entries.len() >= 2, "expected rotated file");
}
#[test]
fn test_rotating_file_count_based() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("count.log");
let policy = LogRotation::Count(3);
let mut rf = RotatingFile::open(&path, policy).unwrap();
rf.write_batch(b"event1\n", 1).unwrap();
rf.write_batch(b"event2\n", 1).unwrap();
rf.write_batch(b"event3\n", 1).unwrap(); let entries: Vec<_> = fs::read_dir(dir.path())
.unwrap()
.filter_map(Result::ok)
.collect();
assert!(entries.len() >= 2, "expected rotated file");
}
}