#![allow(clippy::needless_doctest_main)]
#![doc(html_root_url = "https://docs.rs/stderrlog/0.6.0")]
#[cfg(feature = "timestamps")]
use chrono::Local;
use is_terminal::IsTerminal;
use log::{Level, LevelFilter, Log, Metadata, Record};
use std::cell::RefCell;
use std::fmt;
use std::io::{self, Write};
#[cfg(feature = "timestamps")]
use std::str::FromStr;
use termcolor::{Color, ColorSpec, StandardStream, WriteColor};
pub use termcolor::ColorChoice;
use thread_local::ThreadLocal;
#[cfg(feature = "timestamps")]
#[derive(Clone, Copy, Debug)]
pub enum Timestamp {
Off,
Second,
Millisecond,
Microsecond,
Nanosecond,
}
#[cfg(feature = "timestamps")]
impl FromStr for Timestamp {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ns" => Ok(Timestamp::Nanosecond),
"ms" => Ok(Timestamp::Millisecond),
"us" => Ok(Timestamp::Microsecond),
"sec" => Ok(Timestamp::Second),
"none" | "off" => Ok(Timestamp::Off),
_ => Err("invalid value".into()),
}
}
}
pub struct StdErrLog {
verbosity: LevelFilter,
quiet: bool,
show_level: bool,
#[cfg(feature = "timestamps")]
timestamp: Timestamp,
modules: Vec<String>,
writer: ThreadLocal<RefCell<StandardStream>>,
color_choice: ColorChoice,
show_module_names: bool,
}
impl fmt::Debug for StdErrLog {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut builder = f.debug_struct("StdErrLog");
builder
.field("verbosity", &self.verbosity)
.field("quiet", &self.quiet)
.field("show_level", &self.show_level);
#[cfg(feature = "timestamps")]
builder.field("timestamp", &self.timestamp);
builder
.field("modules", &self.modules)
.field("writer", &"stderr")
.field("color_choice", &self.color_choice)
.field("show_module_names", &self.show_module_names)
.finish()
}
}
impl Clone for StdErrLog {
fn clone(&self) -> StdErrLog {
StdErrLog {
modules: self.modules.clone(),
writer: ThreadLocal::new(),
..*self
}
}
}
impl Log for StdErrLog {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= self.log_level_filter() && self.includes_module(metadata.target())
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let writer = self
.writer
.get_or(|| RefCell::new(StandardStream::stderr(self.color_choice)));
let writer = writer.borrow_mut();
let mut writer = io::LineWriter::new(writer.lock());
let color = match record.metadata().level() {
Level::Error => Color::Red,
Level::Warn => Color::Yellow,
Level::Info => Color::Blue,
Level::Debug => Color::Cyan,
Level::Trace => Color::Magenta,
};
{
writer
.get_mut()
.set_color(ColorSpec::new().set_fg(Some(color)))
.ok();
}
if self.show_module_names {
let _ = write!(writer, "{}: ", record.metadata().target());
}
#[cfg(feature = "timestamps")]
match self.timestamp {
Timestamp::Second => {
let fmt = "%Y-%m-%dT%H:%M:%S%:z";
let _ = write!(writer, "{} - ", Local::now().format(fmt));
}
Timestamp::Millisecond => {
let fmt = "%Y-%m-%dT%H:%M:%S%.3f%:z";
let _ = write!(writer, "{} - ", Local::now().format(fmt));
}
Timestamp::Microsecond => {
let fmt = "%Y-%m-%dT%H:%M:%S%.6f%:z";
let _ = write!(writer, "{} - ", Local::now().format(fmt));
}
Timestamp::Nanosecond => {
let fmt = "%Y-%m-%dT%H:%M:%S%.9f%:z";
let _ = write!(writer, "{} - ", Local::now().format(fmt));
}
Timestamp::Off => {}
}
if self.show_level {
let _ = write!(writer, "{} ", record.level());
}
writer.flush().ok();
{
writer.get_mut().reset().ok();
}
let _ = writeln!(writer, "{}", record.args());
}
fn flush(&self) {
let writer = self
.writer
.get_or(|| RefCell::new(StandardStream::stderr(self.color_choice)));
let mut writer = writer.borrow_mut();
writer.flush().ok();
}
}
pub enum LogLevelNum {
Off,
Error,
Warn,
Info,
Debug,
Trace,
}
impl From<usize> for LogLevelNum {
fn from(verbosity: usize) -> Self {
match verbosity {
0 => LogLevelNum::Error,
1 => LogLevelNum::Warn,
2 => LogLevelNum::Info,
3 => LogLevelNum::Debug,
_ => LogLevelNum::Trace,
}
}
}
impl From<Level> for LogLevelNum {
fn from(l: Level) -> Self {
match l {
Level::Error => LogLevelNum::Error,
Level::Warn => LogLevelNum::Warn,
Level::Info => LogLevelNum::Info,
Level::Debug => LogLevelNum::Debug,
Level::Trace => LogLevelNum::Trace,
}
}
}
impl From<LevelFilter> for LogLevelNum {
fn from(l: LevelFilter) -> Self {
match l {
LevelFilter::Off => LogLevelNum::Off,
LevelFilter::Error => LogLevelNum::Error,
LevelFilter::Warn => LogLevelNum::Warn,
LevelFilter::Info => LogLevelNum::Info,
LevelFilter::Debug => LogLevelNum::Debug,
LevelFilter::Trace => LogLevelNum::Trace,
}
}
}
impl StdErrLog {
pub fn new() -> StdErrLog {
StdErrLog {
verbosity: LevelFilter::Error,
quiet: false,
show_level: true,
#[cfg(feature = "timestamps")]
timestamp: Timestamp::Off,
modules: Vec::new(),
writer: ThreadLocal::new(),
color_choice: ColorChoice::Auto,
show_module_names: false,
}
}
pub fn verbosity<V: Into<LogLevelNum>>(&mut self, verbosity: V) -> &mut StdErrLog {
self.verbosity = match verbosity.into() {
LogLevelNum::Off => LevelFilter::Off,
LogLevelNum::Error => LevelFilter::Error,
LogLevelNum::Warn => LevelFilter::Warn,
LogLevelNum::Info => LevelFilter::Info,
LogLevelNum::Debug => LevelFilter::Debug,
LogLevelNum::Trace => LevelFilter::Trace,
};
self
}
pub fn quiet(&mut self, quiet: bool) -> &mut StdErrLog {
self.quiet = quiet;
self
}
pub fn show_level(&mut self, levels: bool) -> &mut StdErrLog {
self.show_level = levels;
self
}
#[cfg(feature = "timestamps")]
pub fn timestamp(&mut self, timestamp: Timestamp) -> &mut StdErrLog {
self.timestamp = timestamp;
self
}
pub fn color(&mut self, choice: ColorChoice) -> &mut StdErrLog {
self.color_choice = choice;
self
}
pub fn module<T: Into<String>>(&mut self, module: T) -> &mut StdErrLog {
self._module(module.into())
}
pub fn show_module_names(&mut self, show_module_names: bool) -> &mut StdErrLog {
self.show_module_names = show_module_names;
self
}
fn _module(&mut self, module: String) -> &mut StdErrLog {
if let Err(i) = self.modules.binary_search(&module) {
if i == 0 || !is_submodule(&self.modules[i - 1], &module) {
let submodule_count = self.modules[i..]
.iter()
.take_while(|possible_submodule| is_submodule(&module, possible_submodule))
.count();
self.modules.drain(i..i + submodule_count);
self.modules.insert(i, module);
}
}
self
}
pub fn modules<T: Into<String>, I: IntoIterator<Item = T>>(
&mut self,
modules: I,
) -> &mut StdErrLog {
for module in modules {
self.module(module);
}
self
}
fn log_level_filter(&self) -> LevelFilter {
if self.quiet {
LevelFilter::Off
} else {
self.verbosity
}
}
fn includes_module(&self, module_path: &str) -> bool {
if self.modules.is_empty() {
return true;
}
match self
.modules
.binary_search_by(|module| module.as_str().cmp(module_path))
{
Ok(_) => {
true
}
Err(0) => {
false
}
Err(i) => is_submodule(&self.modules[i - 1], module_path),
}
}
pub fn init(&mut self) -> Result<(), log::SetLoggerError> {
self.color_choice = match self.color_choice {
ColorChoice::Auto => {
if io::stderr().is_terminal() {
ColorChoice::Auto
} else {
ColorChoice::Never
}
}
other => other,
};
log::set_max_level(self.log_level_filter());
log::set_boxed_logger(Box::new(self.clone()))
}
}
impl Default for StdErrLog {
fn default() -> Self {
StdErrLog::new()
}
}
pub fn new() -> StdErrLog {
StdErrLog::new()
}
fn is_submodule(parent: &str, possible_child: &str) -> bool {
let parent = parent.as_bytes();
let possible_child = possible_child.as_bytes();
if parent.len() > possible_child.len() {
return false;
}
if parent != &possible_child[..parent.len()] {
return false;
}
parent.len() == possible_child.len()
|| possible_child.get(parent.len()..parent.len() + 2) == Some(b"::")
}
#[cfg(test)]
mod tests {
use super::{is_submodule, StdErrLog};
#[test]
fn submodule() {
assert!(is_submodule("a", "a::b::c::d"));
assert!(is_submodule("a::b::c", "a::b::c::d"));
assert!(is_submodule("a::b::c", "a::b::c"));
assert!(!is_submodule("a::b::c", "a::bad::c"));
assert!(!is_submodule("a::b::c", "a::b::cab"));
assert!(!is_submodule("a::b::c", "a::b::cab::d"));
assert!(!is_submodule("a::b::c", "a::b"));
assert!(!is_submodule("a::b::c", "a::bad"));
}
#[test]
fn test_default_level() {
super::new().module(module_path!()).init().unwrap();
assert_eq!(log::Level::Error, log::max_level())
}
#[test]
fn modules_display_all() {
let logger = StdErrLog::new();
assert!(logger.includes_module("good"));
}
#[test]
fn modules_display_exact_match() {
let mut logger = StdErrLog::new();
logger.module("good");
assert!(logger.includes_module("good"));
assert!(!logger.includes_module("bad"));
}
#[test]
fn modules_display_module() {
let mut logger = StdErrLog::new();
logger.module("good");
assert!(logger.includes_module("good::foo"));
assert!(logger.includes_module("good::bar"));
assert!(logger.includes_module("good"));
assert!(!logger.includes_module("bad"));
}
#[test]
fn modules_display_submodule() {
let mut logger = StdErrLog::new();
logger.module("good::foo");
assert!(logger.includes_module("good::foo"));
assert!(!logger.includes_module("good::bar"));
assert!(!logger.includes_module("good"));
assert!(!logger.includes_module("bad"));
}
}