Skip to content

Commit

Permalink
Implement colored output in console logger.
Browse files Browse the repository at this point in the history
This implementation is based on the prototype provided by @ryantaylor in estk#3.

Adds a new color syntax to the pattern parser which looks like this:
%c:color(...colored text...)

Also changes the default pattern to `%c:highlight(%d %l %t -) %m`.

closes estk#3
  • Loading branch information
nicokoch committed Nov 4, 2015
1 parent b98de0b commit 36e4244
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 11 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ documentation = "https://sfackler.github.io/log4rs/doc/v0.3.3/log4rs"
toml = "0.1"
time = "0.1"
log = "0.3"
term = "0.2"
2 changes: 1 addition & 1 deletion src/appender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub struct ConsoleAppender {
impl Append for ConsoleAppender {
fn append(&mut self, record: &LogRecord) -> Result<(), Box<Error>> {
let mut stdout = self.stdout.lock();
try!(self.pattern.append(&mut stdout, record));
try!(self.pattern.append_console(&mut stdout, record));
try!(stdout.flush());
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
extern crate log;
extern crate time;
extern crate toml as toml_parser;
extern crate term;

use std::borrow::ToOwned;
use std::convert::AsRef;
Expand Down
219 changes: 213 additions & 6 deletions src/pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +53,7 @@ use std::io::Write;

use log::{LogRecord, LogLevel};
use time;
use term;

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
Expand All @@ -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,
Expand Down Expand Up @@ -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()
}
}

Expand All @@ -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() {
Expand Down Expand Up @@ -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())),
};
Expand All @@ -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));
}
Expand All @@ -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,
w: &mut W,
console: bool,
level: LogLevel,
target: &str,
location: &Location,
Expand All @@ -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();
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, "")
}
}
Expand All @@ -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() {
Expand All @@ -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();
Expand All @@ -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,
Expand All @@ -254,6 +460,7 @@ mod tests {
};
let mut buf = vec![];
pw.append_inner(&mut buf,
false,
LogLevel::Debug,
"target",
&LOCATION,
Expand All @@ -273,6 +480,7 @@ mod tests {
};
let mut buf = vec![];
pw.append_inner(&mut buf,
false,
LogLevel::Debug,
"target",
&LOCATION,
Expand All @@ -286,4 +494,3 @@ mod tests {
let _: PatternLayout = Default::default();
}
}

4 changes: 2 additions & 2 deletions test/log.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ kind = "console"

[[appender.console.filter]]
kind = "threshold"
level = "error"
level = "trace"

[appender.file]
kind = "file"
path = "error.log"
pattern = "%d [%t] %l %M:%m"

[root]
level = "warn"
level = "trace"
appenders = ["console"]

[[logger]]
Expand Down
Loading

0 comments on commit 36e4244

Please sign in to comment.