use std::borrow::ToOwned;
use std::default::Default;
use std::error;
use std::fmt;
use std::thread;
use std::io;
use std::io::Write;
use log::{LogRecord, LogLevel};
use time;
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
enum TimeFmt {
Rfc3339,
Str(String),
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
enum Chunk {
Text(String),
Time(TimeFmt),
Level,
Message,
Module,
File,
Line,
Thread,
Target,
}
#[derive(Debug)]
pub struct Error(String);
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(&self.0)
}
}
impl error::Error for Error {
fn description(&self) -> &str {
"Error parsing a pattern"
}
}
#[derive(Debug)]
pub struct PatternLayout {
pattern: Vec<Chunk>,
}
impl Default for PatternLayout {
fn default() -> PatternLayout {
PatternLayout::new("%d %l %t - %m").unwrap()
}
}
impl PatternLayout {
pub fn new(pattern: &str) -> Result<PatternLayout, Error> {
let mut parsed = vec![];
let mut next_text = String::new();
let mut it = pattern.chars().peekable();
while let Some(ch) = it.next() {
if ch == '%' {
let chunk = match it.next() {
Some('%') => {
next_text.push('%');
None
}
Some('d') => {
let fmt = match it.peek() {
Some(&'{') => {
it.next();
let mut fmt = String::new();
loop {
match it.next() {
Some('}') => break,
Some(c) => fmt.push(c),
None => {
return Err(Error("Unterminated time format".to_owned()));
}
}
}
if let Err(err) = time::now().strftime(&*fmt) {
return Err(Error(err.to_string()));
}
TimeFmt::Str(fmt)
}
_ => TimeFmt::Rfc3339,
};
Some(Chunk::Time(fmt))
}
Some('l') => Some(Chunk::Level),
Some('m') => Some(Chunk::Message),
Some('M') => Some(Chunk::Module),
Some('f') => Some(Chunk::File),
Some('L') => Some(Chunk::Line),
Some('T') => Some(Chunk::Thread),
Some('t') => Some(Chunk::Target),
Some(ch) => return Err(Error(format!("Invalid formatter `%{}`", ch))),
None => return Err(Error("Unexpected end of pattern".to_owned())),
};
if let Some(chunk) = chunk {
if !next_text.is_empty() {
parsed.push(Chunk::Text(next_text));
next_text = String::new();
}
parsed.push(chunk);
}
} else {
next_text.push(ch);
}
}
if !next_text.is_empty() {
parsed.push(Chunk::Text(next_text));
}
Ok(PatternLayout {
pattern: parsed,
})
}
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())
}
fn append_inner<W>(&self,
w: &mut W,
level: LogLevel,
target: &str,
location: &Location,
args: &fmt::Arguments)
-> io::Result<()> where W: Write {
for chunk in self.pattern.iter() {
try!(match *chunk {
Chunk::Text(ref text) => write!(w, "{}", text),
Chunk::Time(TimeFmt::Str(ref fmt)) => {
time::now().strftime(&**fmt).map(|time| write!(w, "{}", time))
.unwrap_or(Ok(()))
}
Chunk::Time(TimeFmt::Rfc3339) => write!(w, "{}", time::now().rfc3339()),
Chunk::Level => write!(w, "{}", level),
Chunk::Message => write!(w, "{}", args),
Chunk::Module => write!(w, "{}", location.module_path),
Chunk::File => write!(w, "{}", location.file),
Chunk::Line => write!(w, "{}", location.line),
Chunk::Thread => {
write!(w, "{}", thread::current().name().unwrap_or("<unnamed>"))
}
Chunk::Target => write!(w, "{}", target),
});
}
writeln!(w, "")
}
}
struct Location<'a> {
module_path: &'a str,
file: &'a str,
line: u32,
}
#[cfg(test)]
mod tests {
use std::default::Default;
use std::thread;
use log::LogLevel;
use super::{Chunk, TimeFmt, PatternLayout, Location};
#[test]
fn test_parse() {
let expected = [Chunk::Text("hi".to_string()),
Chunk::Time(TimeFmt::Str("%Y-%m-%d".to_string())),
Chunk::Time(TimeFmt::Rfc3339),
Chunk::Level,
Chunk::Message,
Chunk::Module,
Chunk::File,
Chunk::Line,
Chunk::Thread,
Chunk::Target,
Chunk::Text("%".to_string())];
let actual = PatternLayout::new("hi%d{%Y-%m-%d}%d%l%m%M%f%L%T%t%%").unwrap().pattern;
assert_eq!(expected, actual)
}
#[test]
fn test_invalid_date_format() {
assert!(PatternLayout::new("%d{%q}").is_err());
}
#[test]
fn test_log() {
let pw = PatternLayout::new("%l %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,
LogLevel::Debug,
"target",
&LOCATION,
&format_args!("the message")).unwrap();
assert_eq!(&b"DEBUG the message at mod path in the file:132\n"[..], buf);
}
#[test]
fn test_unnamed_thread() {
thread::scoped(|| {
let pw = PatternLayout::new("%T").unwrap();
static LOCATION: Location<'static> = Location {
module_path: "path",
file: "file",
line: 132,
};
let mut buf = vec![];
pw.append_inner(&mut buf,
LogLevel::Debug,
"target",
&LOCATION,
&format_args!("message")).unwrap();
assert_eq!(b"<unnamed>\n", buf);
}).join();
}
#[test]
fn test_named_thread() {
thread::Builder::new().name("foobar".to_string()).scoped(|| {
let pw = PatternLayout::new("%T").unwrap();
static LOCATION: Location<'static> = Location {
module_path: "path",
file: "file",
line: 132,
};
let mut buf = vec![];
pw.append_inner(&mut buf,
LogLevel::Debug,
"target",
&LOCATION,
&format_args!("message")).unwrap();
assert_eq!(b"foobar\n", buf);
}).unwrap().join();
}
#[test]
fn test_default_okay() {
let _: PatternLayout = Default::default();
}
}