Skip to content

Commit

Permalink
add rotate by date
Browse files Browse the repository at this point in the history
With rotation options: Hourly, Daily, Weekly, Monthly, Yearly.
And timestamp from yesterday, hour before, or now.
Add test to rotate by date.
  • Loading branch information
jb-alvarado authored and Ploppz committed May 9, 2022
1 parent eeafc74 commit cf713f7
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Cargo.lock
logs/
target/
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ chrono = { version = "0.4.11", optional = true }
flate2 = "1.0"

[dev-dependencies]
filetime = "0.2"
quickcheck = "0.9.2"
quickcheck_macros = "0.9.1"
tempdir = "0.3.7"
Expand Down
22 changes: 22 additions & 0 deletions examples/rotate_by_date.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use file_rotate::{
compression::Compression,
suffix::{AppendTimestamp, DateFrom, FileLimit},
ContentLimit, FileRotate, TimeFrequency,
};
use std::io::Write;

fn main() {
let mut log = FileRotate::new(
"logs/log",
AppendTimestamp::with_format("%Y-%m-%d", FileLimit::MaxFiles(7), DateFrom::DateYesterday),
ContentLimit::Time(TimeFrequency::Daily),
Compression::None,
);

// Write a bunch of lines
writeln!(log, "Line 1: Hello World!").expect("write log");
for idx in 2..=10 {
std::thread::sleep(std::time::Duration::from_millis(500));
writeln!(log, "Line {}", idx).expect("write log");
}
}
115 changes: 114 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
unused_qualifications
)]

