diff --git a/Cargo.toml b/Cargo.toml index d7e959ce..a08d8980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,6 @@ variable. categories = ["development-tools::debugging"] keywords = ["logging", "log", "logger"] -[maintenance] -status = "actively-developed" - [workspace] members = [ "ci" @@ -37,4 +34,4 @@ name = "log-in-log" harness = false [features] -default = ["termcolor", "atty", "humantime", "regex"] +default = ["termcolor", "atty", "humantime", "regex"] \ No newline at end of file diff --git a/README.md b/README.md index 332dee80..67c19aa1 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ environment variable that corresponds with the log messages you want to show. ```bash $ RUST_LOG=info ./main -INFO: 2017-11-09T02:12:24Z: main: starting up +[2018-11-03T06:09:06Z INFO default] starting up ``` ### In tests @@ -52,7 +52,7 @@ Tests can use the `env_logger` crate to see log messages generated during that t log = "0.4.0" [dev-dependencies] -env_logger = "0.5.13" +env_logger = { version = "0.5.13", default-features = false } ``` ```rust @@ -93,11 +93,11 @@ $ RUST_LOG=my_lib=info cargo test Running target/debug/my_lib-... running 2 tests -INFO: 2017-11-09T02:12:24Z: my_lib::tests: logging from another test -INFO: 2017-11-09T02:12:24Z: my_lib: add_one called with -8 +[2017-11-09T02:12:24Z INFO my_lib::tests] logging from another test +[2017-11-09T02:12:24Z INFO my_lib] add_one called with -8 test tests::it_handles_negative_numbers ... ok -INFO: 2017-11-09T02:12:24Z: my_lib::tests: can log from the test too -INFO: 2017-11-09T02:12:24Z: my_lib: add_one called with 2 +[2017-11-09T02:12:24Z INFO my_lib::tests] can log from the test too +[2017-11-09T02:12:24Z INFO my_lib] add_one called with 2 test tests::it_adds_one ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured @@ -115,8 +115,8 @@ $ RUST_LOG=my_lib=info cargo test it_adds_one Running target/debug/my_lib-... running 1 test -INFO: 2017-11-09T02:12:24Z: my_lib::tests: can log from the test too -INFO: 2017-11-09T02:12:24Z: my_lib: add_one called with 2 +[2017-11-09T02:12:24Z INFO my_lib::tests] can log from the test too +[2017-11-09T02:12:24Z INFO my_lib] add_one called with 2 test tests::it_adds_one ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured @@ -138,3 +138,9 @@ if env::var("RUST_LOG").is_ok() { } builder.init(); ``` + +## Stability of the default format + +The default format won't optimise for long-term stability, and explicitly makes no guarantees about the stability of its output across major, minor or patch version bumps during `0.x`. + +If you want to capture or interpret the output of `env_logger` programmatically then you should use a custom format. \ No newline at end of file diff --git a/src/fmt/humantime/extern_impl.rs b/src/fmt/humantime/extern_impl.rs index 53c47f63..596a2819 100644 --- a/src/fmt/humantime/extern_impl.rs +++ b/src/fmt/humantime/extern_impl.rs @@ -5,7 +5,7 @@ use humantime::{format_rfc3339_nanos, format_rfc3339_seconds}; use ::fmt::Formatter; -pub(in ::fmt) mod pub_use_in_super { +pub(in ::fmt) mod glob { pub use super::*; } diff --git a/src/fmt/humantime/shim_impl.rs b/src/fmt/humantime/shim_impl.rs index 3f5d4e39..0f753400 100644 --- a/src/fmt/humantime/shim_impl.rs +++ b/src/fmt/humantime/shim_impl.rs @@ -2,6 +2,6 @@ Timestamps aren't available when we don't have a `humantime` dependency. */ -pub(in ::fmt) mod pub_use_in_super { +pub(in ::fmt) mod glob { } diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 58687dc2..143a61b0 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -30,25 +30,23 @@ //! [`Write`]: https://doc.rust-lang.org/stable/std/io/trait.Write.html use std::io::prelude::*; -use std::{io, fmt}; +use std::{io, fmt, mem}; use std::rc::Rc; use std::cell::RefCell; +use std::fmt::Display; -mod termcolor; -mod atty; +use log::Record; + +pub(crate) mod writer; mod humantime; -use self::termcolor::{Buffer, BufferWriter}; -use self::atty::{is_stdout, is_stderr}; +pub use self::humantime::glob::*; +pub use self::writer::glob::*; -pub use self::humantime::pub_use_in_super::*; -pub use self::termcolor::pub_use_in_super::*; +use self::writer::{Writer, Buffer}; -pub(super) mod pub_use_in_super { +pub(crate) mod glob { pub use super::{Target, WriteStyle, Formatter}; - - #[cfg(feature = "termcolor")] - pub use super::Color; } /// A formatter to write logs into. @@ -76,180 +74,216 @@ pub struct Formatter { write_style: WriteStyle, } -/// Log target, either `stdout` or `stderr`. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Target { - /// Logs will be sent to standard output. - Stdout, - /// Logs will be sent to standard error. - Stderr, -} +impl Formatter { + pub(crate) fn new(writer: &Writer) -> Self { + Formatter { + buf: Rc::new(RefCell::new(writer.buffer())), + write_style: writer.write_style(), + } + } -impl Default for Target { - fn default() -> Self { - Target::Stderr + pub(crate) fn write_style(&self) -> WriteStyle { + self.write_style } -} -/// Whether or not to print styles to the target. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum WriteStyle { - /// Try to print styles, but don't force the issue. - Auto, - /// Try very hard to print styles. - Always, - /// Never print styles. - Never, -} + pub(crate) fn print(&self, writer: &Writer) -> io::Result<()> { + writer.print(&self.buf.borrow()) + } -impl Default for WriteStyle { - fn default() -> Self { - WriteStyle::Auto + pub(crate) fn clear(&mut self) { + self.buf.borrow_mut().clear() } } -/// A terminal target with color awareness. -pub(crate) struct Writer { - inner: BufferWriter, - write_style: WriteStyle, +impl Write for Formatter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buf.borrow_mut().write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.buf.borrow_mut().flush() + } } -impl Writer { - pub(crate) fn write_style(&self) -> WriteStyle { - self.write_style +impl fmt::Debug for Formatter { + fn fmt(&self, f: &mut fmt::Formatter)->fmt::Result { + f.debug_struct("Formatter").finish() } } -/// A builder for a terminal writer. -/// -/// The target and style choice can be configured before building. pub(crate) struct Builder { - target: Target, - write_style: WriteStyle, + pub default_format_timestamp: bool, + pub default_format_timestamp_nanos: bool, + pub default_format_module_path: bool, + pub default_format_level: bool, + pub custom_format: Option io::Result<()> + Sync + Send>>, + built: bool, } -impl Builder { - /// Initialize the writer builder with defaults. - pub fn new() -> Self { +impl Default for Builder { + fn default() -> Self { Builder { - target: Default::default(), - write_style: Default::default(), + default_format_timestamp: true, + default_format_timestamp_nanos: false, + default_format_module_path: true, + default_format_level: true, + custom_format: None, + built: false, } } +} - /// Set the target to write to. - pub fn target(&mut self, target: Target) -> &mut Self { - self.target = target; - self - } - - /// Parses a style choice string. - /// - /// See the [Disabling colors] section for more details. - /// - /// [Disabling colors]: ../index.html#disabling-colors - pub fn parse(&mut self, write_style: &str) -> &mut Self { - self.write_style(parse_write_style(write_style)) - } - - /// Whether or not to print style characters when writing. - pub fn write_style(&mut self, write_style: WriteStyle) -> &mut Self { - self.write_style = write_style; - self - } - - /// Build a terminal writer. - pub fn build(&mut self) -> Writer { - let color_choice = match self.write_style { - WriteStyle::Auto => { - if match self.target { - Target::Stderr => is_stderr(), - Target::Stdout => is_stdout(), - } { - WriteStyle::Auto - } else { - WriteStyle::Never - } - }, - color_choice => color_choice, - }; - - let writer = match self.target { - Target::Stderr => BufferWriter::stderr(color_choice), - Target::Stdout => BufferWriter::stdout(color_choice), - }; - - Writer { - inner: writer, - write_style: self.write_style, +impl Builder { + /// Convert the format into a callable function. + /// + /// If the `custom_format` is `Some`, then any `default_format` switches are ignored. + /// If the `custom_format` is `None`, then a default format is returned. + /// Any `default_format` switches set to `false` won't be written by the format. + pub fn build(&mut self) -> Box io::Result<()> + Sync + Send> { + assert!(!self.built, "attempt to re-use consumed builder"); + + let built = mem::replace(self, Builder { + built: true, + ..Default::default() + }); + + if let Some(fmt) = built.custom_format { + fmt + } + else { + Box::new(move |buf, record| { + let fmt = DefaultFormat { + timestamp: built.default_format_timestamp, + timestamp_nanos: built.default_format_timestamp_nanos, + module_path: built.default_format_module_path, + level: built.default_format_level, + written_header_value: false, + buf, + }; + + fmt.write(record) + }) } } } -impl Default for Builder { - fn default() -> Self { - Builder::new() - } +#[cfg(feature = "termcolor")] +type SubtleStyle = StyledValue<'static, &'static str>; +#[cfg(not(feature = "termcolor"))] +type SubtleStyle = &'static str; + +struct DefaultFormat<'a> { + timestamp: bool, + module_path: bool, + level: bool, + timestamp_nanos: bool, + written_header_value: bool, + buf: &'a mut Formatter, } -impl Formatter { - pub(crate) fn new(writer: &Writer) -> Self { - Formatter { - buf: Rc::new(RefCell::new(writer.inner.buffer())), - write_style: writer.write_style(), - } - } +impl<'a> DefaultFormat<'a> { + fn write(mut self, record: &Record) -> io::Result<()> { + self.write_timestamp()?; + self.write_level(record)?; + self.write_module_path(record)?; + self.finish_header()?; - pub(crate) fn write_style(&self) -> WriteStyle { - self.write_style + self.write_args(record) } - pub(crate) fn print(&self, writer: &Writer) -> io::Result<()> { - writer.inner.print(&self.buf.borrow()) + fn subtle_style(&self, text: &'static str) -> SubtleStyle { + #[cfg(feature = "termcolor")] + { + self.buf.style() + .set_color(Color::Black) + .set_intense(true) + .into_value(text) + } + #[cfg(not(feature = "termcolor"))] + { + text + } } - pub(crate) fn clear(&mut self) { - self.buf.borrow_mut().clear() + fn write_header_value(&mut self, value: T) -> io::Result<()> + where + T: Display, + { + if !self.written_header_value { + self.written_header_value = true; + + let open_brace = self.subtle_style("["); + write!(self.buf, "{}{}", open_brace, value) + } else { + write!(self.buf, " {}", value) + } } -} -impl Write for Formatter { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.buf.borrow_mut().write(buf) - } + fn write_level(&mut self, record: &Record) -> io::Result<()> { + if !self.level { + return Ok(()) + } - fn flush(&mut self) -> io::Result<()> { - self.buf.borrow_mut().flush() + let level = { + #[cfg(feature = "termcolor")] + { + self.buf.default_styled_level(record.level()) + } + #[cfg(not(feature = "termcolor"))] + { + record.level() + } + }; + + self.write_header_value(format_args!("{:<5}", level)) } -} -impl fmt::Debug for Writer { - fn fmt(&self, f: &mut fmt::Formatter)->fmt::Result { - f.debug_struct("Writer").finish() + fn write_timestamp(&mut self) -> io::Result<()> { + #[cfg(feature = "humantime")] + { + if !self.timestamp { + return Ok(()) + } + + if self.timestamp_nanos { + let ts_nanos = self.buf.precise_timestamp(); + self.write_header_value(ts_nanos) + } else { + let ts = self.buf.timestamp(); + self.write_header_value(ts) + } + } + #[cfg(not(feature = "humantime"))] + { + let _ = self.timestamp; + let _ = self.timestamp_nanos; + Ok(()) + } } -} -impl fmt::Debug for Formatter { - fn fmt(&self, f: &mut fmt::Formatter)->fmt::Result { - f.debug_struct("Formatter").finish() + fn write_module_path(&mut self, record: &Record) -> io::Result<()> { + if !self.module_path { + return Ok(()) + } + + if let Some(module_path) = record.module_path() { + self.write_header_value(module_path) + } else { + Ok(()) + } } -} -impl fmt::Debug for Builder { - fn fmt(&self, f: &mut fmt::Formatter)->fmt::Result { - f.debug_struct("Logger") - .field("target", &self.target) - .field("write_style", &self.write_style) - .finish() + fn finish_header(&mut self) -> io::Result<()> { + if self.written_header_value { + let close_brace = self.subtle_style("]"); + write!(self.buf, "{} ", close_brace) + } else { + Ok(()) + } } -} -fn parse_write_style(spec: &str) -> WriteStyle { - match spec { - "auto" => WriteStyle::Auto, - "always" => WriteStyle::Always, - "never" => WriteStyle::Never, - _ => Default::default(), + fn write_args(&mut self, record: &Record) -> io::Result<()> { + writeln!(self.buf, "{}", record.args()) } } @@ -257,30 +291,62 @@ fn parse_write_style(spec: &str) -> WriteStyle { mod tests { use super::*; + use log::{Level, Record}; + + fn write(fmt: DefaultFormat) -> String { + let buf = fmt.buf.buf.clone(); + + let record = Record::builder() + .args(format_args!("log message")) + .level(Level::Info) + .file(Some("test.rs")) + .line(Some(144)) + .module_path(Some("test::path")) + .build(); + + fmt.write(&record).expect("failed to write record"); + + let buf = buf.borrow(); + String::from_utf8(buf.bytes().to_vec()).expect("failed to read record") + } + #[test] - fn parse_write_style_valid() { - let inputs = vec![ - ("auto", WriteStyle::Auto), - ("always", WriteStyle::Always), - ("never", WriteStyle::Never), - ]; - - for (input, expected) in inputs { - assert_eq!(expected, parse_write_style(input)); - } + fn default_format_with_header() { + let writer = writer::Builder::new() + .write_style(WriteStyle::Never) + .build(); + + let mut f = Formatter::new(&writer); + + let written = write(DefaultFormat { + timestamp: false, + timestamp_nanos: false, + module_path: true, + level: true, + written_header_value: false, + buf: &mut f, + }); + + assert_eq!("[INFO test::path] log message\n", written); } #[test] - fn parse_write_style_invalid() { - let inputs = vec![ - "", - "true", - "false", - "NEVER!!" - ]; - - for input in inputs { - assert_eq!(WriteStyle::Auto, parse_write_style(input)); - } + fn default_format_no_header() { + let writer = writer::Builder::new() + .write_style(WriteStyle::Never) + .build(); + + let mut f = Formatter::new(&writer); + + let written = write(DefaultFormat { + timestamp: false, + timestamp_nanos: false, + module_path: false, + level: false, + written_header_value: false, + buf: &mut f, + }); + + assert_eq!("log message\n", written); } } \ No newline at end of file diff --git a/src/fmt/termcolor/shim_impl.rs b/src/fmt/termcolor/shim_impl.rs deleted file mode 100644 index 2ab3f7ca..00000000 --- a/src/fmt/termcolor/shim_impl.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::io::{self, Write}; - -use fmt::{WriteStyle, Target}; - -pub(in ::fmt) mod pub_use_in_super { - -} - -pub(in ::fmt) struct BufferWriter { - target: Target, -} - -pub(in ::fmt) struct Buffer(Vec); - -impl BufferWriter { - pub(in ::fmt) fn stderr(_: WriteStyle) -> Self { - BufferWriter { - target: Target::Stderr, - } - } - - pub(in ::fmt) fn stdout(_: WriteStyle) -> Self { - BufferWriter { - target: Target::Stdout, - } - } - - pub(in ::fmt) fn buffer(&self) -> Buffer { - Buffer(Vec::new()) - } - - pub(in ::fmt) fn print(&self, buf: &Buffer) -> io::Result<()> { - match self.target { - Target::Stderr => { - let stderr = io::stderr(); - let mut stderr = stderr.lock(); - stderr.write_all(&buf.0) - }, - Target::Stdout => { - let stdout = io::stdout(); - let mut stdout = stdout.lock(); - stdout.write_all(&buf.0) - }, - } - } -} - -impl Buffer { - pub(in ::fmt) fn clear(&mut self) { - self.0.clear(); - } - - pub(in ::fmt) fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend(buf); - Ok(buf.len()) - } - - pub(in ::fmt) fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} \ No newline at end of file diff --git a/src/fmt/atty.rs b/src/fmt/writer/atty.rs similarity index 100% rename from src/fmt/atty.rs rename to src/fmt/writer/atty.rs diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs new file mode 100644 index 00000000..d628187e --- /dev/null +++ b/src/fmt/writer/mod.rs @@ -0,0 +1,198 @@ +mod termcolor; +mod atty; + +use std::{fmt, io}; +use self::termcolor::BufferWriter; +use self::atty::{is_stdout, is_stderr}; + +pub(in ::fmt) mod glob { + pub use super::termcolor::glob::*; + pub use super::*; +} + +pub(in ::fmt) use self::termcolor::Buffer; + +/// Log target, either `stdout` or `stderr`. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Target { + /// Logs will be sent to standard output. + Stdout, + /// Logs will be sent to standard error. + Stderr, +} + +impl Default for Target { + fn default() -> Self { + Target::Stderr + } +} + +/// Whether or not to print styles to the target. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum WriteStyle { + /// Try to print styles, but don't force the issue. + Auto, + /// Try very hard to print styles. + Always, + /// Never print styles. + Never, +} + +impl Default for WriteStyle { + fn default() -> Self { + WriteStyle::Auto + } +} + +/// A terminal target with color awareness. +pub(crate) struct Writer { + inner: BufferWriter, + write_style: WriteStyle, +} + +impl Writer { + pub fn write_style(&self) -> WriteStyle { + self.write_style + } + + pub(in ::fmt) fn buffer(&self) -> Buffer { + self.inner.buffer() + } + + pub(in ::fmt) fn print(&self, buf: &Buffer) -> io::Result<()> { + self.inner.print(buf) + } +} + +/// A builder for a terminal writer. +/// +/// The target and style choice can be configured before building. +pub(crate) struct Builder { + target: Target, + write_style: WriteStyle, + built: bool, +} + +impl Builder { + /// Initialize the writer builder with defaults. + pub fn new() -> Self { + Builder { + target: Default::default(), + write_style: Default::default(), + built: false, + } + } + + /// Set the target to write to. + pub fn target(&mut self, target: Target) -> &mut Self { + self.target = target; + self + } + + /// Parses a style choice string. + /// + /// See the [Disabling colors] section for more details. + /// + /// [Disabling colors]: ../index.html#disabling-colors + pub fn parse(&mut self, write_style: &str) -> &mut Self { + self.write_style(parse_write_style(write_style)) + } + + /// Whether or not to print style characters when writing. + pub fn write_style(&mut self, write_style: WriteStyle) -> &mut Self { + self.write_style = write_style; + self + } + + /// Build a terminal writer. + pub fn build(&mut self) -> Writer { + assert!(!self.built, "attempt to re-use consumed builder"); + self.built = true; + + let color_choice = match self.write_style { + WriteStyle::Auto => { + if match self.target { + Target::Stderr => is_stderr(), + Target::Stdout => is_stdout(), + } { + WriteStyle::Auto + } else { + WriteStyle::Never + } + }, + color_choice => color_choice, + }; + + let writer = match self.target { + Target::Stderr => BufferWriter::stderr(color_choice), + Target::Stdout => BufferWriter::stdout(color_choice), + }; + + Writer { + inner: writer, + write_style: self.write_style, + } + } +} + +impl Default for Builder { + fn default() -> Self { + Builder::new() + } +} + +impl fmt::Debug for Builder { + fn fmt(&self, f: &mut fmt::Formatter)->fmt::Result { + f.debug_struct("Logger") + .field("target", &self.target) + .field("write_style", &self.write_style) + .finish() + } +} + +impl fmt::Debug for Writer { + fn fmt(&self, f: &mut fmt::Formatter)->fmt::Result { + f.debug_struct("Writer").finish() + } +} + +fn parse_write_style(spec: &str) -> WriteStyle { + match spec { + "auto" => WriteStyle::Auto, + "always" => WriteStyle::Always, + "never" => WriteStyle::Never, + _ => Default::default(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_write_style_valid() { + let inputs = vec![ + ("auto", WriteStyle::Auto), + ("always", WriteStyle::Always), + ("never", WriteStyle::Never), + ]; + + for (input, expected) in inputs { + assert_eq!(expected, parse_write_style(input)); + } + } + + #[test] + fn parse_write_style_invalid() { + let inputs = vec![ + "", + "true", + "false", + "NEVER!!" + ]; + + for input in inputs { + assert_eq!(WriteStyle::Auto, parse_write_style(input)); + } + } +} diff --git a/src/fmt/termcolor/extern_impl.rs b/src/fmt/writer/termcolor/extern_impl.rs similarity index 70% rename from src/fmt/termcolor/extern_impl.rs rename to src/fmt/writer/termcolor/extern_impl.rs index be0c1816..1409cd23 100644 --- a/src/fmt/termcolor/extern_impl.rs +++ b/src/fmt/writer/termcolor/extern_impl.rs @@ -1,8 +1,6 @@ -use std::error::Error; use std::borrow::Cow; use std::fmt; use std::io::{self, Write}; -use std::str::FromStr; use std::cell::RefCell; use std::rc::Rc; @@ -12,7 +10,7 @@ use termcolor::{self, ColorChoice, ColorSpec, WriteColor}; use ::WriteStyle; use ::fmt::Formatter; -pub(in ::fmt) mod pub_use_in_super { +pub(in ::fmt::writer) mod glob { pub use super::*; } @@ -55,7 +53,7 @@ impl Formatter { let mut level_style = self.style(); match level { Level::Trace => level_style.set_color(Color::White), - Level::Debug => level_style.set_color(Color::Blue), + Level::Debug => level_style.set_color(Color::Black).set_intense(true), Level::Info => level_style.set_color(Color::Green), Level::Warn => level_style.set_color(Color::Yellow), Level::Error => level_style.set_color(Color::Red).set_bold(true), @@ -71,23 +69,23 @@ impl Formatter { } } -pub(in ::fmt) struct BufferWriter(termcolor::BufferWriter); +pub(in ::fmt::writer) struct BufferWriter(termcolor::BufferWriter); pub(in ::fmt) struct Buffer(termcolor::Buffer); impl BufferWriter { - pub(in ::fmt) fn stderr(write_style: WriteStyle) -> Self { + pub(in ::fmt::writer) fn stderr(write_style: WriteStyle) -> Self { BufferWriter(termcolor::BufferWriter::stderr(write_style.into_color_choice())) } - pub(in ::fmt) fn stdout(write_style: WriteStyle) -> Self { + pub(in ::fmt::writer) fn stdout(write_style: WriteStyle) -> Self { BufferWriter(termcolor::BufferWriter::stdout(write_style.into_color_choice())) } - pub(in ::fmt) fn buffer(&self) -> Buffer { + pub(in ::fmt::writer) fn buffer(&self) -> Buffer { Buffer(self.0.buffer()) } - pub(in ::fmt) fn print(&self, buf: &Buffer) -> io::Result<()> { + pub(in ::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { self.0.print(&buf.0) } } @@ -105,6 +103,11 @@ impl Buffer { self.0.flush() } + #[cfg(test)] + pub(in ::fmt) fn bytes(&self) -> &[u8] { + self.0.as_slice() + } + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { self.0.set_color(spec) } @@ -323,14 +326,15 @@ impl Style { /// ``` pub fn value(&self, value: T) -> StyledValue { StyledValue { - style: Cow::Borrowed(&self), + style: Cow::Borrowed(self), value } } - fn into_value(self, value: T) -> StyledValue<'static, T> { + /// Wrap a value in the style by taking ownership of it. + pub fn into_value(&mut self, value: T) -> StyledValue<'static, T> { StyledValue { - style: Cow::Owned(self), + style: Cow::Owned(self.clone()), value } } @@ -420,61 +424,6 @@ pub enum Color { __Nonexhaustive, } -/// An error from parsing an invalid color specification. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ParseColorError(ParseColorErrorKind); - -#[derive(Clone, Debug, Eq, PartialEq)] -enum ParseColorErrorKind { - /// An error originating from `termcolor`. - TermColor(termcolor::ParseColorError), - /// An error converting the `termcolor` color to a `env_logger::Color`. - /// - /// This variant should only get reached if a user uses a new spec that's - /// valid for `termcolor`, but not recognised in `env_logger` yet. - Unrecognized { - given: String, - } -} - -impl ParseColorError { - fn termcolor(err: termcolor::ParseColorError) -> Self { - ParseColorError(ParseColorErrorKind::TermColor(err)) - } - - fn unrecognized(given: String) -> Self { - ParseColorError(ParseColorErrorKind::Unrecognized { given }) - } - - /// Return the string that couldn't be parsed as a valid color. - pub fn invalid(&self) -> &str { - match self.0 { - ParseColorErrorKind::TermColor(ref err) => err.invalid(), - ParseColorErrorKind::Unrecognized { ref given, .. } => given, - } - } -} - -impl Error for ParseColorError { - fn description(&self) -> &str { - match self.0 { - ParseColorErrorKind::TermColor(ref err) => err.description(), - ParseColorErrorKind::Unrecognized { .. } => "unrecognized color value", - } - } -} - -impl fmt::Display for ParseColorError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.0 { - ParseColorErrorKind::TermColor(ref err) => fmt::Display::fmt(err, f), - ParseColorErrorKind::Unrecognized { ref given, .. } => { - write!(f, "unrecognized color value '{}'", given) - } - } - } -} - impl Color { fn into_termcolor(self) -> Option { match self { @@ -491,94 +440,4 @@ impl Color { _ => None, } } - - fn from_termcolor(color: termcolor::Color) -> Option { - match color { - termcolor::Color::Black => Some(Color::Black), - termcolor::Color::Blue => Some(Color::Blue), - termcolor::Color::Green => Some(Color::Green), - termcolor::Color::Red => Some(Color::Red), - termcolor::Color::Cyan => Some(Color::Cyan), - termcolor::Color::Magenta => Some(Color::Magenta), - termcolor::Color::Yellow => Some(Color::Yellow), - termcolor::Color::White => Some(Color::White), - termcolor::Color::Ansi256(value) => Some(Color::Ansi256(value)), - termcolor::Color::Rgb(r, g, b) => Some(Color::Rgb(r, g, b)), - _ => None, - } - } } - -impl FromStr for Color { - type Err = ParseColorError; - - fn from_str(s: &str) -> Result { - let tc = termcolor::Color::from_str(s).map_err(ParseColorError::termcolor)?; - Color::from_termcolor(tc).ok_or_else(|| ParseColorError::unrecognized(s.into())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_color_name_valid() { - let inputs = vec![ - "black", - "blue", - "green", - "red", - "cyan", - "magenta", - "yellow", - "white", - ]; - - for input in inputs { - assert!(Color::from_str(input).is_ok()); - } - } - - #[test] - fn parse_color_ansi_valid() { - let inputs = vec![ - "7", - "32", - "0xFF", - ]; - - for input in inputs { - assert!(Color::from_str(input).is_ok()); - } - } - - #[test] - fn parse_color_rgb_valid() { - let inputs = vec![ - "0,0,0", - "0,128,255", - "0x0,0x0,0x0", - "0x33,0x66,0xFF", - ]; - - for input in inputs { - assert!(Color::from_str(input).is_ok()); - } - } - - #[test] - fn parse_color_invalid() { - let inputs = vec![ - "not_a_color", - "256", - "0,0", - "0,0,256", - ]; - - for input in inputs { - let err = Color::from_str(input).unwrap_err(); - assert_eq!(input, err.invalid()); - } - } -} \ No newline at end of file diff --git a/src/fmt/termcolor/mod.rs b/src/fmt/writer/termcolor/mod.rs similarity index 100% rename from src/fmt/termcolor/mod.rs rename to src/fmt/writer/termcolor/mod.rs diff --git a/src/fmt/writer/termcolor/shim_impl.rs b/src/fmt/writer/termcolor/shim_impl.rs new file mode 100644 index 00000000..b785dec0 --- /dev/null +++ b/src/fmt/writer/termcolor/shim_impl.rs @@ -0,0 +1,65 @@ +use std::io; + +use fmt::{WriteStyle, Target}; + +pub(in ::fmt::writer) mod glob { + +} + +pub(in ::fmt::writer) struct BufferWriter { + target: Target, +} + +pub(in ::fmt) struct Buffer(Vec); + +impl BufferWriter { + pub(in ::fmt::writer) fn stderr(_: WriteStyle) -> Self { + BufferWriter { + target: Target::Stderr, + } + } + + pub(in ::fmt::writer) fn stdout(_: WriteStyle) -> Self { + BufferWriter { + target: Target::Stdout, + } + } + + pub(in ::fmt::writer) fn buffer(&self) -> Buffer { + Buffer(Vec::new()) + } + + pub(in ::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { + // This impl uses the `eprint` and `print` macros + // instead of using the streams directly. + // This is so their output can be captured by `cargo test` + let log = String::from_utf8_lossy(&buf.0); + + match self.target { + Target::Stderr => eprint!("{}", log), + Target::Stdout => print!("{}", log), + } + + Ok(()) + } +} + +impl Buffer { + pub(in ::fmt) fn clear(&mut self) { + self.0.clear(); + } + + pub(in ::fmt) fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.extend(buf); + Ok(buf.len()) + } + + pub(in ::fmt) fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + + #[cfg(test)] + pub(in ::fmt) fn bytes(&self) -> &[u8] { + &self.0 + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 90d27df6..89acf0da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,37 +37,37 @@ //! //! ```{.bash} //! $ RUST_LOG=error ./main -//! ERROR: 2017-11-09T02:12:24Z: main: this is printed by default +//! [2017-11-09T02:12:24Z ERROR main] this is printed by default //! ``` //! //! ```{.bash} //! $ RUST_LOG=info ./main -//! ERROR: 2017-11-09T02:12:24Z: main: this is printed by default -//! INFO: 2017-11-09T02:12:24Z: main: the answer was: 12 +//! [2017-11-09T02:12:24Z ERROR main] this is printed by default +//! [2017-11-09T02:12:24Z INFO main] the answer was: 12 //! ``` //! //! ```{.bash} //! $ RUST_LOG=debug ./main -//! DEBUG: 2017-11-09T02:12:24Z: main: this is a debug message -//! ERROR: 2017-11-09T02:12:24Z: main: this is printed by default -//! INFO: 2017-11-09T02:12:24Z: main: the answer was: 12 +//! [2017-11-09T02:12:24Z DEBUG main] this is a debug message +//! [2017-11-09T02:12:24Z ERROR main] this is printed by default +//! [2017-11-09T02:12:24Z INFO main] the answer was: 12 //! ``` //! //! You can also set the log level on a per module basis: //! //! ```{.bash} //! $ RUST_LOG=main=info ./main -//! ERROR: 2017-11-09T02:12:24Z: main: this is printed by default -//! INFO: 2017-11-09T02:12:24Z: main: the answer was: 12 +//! [2017-11-09T02:12:24Z ERROR main] this is printed by default +//! [2017-11-09T02:12:24Z INFO main] the answer was: 12 //! ``` //! //! And enable all logging: //! //! ```{.bash} //! $ RUST_LOG=main ./main -//! DEBUG: 2017-11-09T02:12:24Z: main: this is a debug message -//! ERROR: 2017-11-09T02:12:24Z: main: this is printed by default -//! INFO: 2017-11-09T02:12:24Z: main: the answer was: 12 +//! [2017-11-09T02:12:24Z DEBUG main] this is a debug message +//! [2017-11-09T02:12:24Z ERROR main] this is printed by default +//! [2017-11-09T02:12:24Z INFO main] the answer was: 12 //! ``` //! //! If the binary name contains hyphens, you will need to replace @@ -75,9 +75,9 @@ //! //! ```{.bash} //! $ RUST_LOG=my_app ./my-app -//! DEBUG: 2017-11-09T02:12:24Z: my_app: this is a debug message -//! ERROR: 2017-11-09T02:12:24Z: my_app: this is printed by default -//! INFO: 2017-11-09T02:12:24Z: my_app: the answer was: 12 +//! [2017-11-09T02:12:24Z DEBUG my_app] this is a debug message +//! [2017-11-09T02:12:24Z ERROR my_app] this is printed by default +//! [2017-11-09T02:12:24Z INFO my_app] the answer was: 12 //! ``` //! //! This is because Rust modules and crates cannot contain hyphens @@ -177,6 +177,15 @@ //! } //! ``` //! +//! ### Stability of the default format +//! +//! The default format won't optimise for long-term stability, and explicitly makes no +//! guarantees about the stability of its output across major, minor or patch version +//! bumps during `0.x`. +//! +//! If you want to capture or interpret the output of `env_logger` programmatically +//! then you should use a custom format. +//! //! ## Specifying defaults for environment variables //! //! `env_logger` can read configuration from environment variables. @@ -223,11 +232,8 @@ extern crate humantime; #[cfg(feature = "atty")] extern crate atty; -use std::env; +use std::{env, io}; use std::borrow::Cow; -use std::io::prelude::*; -use std::io; -use std::mem; use std::cell::RefCell; use log::{Log, LevelFilter, Record, SetLoggerError, Metadata}; @@ -235,7 +241,10 @@ use log::{Log, LevelFilter, Record, SetLoggerError, Metadata}; pub mod filter; pub mod fmt; -pub use self::fmt::pub_use_in_super::*; +pub use self::fmt::glob::*; + +use self::filter::Filter; +use self::fmt::writer::{self, Writer}; /// The default name for the environment variable to read filters from. pub const DEFAULT_FILTER_ENV: &'static str = "RUST_LOG"; @@ -285,97 +294,11 @@ struct Var<'a> { /// [`Builder::try_init()`]: struct.Builder.html#method.try_init /// [`Builder`]: struct.Builder.html pub struct Logger { - writer: fmt::Writer, - filter: filter::Filter, + writer: Writer, + filter: Filter, format: Box io::Result<()> + Sync + Send>, } -struct Format { - default_format_timestamp: bool, - default_format_module_path: bool, - default_format_level: bool, - default_format_timestamp_nanos: bool, - custom_format: Option io::Result<()> + Sync + Send>>, -} - -impl Default for Format { - fn default() -> Self { - Format { - default_format_timestamp: true, - default_format_module_path: true, - default_format_level: true, - default_format_timestamp_nanos: false, - custom_format: None, - } - } -} - -impl Format { - /// Convert the format into a callable function. - /// - /// If the `custom_format` is `Some`, then any `default_format` switches are ignored. - /// If the `custom_format` is `None`, then a default format is returned. - /// Any `default_format` switches set to `false` won't be written by the format. - fn into_boxed_fn(self) -> Box io::Result<()> + Sync + Send> { - if let Some(fmt) = self.custom_format { - fmt - } - else { - Box::new(move |buf, record| { - let write_level = if self.default_format_level { - let level = { - #[cfg(feature = "termcolor")] - { - buf.default_styled_level(record.level()) - } - #[cfg(not(feature = "termcolor"))] - { - record.level() - } - }; - - write!(buf, "{:>5} ", level) - } else { - Ok(()) - }; - - let write_ts = { - #[cfg(feature = "humantime")] - { - if self.default_format_timestamp { - if self.default_format_timestamp_nanos { - let ts_nanos = buf.precise_timestamp(); - write!(buf, "{}: ", ts_nanos) - } else { - let ts = buf.timestamp(); - write!(buf, "{}: ", ts) - } - } else { - Ok(()) - } - } - - #[cfg(not(feature = "humantime"))] - { - Ok(()) - } - }; - - let default_format_module_path = (self.default_format_module_path, record.module_path()); - let write_module_path = if let (true, Some(module_path)) = default_format_module_path { - write!(buf, "{}: ", module_path) - } else { - Ok(()) - }; - - let write_args = writeln!(buf, "{}", record.args()); - - write_level.and(write_ts).and(write_module_path).and(write_args) - }) - } - } -} - /// `Builder` acts as builder for initializing a `Logger`. /// /// It can be used to customize the log format, change the environment variable used @@ -407,8 +330,9 @@ impl Format { #[derive(Default)] pub struct Builder { filter: filter::Builder, - writer: fmt::Builder, - format: Format, + writer: writer::Builder, + format: fmt::Builder, + built: bool, } impl Builder { @@ -745,10 +669,13 @@ impl Builder { /// The returned logger implements the `Log` trait and can be installed manually /// or nested within another logger. pub fn build(&mut self) -> Logger { + assert!(!self.built, "attempt to re-use consumed builder"); + self.built = true; + Logger { writer: self.writer.build(), filter: self.filter.build(), - format: mem::replace(&mut self.format, Default::default()).into_boxed_fn(), + format: self.format.build(), } } }