log rotate writer

This commit is contained in:
asxalex 2024-01-30 23:57:06 +08:00
commit 7df635ef18
4 changed files with 254 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
/Cargo.lock
*.log

11
Cargo.toml Normal file
View File

@ -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"

13
examples/exp1/main.rs Normal file
View File

@ -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(())
}

227
src/lib.rs Normal file
View File

@ -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<BufWriter<File>>,
current_gap: CurrentGap,
period: PeriodGap,
}
impl FileRoller {
pub fn new<P>(path: P, max_files: usize, period: PeriodGap) -> Self
where
P: AsRef<Path>,
{
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<usize> {
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<DT: Datelike + Timelike>(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(())
}
}