use log;
use chrono;
use colored::*;
use log::{Level, Metadata, Record, SetLoggerError};
use serde_derive::Deserialize;
use toml;
pub mod prelude;
pub mod color;
pub mod rule;
use color::{pick_color, ColoredLevelConfig};
use crossbeam_channel::{self, Receiver, Sender};
use rule::{Rule, RuleFilter};
use std::{
boxed::Box,
default::Default,
env,
io::{self, Write},
str::FromStr,
thread,
ops::Drop,
};
const DEFAULT_TIMESTAMP_FMT: &str = "%Y-%m-%d %H:%M:%S";
const DEFAULT_CHANNEL_SIZE: usize = 1024;
const DEFAULT_LOG_LEVEL: Level = Level::Info;
const DEFAULT_LOG_LEVEL_STR: &str = "Info";
type MsgT = Box<dyn LogMessageTrait>;
pub struct FastLogger {
level: Level,
rule_filters: Vec<RuleFilter>,
level_colors: ColoredLevelConfig,
sender: Sender<MsgT>,
timestamp_format: String,
handle: Option<thread::JoinHandle<()>>,
}
impl FastLogger {
pub fn level(&self) -> Level {
self.level
}
pub fn rule_filters(&self) -> &Vec<RuleFilter> {
&self.rule_filters
}
pub fn should_log_in(&self, args: &str) -> Option<String> {
if self.rule_filters.is_empty() {
Some(String::default())
} else {
let mut color = Some(String::default());
for rule_filter in self.rule_filters.iter() {
if rule_filter.is_match(args) {
if rule_filter.exclude() {
color = None;
} else {
color = Some(rule_filter.get_color());
}
}
}
color
}
}
pub fn add_rule_filter(&mut self, rule_filter: RuleFilter) {
self.rule_filters.push(rule_filter);
}
pub fn flush_filters(&mut self) {
self.rule_filters.clear();
}
pub fn flush(&self) {
let flush_signal = Box::new(LogMessage::new_flush_signal());
self.sender.try_send(flush_signal)
.unwrap_or_else(|_| {}); }
fn flush_buffer(&self) {
self.flush();
}
pub fn shutdown(&mut self) {
let terminate_signal = Box::new(LogMessage::new_terminate_signal());
self.sender.send(terminate_signal)
.unwrap_or_else(|_| eprintln!("Fail to send thread shutdown signal. Maybe it was already sent ?"));
if let Some(handle) = self.handle.take() {
handle.join().expect("Fail to shutdown the logging thread.");
}
}
}
impl Drop for FastLogger {
fn drop(&mut self) {
self.flush_buffer();
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
impl log::Log for FastLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= self.level()
}
fn log(&self, record: &Record) {
let args = record.args().to_string();
let target = record.target().to_string();
let should_log_in = self.should_log_in(&format!("{} {}", &target, &args));
if self.enabled(record.metadata()) && should_log_in != None {
let msg = LogMessage {
args,
module: record.module_path().unwrap_or("module-name").to_string(),
line: record.line().unwrap_or(000),
file: record.file().unwrap_or("").to_string(),
level: record.level(),
level_to_print: self.level_colors.color(record.level()).to_string(),
thread_name: std::thread::current()
.name()
.unwrap_or(&String::default())
.to_string(),
color: should_log_in,
target: Some(target),
timestamp_format: self.timestamp_format.to_owned(),
..Default::default()
};
self.sender
.send(Box::new(msg))
.expect("Fail to send message to the logging thread.");
}
}
fn flush(&self) {
self.flush_buffer();
}
}
pub struct FastLoggerBuilder {
level: Level,
rule_filters: Vec<RuleFilter>,
level_colors: ColoredLevelConfig,
channel_size: usize,
file_path: Option<String>,
timestamp_format: String,
}
impl<'a> FastLoggerBuilder {
pub fn new() -> Self {
FastLoggerBuilder::default()
}
pub fn from_toml(toml_str: &str) -> Result<Self, toml::de::Error> {
let logger_conf: LoggerConfig = toml::from_str(toml_str)?;
let logger: Option<Logger> = logger_conf.logger;
assert!(
logger.is_some(),
"The 'logger' part might be missing in the toml."
);
Ok(logger.unwrap().into())
}
pub fn level(&self) -> Level {
self.level
}
pub fn set_level(&mut self, level: Level) -> &mut Self {
self.level = level;
self
}
pub fn set_level_from_str(&mut self, level: &str) -> &mut Self {
self.level = Level::from_str(level).unwrap_or_else(|_| {
eprintln!("Fail to parse the logging level from string: '{}'.", level);
self.level
});
self
}
pub fn set_channel_size(&mut self, channel_size: usize) -> &mut Self {
self.channel_size = channel_size;
self
}
pub fn timestamp_format(&mut self, timestamp_fmt: &str) -> &mut Self {
self.timestamp_format = timestamp_fmt.to_owned();
self
}
pub fn add_rule_filter(&mut self, rule_filter: RuleFilter) -> &mut Self {
self.rule_filters.push(rule_filter);
self
}
pub fn rule_filters(&self) -> &[RuleFilter] {
&self.rule_filters
}
pub fn redirect_to_file(&mut self, file_path: &str) -> &mut Self {
self.file_path = Some(String::from(file_path));
self
}
pub fn file_path(&self) -> Option<String> {
self.file_path.clone()
}
pub fn build(&self) -> Result<FastLogger, SetLoggerError> {
let (s, r): (Sender<MsgT>, Receiver<MsgT>) = crossbeam_channel::bounded(self.channel_size);
let logger = FastLogger {
level: self.level,
rule_filters: self.rule_filters.to_owned(),
level_colors: self.level_colors,
sender: s.clone(),
timestamp_format: self.timestamp_format.to_owned(),
handle: None,
};
let handle = match log::set_boxed_logger(Box::new(logger))
.map(|_| log::set_max_level(self.level.to_level_filter()))
{
Ok(_v) => {
if self.file_path.is_some() {
let mut buffer = {
let fp = match &self.file_path {
Some(fp) => fp.to_string(),
None => String::from("dummy.log"),
};
let file_path = std::path::PathBuf::from(&fp);
let file_stream = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&file_path)
.unwrap_or_else(|_| panic!("Fail to log to {:?}.", &file_path));
io::BufWriter::new(file_stream)
};
thread::spawn(move || {
while let Ok(msg) = r.recv() {
if msg.should_terminate() {
drop(r);
buffer.flush().expect("Fail to flush the logging buffer.");
break
} else if msg.should_flush() {
buffer.flush().expect("Fail to flush the logging buffer.")
} else {
writeln!(&mut buffer, "{}", msg.build())
.expect("Fail to log to file.")
}
}
})
} else {
let mut buffer = io::BufWriter::new(io::stderr());
thread::spawn(move || {
while let Ok(msg) = r.recv() {
if msg.should_terminate() {
drop(r);
buffer.flush().expect("Fail to flush the logging buffer.");
break
} else if msg.should_flush() {
buffer.flush().expect("Fail to flush the logging buffer.");
} else {
writeln!(&mut buffer, "{}", msg.build())
.expect("Fail to log to the stderr.")
}
}
})
}
}
Err(e) => {
eprintln!("Attempt to initialize the Logger more than once. '{}'.", e);
thread::spawn(move || {})
}
};
Ok(FastLogger {
level: self.level,
rule_filters: self.rule_filters.to_owned(),
level_colors: self.level_colors,
sender: s.clone(),
timestamp_format: self.timestamp_format.to_owned(),
handle: Some(handle),
})
}
#[allow(dead_code)]
pub fn build_test(&self) -> Result<FastLogger, SetLoggerError> {
let (s, _): (Sender<MsgT>, Receiver<MsgT>) = crossbeam_channel::bounded(self.channel_size);
let logger = FastLogger {
level: self.level,
rule_filters: self.rule_filters.to_owned(),
level_colors: self.level_colors,
sender: s,
timestamp_format: self.timestamp_format.to_owned(),
handle: None,
};
Ok(logger)
}
}
impl Default for FastLoggerBuilder {
fn default() -> Self {
let level = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_LEVEL_STR.to_string());
Self {
level: Level::from_str(&level).unwrap_or(DEFAULT_LOG_LEVEL),
rule_filters: Vec::new(),
level_colors: ColoredLevelConfig::new(),
channel_size: DEFAULT_CHANNEL_SIZE,
file_path: None,
timestamp_format: String::from(DEFAULT_TIMESTAMP_FMT)
}
}
}
pub fn init_simple() -> Result<FastLogger, SetLoggerError> {
FastLoggerBuilder::new().build()
}
struct LogMessage {
args: String,
module: String,
target: Option<String>,
line: u32,
file: String,
level: Level,
level_to_print: String,
thread_name: String,
color: Option<String>,
timestamp_format: String,
should_terminate: bool,
should_flush: bool,
}
impl LogMessage {
fn new_terminate_signal() -> Self {
Self {
should_terminate: true,
..Default::default()
}
}
fn new_flush_signal() -> Self {
Self {
should_flush: true,
..Default::default()
}
}
fn should_terminate(&self) -> bool {
self.should_terminate
}
fn should_flush(&self) -> bool {
self.should_flush
}
}
impl Default for LogMessage {
fn default() -> Self {
Self {
args: String::default(),
module: String::default(),
target: None,
line: 0,
file: String::default(),
level: DEFAULT_LOG_LEVEL,
level_to_print : DEFAULT_LOG_LEVEL_STR.to_owned(),
thread_name: String::default(),
color: None,
timestamp_format: String::default(),
should_terminate: false,
should_flush: false,
}
}
}
trait LogMessageTrait: Send {
fn build(&self) -> String;
fn shutdown(&mut self);
fn should_terminate(&self) -> bool;
fn should_flush(&self) -> bool;
}
impl LogMessageTrait for LogMessage {
fn build(&self) -> String {
let tag_name = self
.target
.to_owned()
.unwrap_or_else(|| format!("{}{}", &self.thread_name, &self.module).to_owned());
let base_color_on = &tag_name.to_owned();
let pseudo_rng_color = pick_color(&base_color_on);
let msg_color = match &self.color {
Some(color) => {
if color.is_empty() {
pseudo_rng_color
} else {
color
}
},
None => pseudo_rng_color,
};
let msg_color = match self.level {
Level::Error => "Red",
Level::Warn => "Yellow",
_ => msg_color,
};
let msg = format!(
"{level} {timestamp} [{tag}] {thread_name} {line} {args}",
args = self.args.color(msg_color),
tag = tag_name.bold().color(pseudo_rng_color),
line = format!("{}:{}", self.file, self.line).italic(),
timestamp = chrono::Local::now().format(&self.timestamp_format),
level = self.level_to_print.bold(),
thread_name = self.thread_name.underline(),
);
msg.to_string()
}
fn shutdown(&mut self) {
self.should_terminate = true;
}
fn should_terminate(&self) -> bool {
self.should_terminate()
}
fn should_flush(&self) -> bool {
self.should_flush()
}
}
#[derive(Debug, Deserialize)]
struct LoggerConfig {
pub logger: Option<Logger>,
}
#[derive(Clone, Debug, Deserialize)]
struct Logger {
level: String,
file: Option<String>,
rules: Option<Vec<Rule>>,
timestamp_format: Option<String>,
}
impl From<Logger> for FastLoggerBuilder {
fn from(logger: Logger) -> Self {
let rule_filters: Vec<RuleFilter> = logger
.rules
.unwrap_or_else(|| vec![])
.into_iter()
.map(|rule| rule.into())
.collect();
FastLoggerBuilder {
level: Level::from_str(&logger.level).unwrap_or(Level::Info),
rule_filters,
file_path: logger.file,
timestamp_format: logger.timestamp_format.unwrap_or_else(|| String::from(DEFAULT_TIMESTAMP_FMT)),
..FastLoggerBuilder::default()
}
}
}
#[test]
fn should_log_test() {
use rule::RuleFilterBuilder;
let mut logger = FastLoggerBuilder::new()
.set_level_from_str("Debug")
.add_rule_filter(
RuleFilterBuilder::new()
.set_pattern("foo")
.set_exclusion(false)
.set_color("Blue")
.build(),
)
.build_test()
.unwrap();
assert_eq!(logger.should_log_in("bar"), Some(String::from("")));
assert_eq!(logger.should_log_in("xfooy"), Some(String::from("Blue")));
logger.add_rule_filter(RuleFilter::new("baz", true, "White"));
assert_eq!(logger.should_log_in("baz"), None);
logger.add_rule_filter(RuleFilter::new("b", false, "Green"));
assert_eq!(logger.should_log_in("xboy"), Some(String::from("Green")));
}
#[test]
fn filtering_back_log_test() {
let toml = r#"
[logger]
level = "debug"
[[logger.rules]]
pattern = ".*"
exclude = true
[[logger.rules]]
pattern = "^holochain"
exclude = false
[[logger.rules]]
pattern = "Cyan"
exclude = false
color = "Cyan"
[[logger.rules]]
pattern = "app-6"
exclude = false
color = "Green"
"#;
let logger_conf: LoggerConfig =
toml::from_str(toml).expect("Fail to deserialize logger from toml.");
let logger: Option<Logger> = logger_conf.logger;
assert!(logger.is_some());
let flb: FastLoggerBuilder = logger.unwrap().into();
let logger = flb.build_test().unwrap();
assert_eq!(logger.should_log_in("rpc"), None);
assert_ne!(logger.should_log_in("holochain"), None);
assert_eq!(logger.should_log_in("app-6"), Some(String::from("Green")));
}
#[test]
fn logger_conf_deserialization_test() {
let toml = r#"
[logger]
level = "debug"
file = "humpty_dumpty.log"
[[logger.rules]]
pattern = ".*"
color = "red"
"#;
let logger_conf: LoggerConfig =
toml::from_str(toml).expect("Fail to deserialize logger from toml.");
let logger: Option<Logger> = logger_conf.logger;
assert!(logger.is_some());
let flb: FastLoggerBuilder = logger.unwrap().into();
assert_eq!(flb.level(), Level::Debug);
assert_eq!(flb.file_path(), Some(String::from("humpty_dumpty.log")));
}
#[test]
fn fastloggerbuilder_conf_deserialization_test() {
let toml = r#"
[logger]
level = "debug"
file = "humpty_dumpty.log"
[[logger.rules]]
pattern = ".*"
color = "red"
"#;
let flb =
FastLoggerBuilder::from_toml(&toml).expect("Fail to init `FastLoggerBuilder` from toml.");
assert_eq!(flb.level(), Level::Debug);
assert_eq!(flb.file_path(), Some(String::from("humpty_dumpty.log")));
}
#[test]
fn log_rules_deserialization_test() {
use rule;
let toml = r#"
[logger]
level = "debug"
[[logger.rules]]
pattern = ".*"
color = "red"
[[logger.rules]]
pattern = "twice"
exclude = true
color = "magenta"
"#;
let logger_conf: LoggerConfig =
toml::from_str(toml).expect("Fail to deserialize logger from toml.");
let logger: Option<Logger> = logger_conf.logger;
assert!(logger.is_some());
let rule0 = rule::Rule {
pattern: String::from(".*"),
exclude: Some(false),
color: Some(String::from("red")),
};
let rule1 = rule::Rule {
pattern: String::from("twice"),
exclude: Some(true),
color: Some(String::from("magenta")),
};
let flb: FastLoggerBuilder = logger.unwrap().into();
let rule0_from_toml: Rule = flb.rule_filters()[0].clone().into();
let rule1_from_toml: Rule = flb.rule_filters()[1].clone().into();
assert_eq!(rule0_from_toml, rule0);
assert_eq!(rule1_from_toml, rule1);
}
#[test]
fn configure_log_level_from_env_test() {
env::set_var("RUST_LOG", "warn");
let flb = FastLoggerBuilder::new();
assert_eq!(flb.level(), Level::Warn);
let logger = flb.build_test().unwrap();
assert_eq!(logger.level(), Level::Warn)
}
#[test]
fn log_to_file_test() {
let toml = r#"
[logger]
level = "debug"
file = "logger_dump.log"
[[logger.rules]]
pattern = ".*"
color = "Yellow"
"#;
FastLoggerBuilder::from_toml(toml).expect("Fail to load logging conf from toml.");
}