extern crate chrono;
use std::error::Error;
use std::fs;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::prelude::Write;
use std::string::ToString;
use crate::log_file_config::LogFileConfig;
struct FileAttr {
write: bool,
append: bool,
create: bool,
truncate: bool,
}
impl FileAttr {
pub fn new() -> FileAttr {
FileAttr {
write: true,
append: true,
create: true,
truncate: false,
}
}
}
pub enum LogLevel {
DEBUG,
INFO,
WARNING,
ERROR,
CRITICAL,
}
pub struct LogFile {
max_size: u64,
config: LogFileConfig,
complete_path: String,
counter: u64,
file: File,
}
impl LogFile {
pub fn new(mut config: LogFileConfig) -> Result<Self, Box<dyn Error>> {
if config.max_size_in_mb == 0 {
config.max_size_in_mb = 1;
}
const MEGABYTE: u64 = 1024u64 * 1024u64;
let max_size = config.max_size_in_mb * MEGABYTE;
let mut counter = 0u64;
let mut path = assemble_path(&config, counter);
loop {
let file_exist = check_if_file_exist(&path);
if !file_exist {
break;
}
let size_ok = is_size_ok(&path, max_size);
if size_ok {
break;
} else {
counter += 1;
path = assemble_path(&config, counter);
}
}
if config.num_files_to_keep == 0 {
config.num_files_to_keep = 1;
}
if config.overwrite && counter > config.num_files_to_keep {
counter -= config.num_files_to_keep;
};
let default_file_attr = FileAttr::new();
let logfile = open(max_size, config, counter, default_file_attr)?;
Ok(logfile)
}
pub fn write(&mut self, level: LogLevel, msg: &str) {
let log_msg = self.build_log_msg(level, msg);
let log_size = self.get_logsize();
if log_size > self.max_size {
self.rotate();
}
self._write(log_msg);
}
pub fn clone(&mut self) -> Self {
LogFile {
max_size: self.max_size,
config: self.config.clone(),
complete_path: self.complete_path.clone(),
counter: self.counter,
file: self.file.try_clone().unwrap(),
}
}
fn _write(&mut self, msg: String) {
match self.file.write_all(&msg.into_bytes()) {
Ok(_) => (),
Err(error) => panic!("panic while writing to file: `{}`", error),
}
}
fn rotate(&mut self) {
let mut file_attr = FileAttr::new();
if self.config.overwrite {
if self.counter == self.config.num_files_to_keep - 1 {
self.counter -= self.config.num_files_to_keep - 1;
} else {
self.counter += 1;
};
file_attr.append = false;
file_attr.truncate = self.config.truncate;
} else {
self.counter += 1;
};
self.complete_path = assemble_path(&self.config, self.counter);
self.file = match open_file(&self.complete_path, file_attr) {
Ok(file) => file,
Err(error) => {
let msg = format!(
"Could not open new log-file `{}`! Reason: `{}`",
self.complete_path, error
);
let msg = self.build_log_msg(LogLevel::CRITICAL, &msg);
self._write(msg);
panic!("panic while rotating files: `{}`", error);
}
};
}
fn get_logsize(&self) -> u64 {
let meta = self.file.metadata().unwrap();
meta.len()
}
fn get_loglevel_str(&self, level: LogLevel) -> String {
match level {
LogLevel::DEBUG => String::from(" [DEBUG ] "),
LogLevel::INFO => String::from(" [INFO ] "),
LogLevel::WARNING => String::from(" [WARNING ] "),
LogLevel::ERROR => String::from(" [ERROR ] "),
LogLevel::CRITICAL => String::from(" [CRITICAL] "),
}
}
fn build_log_msg(&self, level: LogLevel, msg: &str) -> String {
let mut log_msg = String::new();
let time_as_string = get_actual_timestamp();
log_msg.push_str(&time_as_string);
let level_as_string = self.get_loglevel_str(level);
log_msg.push_str(&level_as_string);
log_msg.push_str(&msg);
log_msg.push('\n');
log_msg
}
}
fn open(
max_size: u64,
config: LogFileConfig,
counter: u64,
file_attr: FileAttr,
) -> Result<LogFile, Box<dyn Error>> {
let path = assemble_path(&config, counter);
let f = open_file(&path, file_attr)?;
Ok(LogFile {
max_size,
config,
complete_path: path,
counter,
file: f,
})
}
fn open_file(path: &str, attr: FileAttr) -> Result<File, Box<dyn Error>> {
let f = OpenOptions::new()
.write(attr.write)
.append(attr.append)
.create(attr.create)
.truncate(attr.truncate)
.open(path)?;
Ok(f)
}
fn get_actual_timestamp() -> String {
let timestamp = chrono::Local::now();
timestamp.format("%F %T%.6f").to_string()
}
fn check_if_file_exist(path: &str) -> bool {
fs::metadata(path).is_ok()
}
fn assemble_path(config: &LogFileConfig, counter: u64) -> String {
let path = format!(
"{}{}{}{}",
config.path,
config.name,
counter.to_string(),
config.extension
);
path
}
fn is_size_ok(path: &str, max_size: u64) -> bool {
let mut check = false;
match fs::metadata(path) {
Ok(meta) => {
if meta.len() < max_size {
check = true;
}
}
Err(error) => {
panic!("Something went wrong while checking size!\n`{}`\n", error);
}
};
check
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn assemble_path_test() {
let default_conf = LogFileConfig::new();
let counter = 0u64;
let path = assemble_path(&default_conf, counter);
assert_eq!(path, "./logfile_0.log");
}
#[test]
fn file_exist_test() {
assert_eq!(check_if_file_exist("testfile.txt"), true);
assert_eq!(check_if_file_exist("none_existing_file.txt"), false);
}
#[test]
fn size_is_ok_test() {
assert_eq!(is_size_ok("testfile.txt", 30), true);
assert_eq!(is_size_ok("testfile.txt", 20), false);
}
#[test]
#[should_panic(expected = "Something went wrong while checking size!")]
fn size_is_not_ok_file_not_exist_test() {
assert_eq!(is_size_ok("non_existing_file.txt", 20), true);
}
#[test]
fn get_loglevel_str_test() {
let default = LogFileConfig::new();
let logfile = match LogFile::new(default) {
Ok(file) => file,
Err(error) => {
panic!("Error: `{}`", error);
}
};
assert_eq!(logfile.get_loglevel_str(LogLevel::DEBUG), " [DEBUG ] ");
assert_eq!(logfile.get_loglevel_str(LogLevel::INFO), " [INFO ] ");
assert_eq!(
logfile.get_loglevel_str(LogLevel::WARNING),
" [WARNING ] "
);
assert_eq!(logfile.get_loglevel_str(LogLevel::ERROR), " [ERROR ] ");
assert_eq!(
logfile.get_loglevel_str(LogLevel::CRITICAL),
" [CRITICAL] "
);
}
}