use chrono::{Local, SecondsFormat};
use std::{fs::File, io::prelude::*, path::Path};
pub struct Logger {
loggers: Vec<Box<dyn log::Log>>,
fallback: Option<Box<dyn log::Log>>,
}
impl Logger {
pub fn new() -> Logger {
return Logger {
loggers: Vec::new(),
fallback: None,
};
}
pub fn add_logger(&mut self, logger: Box<dyn log::Log>) {
self.loggers.push(logger);
}
pub fn set_fallback(&mut self, fallback: Box<dyn log::Log>) {
self.fallback = Some(fallback);
}
}
impl log::Log for Logger {
fn enabled(&self, _: &log::Metadata) -> bool {
return true;
}
fn log(&self, record: &log::Record) {
let mut logged = false;
for logger in &self.loggers {
if logger.enabled(record.metadata()) {
logger.log(record);
logged = true;
}
}
if !logged {
if let Some(fallback) = &self.fallback {
fallback.log(record);
}
}
}
fn flush(&self) {}
}
pub struct CustomLogger {
target: String,
filepath: Option<String>,
}
impl CustomLogger {
pub fn new(target: &str, filepath: &str) -> CustomLogger {
let path = Path::new(filepath);
path.parent().map(|p| std::fs::create_dir_all(p).unwrap());
File::create(filepath).unwrap();
CustomLogger {
target: target.to_string(),
filepath: Some(filepath.to_string()),
}
}
}
impl log::Log for CustomLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
if metadata.target() == self.target {
return true;
}
return false;
}
fn log(&self, record: &log::Record) {
if !self.enabled(record.metadata()) {
return;
}
let log_json_text = format!(
r#"{{"severity":"{}","timestamp":"{}","target":"{}","message":"{}"}}"#,
record.level(),
Local::now().to_rfc3339_opts(SecondsFormat::Millis, true),
self.target,
record.args(),
);
let log_print_text = format!(
"[{}] {} {} - {}",
record.level().to_string().to_uppercase(),
self.target,
Local::now().to_rfc3339_opts(SecondsFormat::Millis, true),
record.args(),
);
match self.filepath {
Some(ref filepath) => {
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(filepath)
.unwrap();
let mut bf = std::io::BufWriter::new(file);
bf.write(log_json_text.as_bytes()).unwrap();
bf.write(b"\n").unwrap();
}
None => {
println!("Cannot open file {:?}", self.filepath);
}
}
println!("{}", log_print_text);
}
fn flush(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use log::{debug, info};
use serde_json::Value;
use std::fs::File;
use std::io::Read;
use std::path::Path;
#[test]
fn test_log() {
let log_file = Path::new("tests/output/test.log");
if log_file.exists() {
std::fs::remove_file(log_file).unwrap();
}
let mut logger = Logger::new();
logger.add_logger(Box::new(CustomLogger::new(
"test",
"tests/output/system.log",
)));
logger.set_fallback(Box::new(CustomLogger::new(
"default",
"tests/output/system.log",
)));
log::set_boxed_logger(Box::new(logger)).expect("Failed to set logger");
log::set_max_level(log::LevelFilter::Trace);
info!(target:"test", "Hello, world!");
debug!("Default");
let mut file = File::open("tests/output/system.log").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let v: Value = serde_json::from_str(&contents).unwrap();
assert_eq!(v["severity"], "INFO");
assert_eq!(v["target"], "test");
assert_eq!(v["message"], "Hello, world!");
}
}