use chrono::{self, Timelike};
use log;
use std::fmt;
use std::io::{self, Write};
use std::path;
use std::sync::Mutex;
pub type Flag = u8;
pub const L_NONE: Flag = 0;
pub const L_DATE: Flag = 1;
pub const L_TIME: Flag = 2;
pub const L_MICROSECONDS: Flag = 4;
pub const L_LONG_FILE: Flag = 8;
pub const L_SHORT_FILE: Flag = 16;
pub const L_UTC: Flag = 32;
pub const L_MSG_PREFIX: Flag = 64;
pub const L_LEVEL: Flag = 128;
pub const L_STD: Flag = L_DATE | L_TIME | L_LEVEL;
pub struct LoggerBuilder<W: Write + Send> {
level: log::LevelFilter,
out: Option<W>,
flag: Flag,
prefix: String,
}
impl<W: Write + Send> LoggerBuilder<W> {
pub fn set_level(mut self, level: log::LevelFilter) -> LoggerBuilder<W> {
self.level = level;
self
}
pub fn set_flags(mut self, flag: Flag) -> LoggerBuilder<W> {
self.flag = flag;
self
}
pub fn set_prefix(mut self, prefix: &str) -> LoggerBuilder<W> {
self.prefix = String::from(prefix);
self
}
pub fn build(mut self) -> Logger<W> {
Logger {
level: self.level,
out: Mutex::new(self.out.take().unwrap()),
flag: self.flag,
prefix: self.prefix.clone(),
}
}
}
pub struct Logger<W: Write + Send> {
level: log::LevelFilter,
out: Mutex<W>,
flag: Flag,
prefix: String,
}
pub fn init<W: Write + Send + 'static>(l: Logger<W>) -> Result<(), log::SetLoggerError> {
log::set_max_level(l.level);
log::set_boxed_logger(Box::new(l))
}
impl<W: Write + Send> Logger<W> {
pub fn builder(w: W) -> LoggerBuilder<W> {
LoggerBuilder {
level: log::LevelFilter::Trace,
out: Some(w),
flag: L_STD,
prefix: String::from(""),
}
}
pub fn write_output(
&self,
level: log::Level,
target: &str,
file: Option<&str>,
line: Option<u32>,
s: &str,
) {
if !self.enabled(level) {
return;
}
let now = chrono::offset::Local::now(); let file = match file {
Some(f) => f,
None => "???",
};
let line = match line {
Some(n) => n,
None => 0,
};
let h = self.header(target, file, line, level, now);
let maybe_newline = if s.ends_with("\n") { "" } else { "\n" };
let mut out = self.out.lock().unwrap();
let _ = write!(out, "{}{}{}", h, s, maybe_newline);
}
fn write_record(&self, record: &log::Record) {
self.write_output(
record.level(),
record.target(),
record.file(),
record.line(),
&record.args().to_string(),
);
}
fn header<Tz: chrono::TimeZone>(
&self,
target: &str,
file: &str,
line: u32,
level: log::Level,
now: chrono::DateTime<Tz>,
) -> String
where
Tz::Offset: fmt::Display,
{
header_with_flags(target, file, line, level, now, self.flag, &self.prefix)
}
fn enabled(&self, incoming_level: log::Level) -> bool {
incoming_level <= self.level
}
}
fn format_datetime<Tz: chrono::TimeZone>(buf: &mut String, flag: Flag, now: chrono::DateTime<Tz>)
where
Tz::Offset: fmt::Display,
{
if flag & L_DATE != 0 {
buf.push_str(&format!("{} ", now.format("%Y/%m/%d")));
}
if flag & (L_TIME | L_MICROSECONDS) != 0 {
buf.push_str(&format!("{}", now.format("%H:%M:%S")));
if flag & L_MICROSECONDS != 0 {
let micro = now.nanosecond() / 1000;
buf.push_str(&format!(".{:0>wid$}", micro, wid = 6));
}
buf.push_str(&format!(" "));
}
}
fn header_with_flags<Tz: chrono::TimeZone>(
target: &str,
file: &str,
line: u32,
level: log::Level,
now: chrono::DateTime<Tz>,
flag: Flag,
prefix: &str,
) -> String
where
Tz::Offset: fmt::Display,
{
let mut buf = String::new();
if flag & L_MSG_PREFIX == 0 {
buf.push_str(&format!("{}", prefix));
}
if flag & L_LEVEL != 0 {
buf.push_str(&format!("{: <5} ", level));
}
if flag & (L_DATE | L_TIME | L_MICROSECONDS) != 0 {
if flag & L_UTC != 0 {
let now = now.with_timezone(&chrono::Utc);
format_datetime(&mut buf, flag, now);
} else {
format_datetime(&mut buf, flag, now);
}
}
if flag & (L_LONG_FILE | L_SHORT_FILE) != 0 {
if flag & L_LONG_FILE != 0 {
buf.push_str(&format!("{} ", target));
}
let f = if flag & L_SHORT_FILE != 0 {
match path::Path::new(file).file_name() {
Some(base) => base.to_string_lossy().into_owned(),
None => String::from("???"),
}
} else {
String::from(file)
};
buf.push_str(&format!("{}", f));
buf.push_str(&format!(":{}", line));
buf.push_str(&format!(": "));
}
if flag & L_MSG_PREFIX != 0 {
buf.push_str(&format!("{}", prefix));
}
buf
}
impl Default for Logger<io::Stderr> {
fn default() -> Logger<io::Stderr> {
Logger::builder(io::stderr()).build()
}
}
impl<W: Write + Send> log::Log for Logger<W> {
fn enabled(&self, metadata: &log::Metadata) -> bool {
self.enabled(metadata.level())
}
fn log(&self, record: &log::Record) {
self.write_record(record);
}
fn flush(&self) {
let _ = self.out.lock().unwrap().flush();
}
}
#[doc(hidden)]
pub mod test_util;
#[cfg(test)]
mod tests {
use super::*;
use chrono::prelude::*;
#[test]
fn test_header() {
let time = FixedOffset::east(3600 * 5 + 1800)
.ymd(2020, 10, 3)
.and_hms_micro(1, 2, 3, 9876);
let flags = L_STD | L_MICROSECONDS | L_SHORT_FILE;
let expect = "TRACE 2020/10/03 01:02:03.009876 file.rs:9: ";
let got = header_with_flags(
"foo",
"src/dir/file.rs",
9,
log::Level::Trace,
time,
flags,
"",
);
assert_eq!(expect, got);
let flags = L_DATE | L_TIME | L_UTC | L_LONG_FILE;
let expect = "2020/10/02 19:32:03 foo src/dir/file.rs:9: ";
let got = header_with_flags(
"foo",
"src/dir/file.rs",
9,
log::Level::Info,
time,
flags,
"",
);
assert_eq!(expect, got);
let flags = L_TIME | L_LEVEL;
let prefix = "myprog: ";
let expect = "myprog: INFO 01:02:03 ";
let got = header_with_flags(
"foo",
"src/dir/file.rs",
9,
log::Level::Info,
time,
flags,
prefix,
);
assert_eq!(expect, got);
let flags = L_MSG_PREFIX | L_TIME | L_LEVEL;
let expect = "INFO 01:02:03 myprog: ";
let got = header_with_flags(
"foo",
"src/dir/file.rs",
9,
log::Level::Info,
time,
flags,
prefix,
);
assert_eq!(expect, got);
}
}