log rotate writer
This commit is contained in:
commit
7df635ef18
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
*.log
|
||||
11
Cargo.toml
Normal file
11
Cargo.toml
Normal 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
13
examples/exp1/main.rs
Normal 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
227
src/lib.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user