diff --git a/ci/src/main.rs b/ci/src/main.rs index d3619ca1..e7280f1c 100644 --- a/ci/src/main.rs +++ b/ci/src/main.rs @@ -29,7 +29,7 @@ fn main() { }) .collect::>(); - if failed.len() > 0 { + if !failed.is_empty() { for failed in failed { eprintln!("FAIL: {:?}", failed); } diff --git a/ci/src/permute.rs b/ci/src/permute.rs index 059d0864..badff5b6 100644 --- a/ci/src/permute.rs +++ b/ci/src/permute.rs @@ -6,7 +6,7 @@ where { let mut permutations = BTreeSet::new(); - if input.len() == 0 { + if input.is_empty() { return permutations; } diff --git a/ci/src/task.rs b/ci/src/task.rs index 2b6fab91..85f7ce63 100644 --- a/ci/src/task.rs +++ b/ci/src/task.rs @@ -21,12 +21,12 @@ impl Default for TestArgs { impl TestArgs { fn features_string(&self) -> Option { - if self.features.len() == 0 { + if self.features.is_empty() { return None; } let s = self.features.iter().fold(String::new(), |mut s, f| { - if s.len() > 0 { + if !s.is_empty() { s.push_str(" "); } s.push_str(f); diff --git a/examples/custom_target.rs b/examples/custom_target.rs new file mode 100644 index 00000000..384d244a --- /dev/null +++ b/examples/custom_target.rs @@ -0,0 +1,81 @@ +/*! +Using `env_logger`. + +Before running this example, try setting the `MY_LOG_LEVEL` environment variable to `info`: + +```no_run,shell +$ export MY_LOG_LEVEL='info' +``` + +Also try setting the `MY_LOG_STYLE` environment variable to `never` to disable colors +or `auto` to enable them: + +```no_run,shell +$ export MY_LOG_STYLE=never +``` +*/ + +#[macro_use] +extern crate log; + +use env_logger::{Builder, Env, Target}; +use std::{ + io, + sync::mpsc::{channel, Sender}, +}; + +// This struct is used as an adaptor, it implements io::Write and forwards the buffer to a mpsc::Sender +struct WriteAdapter { + sender: Sender, +} + +impl io::Write for WriteAdapter { + // On write we forward each u8 of the buffer to the sender and return the length of the buffer + fn write(&mut self, buf: &[u8]) -> io::Result { + for chr in buf { + self.sender.send(*chr).unwrap(); + } + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +fn main() { + // The `Env` lets us tweak what the environment + // variables to read are and what the default + // value is if they're missing + let env = Env::default() + .filter_or("MY_LOG_LEVEL", "trace") + // Normally using a pipe as a target would mean a value of false, but this forces it to be true. + .write_style_or("MY_LOG_STYLE", "always"); + + // Create the channel for the log messages + let (rx, tx) = channel(); + + Builder::from_env(env) + // The Sender of the channel is given to the logger + // A wrapper is needed, because the `Sender` itself doesn't implement `std::io::Write`. + .target(Target::Pipe(Box::new(WriteAdapter { sender: rx }))) + .init(); + + trace!("some trace log"); + debug!("some debug log"); + info!("some information log"); + warn!("some warning log"); + error!("some error log"); + + // Collect all messages send to the channel and parse the result as a string + String::from_utf8(tx.try_iter().collect::>()) + .unwrap() + // Split the result into lines so a prefix can be added to each line + .split('\n') + .for_each(|msg| { + // Print the message with a prefix if it has any content + if !msg.is_empty() { + println!("from pipe: {}", msg) + } + }); +} diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index 6ee63a39..5bb53539 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -3,22 +3,24 @@ mod termcolor; use self::atty::{is_stderr, is_stdout}; use self::termcolor::BufferWriter; -use std::{fmt, io}; +use std::{fmt, io, mem, sync::Mutex}; -pub(in crate::fmt) mod glob { +pub(super) mod glob { pub use super::termcolor::glob::*; pub use super::*; } -pub(in crate::fmt) use self::termcolor::Buffer; +pub(super) use self::termcolor::Buffer; -/// Log target, either `stdout` or `stderr`. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +/// Log target, either `stdout`, `stderr` or a custom pipe. +#[non_exhaustive] pub enum Target { /// Logs will be sent to standard output. Stdout, /// Logs will be sent to standard error. Stderr, + /// Logs will be sent to a custom pipe. + Pipe(Box), } impl Default for Target { @@ -27,6 +29,61 @@ impl Default for Target { } } +impl fmt::Debug for Target { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Stdout => "stdout", + Self::Stderr => "stderr", + Self::Pipe(_) => "pipe", + } + ) + } +} + +/// Log target, either `stdout`, `stderr` or a custom pipe. +/// +/// Same as `Target`, except the pipe is wrapped in a mutex for interior mutability. +pub(super) enum WritableTarget { + /// Logs will be sent to standard output. + Stdout, + /// Logs will be sent to standard error. + Stderr, + /// Logs will be sent to a custom pipe. + Pipe(Box>), +} + +impl From for WritableTarget { + fn from(target: Target) -> Self { + match target { + Target::Stdout => Self::Stdout, + Target::Stderr => Self::Stderr, + Target::Pipe(pipe) => Self::Pipe(Box::new(Mutex::new(pipe))), + } + } +} + +impl Default for WritableTarget { + fn default() -> Self { + Self::from(Target::default()) + } +} + +impl fmt::Debug for WritableTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Stdout => "stdout", + Self::Stderr => "stderr", + Self::Pipe(_) => "pipe", + } + ) + } +} /// Whether or not to print styles to the target. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum WriteStyle { @@ -55,11 +112,11 @@ impl Writer { self.write_style } - pub(in crate::fmt) fn buffer(&self) -> Buffer { + pub(super) fn buffer(&self) -> Buffer { self.inner.buffer() } - pub(in crate::fmt) fn print(&self, buf: &Buffer) -> io::Result<()> { + pub(super) fn print(&self, buf: &Buffer) -> io::Result<()> { self.inner.print(buf) } } @@ -67,8 +124,9 @@ impl Writer { /// A builder for a terminal writer. /// /// The target and style choice can be configured before building. +#[derive(Debug)] pub(crate) struct Builder { - target: Target, + target: WritableTarget, write_style: WriteStyle, is_test: bool, built: bool, @@ -87,7 +145,7 @@ impl Builder { /// Set the target to write to. pub(crate) fn target(&mut self, target: Target) -> &mut Self { - self.target = target; + self.target = target.into(); self } @@ -119,9 +177,10 @@ impl Builder { let color_choice = match self.write_style { WriteStyle::Auto => { - if match self.target { - Target::Stderr => is_stderr(), - Target::Stdout => is_stdout(), + if match &self.target { + WritableTarget::Stderr => is_stderr(), + WritableTarget::Stdout => is_stdout(), + WritableTarget::Pipe(_) => false, } { WriteStyle::Auto } else { @@ -131,9 +190,10 @@ impl Builder { color_choice => color_choice, }; - let writer = match self.target { - Target::Stderr => BufferWriter::stderr(self.is_test, color_choice), - Target::Stdout => BufferWriter::stdout(self.is_test, color_choice), + let writer = match mem::take(&mut self.target) { + WritableTarget::Stderr => BufferWriter::stderr(self.is_test, color_choice), + WritableTarget::Stdout => BufferWriter::stdout(self.is_test, color_choice), + WritableTarget::Pipe(pipe) => BufferWriter::pipe(self.is_test, color_choice, pipe), }; Writer { @@ -149,15 +209,6 @@ impl Default for Builder { } } -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() diff --git a/src/fmt/writer/termcolor/extern_impl.rs b/src/fmt/writer/termcolor/extern_impl.rs index a6eaf425..11012fb1 100644 --- a/src/fmt/writer/termcolor/extern_impl.rs +++ b/src/fmt/writer/termcolor/extern_impl.rs @@ -3,11 +3,12 @@ use std::cell::RefCell; use std::fmt; use std::io::{self, Write}; use std::rc::Rc; +use std::sync::Mutex; use log::Level; use termcolor::{self, ColorChoice, ColorSpec, WriteColor}; -use crate::fmt::{Formatter, Target, WriteStyle}; +use crate::fmt::{Formatter, WritableTarget, WriteStyle}; pub(in crate::fmt::writer) mod glob { pub use super::*; @@ -70,46 +71,71 @@ impl Formatter { pub(in crate::fmt::writer) struct BufferWriter { inner: termcolor::BufferWriter, - test_target: Option, + test_target: Option, } pub(in crate::fmt) struct Buffer { inner: termcolor::Buffer, - test_target: Option, + has_test_target: bool, } impl BufferWriter { pub(in crate::fmt::writer) fn stderr(is_test: bool, write_style: WriteStyle) -> Self { BufferWriter { inner: termcolor::BufferWriter::stderr(write_style.into_color_choice()), - test_target: if is_test { Some(Target::Stderr) } else { None }, + test_target: if is_test { + Some(WritableTarget::Stderr) + } else { + None + }, } } pub(in crate::fmt::writer) fn stdout(is_test: bool, write_style: WriteStyle) -> Self { BufferWriter { inner: termcolor::BufferWriter::stdout(write_style.into_color_choice()), - test_target: if is_test { Some(Target::Stdout) } else { None }, + test_target: if is_test { + Some(WritableTarget::Stdout) + } else { + None + }, + } + } + + pub(in crate::fmt::writer) fn pipe( + is_test: bool, + write_style: WriteStyle, + pipe: Box>, + ) -> Self { + BufferWriter { + // The inner Buffer is never printed from, but it is still needed to handle coloring and other formating + inner: termcolor::BufferWriter::stderr(write_style.into_color_choice()), + test_target: if is_test { + Some(WritableTarget::Pipe(pipe)) + } else { + None + }, } } pub(in crate::fmt::writer) fn buffer(&self) -> Buffer { Buffer { inner: self.inner.buffer(), - test_target: self.test_target, + has_test_target: self.test_target.is_some(), } } pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { - if let Some(target) = self.test_target { + if let Some(target) = &self.test_target { // This impl uses the `eprint` and `print` macros // instead of `termcolor`'s buffer. // This is so their output can be captured by `cargo test` let log = String::from_utf8_lossy(buf.bytes()); match target { - Target::Stderr => eprint!("{}", log), - Target::Stdout => print!("{}", log), + WritableTarget::Stderr => eprint!("{}", log), + WritableTarget::Stdout => print!("{}", log), + WritableTarget::Pipe(pipe) => write!(pipe.lock().unwrap(), "{}", log)?, } Ok(()) @@ -138,7 +164,7 @@ impl Buffer { fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { // Ignore styles for test captured logs because they can't be printed - if self.test_target.is_none() { + if !self.has_test_target { self.inner.set_color(spec) } else { Ok(()) @@ -147,7 +173,7 @@ impl Buffer { fn reset(&mut self) -> io::Result<()> { // Ignore styles for test captured logs because they can't be printed - if self.test_target.is_none() { + if !self.has_test_target { self.inner.reset() } else { Ok(()) diff --git a/src/fmt/writer/termcolor/shim_impl.rs b/src/fmt/writer/termcolor/shim_impl.rs index 563f8ad4..bfc31d08 100644 --- a/src/fmt/writer/termcolor/shim_impl.rs +++ b/src/fmt/writer/termcolor/shim_impl.rs @@ -1,11 +1,11 @@ -use std::io; +use std::{io, sync::Mutex}; -use crate::fmt::{Target, WriteStyle}; +use crate::fmt::{WritableTarget, WriteStyle}; pub(in crate::fmt::writer) mod glob {} pub(in crate::fmt::writer) struct BufferWriter { - target: Target, + target: WritableTarget, } pub(in crate::fmt) struct Buffer(Vec); @@ -13,13 +13,23 @@ pub(in crate::fmt) struct Buffer(Vec); impl BufferWriter { pub(in crate::fmt::writer) fn stderr(_is_test: bool, _write_style: WriteStyle) -> Self { BufferWriter { - target: Target::Stderr, + target: WritableTarget::Stderr, } } pub(in crate::fmt::writer) fn stdout(_is_test: bool, _write_style: WriteStyle) -> Self { BufferWriter { - target: Target::Stdout, + target: WritableTarget::Stdout, + } + } + + pub(in crate::fmt::writer) fn pipe( + _is_test: bool, + _write_style: WriteStyle, + pipe: Box>, + ) -> Self { + BufferWriter { + target: WritableTarget::Pipe(pipe), } } @@ -30,12 +40,12 @@ impl BufferWriter { pub(in crate::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), + // This is so their output can be captured by `cargo test`. + match &self.target { + // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. + WritableTarget::Pipe(pipe) => pipe.lock().unwrap().write_all(&buf.0)?, + WritableTarget::Stdout => print!("{}", String::from_utf8_lossy(&buf.0)), + WritableTarget::Stderr => eprint!("{}", String::from_utf8_lossy(&buf.0)), } Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 31ea7c3d..be9cf58f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -708,7 +708,10 @@ impl Builder { /// Sets the target for the log output. /// - /// Env logger can log to either stdout or stderr. The default is stderr. + /// Env logger can log to either stdout, stderr or a custom pipe. The default is stderr. + /// + /// The custom pipe can be used to send the log messages to a custom sink (for example a file). + /// Do note that direct writes to a file can become a bottleneck due to IO operation times. /// /// # Examples ///