use crate::deferred_now::DeferredNow;
use crate::flexi_error::FlexiLoggerError;
use crate::formats::default_format;
use crate::logger::{Age, Cleanup, Criterion, Naming};
use crate::writers::log_writer::LogWriter;
use crate::FormatFunction;
use chrono::{DateTime, Datelike, Local, Timelike};
use log::Record;
use std::borrow::BorrowMut;
use std::cell::RefCell;
use std::cmp::max;
use std::env;
use std::fs::{File, OpenOptions};
#[cfg(feature = "ziplogs")]
use std::io::Read;
use std::io::{BufRead, BufReader, LineWriter, Write};
use std::ops::{Add, DerefMut};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
const CURRENT_INFIX: &str = "_rCURRENT";
fn number_infix(idx: u32) -> String {
format!("_r{:0>5}", idx)
}
struct RotationConfig {
criterion: Criterion,
naming: Naming,
cleanup: Cleanup,
}
struct FilenameConfig {
directory: PathBuf,
file_basename: String,
suffix: String,
use_timestamp: bool,
}
struct FileLogWriterConfig {
format: FormatFunction,
print_message: bool,
append: bool,
filename_config: FilenameConfig,
o_create_symlink: Option<PathBuf>,
use_windows_line_ending: bool,
}
impl FileLogWriterConfig {
pub fn default() -> FileLogWriterConfig {
FileLogWriterConfig {
format: default_format,
print_message: false,
filename_config: FilenameConfig {
directory: PathBuf::from("."),
file_basename: String::new(),
suffix: "log".to_string(),
use_timestamp: true,
},
append: false,
o_create_symlink: None,
use_windows_line_ending: false,
}
}
}
pub struct FileLogWriterBuilder {
discriminant: Option<String>,
config: FileLogWriterConfig,
o_rotation_config: Option<RotationConfig>,
max_log_level: log::LevelFilter,
}
impl FileLogWriterBuilder {
pub fn print_message(mut self) -> FileLogWriterBuilder {
self.config.print_message = true;
self
}
pub fn format(mut self, format: FormatFunction) -> FileLogWriterBuilder {
self.config.format = format;
self
}
pub fn directory<P: Into<PathBuf>>(mut self, directory: P) -> FileLogWriterBuilder {
self.config.filename_config.directory = directory.into();
self
}
pub fn suffix<S: Into<String>>(mut self, suffix: S) -> FileLogWriterBuilder {
self.config.filename_config.suffix = suffix.into();
self
}
pub fn suppress_timestamp(mut self) -> FileLogWriterBuilder {
self.config.filename_config.use_timestamp = false;
self
}
pub fn rotate(
mut self,
criterion: Criterion,
naming: Naming,
cleanup: Cleanup,
) -> FileLogWriterBuilder {
self.o_rotation_config = Some(RotationConfig {
criterion,
naming,
cleanup,
});
self.config.filename_config.use_timestamp = false;
self
}
pub fn append(mut self) -> FileLogWriterBuilder {
self.config.append = true;
self
}
pub fn discriminant<S: Into<String>>(mut self, discriminant: S) -> FileLogWriterBuilder {
self.discriminant = Some(discriminant.into());
self
}
pub fn create_symlink<P: Into<PathBuf>>(mut self, symlink: P) -> FileLogWriterBuilder {
self.config.o_create_symlink = Some(symlink.into());
self
}
pub fn use_windows_line_ending(mut self) -> FileLogWriterBuilder {
self.config.use_windows_line_ending = true;
self
}
pub fn try_build(mut self) -> Result<FileLogWriter, FlexiLoggerError> {
let p_directory = Path::new(&self.config.filename_config.directory);
std::fs::create_dir_all(&p_directory)?;
if !std::fs::metadata(&p_directory)?.is_dir() {
return Err(FlexiLoggerError::BadDirectory);
};
let arg0 = env::args().nth(0).unwrap_or_else(|| "rs".to_owned());
self.config.filename_config.file_basename =
Path::new(&arg0).file_stem().unwrap().to_string_lossy().to_string();
if let Some(discriminant) = self.discriminant {
self.config.filename_config.file_basename += &format!("_{}", discriminant);
}
if self.config.filename_config.use_timestamp {
self.config.filename_config.file_basename +=
&Local::now().format("_%Y-%m-%d_%H-%M-%S").to_string();
};
Ok(FileLogWriter {
state: Mutex::new(FileLogWriterState::try_new(
&self.config,
&self.o_rotation_config,
)?),
config: self.config,
max_log_level: self.max_log_level,
})
}
}
impl FileLogWriterBuilder {
pub fn o_print_message(mut self, print_message: bool) -> FileLogWriterBuilder {
self.config.print_message = print_message;
self
}
pub fn o_directory<P: Into<PathBuf>>(mut self, directory: Option<P>) -> FileLogWriterBuilder {
self.config.filename_config.directory = directory
.map(Into::into)
.unwrap_or_else(|| PathBuf::from("."));
self
}
pub fn o_timestamp(mut self, use_timestamp: bool) -> FileLogWriterBuilder {
self.config.filename_config.use_timestamp = use_timestamp;
self
}
pub fn o_rotate(
mut self,
rotate_config: Option<(Criterion, Naming, Cleanup)>,
) -> FileLogWriterBuilder {
match rotate_config {
Some((criterion, naming, cleanup)) => {
self.o_rotation_config = Some(RotationConfig {
criterion,
naming,
cleanup,
});
self.config.filename_config.use_timestamp = false;
}
None => {
self.o_rotation_config = None;
self.config.filename_config.use_timestamp = true;
}
}
self
}
pub fn o_append(mut self, append: bool) -> FileLogWriterBuilder {
self.config.append = append;
self
}
pub fn o_discriminant<S: Into<String>>(
mut self,
discriminant: Option<S>,
) -> FileLogWriterBuilder {
self.discriminant = discriminant.map(Into::into);
self
}
pub fn o_create_symlink<S: Into<PathBuf>>(
mut self,
symlink: Option<S>,
) -> FileLogWriterBuilder {
self.config.o_create_symlink = symlink.map(Into::into);
self
}
}
#[derive(Clone, Copy)]
enum IdxState {
Start,
Idx(u32),
}
enum NamingState {
CreatedAt,
IdxState(IdxState),
}
enum RollState {
Size(u64, u64),
Age(Age),
}
struct RotationState {
naming_state: NamingState,
roll_state: RollState,
created_at: DateTime<Local>,
cleanup: Cleanup,
}
impl RotationState {
fn rotation_necessary(&self) -> bool {
match &self.roll_state {
RollState::Size(max_size, current_size) => current_size > max_size,
RollState::Age(age) => {
let now = Local::now();
match age {
Age::Day => self.created_at.num_days_from_ce() != now.num_days_from_ce(),
Age::Hour => {
self.created_at.num_days_from_ce() != now.num_days_from_ce()
|| self.created_at.hour() != now.hour()
}
Age::Minute => {
self.created_at.num_days_from_ce() != now.num_days_from_ce()
|| self.created_at.hour() != now.hour()
|| self.created_at.minute() != now.minute()
}
Age::Second => {
self.created_at.num_days_from_ce() != now.num_days_from_ce()
|| self.created_at.hour() != now.hour()
|| self.created_at.minute() != now.minute()
|| self.created_at.second() != now.second()
}
}
}
}
}
}
struct FileLogWriterState {
line_writer: Option<LineWriter<File>>,
o_rotation_state: Option<RotationState>,
line_ending: &'static [u8],
}
impl FileLogWriterState {
fn try_new(
config: &FileLogWriterConfig,
o_rotation_config: &Option<RotationConfig>,
) -> Result<FileLogWriterState, FlexiLoggerError> {
let (line_writer, o_rotation_state) = match o_rotation_config {
None => {
let (line_writer, _created_at, _p_path) = get_linewriter(config, false)?;
(line_writer, None)
}
Some(rotate_config) => {
let naming_state = match rotate_config.naming {
Naming::Timestamps => {
if !config.append {
rotate_output_file_to_date(
&get_creation_date(&get_filepath(
Some(CURRENT_INFIX),
&config.filename_config,
))?,
config,
)?;
}
NamingState::CreatedAt
}
Naming::Numbers => {
let mut rotation_state = get_highest_rotate_idx(&config.filename_config);
if !config.append {
rotation_state = rotate_output_file_to_idx(rotation_state, config)?;
}
NamingState::IdxState(rotation_state)
}
};
let (line_writer, created_at, p_path) = get_linewriter(config, true)?;
let cleanup = rotate_config.cleanup;
let roll_state = match &rotate_config.criterion {
Criterion::Age(age) => RollState::Age(*age),
Criterion::Size(size) => {
let written_bytes = if config.append {
std::fs::metadata(&p_path)?.len()
} else {
0
};
RollState::Size(*size, written_bytes)
}
};
(
line_writer,
Some(RotationState {
naming_state,
roll_state,
created_at,
cleanup,
}),
)
}
};
Ok(FileLogWriterState {
line_writer: Some(line_writer),
o_rotation_state,
line_ending: if config.use_windows_line_ending {
b"\r\n"
} else {
b"\n"
},
})
}
fn line_writer(&mut self) -> &mut LineWriter<File> {
self.line_writer
.as_mut()
.expect("FlexiLogger: line_writer unexpectedly not available")
}
#[inline]
fn mount_next_linewriter_if_necessary(
&mut self,
config: &FileLogWriterConfig,
) -> Result<(), FlexiLoggerError> {
if let Some(ref mut rotation_state) = self.o_rotation_state {
if rotation_state.rotation_necessary() {
self.line_writer = None;
match rotation_state.naming_state {
NamingState::CreatedAt => {
rotate_output_file_to_date(&rotation_state.created_at, config)?;
}
NamingState::IdxState(ref mut idx_state) => {
*idx_state = rotate_output_file_to_idx(*idx_state, config)?;
}
}
let (line_writer, created_at, _) = get_linewriter(config, true)?;
self.line_writer = Some(line_writer);
rotation_state.created_at = created_at;
if let RollState::Size(_max_size, ref mut current_size) = rotation_state.roll_state
{
*current_size = 0;
}
let cleanup_config: &Cleanup = &rotation_state.cleanup;
let filename_config: &FilenameConfig = &config.filename_config;
remove_or_zip_too_old_logfiles(cleanup_config, filename_config)?;
}
}
Ok(())
}
}
impl Write for FileLogWriterState {
#[inline]
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.line_writer().write_all(buf)?;
if let Some(ref mut rotation_state) = self.o_rotation_state {
if let RollState::Size(_max_size, ref mut current_size) = rotation_state.roll_state {
*current_size += buf.len() as u64;
}
};
Ok(buf.len())
}
#[inline]
fn flush(&mut self) -> std::io::Result<()> {
self.line_writer().flush()
}
}
fn get_filepath(o_infix: Option<&str>, config: &FilenameConfig) -> PathBuf {
let mut s_filename = String::with_capacity(
config.file_basename.len() + o_infix.map(str::len).unwrap_or(0) + 1 + config.suffix.len(),
) + &config.file_basename;
if let Some(infix) = o_infix {
s_filename += infix;
};
s_filename += ".";
s_filename += &config.suffix;
let mut p_path = config.directory.to_path_buf();
p_path.push(s_filename);
p_path
}
fn get_linewriter(
config: &FileLogWriterConfig,
with_rotation: bool,
) -> Result<(LineWriter<File>, DateTime<Local>, PathBuf), FlexiLoggerError> {
let o_infix = if with_rotation {
Some(CURRENT_INFIX)
} else {
None
};
let p_path = get_filepath(o_infix, &config.filename_config);
if config.print_message {
println!("Log is written to {}", &p_path.display());
}
if let Some(ref link) = config.o_create_symlink {
self::platform::create_symlink_if_possible(link, &p_path);
}
let lw = LineWriter::new(
OpenOptions::new()
.write(true)
.create(true)
.append(config.append)
.truncate(!config.append)
.open(&p_path)?,
);
Ok((lw, get_creation_date(&p_path)?, p_path))
}
fn get_highest_rotate_idx(filename_config: &FilenameConfig) -> IdxState {
match list_of_log_and_zip_files(filename_config) {
Err(e) => {
eprintln!("Listing rotated log files failed with {}", e);
IdxState::Start
}
Ok(globresults) => {
let mut highest_idx = IdxState::Start;
for globresult in globresults {
match globresult {
Err(e) => eprintln!("Error when reading directory for log files: {:?}", e),
Ok(pathbuf) => {
let filename = pathbuf.file_stem().unwrap().to_string_lossy();
let mut it = filename.rsplit("_r");
let idx: u32 = it.next().unwrap().parse().unwrap_or(0);
highest_idx = match highest_idx {
IdxState::Start => IdxState::Idx(idx),
IdxState::Idx(prev) => IdxState::Idx(max(prev, idx)),
};
}
}
}
highest_idx
}
}
}
fn list_of_log_and_zip_files(
filename_config: &FilenameConfig,
) -> Result<std::iter::Chain<glob::Paths, glob::Paths>, FlexiLoggerError> {
let fn_pattern = String::with_capacity(180)
.add(&filename_config.file_basename)
.add("_r[0-9]*")
.add(".");
let mut log_pattern = filename_config.directory.clone();
log_pattern.push(fn_pattern.clone().add(&filename_config.suffix));
let mut zip_pattern = filename_config.directory.clone();
zip_pattern.push(fn_pattern.clone().add("zip"));
Ok(glob::glob(&log_pattern.as_os_str().to_string_lossy())?
.chain(glob::glob(&zip_pattern.as_os_str().to_string_lossy())?))
}
fn remove_or_zip_too_old_logfiles(
cleanup_config: &Cleanup,
filename_config: &FilenameConfig,
) -> Result<(), FlexiLoggerError> {
let (log_limit, zip_limit) = match *cleanup_config {
Cleanup::Never => {
return Ok(());
}
Cleanup::KeepLogFiles(log_limit) => (log_limit, 0),
#[cfg(feature = "ziplogs")]
Cleanup::KeepZipFiles(zip_limit) => (0, zip_limit),
#[cfg(feature = "ziplogs")]
Cleanup::KeepLogAndZipFiles(log_limit, zip_limit) => (log_limit, zip_limit),
};
let mut file_list: Vec<_> = list_of_log_and_zip_files(&filename_config)?
.filter_map(Result::ok)
.collect();
file_list.sort_unstable();
let total_number_of_files = file_list.len();
for (index, file) in file_list.iter().enumerate() {
if total_number_of_files - index > log_limit + zip_limit {
std::fs::remove_file(&file)?;
} else if total_number_of_files - index > log_limit {
#[cfg(feature = "ziplogs")]
{
if let Some(extension) = file.extension() {
if extension != "zip" {
let mut old_file = File::open(file)?;
let mut zip_file = file.clone();
zip_file.set_extension("zip");
let mut zip = zip::ZipWriter::new(File::create(zip_file)?);
let options = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Bzip2);
zip.start_file(file.file_name().unwrap().to_string_lossy(), options)?;
{
let mut buf = Vec::<u8>::new();
old_file.read_to_end(&mut buf)?;
zip.write_all(&buf)?;
}
zip.finish()?;
std::fs::remove_file(&file)?;
}
}
}
}
}
Ok(())
}
fn rotate_output_file_to_date(
creation_date: &DateTime<Local>,
config: &FileLogWriterConfig,
) -> Result<(), FlexiLoggerError> {
let current_path = get_filepath(Some(CURRENT_INFIX), &config.filename_config);
let mut rotated_path = get_filepath(
Some(&creation_date.format("_r%Y-%m-%d_%H-%M-%S").to_string()),
&config.filename_config,
);
let mut i = 0_u32;
while (*rotated_path).exists() {
rotated_path = get_filepath(
Some(
&creation_date
.format("_r%Y-%m-%d_%H-%M-%S")
.to_string()
.add(&format!("-restart-{}", i)),
),
&config.filename_config,
);
i += 1;
}
match std::fs::rename(¤t_path, &rotated_path) {
Ok(()) => Ok(()),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(())
} else {
Err(FlexiLoggerError::Io(e))
}
}
}
}
fn rotate_output_file_to_idx(
idx_state: IdxState,
config: &FileLogWriterConfig,
) -> Result<IdxState, FlexiLoggerError> {
let new_idx = match idx_state {
IdxState::Start => 0,
IdxState::Idx(idx) => idx + 1,
};
match std::fs::rename(
get_filepath(Some(CURRENT_INFIX), &config.filename_config),
get_filepath(Some(&number_infix(new_idx)), &config.filename_config),
) {
Ok(()) => Ok(IdxState::Idx(new_idx)),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(idx_state)
} else {
Err(FlexiLoggerError::Io(e))
}
}
}
}
#[allow(unused_variables)]
fn get_creation_date(path: &PathBuf) -> Result<DateTime<Local>, FlexiLoggerError> {
#[cfg(any(target_os = "windows", target_os = "linux"))]
return get_fake_creation_date();
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
match try_get_creation_date(path) {
Ok(d) => Ok(d),
Err(e) => get_fake_creation_date(),
}
}
fn get_fake_creation_date() -> Result<DateTime<Local>, FlexiLoggerError> {
Ok(Local::now())
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
fn try_get_creation_date(path: &PathBuf) -> Result<DateTime<Local>, FlexiLoggerError> {
Ok(std::fs::metadata(path)?.created()?.into())
}
pub struct FileLogWriter {
config: FileLogWriterConfig,
state: Mutex<FileLogWriterState>,
max_log_level: log::LevelFilter,
}
impl FileLogWriter {
pub fn builder() -> FileLogWriterBuilder {
FileLogWriterBuilder {
discriminant: None,
o_rotation_config: None,
config: FileLogWriterConfig::default(),
max_log_level: log::LevelFilter::Trace,
}
}
#[inline]
pub fn format(&self) -> FormatFunction {
self.config.format
}
#[doc(hidden)]
pub fn validate_logs(&self, expected: &[(&'static str, &'static str, &'static str)]) -> bool {
let mut state_guard = self.state.lock().unwrap();
let path = get_filepath(
state_guard
.borrow_mut()
.o_rotation_state
.as_ref()
.map(|_| CURRENT_INFIX),
&self.config.filename_config,
);
let f = File::open(path).unwrap();
let mut reader = BufReader::new(f);
let mut line = String::new();
for tuple in expected {
line.clear();
reader.read_line(&mut line).unwrap();
assert!(
line.contains(&tuple.0),
"Did not find tuple.0 = {}",
tuple.0
);
assert!(
line.contains(&tuple.1),
"Did not find tuple.1 = {}",
tuple.1
);
assert!(
line.contains(&tuple.2),
"Did not find tuple.2 = {}",
tuple.2
);
}
false
}
}
impl LogWriter for FileLogWriter {
#[inline]
fn write(&self, now: &mut DeferredNow, record: &Record) -> std::io::Result<()> {
thread_local! {
static RECURSION_DETECTOR: RefCell<()> = RefCell::new(());
}
RECURSION_DETECTOR.with(|rd| match rd.try_borrow_mut() {
Ok(_) => {
let mut state_guard = self.state.lock().unwrap();
let state = state_guard.deref_mut();
state
.mount_next_linewriter_if_necessary(&self.config)
.unwrap_or_else(|e| {
eprintln!("FlexiLogger: opening file failed with {}", e);
});
(self.config.format)(state, now, record).unwrap_or_else(|e| write_err(ERR_1, e));
state
.write_all(state.line_ending)
.unwrap_or_else(|e| write_err(ERR_2, e));
}
Err(_e) => {
}
});
Ok(())
}
#[inline]
fn flush(&self) -> std::io::Result<()> {
let mut state_guard = self.state.lock().unwrap();
state_guard.deref_mut().line_writer().flush()
}
#[inline]
fn max_log_level(&self) -> log::LevelFilter {
self.max_log_level
}
}
const ERR_1: &str = "FileLogWriter: formatting failed with ";
const ERR_2: &str = "FileLogWriter: writing failed with ";
fn write_err(msg: &str, err: std::io::Error) {
eprintln!("{} with {}", msg, err);
}
mod platform {
use std::path::{Path, PathBuf};
pub fn create_symlink_if_possible(link: &PathBuf, path: &Path) {
linux_create_symlink(link, path);
}
#[cfg(target_os = "linux")]
fn linux_create_symlink(link: &PathBuf, logfile: &Path) {
if std::fs::metadata(link).is_ok() {
let _ = std::fs::remove_file(link);
}
if let Err(e) = std::os::unix::fs::symlink(&logfile, link) {
if !e.to_string().contains("Operation not supported") {
eprintln!(
"Cannot create symlink {:?} for logfile \"{}\": {:?}",
link,
&logfile.display(),
e
);
}
}
}
#[cfg(not(target_os = "linux"))]
fn linux_create_symlink(_: &PathBuf, _: &Path) {}
}
#[cfg(test)]
mod test {
use crate::writers::LogWriter;
use crate::{Cleanup, Criterion, DeferredNow, Naming};
use chrono::Local;
use std::ops::Add;
use std::path::{Path, PathBuf};
const DIRECTORY: &str = r"log_files/rotate";
const ONE: &str = "ONE";
const TWO: &str = "TWO";
const THREE: &str = "THREE";
const FOUR: &str = "FOUR";
const FIVE: &str = "FIVE";
const SIX: &str = "SIX";
const SEVEN: &str = "SEVEN";
const EIGHT: &str = "EIGHT";
const NINE: &str = "NINE";
#[test]
fn test_rotate_no_append_numbers() {
let ts = Local::now()
.format("false-numbers-%Y-%m-%d_%H-%M-%S")
.to_string();
let naming = Naming::Numbers;
assert!(not_exists("00000", &ts));
assert!(not_exists("00001", &ts));
assert!(not_exists("CURRENT", &ts));
write_loglines(false, naming, &ts, &[ONE]);
assert!(not_exists("00000", &ts));
assert!(not_exists("00001", &ts));
assert!(contains("CURRENT", &ts, ONE));
write_loglines(false, naming, &ts, &[TWO]);
assert!(contains("00000", &ts, ONE));
assert!(not_exists("00001", &ts));
assert!(contains("CURRENT", &ts, TWO));
remove("CURRENT", &ts);
assert!(not_exists("CURRENT", &ts));
write_loglines(false, naming, &ts, &[TWO]);
assert!(contains("00000", &ts, ONE));
assert!(not_exists("00001", &ts));
assert!(contains("CURRENT", &ts, TWO));
write_loglines(false, naming, &ts, &[THREE]);
assert!(contains("00000", &ts, ONE));
assert!(contains("00001", &ts, TWO));
assert!(contains("CURRENT", &ts, THREE));
}
#[test]
fn test_rotate_with_append_numbers() {
let ts = Local::now()
.format("true-numbers-%Y-%m-%d_%H-%M-%S")
.to_string();
let naming = Naming::Numbers;
assert!(not_exists("00000", &ts));
assert!(not_exists("00001", &ts));
assert!(not_exists("CURRENT", &ts));
write_loglines(true, naming, &ts, &[ONE, TWO, THREE]);
assert!(contains("00000", &ts, ONE));
assert!(contains("00000", &ts, TWO));
assert!(not_exists("00001", &ts));
assert!(contains("CURRENT", &ts, THREE));
write_loglines(true, naming, &ts, &[FOUR, FIVE, SIX]);
assert!(contains("00000", &ts, ONE));
assert!(contains("00000", &ts, TWO));
assert!(contains("00001", &ts, THREE));
assert!(contains("00001", &ts, FOUR));
assert!(contains("CURRENT", &ts, FIVE));
assert!(contains("CURRENT", &ts, SIX));
remove("CURRENT", &ts);
remove("00001", &ts);
assert!(not_exists("CURRENT", &ts));
write_loglines(true, naming, &ts, &[THREE, FOUR, FIVE, SIX]);
assert!(contains("00000", &ts, ONE));
assert!(contains("00000", &ts, TWO));
assert!(contains("00001", &ts, THREE));
assert!(contains("00001", &ts, FOUR));
assert!(contains("CURRENT", &ts, FIVE));
assert!(contains("CURRENT", &ts, SIX));
write_loglines(true, naming, &ts, &[SEVEN, EIGHT, NINE]);
assert!(contains("00002", &ts, FIVE));
assert!(contains("00002", &ts, SIX));
assert!(contains("00003", &ts, SEVEN));
assert!(contains("00003", &ts, EIGHT));
assert!(contains("CURRENT", &ts, NINE));
}
#[test]
fn test_rotate_no_append_timestamps() {
let ts = Local::now()
.format("false-timestamps-%Y-%m-%d_%H-%M-%S")
.to_string();
let basename = String::from(DIRECTORY).add("/").add(
&Path::new(&std::env::args().next().unwrap())
.file_stem().unwrap()
.to_string_lossy().to_string(),
);
let naming = Naming::Timestamps;
assert!(list_rotated_files(&basename, &ts).is_empty());
assert!(not_exists("CURRENT", &ts));
write_loglines(false, naming, &ts, &[ONE]);
assert!(list_rotated_files(&basename, &ts).is_empty());
assert!(contains("CURRENT", &ts, ONE));
std::thread::sleep(std::time::Duration::from_secs(2));
write_loglines(false, naming, &ts, &[TWO]);
assert_eq!(list_rotated_files(&basename, &ts).len(), 1);
assert!(contains("CURRENT", &ts, TWO));
std::thread::sleep(std::time::Duration::from_secs(2));
write_loglines(false, naming, &ts, &[THREE]);
assert_eq!(list_rotated_files(&basename, &ts).len(), 2);
assert!(contains("CURRENT", &ts, THREE));
}
#[test]
fn test_rotate_with_append_timestamps() {
let ts = Local::now()
.format("true-timestamps-%Y-%m-%d_%H-%M-%S")
.to_string();
let basename = String::from(DIRECTORY).add("/").add(
&Path::new(&std::env::args().next().unwrap())
.file_stem().unwrap()
.to_string_lossy().to_string(),
);
let naming = Naming::Timestamps;
assert!(list_rotated_files(&basename, &ts).is_empty());
assert!(not_exists("CURRENT", &ts));
write_loglines(true, naming, &ts, &[ONE, TWO, THREE]);
assert_eq!(list_rotated_files(&basename, &ts).len(), 1);
assert!(contains("CURRENT", &ts, THREE));
write_loglines(true, naming, &ts, &[FOUR, FIVE, SIX]);
assert!(contains("CURRENT", &ts, FIVE));
assert!(contains("CURRENT", &ts, SIX));
assert_eq!(list_rotated_files(&basename, &ts).len(), 2);
}
fn remove(s: &str, discr: &str) {
std::fs::remove_file(get_hackyfilepath(s, discr)).unwrap();
}
fn not_exists(s: &str, discr: &str) -> bool {
!get_hackyfilepath(s, discr).exists()
}
fn contains(s: &str, discr: &str, text: &str) -> bool {
match std::fs::read_to_string(get_hackyfilepath(s, discr)) {
Err(_) => false,
Ok(s) => s.contains(text),
}
}
fn get_hackyfilepath(infix: &str, discr: &str) -> Box<Path> {
let arg0 = std::env::args().nth(0).unwrap();
let mut s_filename = Path::new(&arg0)
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
s_filename += "_";
s_filename += discr;
s_filename += "_r";
s_filename += infix;
s_filename += ".log";
let mut path_buf = PathBuf::from(DIRECTORY);
path_buf.push(s_filename);
path_buf.into_boxed_path()
}
fn write_loglines(append: bool, naming: Naming, discr: &str, texts: &[&'static str]) {
let flw = get_file_log_writer(append, naming, discr);
for text in texts {
flw.write(
&mut DeferredNow::new(),
&log::Record::builder()
.args(format_args!("{}", text))
.level(log::Level::Error)
.target("myApp")
.file(Some("server.rs"))
.line(Some(144))
.module_path(Some("server"))
.build(),
)
.unwrap();
}
}
fn get_file_log_writer(
append: bool,
naming: Naming,
discr: &str,
) -> crate::writers::FileLogWriter {
super::FileLogWriter::builder()
.directory(DIRECTORY)
.discriminant(discr)
.rotate(
Criterion::Size(if append { 28 } else { 10 }),
naming,
Cleanup::Never,
)
.o_append(append)
.try_build()
.unwrap()
}
fn list_rotated_files(basename: &str, discr: &str) -> Vec<String> {
let fn_pattern = String::with_capacity(180)
.add(basename)
.add("_")
.add(discr)
.add("_r2[0-9]*")
.add(".log");
println!("PATTERN = {}", fn_pattern);
glob::glob(&fn_pattern)
.unwrap()
.map(|r| r.unwrap().into_os_string().to_string_lossy().to_string())
.collect()
}
}