mod fsstats;
use chrono::Local;
use log::{info, warn};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::fs;
use std::io::{BufWriter, Error, Result, Write};
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::time::Instant;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LogWriterConfig {
pub target_dir: PathBuf,
pub prefix: String,
pub suffix: String,
pub max_use_of_total: Option<f64>,
pub max_use_bytes: Option<u64>,
pub min_avail_of_total: Option<f64>,
pub warn_if_avail_reached: bool,
pub min_avail_bytes: Option<usize>,
pub max_file_size: usize,
pub max_file_age: Option<u64>,
pub reserved: Option<usize>,
}
pub struct LogWriter<T: LogWriterCallbacks + Sized + Clone + Debug> {
cfg: LogWriterConfig,
current: BufWriter<fs::File>,
current_name: String,
current_size: usize,
write_start: Instant,
callbacks: T,
}
pub trait LogWriterCallbacks: Sized + Clone + Debug {
fn start_file(&mut self, log_writer: &mut LogWriter<Self>) -> Result<()>;
fn end_file(&mut self, log_writer: &mut LogWriter<Self>) -> Result<()>;
}
#[derive(Clone, Debug)]
pub struct NoopLogWriterCallbacks;
impl LogWriterCallbacks for NoopLogWriterCallbacks {
fn start_file(&mut self, _log_writer: &mut LogWriter<Self>) -> Result<()> {
Ok(())
}
fn end_file(&mut self, _log_writer: &mut LogWriter<Self>) -> Result<()> {
Ok(())
}
}
fn create_next_file(cfg: &LogWriterConfig) -> Result<(String, BufWriter<fs::File>)> {
let name = format!(
"{}{}{}",
cfg.prefix,
Local::now().format("%Y-%m-%d-%H-%M-%S"),
cfg.suffix
);
let file = fs::OpenOptions::new()
.write(true)
.create(true)
.open(cfg.target_dir.join(&name))?;
Ok((name, BufWriter::new(file)))
}
impl LogWriter<NoopLogWriterCallbacks> {
pub fn new(cfg: LogWriterConfig) -> Result<Self> {
LogWriter::new_with_callbacks(cfg, NoopLogWriterCallbacks)
}
}
impl<T: LogWriterCallbacks + Sized + Clone + Debug> LogWriter<T> {
pub fn new_with_callbacks(cfg: LogWriterConfig, callbacks: T) -> Result<Self> {
fs::create_dir_all(&cfg.target_dir)?;
let (current_name, current) = create_next_file(&cfg)?;
let mut log_writer = Self {
cfg,
current_name,
current,
current_size: 0,
write_start: Instant::now(),
callbacks,
};
log_writer.callbacks.clone().start_file(&mut log_writer)?;
Ok(log_writer)
}
fn file_listing<'a>(&'a self) -> Result<impl Iterator<Item = (fs::DirEntry, String)> + 'a> {
let prefix = self.cfg.prefix.clone();
let suffix = self.cfg.suffix.clone();
let iter = fs::read_dir(&self.cfg.target_dir)?
.filter_map(|x| x.ok())
.filter(|x| x.file_type().and_then(|t| Ok(t.is_file())).unwrap_or(false))
.filter_map(|file| match file.file_name().into_string() {
Ok(file_name) => Some((file, file_name)),
Err(_) => None,
})
.filter(move |(_, file_name)| {
file_name.starts_with(&prefix) && file_name.ends_with(&suffix)
});
Ok(iter)
}
fn enough_space(&mut self, len: usize) -> Result<bool> {
let fsstat = fsstats::statvfs(&self.cfg.target_dir)?;
let mut size_limit = u64::MAX;
if let Some(max_use_bytes) = self.cfg.max_use_bytes {
size_limit = std::cmp::min(size_limit, max_use_bytes);
}
if let Some(max_use_of_total) = self.cfg.max_use_of_total {
let mut max_use_bytes = (max_use_of_total * fsstat.total_space as f64) as u64;
if let Some(reserved) = self.cfg.reserved {
max_use_bytes -= reserved as u64;
};
size_limit = std::cmp::min(size_limit, max_use_bytes);
}
if size_limit != u64::MAX {
let mut used = 0u64;
for (entry, _) in self.file_listing()? {
let path = entry.path();
let meta = match entry.metadata() {
Err(_) => {
info!(
"could not get metadata for \"{:?}\", ignoring for size calculation",
&path
);
continue;
}
Ok(meta) => meta,
};
used += meta.blocks() * 512;
}
if used > size_limit {
return Ok(false);
}
}
if let Some(min_avail_of_total) = self.cfg.min_avail_of_total {
let avail = fsstat.available_space - len as u64;
let avail_of_total = avail as f64 / fsstat.total_space as f64;
if avail_of_total < min_avail_of_total {
if self.cfg.warn_if_avail_reached {
warn!("min_avail_of_total reached, you said this shouldn't happen");
}
return Ok(false);
}
}
Ok(true)
}
fn cleanup(&mut self) -> Result<bool> {
let mut entries: Vec<_> = self.file_listing()?.collect();
entries.sort_by(|(_, a), (_, b)| a.cmp(&b));
let (oldest_file, file_name) = match entries.get(0) {
Some(v) => v,
None => {
warn!("log-writer can not free space: no files to delete");
return Err(Error::from_raw_os_error(libc::ENOSPC));
}
};
if *file_name == self.current_name {
warn!("log-writer can not free space: oldest file is current file");
return Err(Error::from_raw_os_error(libc::ENOSPC));
}
fs::remove_file(oldest_file.path())?;
Ok(true)
}
fn next_file(&mut self) -> Result<()> {
let (next_name, next) = create_next_file(&self.cfg)?;
self.callbacks.clone().end_file(self)?;
self.current.flush()?;
self.current_name = next_name;
self.current_size = 0;
self.write_start = Instant::now();
self.current = next;
self.callbacks.clone().start_file(self)?;
Ok(())
}
}
impl<T: LogWriterCallbacks + Sized + Clone + Debug> Write for LogWriter<T> {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
if self.current_size + buf.len() > self.cfg.max_file_size {
self.next_file()?;
}
if let Some(max_file_age) = self.cfg.max_file_age {
if Instant::now().duration_since(self.write_start).as_secs() > max_file_age {
self.next_file()?;
}
}
while !self.enough_space(buf.len())? {
if !self.cleanup()? {
warn!("could not free enough space, this might cause strange behaviour");
break;
}
}
let written = self.current.write(buf)?;
self.current_size += written;
Ok(written)
}
fn flush(&mut self) -> Result<()> {
self.current.flush()
}
}