#![deny(
missing_docs,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unused_import_braces,
unused_qualifications
)]
use chrono::prelude::*;
use compression::*;
use std::io::{BufRead, BufReader};
use std::{
cmp::Ordering,
collections::BTreeSet,
fs::{self, File, OpenOptions},
io::{self, Write},
path::{Path, PathBuf},
};
use suffix::*;
pub mod compression;
pub mod suffix;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug)]
pub enum TimeFrequency {
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
}
#[derive(Clone, Debug)]
pub enum ContentLimit {
Bytes(usize),
Lines(usize),
Time(TimeFrequency),
BytesSurpassed(usize),
None,
}
#[derive(Clone, Debug, Eq)]
pub struct SuffixInfo<Repr> {
pub suffix: Repr,
pub compressed: bool,
}
impl<R: PartialEq> PartialEq for SuffixInfo<R> {
fn eq(&self, other: &Self) -> bool {
self.suffix == other.suffix
}
}
impl<Repr: Representation> SuffixInfo<Repr> {
pub fn to_path(&self, basepath: &Path) -> PathBuf {
let path = self.suffix.to_path(basepath);
if self.compressed {
PathBuf::from(format!("{}.gz", path.display()))
} else {
path
}
}
}
impl<Repr: Representation> Ord for SuffixInfo<Repr> {
fn cmp(&self, other: &Self) -> Ordering {
self.suffix.cmp(&other.suffix)
}
}
impl<Repr: Representation> PartialOrd for SuffixInfo<Repr> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug)]
pub struct FileRotate<S: SuffixScheme> {
basepath: PathBuf,
file: Option<File>,
modified: Option<DateTime<Local>>,
content_limit: ContentLimit,
count: usize,
compression: Compression,
suffix_scheme: S,
suffixes: BTreeSet<SuffixInfo<S::Repr>>,
open_options: Option<OpenOptions>,
}
impl<S: SuffixScheme> FileRotate<S> {
pub fn new<P: AsRef<Path>>(
path: P,
suffix_scheme: S,
content_limit: ContentLimit,
compression: Compression,
open_options: Option<OpenOptions>,
) -> Self {
match content_limit {
ContentLimit::Bytes(bytes) => {
assert!(bytes > 0);
}
ContentLimit::Lines(lines) => {
assert!(lines > 0);
}
ContentLimit::Time(_) => {}
ContentLimit::BytesSurpassed(bytes) => {
assert!(bytes > 0);
}
ContentLimit::None => {}
};
let basepath = path.as_ref().to_path_buf();
fs::create_dir_all(basepath.parent().unwrap()).expect("create dir");
let mut s = Self {
file: None,
modified: None,
basepath,
content_limit,
count: 0,
compression,
suffixes: BTreeSet::new(),
suffix_scheme,
open_options,
};
s.ensure_log_directory_exists();
s.scan_suffixes();
s
}
fn ensure_log_directory_exists(&mut self) {
let path = self.basepath.parent().unwrap();
if !path.exists() {
let _ = fs::create_dir_all(path).expect("create dir");
self.scan_suffixes();
}
if !self.basepath.exists() || self.file.is_none() {
self.open_file();
match self.file {
None => self.count = 0,
Some(ref mut file) => {
match self.content_limit {
ContentLimit::Bytes(_) | ContentLimit::BytesSurpassed(_) => {
if let Ok(metadata) = file.metadata() {
self.count = metadata.len() as usize;
} else {
self.count = 0;
}
}
ContentLimit::Lines(_) => {
self.count = BufReader::new(file).lines().count();
}
ContentLimit::Time(_) => {
self.modified = mtime(file);
}
ContentLimit::None => {}
}
}
}
}
}
fn open_file(&mut self) {
let open_options = self.open_options.clone().unwrap_or_else(|| {
let mut o = OpenOptions::new();
o.read(true).create(true).append(true);
o
});
self.file = open_options.open(&self.basepath).ok();
}
fn scan_suffixes(&mut self) {
self.suffixes = self.suffix_scheme.scan_suffixes(&self.basepath);
}
pub fn log_paths(&mut self) -> Vec<PathBuf> {
self.suffixes
.iter()
.rev()
.map(|suffix| suffix.to_path(&self.basepath))
.collect::<Vec<_>>()
}
fn move_file_with_suffix(
&mut self,
old_suffix_info: Option<SuffixInfo<S::Repr>>,
) -> io::Result<SuffixInfo<S::Repr>> {
let newest_suffix = self.suffixes.iter().next().map(|info| &info.suffix);
let new_suffix = self.suffix_scheme.rotate_file(
&self.basepath,
newest_suffix,
&old_suffix_info.clone().map(|i| i.suffix),
)?;
let new_suffix_info = SuffixInfo {
suffix: new_suffix,
compressed: old_suffix_info
.as_ref()
.map(|x| x.compressed)
.unwrap_or(false),
};
let new_path = new_suffix_info.to_path(&self.basepath);
let existing_suffix_info = self.suffixes.get(&new_suffix_info).cloned();
let newly_created_suffix = if let Some(existing_suffix_info) = existing_suffix_info {
self.suffixes.replace(new_suffix_info);
self.move_file_with_suffix(Some(existing_suffix_info))?
} else {
new_suffix_info
};
let old_path = match old_suffix_info {
Some(suffix) => suffix.to_path(&self.basepath),
None => self.basepath.clone(),
};
assert!(old_path.exists());
assert!(!new_path.exists());
fs::rename(old_path, new_path)?;
Ok(newly_created_suffix)
}
pub fn rotate(&mut self) -> io::Result<()> {
self.ensure_log_directory_exists();
let _ = self.file.take();
let new_suffix_info = self.move_file_with_suffix(None)?;
self.suffixes.insert(new_suffix_info);
self.open_file();
self.count = 0;
self.handle_old_files()?;
Ok(())
}
fn handle_old_files(&mut self) -> io::Result<()> {
let mut youngest_old = None;
let mut result = Ok(());
for (i, suffix) in self.suffixes.iter().enumerate().rev() {
if self.suffix_scheme.too_old(&suffix.suffix, i) {
result = result.and(fs::remove_file(suffix.to_path(&self.basepath)));
youngest_old = Some((*suffix).clone());
} else {
break;
}
}
if let Some(youngest_old) = youngest_old {
let _ = self.suffixes.split_off(&youngest_old);
}
if let Compression::OnRotate(max_file_n) = self.compression {
let n = (self.suffixes.len() as i32 - max_file_n as i32).max(0) as usize;
let suffixes_to_compress = self
.suffixes
.iter()
.rev()
.take(n)
.filter(|info| !info.compressed)
.cloned()
.collect::<Vec<_>>();
for info in suffixes_to_compress {
let path = info.suffix.to_path(&self.basepath);
compress(&path)?;
self.suffixes.replace(SuffixInfo {
compressed: true,
..info
});
}
}
result
}
}
impl<S: SuffixScheme> Write for FileRotate<S> {
fn write(&mut self, mut buf: &[u8]) -> io::Result<usize> {
let written = buf.len();
match self.content_limit {
ContentLimit::Bytes(bytes) => {
while self.count + buf.len() > bytes {
let bytes_left = bytes.saturating_sub(self.count);
if let Some(ref mut file) = self.file {
file.write_all(&buf[..bytes_left])?;
}
self.rotate()?;
buf = &buf[bytes_left..];
}
self.count += buf.len();
if let Some(ref mut file) = self.file {
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')
{
if let Some(ref mut file) = self.file {
file.write_all(&buf[..idx + 1])?;
}
self.count += 1;
buf = &buf[idx + 1..];
if self.count >= lines {
self.rotate()?;
}
}
if let Some(ref mut file) = self.file {
file.write_all(buf)?;
}
}
ContentLimit::BytesSurpassed(bytes) => {
if self.count > bytes {
self.rotate()?
}
if let Some(ref mut file) = self.file {
file.write_all(buf)?;
}
self.count += buf.len();
}
ContentLimit::None => {
if let Some(ref mut file) = self.file {
file.write_all(buf)?;
}
}
}
Ok(written)
}
fn flush(&mut self) -> io::Result<()> {
self.file
.as_mut()
.map(|file| file.flush())
.unwrap_or(Ok(()))
}
}
#[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
}
#[cfg(test)]
fn mtime(_: &File) -> Option<DateTime<Local>> {
Some(now())
}
#[cfg(not(test))]
fn now() -> DateTime<Local> {
Local::now()
}
#[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;