#![cfg_attr(feature = "nightly", feature(thread_id_value))]
#[cfg(feature = "colored")]
use colored::*;
use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
use std::{collections::HashMap, str::FromStr};
#[cfg(feature = "timestamps")]
use time::{format_description::FormatItem, OffsetDateTime, UtcOffset};
#[cfg(feature = "timestamps")]
const TIMESTAMP_FORMAT_OFFSET: &[FormatItem] = time::macros::format_description!(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3][offset_hour sign:mandatory]:[offset_minute]"
);
#[cfg(feature = "timestamps")]
const TIMESTAMP_FORMAT_UTC: &[FormatItem] =
time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z");
#[cfg(feature = "timestamps")]
#[derive(PartialEq)]
enum Timestamps {
None,
Local,
Utc,
UtcOffset(UtcOffset),
}
pub struct SimpleLogger {
default_level: LevelFilter,
module_levels: Vec<(String, LevelFilter)>,
#[cfg(feature = "threads")]
threads: bool,
#[cfg(feature = "timestamps")]
timestamps: Timestamps,
#[cfg(feature = "timestamps")]
timestamps_format: Option<&'static [FormatItem<'static>]>,
#[cfg(feature = "colored")]
colors: bool,
}
impl SimpleLogger {
#[must_use = "You must call init() to begin logging"]
pub fn new() -> SimpleLogger {
SimpleLogger {
default_level: LevelFilter::Trace,
module_levels: Vec::new(),
#[cfg(feature = "threads")]
threads: false,
#[cfg(feature = "timestamps")]
timestamps: Timestamps::Utc,
#[cfg(feature = "timestamps")]
timestamps_format: None,
#[cfg(feature = "colored")]
colors: true,
}
}
#[must_use = "You must call init() to begin logging"]
#[deprecated(
since = "1.12.0",
note = "Use [`env`](#method.env) instead. Will be removed in version 2.0.0."
)]
pub fn from_env() -> SimpleLogger {
SimpleLogger::new().with_level(log::LevelFilter::Error).env()
}
#[must_use = "You must call init() to begin logging"]
pub fn env(mut self) -> SimpleLogger {
self.default_level = std::env::var("RUST_LOG")
.ok()
.as_deref()
.map(log::LevelFilter::from_str)
.and_then(Result::ok)
.unwrap_or(self.default_level);
self
}
#[must_use = "You must call init() to begin logging"]
pub fn with_level(mut self, level: LevelFilter) -> SimpleLogger {
self.default_level = level;
self
}
#[must_use = "You must call init() to begin logging"]
pub fn with_module_level(mut self, target: &str, level: LevelFilter) -> SimpleLogger {
self.module_levels.push((target.to_string(), level));
#[cfg(test)]
self.module_levels
.sort_by_key(|(name, _level)| name.len().wrapping_neg());
self
}
#[must_use = "You must call init() to begin logging"]
#[deprecated(
since = "1.11.0",
note = "Use [`with_module_level`](#method.with_module_level) instead. Will be removed in version 2.0.0."
)]
pub fn with_target_levels(mut self, target_levels: HashMap<String, LevelFilter>) -> SimpleLogger {
self.module_levels = target_levels.into_iter().collect();
#[cfg(test)]
self.module_levels
.sort_by_key(|(name, _level)| name.len().wrapping_neg());
self
}
#[must_use = "You must call init() to begin logging"]
#[cfg(feature = "threads")]
pub fn with_threads(mut self, threads: bool) -> SimpleLogger {
self.threads = threads;
self
}
#[must_use = "You must call init() to begin logging"]
#[cfg(feature = "timestamps")]
#[deprecated(
since = "1.16.0",
note = "Use [`with_local_timestamps`] or [`with_utc_timestamps`] instead. Will be removed in version 2.0.0."
)]
pub fn with_timestamps(mut self, timestamps: bool) -> SimpleLogger {
if timestamps {
self.timestamps = Timestamps::Local
} else {
self.timestamps = Timestamps::None
}
self
}
#[must_use = "You must call init() to begin logging"]
#[cfg(feature = "timestamps")]
pub fn with_timestamp_format(mut self, format: &'static [FormatItem<'static>]) -> SimpleLogger {
self.timestamps_format = Some(format);
self
}
#[must_use = "You must call init() to begin logging"]
#[cfg(feature = "timestamps")]
pub fn without_timestamps(mut self) -> SimpleLogger {
self.timestamps = Timestamps::None;
self
}
#[must_use = "You must call init() to begin logging"]
#[cfg(feature = "timestamps")]
pub fn with_local_timestamps(mut self) -> SimpleLogger {
self.timestamps = Timestamps::Local;
self
}
#[must_use = "You must call init() to begin logging"]
#[cfg(feature = "timestamps")]
pub fn with_utc_timestamps(mut self) -> SimpleLogger {
self.timestamps = Timestamps::Utc;
self
}
#[must_use = "You must call init() to begin logging"]
#[cfg(feature = "timestamps")]
pub fn with_utc_offset(mut self, offset: UtcOffset) -> SimpleLogger {
self.timestamps = Timestamps::UtcOffset(offset);
self
}
#[must_use = "You must call init() to begin logging"]
#[cfg(feature = "colored")]
pub fn with_colors(mut self, colors: bool) -> SimpleLogger {
self.colors = colors;
self
}
pub fn init(mut self) -> Result<(), SetLoggerError> {
#[cfg(all(windows, feature = "colored"))]
set_up_color_terminal();
self.module_levels
.sort_by_key(|(name, _level)| name.len().wrapping_neg());
let max_level = self.module_levels.iter().map(|(_name, level)| level).copied().max();
let max_level = max_level
.map(|lvl| lvl.max(self.default_level))
.unwrap_or(self.default_level);
log::set_max_level(max_level);
log::set_boxed_logger(Box::new(self))?;
Ok(())
}
}
impl Default for SimpleLogger {
fn default() -> Self {
SimpleLogger::new()
}
}
impl Log for SimpleLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
&metadata.level().to_level_filter()
<= self
.module_levels
.iter()
.find(|(name, _level)| metadata.target().starts_with(name))
.map(|(_name, level)| level)
.unwrap_or(&self.default_level)
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let level_string = {
#[cfg(feature = "colored")]
{
if self.colors {
match record.level() {
Level::Error => format!("{:<5}", record.level().to_string()).red().to_string(),
Level::Warn => format!("{:<5}", record.level().to_string()).yellow().to_string(),
Level::Info => format!("{:<5}", record.level().to_string()).cyan().to_string(),
Level::Debug => format!("{:<5}", record.level().to_string()).purple().to_string(),
Level::Trace => format!("{:<5}", record.level().to_string()).normal().to_string(),
}
} else {
format!("{:<5}", record.level().to_string())
}
}
#[cfg(not(feature = "colored"))]
{
format!("{:<5}", record.level().to_string())
}
};
let target = if !record.target().is_empty() {
record.target()
} else {
record.module_path().unwrap_or_default()
};
let thread = {
#[cfg(feature = "threads")]
if self.threads {
let thread = std::thread::current();
format!("@{}", {
#[cfg(feature = "nightly")]
{
thread.name().unwrap_or(&thread.id().as_u64().to_string())
}
#[cfg(not(feature = "nightly"))]
{
thread.name().unwrap_or("?")
}
})
} else {
"".to_string()
}
#[cfg(not(feature = "threads"))]
""
};
let timestamp = {
#[cfg(feature = "timestamps")]
match self.timestamps {
Timestamps::None => "".to_string(),
Timestamps::Local => format!(
"{} ",
OffsetDateTime::now_local()
.expect(concat!(
"Could not determine the UTC offset on this system. ",
"Consider displaying UTC time instead. ",
"Possible causes are that the time crate does not implement \"local_offset_at\" ",
"on your system, or that you are running in a multi-threaded environment and ",
"the time crate is returning \"None\" from \"local_offset_at\" to avoid unsafe ",
"behaviour. See the time crate's documentation for more information. ",
"(https://time-rs.github.io/internal-api/time/index.html#feature-flags)"
))
.format(&self.timestamps_format.unwrap_or(TIMESTAMP_FORMAT_OFFSET))
.unwrap()
),
Timestamps::Utc => format!(
"{} ",
OffsetDateTime::now_utc()
.format(&self.timestamps_format.unwrap_or(TIMESTAMP_FORMAT_UTC))
.unwrap()
),
Timestamps::UtcOffset(offset) => format!(
"{} ",
OffsetDateTime::now_utc()
.to_offset(offset)
.format(&self.timestamps_format.unwrap_or(TIMESTAMP_FORMAT_OFFSET))
.unwrap()
),
}
#[cfg(not(feature = "timestamps"))]
""
};
let message = format!("{}{} [{}{}] {}", timestamp, level_string, target, thread, record.args());
#[cfg(not(feature = "stderr"))]
println!("{}", message);
#[cfg(feature = "stderr")]
eprintln!("{}", message);
}
}
fn flush(&self) {}
}
#[cfg(all(windows, feature = "colored"))]
fn set_up_color_terminal() {
use atty::Stream;
if atty::is(Stream::Stdout) {
unsafe {
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::System::Console::{
GetConsoleMode, GetStdHandle, SetConsoleMode, CONSOLE_MODE, ENABLE_VIRTUAL_TERMINAL_PROCESSING,
STD_OUTPUT_HANDLE,
};
let stdout = GetStdHandle(STD_OUTPUT_HANDLE);
if stdout == INVALID_HANDLE_VALUE {
return;
}
let mut mode: CONSOLE_MODE = 0;
if GetConsoleMode(stdout, &mut mode) == 0 {
return;
}
SetConsoleMode(stdout, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
}
pub fn init() -> Result<(), SetLoggerError> {
SimpleLogger::new().init()
}
#[cfg(feature = "timestamps")]
pub fn init_utc() -> Result<(), SetLoggerError> {
SimpleLogger::new().with_utc_timestamps().init()
}
pub fn init_with_env() -> Result<(), SetLoggerError> {
SimpleLogger::new().env().init()
}
pub fn init_with_level(level: Level) -> Result<(), SetLoggerError> {
SimpleLogger::new().with_level(level.to_level_filter()).init()
}
#[deprecated(
since = "1.12.0",
note = "Use [`init_with_env`] instead, which does not unwrap the result. Will be removed in version 2.0.0."
)]
pub fn init_by_env() {
init_with_env().unwrap()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_module_levels_allowlist() {
let logger = SimpleLogger::new()
.with_level(LevelFilter::Off)
.with_module_level("my_crate", LevelFilter::Info);
assert!(logger.enabled(&create_log("my_crate", Level::Info)));
assert!(logger.enabled(&create_log("my_crate::module", Level::Info)));
assert!(!logger.enabled(&create_log("my_crate::module", Level::Debug)));
assert!(!logger.enabled(&create_log("not_my_crate", Level::Debug)));
assert!(!logger.enabled(&create_log("not_my_crate::module", Level::Error)));
}
#[test]
fn test_module_levels_denylist() {
let logger = SimpleLogger::new()
.with_level(LevelFilter::Debug)
.with_module_level("my_crate", LevelFilter::Trace)
.with_module_level("chatty_dependency", LevelFilter::Info);
assert!(logger.enabled(&create_log("my_crate", Level::Info)));
assert!(logger.enabled(&create_log("my_crate", Level::Trace)));
assert!(logger.enabled(&create_log("my_crate::module", Level::Info)));
assert!(logger.enabled(&create_log("my_crate::module", Level::Trace)));
assert!(logger.enabled(&create_log("not_my_crate", Level::Debug)));
assert!(!logger.enabled(&create_log("not_my_crate::module", Level::Trace)));
assert!(logger.enabled(&create_log("chatty_dependency", Level::Info)));
assert!(!logger.enabled(&create_log("chatty_dependency", Level::Debug)));
assert!(!logger.enabled(&create_log("chatty_dependency::module", Level::Debug)));
assert!(logger.enabled(&create_log("chatty_dependency::module", Level::Warn)));
}
#[test]
#[cfg(feature = "timestamps")]
fn test_timestamps_defaults() {
let builder = SimpleLogger::new();
assert!(builder.timestamps == Timestamps::Utc);
}
#[test]
#[cfg(feature = "timestamps")]
#[allow(deprecated)]
fn test_with_timestamps() {
let builder = SimpleLogger::new().with_timestamps(false);
assert!(builder.timestamps == Timestamps::None);
}
#[test]
#[cfg(feature = "timestamps")]
fn test_with_utc_timestamps() {
let builder = SimpleLogger::new().with_utc_timestamps();
assert!(builder.timestamps == Timestamps::Utc);
}
#[test]
#[cfg(feature = "timestamps")]
fn test_with_local_timestamps() {
let builder = SimpleLogger::new().with_local_timestamps();
assert!(builder.timestamps == Timestamps::Local);
}
#[test]
#[cfg(feature = "timestamps")]
#[allow(deprecated)]
fn test_with_timestamps_format() {
let builder =
SimpleLogger::new().with_timestamp_format(time::macros::format_description!("[hour]:[minute]:[second]"));
assert!(builder.timestamps_format.is_some());
}
#[test]
#[cfg(feature = "colored")]
fn test_with_colors() {
let mut builder = SimpleLogger::new();
assert!(builder.colors == true);
builder = builder.with_colors(false);
assert!(builder.colors == false);
}
fn create_log(name: &str, level: Level) -> Metadata {
let mut builder = Metadata::builder();
builder.level(level);
builder.target(name);
builder.build()
}
}