Skip to content

Commit

Permalink
Allow writing logs to a custom output target (Target::Pipe)
Browse files Browse the repository at this point in the history
Co-authored-by: TilCreator <tilcreator@tc-j.de>
  • Loading branch information
jplatte and TilCreator committed May 25, 2021
1 parent 2151771 commit 1a8379a
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 51 deletions.
2 changes: 1 addition & 1 deletion ci/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ fn main() {
})
.collect::<Vec<_>>();

if failed.len() > 0 {
if !failed.is_empty() {
for failed in failed {
eprintln!("FAIL: {:?}", failed);
}
Expand Down
2 changes: 1 addition & 1 deletion ci/src/permute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ where
{
let mut permutations = BTreeSet::new();

if input.len() == 0 {
if input.is_empty() {
return permutations;
}

Expand Down
4 changes: 2 additions & 2 deletions ci/src/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ impl Default for TestArgs {

impl TestArgs {
fn features_string(&self) -> Option<String> {
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);
Expand Down
81 changes: 81 additions & 0 deletions examples/custom_target.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
}

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<usize> {
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::<Vec<u8>>())
.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)
}
});
}
99 changes: 75 additions & 24 deletions src/fmt/writer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn io::Write + Send + 'static>),
}

impl Default for Target {
Expand All @@ -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<Mutex<dyn io::Write + Send + 'static>>),
}

impl From<Target> 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 {
Expand Down Expand Up @@ -55,20 +112,21 @@ 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)
}
}

/// 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,
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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()
Expand Down
48 changes: 37 additions & 11 deletions src/fmt/writer/termcolor/extern_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -70,46 +71,71 @@ impl Formatter {

pub(in crate::fmt::writer) struct BufferWriter {
inner: termcolor::BufferWriter,
test_target: Option<Target>,
test_target: Option<WritableTarget>,
}

pub(in crate::fmt) struct Buffer {
inner: termcolor::Buffer,
test_target: Option<Target>,
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<Mutex<dyn io::Write + Send + 'static>>,
) -> 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(())
Expand Down Expand Up @@ -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(())
Expand All @@ -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(())
Expand Down
Loading

0 comments on commit 1a8379a

Please sign in to comment.