Skip to content
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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
Copy link
Collaborator

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?

//!
//! * `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,
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 term::stdout() as well. Maybe this method should take in a W: Terminal instead?

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();
Copy link
Collaborator

Choose a reason for hiding this comment

The 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, "")
}
}
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