#![deny(warnings)]
use chrono::prelude::*;
use std::{
convert::TryFrom,
fs::{self, File, OpenOptions},
io::{self, BufWriter, Write},
path::Path,
};
use symlink::{remove_symlink_auto, symlink_auto};
pub trait RollingCondition {
fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool;
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RollingFrequency {
EveryDay,
EveryHour,
EveryMinute,
}
impl RollingFrequency {
pub fn equivalent_datetime(&self, dt: &DateTime<Local>) -> DateTime<Local> {
match self {
RollingFrequency::EveryDay => Local
.with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
.unwrap(),
RollingFrequency::EveryHour => Local
.with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
.unwrap(),
RollingFrequency::EveryMinute => Local
.with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), 0)
.unwrap(),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct RollingConditionBasic {
last_write_opt: Option<DateTime<Local>>,
frequency_opt: Option<RollingFrequency>,
max_size_opt: Option<u64>,
}
impl RollingConditionBasic {
pub fn new() -> RollingConditionBasic {
RollingConditionBasic {
last_write_opt: None,
frequency_opt: None,
max_size_opt: None,
}
}
pub fn frequency(mut self, x: RollingFrequency) -> RollingConditionBasic {
self.frequency_opt = Some(x);
self
}
pub fn daily(mut self) -> RollingConditionBasic {
self.frequency_opt = Some(RollingFrequency::EveryDay);
self
}
pub fn hourly(mut self) -> RollingConditionBasic {
self.frequency_opt = Some(RollingFrequency::EveryHour);
self
}
pub fn minutely(mut self) -> RollingConditionBasic {
self.frequency_opt = Some(RollingFrequency::EveryMinute);
self
}
pub fn max_size(mut self, x: u64) -> RollingConditionBasic {
self.max_size_opt = Some(x);
self
}
}
impl Default for RollingConditionBasic {
fn default() -> Self {
RollingConditionBasic::new().frequency(RollingFrequency::EveryDay)
}
}
impl RollingCondition for RollingConditionBasic {
fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
let mut rollover = false;
if let Some(frequency) = self.frequency_opt.as_ref() {
if let Some(last_write) = self.last_write_opt.as_ref() {
if frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) {
rollover = true;
}
}
}
if let Some(max_size) = self.max_size_opt.as_ref() {
if current_filesize >= *max_size {
rollover = true;
}
}
self.last_write_opt = Some(*now);
rollover
}
}
#[derive(Debug)]
pub struct RollingFileAppender<RC>
where
RC: RollingCondition,
{
condition: RC,
folder: String,
prefix: String,
max_files: usize,
buffer_capacity: Option<usize>,
current_filesize: u64,
writer_opt: Option<BufWriter<File>>,
}
impl<RC> RollingFileAppender<RC>
where
RC: RollingCondition,
{
pub fn new(folder: &str, prefix: &str, condition: RC, max_files: usize) -> io::Result<RollingFileAppender<RC>> {
Self::_new(folder, prefix, condition, max_files, None)
}
pub fn new_with_buffer_capacity(
folder: &str,
prefix: &str,
condition: RC,
max_files: usize,
buffer_capacity: usize,
) -> io::Result<RollingFileAppender<RC>> {
Self::_new(folder, prefix, condition, max_files, Some(buffer_capacity))
}
fn _new(
folder: &str,
prefix: &str,
condition: RC,
max_files: usize,
buffer_capacity: Option<usize>,
) -> io::Result<RollingFileAppender<RC>> {
let folder = folder.to_string();
let prefix = prefix.to_string();
let mut rfa = RollingFileAppender {
condition,
folder,
prefix,
max_files,
buffer_capacity,
current_filesize: 0,
writer_opt: None,
};
rfa.open_writer_if_needed(&Local::now())?;
Ok(rfa)
}
fn check_and_remove_log_file(&mut self) -> io::Result<()> {
let files = std::fs::read_dir(&self.folder)?;
let mut log_files = vec![];
for f in files {
if let Ok(f) = f {
let fname = f.file_name().to_string_lossy().to_string();
if fname.starts_with(&self.prefix) && fname != self.prefix {
log_files.push(fname);
}
}
}
log_files.sort_by(|a, b| b.cmp(a));
if log_files.len() > self.max_files {
for f in log_files.drain(self.max_files..) {
let p = Path::new(&self.folder).join(f);
if let Err(e) = fs::remove_file(&p) {
tracing::error!("WARNING: Failed to remove old logfile {}: {}", p.to_string_lossy(), e);
}
}
}
Ok(())
}
pub fn rollover(&mut self, now: &DateTime<Local>) -> io::Result<()> {
self.flush()?;
self.writer_opt.take();
self.current_filesize = 0;
self.open_writer_if_needed(now)
}
pub fn condition_ref(&self) -> &RC {
&self.condition
}
pub fn condition_mut(&mut self) -> &mut RC {
&mut self.condition
}
fn new_file_name(&self, now: &DateTime<Local>) -> String {
let data_str = now.format("%Y%m%d.%H%M%S").to_string();
format!("{}.{}", self.prefix, data_str)
}
fn open_writer_if_needed(&mut self, now: &DateTime<Local>) -> io::Result<()> {
if self.writer_opt.is_none() {
let p = self.new_file_name(now);
let new_file_path = std::path::Path::new(&self.folder).join(&p);
let f = OpenOptions::new().append(true).create(true).open(&new_file_path)?;
self.writer_opt = Some(if let Some(capacity) = self.buffer_capacity {
BufWriter::with_capacity(capacity, f)
} else {
BufWriter::new(f)
});
{
let folder = std::path::Path::new(&self.folder);
if let Ok(path) = folder.canonicalize() {
let latest_log_symlink = path.join(&self.prefix);
let _ = remove_symlink_auto(folder.join(&self.prefix));
let _ = symlink_auto(&new_file_path.canonicalize().unwrap(), &latest_log_symlink);
}
}
self.current_filesize = fs::metadata(&p).map_or(0, |m| m.len());
self.check_and_remove_log_file()?;
}
Ok(())
}
pub fn write_with_datetime(&mut self, buf: &[u8], now: &DateTime<Local>) -> io::Result<usize> {
if self.condition.should_rollover(now, self.current_filesize) {
if let Err(e) = self.rollover(now) {
eprintln!("WARNING: Failed to rotate logfile {}", e);
}
}
self.open_writer_if_needed(now)?;
if let Some(writer) = self.writer_opt.as_mut() {
let buf_len = buf.len();
writer.write_all(buf).map(|_| {
self.current_filesize += u64::try_from(buf_len).unwrap_or(u64::MAX);
buf_len
})
} else {
Err(io::Error::new(
io::ErrorKind::Other,
"unexpected condition: writer is missing",
))
}
}
}
impl<RC> io::Write for RollingFileAppender<RC>
where
RC: RollingCondition,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let now = Local::now();
self.write_with_datetime(buf, &now)
}
fn flush(&mut self) -> io::Result<()> {
if let Some(writer) = self.writer_opt.as_mut() {
writer.flush()?;
}
Ok(())
}
}
pub type BasicRollingFileAppender = RollingFileAppender<RollingConditionBasic>;
#[cfg(test)]
mod t {
#[test]
fn test_number_of_log_files() {
use super::*;
let folder = "./log";
let prefix = "log.log";
let _ = std::fs::remove_dir_all(folder);
std::fs::create_dir(folder).unwrap();
let condition = RollingConditionBasic::new().hourly();
let max_files = 3;
let mut rfa = RollingFileAppender::new(folder, prefix, condition, max_files).unwrap();
rfa.write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
.unwrap();
rfa.write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 0).unwrap())
.unwrap();
rfa.write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap())
.unwrap();
rfa.write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap())
.unwrap();
rfa.write_with_datetime(b"Line 5\n", &Local.with_ymd_and_hms(2022, 5, 31, 2, 4, 0).unwrap())
.unwrap();
rfa.flush().unwrap();
let files = std::fs::read_dir(folder).unwrap();
let mut log_files = vec![];
for f in files {
if let Ok(f) = f {
let fname = f.file_name().to_string_lossy().to_string();
if fname.starts_with(prefix) && fname != "log.log" {
log_files.push(fname);
}
}
}
assert_eq!(log_files.len(), max_files);
}
}