1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
// Copyright (c) 2020 Allen Wild
// SPDX-License-Identifier: MIT OR Apache-2.0
//! # yall: Yet Another Little Logger
//!
//! A simple lightweight backend for the [`log`](::log) crate.
//!
//! * Logs to stderr
//! * Simple standard terminal colors, no RGB or 256-color themes that may clash with the
//! terminal theme
//! * By default, color is auto-detected based on whether stderr is a tty, but can be forced
//! on or off with the [`Logger::color`] method.
//! * Info level messages are unformatted with no color or prefix
//! * Error/Warn/Debug/Trace messages are Red/Yellow/Cyan/Blue, respectively
//! * Debug and Trace levels show the filename and line number.
//! * Minimal dependencies
//! * Configured with code rather than environment variables
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;
/// Re-exports of the error, warn, info, debug, and trace macros in the log crate.
///
/// Convenient for glob-importing with `use yall::log_macros::*;`
pub mod log_macros {
#[doc(no_inline)]
pub use log::{debug, error, info, log_enabled, trace, warn};
}
/// Whether to enable colored output, the usual suspects.
#[derive(Debug)]
pub enum ColorMode {
/// Enable color automatically if stderr is a tty, plus the `TERM` and `NO_COLOR` environment
/// variable checks done by `termcolor`'s [`ColorChoice::Auto`] variant.
Auto,
/// Always enable colored output.
Always,
/// Never enable colored output.
Never,
}
impl ColorMode {
/// Internal function to map ColorMode to a termcolor::ColorChoice that Logger uses internally.
/// This is mainly to keep termcolor out of yall's API.
fn to_color_choice(&self) -> ColorChoice {
match self {
ColorMode::Auto => {
if io::stderr().is_terminal() {
// termcolor will check for TERM and NO_COLOR when creating a StandardStream
ColorChoice::Auto
} else {
ColorChoice::Never
}
}
ColorMode::Always => ColorChoice::Always,
ColorMode::Never => ColorChoice::Never,
}
}
}
impl Default for ColorMode {
/// The default ColorMode is `Auto`
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 {
// The set_* functions return &mut, so we need to_owned() to convert back to an actual
// value. Since ColorSpec doesn't implement Copy, we can't just dereference.
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,
}
}
}
/// Internal extension trait for working with log::LevelFilter as an integer. Since LevelFilter is
/// Copy, all these methods take self by value to avoid unnecessary pointers.
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;
}
// LevelFilter is Copy and repr(usize) and the match blocks here are the same as LevelFilter's
// discriminant order, so they compile down to almost nothing. from_int is one branch for val>5,
// to_int is a single move that basically gets inlined into a nop.
// Not that this is hot code anyway...
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))
}
}
/// The main struct of this crate which implements the [`Log`] trait.
///
/// Create one using [`with_level`](Self::with_level) or
/// [`with_verbosity`](Self::with_verbosity) and then call [`init`](Self::init) or
/// [`try_init`](Self::try_init) on it.
pub struct Logger {
level: LevelFilter,
colors: LogColors,
use_full_filename: bool,
out: Mutex<StandardStream>,
}
// StandardStream doesn't impl Debug, so we can't derive it. Instead do this manual implementation
// with a dummy value for out.
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 {
/// Create a Logger with the default Info level
fn default() -> Self {
Self::new()
}
}
impl Logger {
/// Create a Logger with the default Info level
pub fn new() -> Logger {
Self::with_level(LevelFilter::Info)
}
/// Create a Logger with the given level.
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())),
}
}
/// Create a Logger with the given "verbosity" number. Useful for translating a number of -v
/// flags in command-line arguments.
///
/// 0 = Off, 1 = Error, 2 = Warn, 3 = Info, 4 = Debug, 5+ = Trace
pub fn with_verbosity(level: u8) -> Logger {
Self::with_level(LevelFilter::from_int(level))
}
/// Increase the verbosity level by the amount given. Takes a `u8` as returned by
/// `clap::ArgMatches::get_count`.
pub fn verbose(mut self, change: u8) -> Logger {
self.level = self.level.add(change);
self
}
/// Decrease the verbosity level by the amount given. Takes a `u8` as returned by
/// `clap::ArgMatches::get_count`.
pub fn quiet(mut self, change: u8) -> Logger {
self.level = self.level.sub(change);
self
}
/// Sets the color mode, see [`ColorMode`] for details.
pub fn color(mut self, c: ColorMode) -> Logger {
// we can't change the ColorChoice of a StandardStream, but we can just re-create it
self.out = Mutex::new(StandardStream::stderr(c.to_color_choice()));
self
}
/// By default, yall will shorten the filename displayed in Debug and Trace logs by removing
/// a "src/" prefix and ".rs" suffix, if present. Use this function to disable that and print
/// the full unchanged filename.
pub fn full_filename(mut self, full: bool) -> Logger {
self.use_full_filename = full;
self
}
/// Register this as the global logger with the [`log`](::log) crate. May fail if the application has
/// already set a logger.
pub fn try_init(self) -> Result<(), SetLoggerError> {
log::set_max_level(self.level);
log::set_boxed_logger(Box::new(self))
}
/// Same as [`try_init`](Self::try_init) but panic on failure.
pub fn init(self) {
self.try_init().expect("failed to initialize logger");
}
/// Internal wrapper function for the meat of the logging that returns a Result, in case the
/// termcolors printing fails somehow. Assumes that we've already checked that the record's
/// log level is in fact enabled.
fn print_log(&self, r: &Record) -> io::Result<()> {
let level = r.level();
// strip "src/" prefix and ".rs" suffix
let mut filename = r.file().unwrap_or("?");
if !self.use_full_filename && (level == Level::Debug || level == Level::Trace) {
// we could use str::strip_{prefix,suffix} here, but they're not stable until
// rust 1.45 and return Options which is kinda clunky.
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) {
// uh oh, something in termcolor failed
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();
}
}