commit 7df635ef18492cbf9c05d53bc6839238e8b503eb Author: asxalex Date: Tue Jan 30 23:57:06 2024 +0800 log rotate writer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a255f58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +*.log diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cb1cdac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rolling-file" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.79" +chrono = "0.4.33" +time = "0.3.31" diff --git a/examples/exp1/main.rs b/examples/exp1/main.rs new file mode 100644 index 0000000..a2f1b37 --- /dev/null +++ b/examples/exp1/main.rs @@ -0,0 +1,13 @@ +use rolling_file::{FileRoller, PeriodGap}; +use std::io::Write; +use std::thread; +use std::time::Duration; + +fn main() -> std::io::Result<()> { + let mut roller = FileRoller::new("output", 8, PeriodGap::Secondly); + for _ in 0..100 { + let _ = roller.write("hello".as_bytes())?; + thread::sleep(Duration::from_millis(100)); + } + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0116b2e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,227 @@ +use std::env; +use std::ffi::OsString; +use std::fs; +use std::io; + +use chrono::NaiveDateTime; +use chrono::{Datelike, Local, Timelike}; +use std::time::Duration; + +use std::fs::OpenOptions; +use std::fs::{create_dir_all, File}; +use std::io::{BufWriter, Write}; +use std::path::{Path, PathBuf}; + +type CurrentGap = u64; + +// gap of the file +pub enum PeriodGap { + Secondly, + Minutely, + Hourly, + Daily, + Monthly, +} + +pub struct FileRoller { + base_dir: OsString, + max_files: usize, + writer: Option>, + + current_gap: CurrentGap, + period: PeriodGap, +} + +impl FileRoller { + pub fn new

(path: P, max_files: usize, period: PeriodGap) -> Self + where + P: AsRef, + { + let _ = create_dir_all(&path); + let current_dir; + if let Ok(dir) = env::current_dir() { + current_dir = dir; + } else { + current_dir = PathBuf::from(""); + } + let logdir = current_dir.as_path().join(path); + // let current_gap = get_current_gap(&period); + let res = Self { + base_dir: OsString::from(logdir.as_os_str()), + max_files, + writer: None, + current_gap: 0, + period, + }; + println!("got log dir: {:?}", res.base_dir); + res + } + + fn rollover(&mut self) -> anyhow::Result<()> { + let now = get_current_gap(&self.period); + if self.current_gap == now { + return Ok(()); + } + self.current_gap = now; + println!("flushing 1"); + self.flush()?; + println!("flushing 2"); + self.writer.take(); + println!("flushing 3"); + self.open_writer_if_needed()?; + println!("flushing 4"); + let _ = self.delete_old_file(); + println!("flushing 5"); + Ok(()) + } + + fn delete_old_file(&self) -> anyhow::Result<()> { + let duration = get_gap_duration(&self.period, self.max_files); + for entry in fs::read_dir(&self.base_dir)? { + let entry = entry?; + let metadata = entry.metadata()?; + if metadata.is_dir() { + continue; + } + if let Some(filename) = entry.file_name().to_str() { + let fnames: Vec<_> = filename.split(".").collect(); + if fnames.len() == 2 { + if let Ok(dt) = NaiveDateTime::parse_from_str(fnames[0], "%Y%m%d%H%M%S") { + let dt = dt + duration; + let created_at = get_gap_with_time(&self.period, dt); + if self.current_gap >= created_at { + let filepath = PathBuf::from(&self.base_dir).join(entry.file_name()); + let filename = filepath.as_path(); + let _ = fs::remove_file(filename); + } + } + } + } + } + Ok(()) + } + + fn open_writer_if_needed(&mut self) -> io::Result<()> { + if self.writer.is_none() { + let gap = get_current_gap(&self.period); + self.current_gap = gap; + let basepath = PathBuf::from(&self.base_dir).join(format!("{}.log", gap)); + let filename = basepath.as_path(); + println!("opening filename: {:?}", filename); + let fin = OpenOptions::new() + .append(true) + .create(true) + // .write(true) + .open(filename)?; + println!("write ok"); + self.writer = Some(BufWriter::new(fin)); + } + Ok(()) + } +} + +impl io::Write for FileRoller { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if let Err(e) = self.rollover() { + eprintln!( + "WARNING: Failed to rotate logfile {}: {}", + self.base_dir.to_string_lossy(), + e + ); + } + if let Some(writer) = self.writer.as_mut() { + writer.write_all(buf).map(|_| buf.len()) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "unexpected condition: writer missing", + )) + } + } + + fn flush(&mut self) -> io::Result<()> { + if let Some(writer) = self.writer.as_mut() { + writer.flush()?; + } + Ok(()) + } +} + +fn get_gap_duration(period: &PeriodGap, maxfile: usize) -> Duration { + match *period { + PeriodGap::Secondly => Duration::from_secs(1 * maxfile as u64), + PeriodGap::Minutely => Duration::from_secs(60 * maxfile as u64), + PeriodGap::Hourly => Duration::from_secs(3600 * maxfile as u64), + PeriodGap::Daily => Duration::from_secs(24 * 3600 * maxfile as u64), + PeriodGap::Monthly => Duration::from_secs(3600 * 24 * 30 * maxfile as u64), + } +} + +fn get_gap_with_time(period: &PeriodGap, local: DT) -> u64 { + match *period { + PeriodGap::Secondly => { + local.second() as u64 + + local.minute() as u64 * 100 + + local.hour() as u64 * 10_000 + + local.day() as u64 * 1_000_000 + + local.month() as u64 * 100_000_000 + + local.year() as u64 * 10_000_000_000 + } + PeriodGap::Minutely => { + local.minute() as u64 + + local.hour() as u64 * 100 + + local.day() as u64 * 10_000 + + local.month() as u64 * 1_000_000 + + local.year() as u64 * 100_000_000 + } + + PeriodGap::Hourly => { + local.hour() as u64 + + local.day() as u64 * 100 + + local.month() as u64 * 10_000 + + local.year() as u64 * 1_000_000 + } + PeriodGap::Daily => { + local.day() as u64 + local.month() as u64 * 100 + local.year() as u64 * 10_000 + } + PeriodGap::Monthly => local.month() as u64 + local.year() as u64 * 100, + } +} + +fn get_current_gap(period: &PeriodGap) -> u64 { + let local = Local::now(); + return get_gap_with_time(period, local); +} + +/* +fn test_offset() { + let local = Local::now(); + println!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", + local.year(), + local.month(), + local.day(), + local.hour(), + local.minute(), + local.second() + ) +} +*/ + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use std::thread; + use std::time::Duration; + + #[test] + fn it_works() -> io::Result<()> { + let mut roller = FileRoller::new("output", 8, PeriodGap::Secondly); + for _ in 0..200 { + let _ = roller.write_all("hello\n".as_bytes())?; + thread::sleep(Duration::from_millis(100)); + } + Ok(()) + } +}