use chrono::prelude::*;
use compression::*;
use std::io::{BufRead, BufReader};
use std::{
Expand All @@ -299,13 +300,30 @@ mod tests;

// ---

/// At which frequency to rotate the file.
#[derive(Clone, Copy, Debug)]
pub enum TimeFrequency {
/// Rotate every hour.
Hourly,
/// Rotate one time a day.
Daily,
/// Rotate ones a week.
Weekly,
/// Rotate every month.
Monthly,
/// Rotate yearly.
Yearly,
}

/// When to move files: Condition on which a file is rotated.
#[derive(Clone, Debug)]
pub enum ContentLimit {
/// Cut the log at the exact size in bytes.
Bytes(usize),
/// Cut the log file at line breaks.
Lines(usize),
/// Cut the log at time interval.
Time(TimeFrequency),
/// Cut the log file after surpassing size in bytes (but having written a complete buffer from a write call.)
BytesSurpassed(usize),
}
Expand Down Expand Up @@ -352,6 +370,7 @@ impl<Repr: Representation> PartialOrd for SuffixInfo<Repr> {
pub struct FileRotate<S: SuffixScheme> {
basepath: PathBuf,
file: Option<File>,
modified: Option<DateTime<Local>>,
content_limit: ContentLimit,
count: usize,
compression: Compression,
Expand Down Expand Up @@ -384,6 +403,7 @@ impl<S: SuffixScheme> FileRotate<S> {
ContentLimit::Lines(lines) => {
assert!(lines > 0);
}
ContentLimit::Time(_) => {}
ContentLimit::BytesSurpassed(bytes) => {
assert!(bytes > 0);
}
Expand All @@ -394,6 +414,7 @@ impl<S: SuffixScheme> FileRotate<S> {

let mut s = Self {
file: None,
modified: None,
basepath,
content_limit,
count: 0,
Expand Down Expand Up @@ -434,6 +455,9 @@ impl<S: SuffixScheme> FileRotate<S> {
ContentLimit::Lines(_) => {
self.count = BufReader::new(file).lines().count();
}
ContentLimit::Time(_) => {
self.modified = mtime(file);
}
}
}
}
Expand Down Expand Up @@ -521,7 +545,6 @@ impl<S: SuffixScheme> FileRotate<S> {
self.suffixes.insert(new_suffix_info);

self.file = Some(File::create(&self.basepath)?);

self.count = 0;

self.handle_old_files()?;
Expand Down Expand Up @@ -593,6 +616,52 @@ impl<S: SuffixScheme> Write for FileRotate<S> {
file.write_all(buf)?;
}
}
ContentLimit::Time(time) => {
let local: DateTime<Local> = now();

if let Some(modified) = self.modified {
match time {
TimeFrequency::Hourly => {
if local.hour() != modified.hour()
|| local.day() != modified.day()
|| local.month() != modified.month()
|| local.year() != modified.year()
{
self.rotate()?;
}
}
TimeFrequency::Daily => {
if local.date() > modified.date() {
self.rotate()?;
}
}
TimeFrequency::Weekly => {
if local.iso_week().week() != modified.iso_week().week()
|| local.year() > modified.year()
{
self.rotate()?;
}
}
TimeFrequency::Monthly => {
if local.month() != modified.month() || local.year() != modified.year()
{
self.rotate()?;
}
}
TimeFrequency::Yearly => {
if local.year() > modified.year() {
self.rotate()?;
}
}
}
}

if let Some(ref mut file) = self.file {
file.write_all(buf)?;

self.modified = Some(local);
}
}
ContentLimit::Lines(lines) => {
while let Some((idx, _)) = buf.iter().enumerate().find(|(_, byte)| *byte == &b'\n')
{
Expand Down Expand Up @@ -629,3 +698,47 @@ impl<S: SuffixScheme> Write for FileRotate<S> {
.unwrap_or(Ok(()))
}
}

/// Get modification time, in non test case.
#[cfg(not(test))]
fn mtime(file: &File) -> Option<DateTime<Local>> {
if let Ok(time) = file.metadata().and_then(|metadata| metadata.modified()) {
return Some(time.into());
}

None
}

/// Get modification time, in test case.
#[cfg(test)]
fn mtime(_: &File) -> Option<DateTime<Local>> {
Some(now())
}

/// Get system time, in non test case.
#[cfg(not(test))]
fn now() -> DateTime<Local> {
Local::now()
}

/// Get mocked system time, in test case.
#[cfg(test)]
pub mod mock_time {
use super::*;
use std::cell::RefCell;

thread_local! {
static MOCK_TIME: RefCell<Option<DateTime<Local>>> = RefCell::new(None);
}

pub fn now() -> DateTime<Local> {
MOCK_TIME.with(|cell| cell.borrow().as_ref().cloned().unwrap_or_else(Local::now))
}

pub fn set_mock_time(time: DateTime<Local>) {
MOCK_TIME.with(|cell| *cell.borrow_mut() = Some(time));
}
}

#[cfg(test)]
pub use mock_time::now;
40 changes: 35 additions & 5 deletions src/suffix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! This behaviour is fully extensible through the [SuffixScheme] trait, and two behaviours are
//! provided: [AppendCount] and [AppendTimestamp]
//!
use super::now;
use crate::SuffixInfo;
#[cfg(feature = "chrono04")]
use chrono::{offset::Local, Duration, NaiveDateTime};
Expand Down Expand Up @@ -140,6 +141,16 @@ impl SuffixScheme for AppendCount {
}
}

/// Add timestamp from:
pub enum DateFrom {
/// Date yesterday, to represent the timestamps within the log file.
DateYesterday,
/// Date from hour ago, useful with rotate hourly.
DateHourAgo,
/// Date from now.
Now,
}

/// Append current timestamp as suffix when rotating files.
/// If the timestamp already exists, an additional number is appended.
///
Expand All @@ -152,6 +163,8 @@ pub struct AppendTimestamp {
pub format: &'static str,
/// The file limit, e.g. when to delete an old file - by age (given by suffix) or by number of files
pub file_limit: FileLimit,
/// Add timestamp from DateFrom
pub date_from: DateFrom,
}

#[cfg(feature = "chrono04")]
Expand All @@ -161,11 +174,16 @@ impl AppendTimestamp {
Self {
format: "%Y%m%dT%H%M%S",
file_limit,
date_from: DateFrom::Now,
}
}
/// Create new AppendTimestamp suffix scheme
pub fn with_format(format: &'static str, file_limit: FileLimit) -> Self {
Self { format, file_limit }
pub fn with_format(format: &'static str, file_limit: FileLimit, date_from: DateFrom) -> Self {
Self {
format,
file_limit,
date_from,
}
}
}

Expand Down Expand Up @@ -214,10 +232,22 @@ impl SuffixScheme for AppendTimestamp {
) -> io::Result<TimestampSuffix> {
assert!(suffix.is_none());
if suffix.is_none() {
let now = Local::now().format(self.format).to_string();
let mut now = now();

match self.date_from {
DateFrom::DateYesterday => {
now = now - Duration::days(1);
}
DateFrom::DateHourAgo => {
now = now - Duration::hours(1);
}
_ => {}
};

let fmt_now = now.format(self.format).to_string();

let number = if let Some(newest_suffix) = newest_suffix {
if newest_suffix.timestamp == now {
if newest_suffix.timestamp == fmt_now {
Some(newest_suffix.number.unwrap_or(0) + 1)
} else {
None
Expand All @@ -226,7 +256,7 @@ impl SuffixScheme for AppendTimestamp {
None
};
Ok(TimestampSuffix {
timestamp: now,
timestamp: fmt_now,
number,
})
} else {
Expand Down
Loading

0 comments on commit cf713f7

Please sign in to comment.