-
-
Notifications
You must be signed in to change notification settings - Fork 148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement colored output in console logger. #5
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,35 @@ | |
//! * `%T` - The name of the thread that the log message came from. | ||
//! * `%t` - The target of the log message. | ||
//! | ||
//! # Color Codes | ||
//! | ||
//! When logging to console, color codes can be used to display text in different colors. | ||
//! | ||
//! The basic syntax looks like this: | ||
//! `%c:{color}(...)` , where {color} is one of the following: | ||
//! | ||
//! * `black` | ||
//! * `red` | ||
//! * `green` | ||
//! * `yellow` | ||
//! * `blue` | ||
//! * `magenta` | ||
//! * `cyan` | ||
//! * `white` | ||
//! * `boldBlack` | ||
//! * `boldRed` | ||
//! * `boldGreen` | ||
//! * `boldYellow` | ||
//! * `boldBlue` | ||
//! * `boldMagenta` | ||
//! * `boldCyan` | ||
//! * `boldWhite` | ||
//! * `highlight` - Colors the text depending on the log level | ||
//! | ||
//! **Example**: | ||
//! | ||
//! `%c:green(This text is green) %c:highlight(%d %l %t -) %m` | ||
|
||
|
||
use std::borrow::ToOwned; | ||
use std::default::Default; | ||
|
@@ -24,6 +53,7 @@ use std::io::Write; | |
|
||
use log::{LogRecord, LogLevel}; | ||
use time; | ||
use term; | ||
|
||
#[derive(Debug)] | ||
#[cfg_attr(test, derive(PartialEq))] | ||
|
@@ -32,11 +62,35 @@ enum TimeFmt { | |
Str(String), | ||
} | ||
|
||
#[derive(Debug)] | ||
#[cfg_attr(test, derive(PartialEq))] | ||
enum ColorFmt { | ||
Black, | ||
Red, | ||
Green, | ||
Yellow, | ||
Blue, | ||
Magenta, | ||
Cyan, | ||
White, | ||
BoldBlack, | ||
BoldRed, | ||
BoldGreen, | ||
BoldYellow, | ||
BoldBlue, | ||
BoldMagenta, | ||
BoldCyan, | ||
BoldWhite, | ||
Highlight, | ||
DefaultColor, | ||
} | ||
|
||
#[derive(Debug)] | ||
#[cfg_attr(test, derive(PartialEq))] | ||
enum Chunk { | ||
Text(String), | ||
Time(TimeFmt), | ||
Color(ColorFmt), | ||
Level, | ||
Message, | ||
Module, | ||
|
@@ -69,9 +123,9 @@ pub struct PatternLayout { | |
} | ||
|
||
impl Default for PatternLayout { | ||
/// Returns a `PatternLayout` using the default pattern of `%d %l %t - %m`. | ||
/// Returns a `PatternLayout` using the default pattern of `%c:highlight(%d %l %t -) %m. | ||
fn default() -> PatternLayout { | ||
PatternLayout::new("%d %l %t - %m").unwrap() | ||
PatternLayout::new("%c:highlight(%d %l %t -) %m").unwrap() | ||
} | ||
} | ||
|
||
|
@@ -84,6 +138,9 @@ impl PatternLayout { | |
let mut next_text = String::new(); | ||
let mut it = pattern.chars().peekable(); | ||
|
||
// Indicates if there is a previously opened `(` that has not been closed yet. | ||
let mut pending_close_colorfmt = false; | ||
|
||
while let Some(ch) = it.next() { | ||
if ch == '%' { | ||
let chunk = match it.next() { | ||
|
@@ -121,6 +178,47 @@ impl PatternLayout { | |
Some('L') => Some(Chunk::Line), | ||
Some('T') => Some(Chunk::Thread), | ||
Some('t') => Some(Chunk::Target), | ||
Some('c') => { | ||
match it.peek() { | ||
Some(&':') => { | ||
it.next(); | ||
let mut color_string = String::new(); | ||
loop { | ||
match it.next() { | ||
Some('(') => break, | ||
Some(c) => color_string.push(c), | ||
None => { | ||
return Err(Error("Did not find opening bracket for color format".to_owned())); | ||
} | ||
} | ||
} | ||
|
||
let color_fmt = match &*color_string { | ||
"black" => ColorFmt::Black, | ||
"red" => ColorFmt::Red, | ||
"green" => ColorFmt::Green, | ||
"yellow" => ColorFmt::Yellow, | ||
"blue" => ColorFmt::Blue, | ||
"magenta" => ColorFmt::Magenta, | ||
"cyan" => ColorFmt::Cyan, | ||
"white" => ColorFmt::White, | ||
"boldBlack" => ColorFmt::BoldBlack, | ||
"boldRed" => ColorFmt::BoldRed, | ||
"boldGreen" => ColorFmt::BoldGreen, | ||
"boldYellow" => ColorFmt::BoldYellow, | ||
"boldBlue" => ColorFmt::BoldBlue, | ||
"boldMagenta" => ColorFmt::BoldMagenta, | ||
"boldCyan" => ColorFmt::BoldCyan, | ||
"boldWhite" => ColorFmt::BoldWhite, | ||
"highlight" => ColorFmt::Highlight, | ||
_ => return Err(Error(format!("Unrecognized color string `{}`", color_string))), | ||
}; | ||
pending_close_colorfmt = true; | ||
Some(Chunk::Color(color_fmt)) | ||
} | ||
_ => return Err(Error(format!("Invalid formatter `%c`"))), | ||
} | ||
} | ||
Some(ch) => return Err(Error(format!("Invalid formatter `%{}`", ch))), | ||
None => return Err(Error("Unexpected end of pattern".to_owned())), | ||
}; | ||
|
@@ -132,11 +230,26 @@ impl PatternLayout { | |
} | ||
parsed.push(chunk); | ||
} | ||
} else if ch == ')' { | ||
if pending_close_colorfmt { | ||
if !next_text.is_empty() { | ||
parsed.push(Chunk::Text(next_text)); | ||
next_text = String::new(); | ||
} | ||
parsed.push(Chunk::Color(ColorFmt::DefaultColor)); | ||
pending_close_colorfmt = false; | ||
} else { | ||
next_text.push(ch); | ||
} | ||
} else { | ||
next_text.push(ch); | ||
} | ||
} | ||
|
||
if pending_close_colorfmt { | ||
return Err(Error("Unexpected end of color format".to_owned())) | ||
} | ||
|
||
if !next_text.is_empty() { | ||
parsed.push(Chunk::Text(next_text)); | ||
} | ||
|
@@ -147,18 +260,30 @@ impl PatternLayout { | |
} | ||
|
||
/// Writes the specified `LogRecord` to the specified `Write`r according | ||
/// to its pattern. | ||
/// to its pattern. This method should *not* be used for console `Write`rs. | ||
pub fn append<W>(&self, w: &mut W, record: &LogRecord) -> io::Result<()> where W: Write { | ||
let location = Location { | ||
module_path: record.location().module_path(), | ||
file: record.location().file(), | ||
line: record.location().line(), | ||
}; | ||
self.append_inner(w, record.level(), record.target(), &location, record.args()) | ||
self.append_inner(w, false, record.level(), record.target(), &location, record.args()) | ||
} | ||
|
||
/// Writes the specified `LogRecord` to the specified `Write`r according | ||
/// to its pattern. This method should be used for console `Write`rs. | ||
pub fn append_console<W>(&self, w: &mut W, record: &LogRecord) -> io::Result<()> where W: Write { | ||
let location = Location { | ||
module_path: record.location().module_path(), | ||
file: record.location().file(), | ||
line: record.location().line(), | ||
}; | ||
self.append_inner(w, true, record.level(), record.target(), &location, record.args()) | ||
} | ||
|
||
fn append_inner<W>(&self, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems weird to pass a writer to this method but it internally pulls a |
||
w: &mut W, | ||
console: bool, | ||
level: LogLevel, | ||
target: &str, | ||
location: &Location, | ||
|
@@ -181,8 +306,45 @@ impl PatternLayout { | |
write!(w, "{}", thread::current().name().unwrap_or("<unnamed>")) | ||
} | ||
Chunk::Target => write!(w, "{}", target), | ||
Chunk::Color(ref colorfmt) => { | ||
// Only deal with colors when logging to console | ||
if !console { continue } | ||
let mut w = term::stdout().unwrap(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This unwrap seems concerning - it'll fire if stdout is redirected to a file iirc. |
||
match *colorfmt { | ||
ColorFmt::Black => w.fg(term::color::BLACK).map(|_| ()), | ||
ColorFmt::Red => w.fg(term::color::RED).map(|_| ()), | ||
ColorFmt::Green => w.fg(term::color::GREEN).map(|_| ()), | ||
ColorFmt::Yellow => w.fg(term::color::YELLOW).map(|_| ()), | ||
ColorFmt::Blue => w.fg(term::color::BLUE).map(|_| ()), | ||
ColorFmt::Magenta => w.fg(term::color::MAGENTA).map(|_| ()), | ||
ColorFmt::Cyan => w.fg(term::color::CYAN).map(|_| ()), | ||
ColorFmt::White => w.fg(term::color::WHITE).map(|_| ()), | ||
ColorFmt::BoldBlack => w.fg(term::color::BRIGHT_BLACK).map(|_| ()), | ||
ColorFmt::BoldRed => w.fg(term::color::BRIGHT_RED).map(|_| ()), | ||
ColorFmt::BoldGreen => w.fg(term::color::BRIGHT_GREEN).map(|_| ()), | ||
ColorFmt::BoldYellow => w.fg(term::color::BRIGHT_YELLOW).map(|_| ()), | ||
ColorFmt::BoldBlue => w.fg(term::color::BRIGHT_BLUE).map(|_| ()), | ||
ColorFmt::BoldMagenta => w.fg(term::color::BRIGHT_MAGENTA).map(|_| ()), | ||
ColorFmt::BoldCyan => w.fg(term::color::BRIGHT_CYAN).map(|_| ()), | ||
ColorFmt::BoldWhite => w.fg(term::color::BRIGHT_WHITE).map(|_| ()), | ||
ColorFmt::DefaultColor => w.reset().map(|_| ()), | ||
ColorFmt::Highlight => { | ||
match level { | ||
LogLevel::Trace => w.fg(term::color::BLUE).map(|_| ()), | ||
LogLevel::Debug => w.fg(term::color::CYAN).map(|_| ()), | ||
LogLevel::Info => w.fg(term::color::GREEN).map(|_| ()), | ||
LogLevel::Warn => w.fg(term::color::YELLOW).map(|_| ()), | ||
LogLevel::Error => w.fg(term::color::RED).map(|_| ()), | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
// if console { | ||
// let mut w = term::stdout().unwrap(); | ||
// w.reset().unwrap(); | ||
// } | ||
writeln!(w, "") | ||
} | ||
} | ||
|
@@ -200,7 +362,7 @@ mod tests { | |
|
||
use log::LogLevel; | ||
|
||
use super::{Chunk, TimeFmt, PatternLayout, Location}; | ||
use super::{Chunk, TimeFmt, PatternLayout, Location, ColorFmt}; | ||
|
||
#[test] | ||
fn test_parse() { | ||
|
@@ -219,11 +381,34 @@ mod tests { | |
assert_eq!(actual, expected) | ||
} | ||
|
||
#[test] | ||
fn test_parse_with_colors() { | ||
let expected = [Chunk::Color(ColorFmt::Yellow), | ||
Chunk::Text("hi".to_string()), | ||
Chunk::Color(ColorFmt::DefaultColor), | ||
Chunk::Time(TimeFmt::Str("%Y-%m-%d".to_string())), | ||
Chunk::Color(ColorFmt::Highlight), | ||
Chunk::Time(TimeFmt::Rfc3339), | ||
Chunk::Color(ColorFmt::DefaultColor), | ||
Chunk::Text("()".to_string()), | ||
Chunk::Level, | ||
]; | ||
let actual = PatternLayout::new("%c:yellow(hi)%d{%Y-%m-%d}%c:highlight(%d)()%l").unwrap().pattern; | ||
assert_eq!(actual, expected) | ||
} | ||
|
||
#[test] | ||
fn test_invalid_date_format() { | ||
assert!(PatternLayout::new("%d{%q}").is_err()); | ||
} | ||
|
||
#[test] | ||
fn test_invalid_color_formats() { | ||
assert!(PatternLayout::new("%c:darkBlack(Is this dark black?)").is_err()); | ||
assert!(PatternLayout::new("%c:green(Oops no closing bracket").is_err()); | ||
assert!(PatternLayout::new("%c(What color is this?)").is_err()); | ||
} | ||
|
||
#[test] | ||
fn test_log() { | ||
let pw = PatternLayout::new("%l %m at %M in %f:%L").unwrap(); | ||
|
@@ -235,6 +420,27 @@ mod tests { | |
}; | ||
let mut buf = vec![]; | ||
pw.append_inner(&mut buf, | ||
false, | ||
LogLevel::Debug, | ||
"target", | ||
&LOCATION, | ||
&format_args!("the message")).unwrap(); | ||
|
||
assert_eq!(buf, &b"DEBUG the message at mod path in the file:132\n"[..]); | ||
} | ||
|
||
#[test] | ||
fn test_log_with_colors() { | ||
let pw = PatternLayout::new("%l %c:red(%m) at %M in %f:%L").unwrap(); | ||
|
||
static LOCATION: Location<'static> = Location { | ||
module_path: "mod path", | ||
file: "the file", | ||
line: 132, | ||
}; | ||
let mut buf = vec![]; | ||
pw.append_inner(&mut buf, | ||
true, | ||
LogLevel::Debug, | ||
"target", | ||
&LOCATION, | ||
|
@@ -254,6 +460,7 @@ mod tests { | |
}; | ||
let mut buf = vec![]; | ||
pw.append_inner(&mut buf, | ||
false, | ||
LogLevel::Debug, | ||
"target", | ||
&LOCATION, | ||
|
@@ -273,6 +480,7 @@ mod tests { | |
}; | ||
let mut buf = vec![]; | ||
pw.append_inner(&mut buf, | ||
false, | ||
LogLevel::Debug, | ||
"target", | ||
&LOCATION, | ||
|
@@ -286,4 +494,3 @@ mod tests { | |
let _: PatternLayout = Default::default(); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like it would make more sense to use
{}
instead of()
to stay in line with%d
formats?