diff --git a/examples/custom_target.rs b/examples/custom_target.rs new file mode 100644 index 00000000..766df464 --- /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.len() > 0 { + println!("from pipe: {}", msg) + } + }); +} diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index 6ee63a39..9f715e9c 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -3,7 +3,10 @@ mod termcolor; use self::atty::{is_stderr, is_stdout}; use self::termcolor::BufferWriter; -use std::{fmt, io}; +use std::{ + fmt, io, + sync::{Arc, Mutex}, +}; pub(in crate::fmt) mod glob { pub use super::termcolor::glob::*; @@ -12,13 +15,15 @@ pub(in crate::fmt) mod glob { pub(in crate::fmt) 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 +32,48 @@ impl Default for Target { } } +impl fmt::Debug for Target { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!( + f, + "{}", + match self { + Self::Stdout => "stdout", + Self::Stderr => "stderr", + Self::Pipe(_) => "pipe", + } + ) + } +} + +/// Log target, either `stdout`, `stderr` or a custom pipe. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub(in crate::fmt) enum TargetType { + /// 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, +} + +impl From<&Target> for TargetType { + fn from(target: &Target) -> Self { + match target { + Target::Stdout => Self::Stdout, + Target::Stderr => Self::Stderr, + Target::Pipe(_) => Self::Pipe, + } + } +} + +impl Default for TargetType { + fn default() -> Self { + Self::from(&Target::default()) + } +} + /// Whether or not to print styles to the target. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum WriteStyle { @@ -68,7 +115,8 @@ impl Writer { /// /// The target and style choice can be configured before building. pub(crate) struct Builder { - target: Target, + target_type: TargetType, + target_pipe: Option>>, write_style: WriteStyle, is_test: bool, built: bool, @@ -78,7 +126,8 @@ impl Builder { /// Initialize the writer builder with defaults. pub(crate) fn new() -> Self { Builder { - target: Default::default(), + target_type: Default::default(), + target_pipe: None, write_style: Default::default(), is_test: false, built: false, @@ -87,7 +136,11 @@ impl Builder { /// Set the target to write to. pub(crate) fn target(&mut self, target: Target) -> &mut Self { - self.target = target; + self.target_type = TargetType::from(&target); + self.target_pipe = match target { + Target::Stdout | Target::Stderr => None, + Target::Pipe(pipe) => Some(Arc::new(Mutex::new(pipe))), + }; self } @@ -119,9 +172,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_type { + TargetType::Stderr => is_stderr(), + TargetType::Stdout => is_stdout(), + TargetType::Pipe => false, } { WriteStyle::Auto } else { @@ -131,9 +185,12 @@ 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 self.target_type { + TargetType::Stderr => BufferWriter::stderr(self.is_test, color_choice), + TargetType::Stdout => BufferWriter::stdout(self.is_test, color_choice), + TargetType::Pipe => { + BufferWriter::pipe(color_choice, self.target_pipe.as_ref().unwrap().clone()) + } }; Writer { @@ -152,7 +209,7 @@ 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("target", &self.target_type) .field("write_style", &self.write_style) .finish() } diff --git a/src/fmt/writer/termcolor/extern_impl.rs b/src/fmt/writer/termcolor/extern_impl.rs index a6eaf425..f36237f5 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::{Arc, Mutex}; use log::Level; use termcolor::{self, ColorChoice, ColorSpec, WriteColor}; -use crate::fmt::{Formatter, Target, WriteStyle}; +use crate::fmt::{Formatter, TargetType, WriteStyle}; pub(in crate::fmt::writer) mod glob { pub use super::*; @@ -70,46 +71,72 @@ impl Formatter { pub(in crate::fmt::writer) struct BufferWriter { inner: termcolor::BufferWriter, - test_target: Option, + test_target_type: Option, + target_pipe: Option>>, } pub(in crate::fmt) struct Buffer { inner: termcolor::Buffer, - test_target: Option, + test_target_type: Option, } 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_type: if is_test { + Some(TargetType::Stderr) + } else { + None + }, + target_pipe: 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_type: if is_test { + Some(TargetType::Stdout) + } else { + None + }, + target_pipe: None, + } + } + + pub(in crate::fmt::writer) fn pipe( + write_style: WriteStyle, + target_pipe: Arc>, + ) -> 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_type: None, + target_pipe: Some(target_pipe), } } pub(in crate::fmt::writer) fn buffer(&self) -> Buffer { Buffer { inner: self.inner.buffer(), - test_target: self.test_target, + test_target_type: self.test_target_type, } } pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { - if let Some(target) = self.test_target { + if let Some(pipe) = &self.target_pipe { + pipe.lock().unwrap().write_all(&buf.bytes()) + } else if let Some(target) = self.test_target_type { // 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), + TargetType::Stderr => eprint!("{}", log), + TargetType::Stdout => print!("{}", log), + TargetType::Pipe => unreachable!(), } Ok(()) @@ -138,7 +165,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.test_target_type.is_none() { self.inner.set_color(spec) } else { Ok(()) @@ -147,7 +174,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.test_target_type.is_none() { 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..a74e810c 100644 --- a/src/fmt/writer/termcolor/shim_impl.rs +++ b/src/fmt/writer/termcolor/shim_impl.rs @@ -1,11 +1,15 @@ -use std::io; +use std::{ + io, + sync::{Arc, Mutex}, +}; -use crate::fmt::{Target, WriteStyle}; +use crate::fmt::{TargetType, WriteStyle}; pub(in crate::fmt::writer) mod glob {} pub(in crate::fmt::writer) struct BufferWriter { - target: Target, + target: TargetType, + target_pipe: Option>>, } pub(in crate::fmt) struct Buffer(Vec); @@ -13,13 +17,25 @@ 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: TargetType::Stderr, + target_pipe: None, } } pub(in crate::fmt::writer) fn stdout(_is_test: bool, _write_style: WriteStyle) -> Self { BufferWriter { - target: Target::Stdout, + target: TargetType::Stdout, + target_pipe: None, + } + } + + pub(in crate::fmt::writer) fn pipe( + _write_style: WriteStyle, + target_pipe: Arc>, + ) -> Self { + BufferWriter { + target: TargetType::Pipe, + target_pipe: Some(target_pipe), } } @@ -30,12 +46,18 @@ 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); - + // This is so their output can be captured by `cargo test`. match self.target { - Target::Stderr => eprint!("{}", log), - Target::Stdout => print!("{}", log), + // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. + TargetType::Pipe => self + .target_pipe + .as_ref() + .unwrap() + .lock() + .unwrap() + .write_all(&buf.0)?, + TargetType::Stdout => print!("{}", String::from_utf8_lossy(&buf.0)), + TargetType::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 ///