use std::fmt;
use std::io::{self, IsTerminal, Write};
use std::sync::Mutex;
use log::{Level, Log, Metadata, Record, SetLoggerError};
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
#[doc(no_inline)]
pub use log::LevelFilter;
pub mod log_macros {
#[doc(no_inline)]
pub use log::{debug, error, info, log_enabled, trace, warn};
}
#[derive(Debug)]
pub enum ColorMode {
Auto,
Always,
Never,
}
impl ColorMode {
fn to_color_choice(&self) -> ColorChoice {
match self {
ColorMode::Auto => {
if io::stderr().is_terminal() {
ColorChoice::Auto
} else {
ColorChoice::Never
}
}
ColorMode::Always => ColorChoice::Always,
ColorMode::Never => ColorChoice::Never,
}
}
}
impl Default for ColorMode {
fn default() -> Self {
Self::Auto
}
}
#[derive(Debug)]
struct LogColors {
error: ColorSpec,
warn: ColorSpec,
info: ColorSpec,
debug: ColorSpec,
trace: ColorSpec,
}
impl LogColors {
pub fn new() -> Self {
let error = ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true).to_owned();
let warn = ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true).to_owned();
let info = ColorSpec::new();
let debug = ColorSpec::new().set_fg(Some(Color::Cyan)).to_owned();
let trace = ColorSpec::new().set_fg(Some(Color::Blue)).to_owned();
Self { error, warn, info, debug, trace }
}
pub fn get(&self, l: Level) -> &ColorSpec {
match l {
Level::Error => &self.error,
Level::Warn => &self.warn,
Level::Info => &self.info,
Level::Debug => &self.debug,
Level::Trace => &self.trace,
}
}
}
trait LevelFilterExt {
fn from_int(val: u8) -> Self;
fn to_int(self) -> u8;
fn add(self, change: u8) -> Self;
fn sub(self, change: u8) -> Self;
}
impl LevelFilterExt for LevelFilter {
fn from_int(val: u8) -> Self {
match val {
0 => LevelFilter::Off,
1 => LevelFilter::Error,
2 => LevelFilter::Warn,
3 => LevelFilter::Info,
4 => LevelFilter::Debug,
_ => LevelFilter::Trace,
}
}
fn to_int(self) -> u8 {
match self {
LevelFilter::Off => 0,
LevelFilter::Error => 1,
LevelFilter::Warn => 2,
LevelFilter::Info => 3,
LevelFilter::Debug => 4,
LevelFilter::Trace => 5,
}
}
fn add(self, change: u8) -> Self {
Self::from_int(self.to_int().saturating_add(change))
}
fn sub(self, change: u8) -> Self {
Self::from_int(self.to_int().saturating_sub(change))
}
}
pub struct Logger {
level: LevelFilter,
colors: LogColors,
use_full_filename: bool,
out: Mutex<StandardStream>,
}
impl fmt::Debug for Logger {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Logger")
.field("level", &self.level)
.field("colors", &self.colors)
.field("use_full_filename", &self.use_full_filename)
.field("out", &"Mutex<termcolor::StandardStream::stderr>")
.finish()
}
}
impl Default for Logger {
fn default() -> Self {
Self::new()
}
}
impl Logger {
pub fn new() -> Logger {
Self::with_level(LevelFilter::Info)
}
pub fn with_level(level: LevelFilter) -> Logger {
Self {
level,
colors: LogColors::new(),
use_full_filename: false,
out: Mutex::new(StandardStream::stderr(ColorMode::default().to_color_choice())),
}
}
pub fn with_verbosity(level: u8) -> Logger {
Self::with_level(LevelFilter::from_int(level))
}
pub fn verbose(mut self, change: u8) -> Logger {
self.level = self.level.add(change);
self
}
pub fn quiet(mut self, change: u8) -> Logger {
self.level = self.level.sub(change);
self
}
pub fn color(mut self, c: ColorMode) -> Logger {
self.out = Mutex::new(StandardStream::stderr(c.to_color_choice()));
self
}
pub fn full_filename(mut self, full: bool) -> Logger {
self.use_full_filename = full;
self
}
pub fn try_init(self) -> Result<(), SetLoggerError> {
log::set_max_level(self.level);
log::set_boxed_logger(Box::new(self))
}
pub fn init(self) {
self.try_init().expect("failed to initialize logger");
}
fn print_log(&self, r: &Record) -> io::Result<()> {
let level = r.level();
let mut filename = r.file().unwrap_or("?");
if !self.use_full_filename && (level == Level::Debug || level == Level::Trace) {
if filename.starts_with("src/") {
filename = &filename[4..];
}
if filename.ends_with(".rs") {
filename = &filename[..(filename.len() - 3)];
}
}
let mut out = self.out.lock().unwrap();
out.set_color(self.colors.get(level))?;
match level {
Level::Error => writeln!(out, "[ERROR] {}", r.args()),
Level::Warn => writeln!(out, "[WARN] {}", r.args()),
Level::Info => writeln!(out, "{}", r.args()),
Level::Debug => {
writeln!(out, "[DEBUG][{}:{}] {}", filename, r.line().unwrap_or(0), r.args())
}
Level::Trace => {
writeln!(out, "[TRACE][{}:{}] {}", filename, r.line().unwrap_or(0), r.args())
}
}?;
out.reset()?;
Ok(())
}
}
impl Log for Logger {
fn enabled(&self, m: &Metadata) -> bool {
m.level() <= self.level
}
fn log(&self, r: &Record) {
if !self.enabled(r.metadata()) {
return;
}
if let Err(e) = self.print_log(r) {
eprintln!("LOGGING ERROR: failed to write log message because of '{}'", e);
eprintln!("Original message: {}: {}", r.level(), r.args());
}
}
fn flush(&self) {
let mut out = self.out.lock().unwrap();
let _ = out.flush();
}
}