#[doc(inline)]
use chrono::Utc;
pub use log::{debug, error, info, trace, warn, LevelFilter};
use log::{Level, Log, Metadata, ParseLevelError, Record, SetLoggerError};
use std::{
env::{self, VarError},
str::FromStr, fs::{OpenOptions, File, self}, io::{self, Write, stdout}, path::PathBuf,
};
const COLOR_RED: &str = "\x1B[38;5;196m";
const COLOR_LIME: &str = "\x1B[38;5;10m";
const COLOR_YELLOY: &str = "\x1B[38;5;11m";
const COLOR_CYAN: &str = "\x1B[38;5;14m";
const COLOR_DEFAULT: &str = "\x1B[0m";
const STR_ERROR: &str = "ERROR ";
const STR_WARN: &str = "WARN ";
const STR_INFO: &str = "INFO ";
const STR_DEBUG: &str = "DEBUG ";
const STR_TRACE: &str = "TRACE ";
const DEFAULT_TIMESTAMP_FORMAT: &str="%Y/%m/%d %H.%M.%S";
const DEFAULT_SEP: &str = " : ";
#[derive(Debug)]
pub enum DLogError {
#[doc(hidden)]
Level(ParseLevelError),
#[doc(hidden)]
Env(VarError),
#[doc(hidden)]
Err(io::Error),
#[doc(hidden)]
None,
}
#[derive(Debug)]
pub struct DLog {
level: LevelFilter,
target: Option<String>,
show_color_enabled: bool,
log_on_stdout: bool,
log_on_file: bool,
filename: PathBuf,
file: Option<File>,
max_file_size: u64,
max_files_count: u64,
timestamp_format: String,
show_timestamp_enabled: bool,
show_level_enabled: bool,
separator: String,
}
impl Default for DLog {
fn default() -> Self {
Self::new()
}
}
impl DLog {
pub fn new() -> Self {
Self {
level: LevelFilter::Trace,
target: None,
show_color_enabled: false,
log_on_stdout: true,
log_on_file:false,
filename: PathBuf::new(),
file: None,
max_file_size: 0, max_files_count: 0,
show_timestamp_enabled: true,
timestamp_format: String::from(DEFAULT_TIMESTAMP_FORMAT),
show_level_enabled: true,
separator: String::from(DEFAULT_SEP),
}
}
pub fn with_file(mut self, filename: &str) -> Result<Self, DLogError> {
match self.open_file(filename) {
Ok(file) => {
self.file=Some(file);
self.log_on_file=true;
self.filename=PathBuf::from(filename);
Ok(self)
},
Err(err) => {
self.file=None;
self.log_on_file=false;
self.filename.clear();
Err(DLogError::Err(err))
}
}
}
pub fn with_color(mut self) -> Self {
self.enabled_colors(true);
self
}
pub fn widh_target_filter<S: AsRef<str>>(mut self, target: S) -> Self {
let target = target.as_ref().replace('-', "_");
self.target = Some(target);
self
}
pub fn widh_level(&mut self, level: LevelFilter) -> &mut Self {
self.level = level;
self
}
pub fn with_level_from_env<S: AsRef<str>>(self, name: S) -> Result<Self, DLogError> {
match env::var(name.as_ref()) {
Ok(s) => self.widh_level_from_str(&s),
Err(err) => Err(DLogError::Env(err)),
}
}
pub fn widh_level_from_str<S: AsRef<str>>(mut self, s: S) -> Result<Self, DLogError> {
match LevelFilter::from_str(s.as_ref()) {
Ok(level) => {
self.level = level;
Ok(self)
}
Err(err) => Err(DLogError::Level(err)),
}
}
pub fn widh_timestamp_format(mut self, format: &str) -> Self {
self.timestamp_format=String::from(format);
self
}
pub fn widh_custom_separator(mut self, new_sep: &str) -> Self{
self.separator=new_sep.to_string();
self
}
pub fn without_console(mut self) -> Self {
self.log_on_stdout=false;
self
}
pub fn init_logger(self) -> Result<(),SetLoggerError> {
log::set_boxed_logger(Box::new(self)).map(|()| log::set_max_level(LevelFilter::Trace))
}
pub fn enable_console(&mut self, enabled: bool) {
self.log_on_stdout=enabled;
}
pub fn enable_file(&mut self, enabled: bool) {
self.log_on_file=enabled;
}
pub fn enabled_colors(&mut self, enabled: bool) {
self.show_color_enabled=enabled;
}
pub fn enable_timestamp_print(&mut self, enabled: bool) {
self.show_timestamp_enabled=enabled;
}
pub fn enable_level_print(&mut self, enabled: bool) {
self.show_level_enabled=enabled;
}
pub fn e(&self, msg: &str) {
self.write(Level::Error, msg);
}
pub fn w(&self, msg: &str) {
self.write(Level::Warn, msg);
}
pub fn i(&self, msg: &str) {
self.write(Level::Info, msg);
}
pub fn d(&self, msg: &str) {
self.write(Level::Debug, msg);
}
pub fn t(&self, msg: &str) {
self.write(Level::Trace, msg);
}
fn write(&self, level: Level, msg: &str) {
let timestamp_str = Utc::now().format(&self.timestamp_format).to_string() + &self.separator;
let level_str=self.level_to_str(level).to_string() + &self.separator;
if self.log_on_stdout {
write!(stdout(),"{}\n",
if self.show_color_enabled {self.level_to_color(level).to_string()} else {String::new()} +
if self.show_timestamp_enabled {×tamp_str} else {""} +
if self.show_level_enabled {&level_str} else {""} +
msg +
if self.show_color_enabled {&COLOR_DEFAULT} else {""}
).ok();
}
if self.log_on_file {
self.write_file(
&(
if self.show_timestamp_enabled {timestamp_str} else {String::new()} +
if self.show_level_enabled {&level_str} else {""} +
msg
)
).ok();
}
}
fn level_to_str(&self, level: Level) -> &'static str {
match level {
Level::Error => STR_ERROR,
Level::Warn => STR_WARN,
Level::Info => STR_INFO,
Level::Debug => STR_DEBUG,
Level::Trace => STR_TRACE,
}
}
fn level_to_color(&self, level: Level) -> &'static str {
match level {
Level::Error => COLOR_RED,
Level::Warn => COLOR_YELLOY,
Level::Info => COLOR_DEFAULT,
Level::Debug => COLOR_CYAN,
Level::Trace => COLOR_LIME,
}
}
pub fn get_status(&self) -> String {
let max_file_size=self.max_file_size.to_string();
let max_files_count=self.max_files_count.to_string();
let mut filename_str=String::new();
if self.log_on_file {
let binding = self.filename.canonicalize().ok().unwrap_or_default();
filename_str.push_str("Current filename = ");
filename_str.push_str(binding.to_str().unwrap_or_default());
filename_str.push('\n');
}
let status_info=String::new() +
"----------- durylog current settings -----------" + "\n" +
"Show Colors = " + &self.show_color_enabled.to_string() + "\n" +
"Show Level = " + &self.show_level_enabled.to_string() + "\n" +
"Show Timestamp = " + &self.show_timestamp_enabled.to_string() + "\n" +
"Timestamp Format = " + &self.timestamp_format.to_string() + "\n" +
"Tags separator = '" + &self.separator + "'\n" +
"Level = " + &self.level.to_string() + "\n" +
"Log on stdout = " + &self.log_on_stdout.to_string() + "\n" +
"Log on file = " + &self.log_on_file.to_string() + "\n" +
if self.log_on_file {&filename_str} else {""} +
"Max file size = " + if self.max_file_size > 0 {&max_file_size} else {"no limit"} + "\n" +
"Max files count = " + if self.max_files_count > 0 {&max_files_count} else {"no limit"} + "\n" +
"---------------------------------------------";
status_info
}
fn open_file(&self, filename: &str) -> io::Result<File> {
let f = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.append(true)
.open(filename)?;
Ok(f)
}
fn write_file(&self, msg: &str) -> Result<usize,DLogError> {
if let Some(file) = &self.file {
let mut f=file;
let s=format!("{}\n",msg);
match f.write(s.as_bytes()) {
Ok(b_written) => {
self.check_storage()?;
return Ok(b_written);
},
Err(err) => {
return Err(DLogError::Err(err));
},
}
}
Ok(0)
}
fn check_storage(&self) -> Result<(),DLogError> {
if let Some(file) = &self.file {
let f=file;
match f.metadata() {
Ok(metadata) => {
if self.max_file_size > 0 && metadata.len() > self.max_file_size {
self.write(Level::Trace, &format!("Current log size {} exceed {}, need to rotate",metadata.len(),self.max_file_size));
self.rotate_files()?;
};
return Ok(());
}
Err(err) => return Err(DLogError::Err(err)),
}
}
Ok(())
}
fn rotate_files(&self) -> Result<(),DLogError>{
let mut files_list=self.get_files()?;
files_list.sort();
if self.max_files_count > 0 && files_list.len() >= self.max_files_count as usize {
self.write(Level::Trace, "Files that needs to be deleted:");
files_list.into_iter().nth(self.max_files_count.saturating_sub(1) as usize).map(|path| {
self.write(Level::Trace, path.to_str().unwrap());
});
}
Ok(())
}
fn get_files(&self) -> Result<Vec<PathBuf>, DLogError> {
match fs::read_dir(&self.filename.parent().unwrap()) {
Ok(r) => {
return Ok(
r.into_iter()
.filter(|r| r.is_ok()) .map(|r| {
r.unwrap().path().canonicalize().unwrap()
}) .filter(|r| r.is_file()) .filter(|r|r.extension().unwrap().eq_ignore_ascii_case(&self.filename.extension().unwrap())) .collect())
},
Err(err) => return Err(DLogError::Err(err)),
}
}
}
impl Log for DLog {
fn enabled(&self, metadata: &Metadata) -> bool {
if let Some(level) = self.level.to_level() {
if level >= metadata.level() {
return match &self.target {
Some(t) => metadata.target().starts_with(t),
None => true,
};
}
}
false
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
self.write(record.level(), record.args().to_string().as_str());
}
}
fn flush(&self) {}
}