From dfc6229d0545a6a99c67033c481dfe618eabc8b8 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 23 Jun 2021 11:06:13 -0700 Subject: [PATCH 1/8] subscriber: add `MakeWriter` combinators Signed-off-by: Eliza Weisman --- tracing-subscriber/src/fmt/writer.rs | 313 +++++++++++++++++++++++++-- 1 file changed, 297 insertions(+), 16 deletions(-) diff --git a/tracing-subscriber/src/fmt/writer.rs b/tracing-subscriber/src/fmt/writer.rs index 8ec785ff6a..cd1fdceaf2 100644 --- a/tracing-subscriber/src/fmt/writer.rs +++ b/tracing-subscriber/src/fmt/writer.rs @@ -223,6 +223,23 @@ pub trait MakeWriter<'a> { } } +pub trait MakeWriterExt<'a>: MakeWriter<'a> { + fn with_max_level(self, level: tracing_core::Level) -> WithMaxLevel + where + Self: Sized, + { + WithMaxLevel::new(self, level) + } + + fn and(self, other: B) -> Tee + where + Self: Sized, + B: MakeWriter<'a> + Sized, + { + Tee::new(self, other) + } +} + /// A type implementing [`io::Write`] for a [`MutexGuard`] where the type /// inside the [`Mutex`] implements [`io::Write`]. /// @@ -291,6 +308,34 @@ pub struct BoxMakeWriter { inner: Box MakeWriter<'a, Writer = Box> + Send + Sync>, } +/// A [writer] that is one of two types implementing [`io::Write`][writer]. +/// +/// This may be used by [`MakeWriter`] implementations that may conditionally +/// return one of two writers. +/// +/// [writer]: std::io::Write +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum EitherWriter { + /// A writer of type `A`. + A(A), + /// A writer of type `B`. + B(B), +} + +pub type OptionalWriter = EitherWriter; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct WithMaxLevel { + make: M, + level: tracing_core::Level, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Tee { + a: A, + b: B, +} + impl<'a, F, W> MakeWriter<'a> for F where F: Fn() -> W, @@ -427,14 +472,182 @@ where } } +// === impl EitherWriter === + +impl io::Write for EitherWriter +where + A: io::Write, + B: io::Write, +{ + #[inline] + fn write(&mut self, buf: &[u8]) -> io::Result { + match self { + EitherWriter::A(a) => a.write(buf), + EitherWriter::B(b) => b.write(buf), + } + } + + #[inline] + fn flush(&mut self) -> io::Result<()> { + match self { + EitherWriter::A(a) => a.flush(), + EitherWriter::B(b) => b.flush(), + } + } + + #[inline] + fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { + match self { + EitherWriter::A(a) => a.write_vectored(bufs), + EitherWriter::B(b) => b.write_vectored(bufs), + } + } + + #[inline] + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + match self { + EitherWriter::A(a) => a.write_all(buf), + EitherWriter::B(b) => b.write_all(buf), + } + } + + #[inline] + fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> io::Result<()> { + match self { + EitherWriter::A(a) => a.write_fmt(fmt), + EitherWriter::B(b) => b.write_fmt(fmt), + } + } +} + +impl OptionalWriter { + pub fn none() -> Self { + EitherWriter::B(std::io::sink()) + } + + pub fn some(t: T) -> Self { + EitherWriter::A(t) + } +} + +impl From> for OptionalWriter { + fn from(opt: Option) -> Self { + match opt { + Some(writer) => Self::some(writer), + None => Self::none(), + } + } +} +// === impl WithMaxLevel === + +impl WithMaxLevel { + pub fn new(make: M, level: tracing_core::Level) -> Self { + Self { make, level } + } +} + +impl<'a, M: MakeWriter<'a>> MakeWriter<'a> for WithMaxLevel { + type Writer = OptionalWriter; + + #[inline] + fn make_writer(&'a self) -> Self::Writer { + // If we don't know the level, assume it's disabled. + OptionalWriter::none() + } + + #[inline] + fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer { + if meta.level() <= &self.level { + return OptionalWriter::some(self.make.make_writer_for(meta)); + } + OptionalWriter::none() + } +} + +// === impl Tee === + +impl Tee { + pub fn new(a: A, b: B) -> Self { + Self { a, b } + } +} + +impl<'a, A, B> MakeWriter<'a> for Tee +where + A: MakeWriter<'a>, + B: MakeWriter<'a>, +{ + type Writer = Tee; + + #[inline] + fn make_writer(&'a self) -> Self::Writer { + Tee::new(self.a.make_writer(), self.b.make_writer()) + } + + #[inline] + fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer { + Tee::new(self.a.make_writer_for(meta), self.b.make_writer_for(meta)) + } +} + +macro_rules! impl_tee { + ($self_:ident.$f:ident($($arg:ident),*)) => { + { + let res_a = $self_.a.$f($($arg),*); + let res_b = $self_.b.$f($($arg),*); + (res_a?, res_b?) + } + } +} + +impl io::Write for Tee +where + A: io::Write, + B: io::Write, +{ + #[inline] + fn write(&mut self, buf: &[u8]) -> io::Result { + let (a, b) = impl_tee!(self.write(buf)); + Ok(std::cmp::max(a, b)) + } + + #[inline] + fn flush(&mut self) -> io::Result<()> { + impl_tee!(self.flush()); + Ok(()) + } + + #[inline] + fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { + let (a, b) = impl_tee!(self.write_vectored(bufs)); + Ok(std::cmp::max(a, b)) + } + + #[inline] + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + impl_tee!(self.write_all(buf)); + Ok(()) + } + + #[inline] + fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> io::Result<()> { + impl_tee!(self.write_fmt(fmt)); + Ok(()) + } +} + +// === blanket impls === + +impl<'a, M> MakeWriterExt<'a> for M where M: MakeWriter<'a> {} + #[cfg(test)] mod test { - use super::MakeWriter; + use super::*; use crate::fmt::format::Format; use crate::fmt::test::{MockMakeWriter, MockWriter}; use crate::fmt::Collector; use std::sync::{Arc, Mutex}; - use tracing::error; + use tracing::{debug, error, info, trace, warn}; use tracing_core::dispatch::{self, Dispatch}; fn test_writer(make_writer: T, msg: &str, buf: &Mutex>) @@ -443,21 +656,13 @@ mod test { { let subscriber = { #[cfg(feature = "ansi")] - { - let f = Format::default().without_time().with_ansi(false); - Collector::builder() - .event_format(f) - .with_writer(make_writer) - .finish() - } + let f = Format::default().without_time().with_ansi(false); #[cfg(not(feature = "ansi"))] - { - let f = Format::default().without_time(); - Collector::builder() - .event_format(f) - .with_writer(make_writer) - .finish() - } + let f = Format::default().without_time(); + Collector::builder() + .event_format(f) + .with_writer(make_writer) + .finish() }; let dispatch = Dispatch::from(subscriber); @@ -470,6 +675,19 @@ mod test { assert!(actual.contains(expected.as_str())); } + fn has_lines(buf: &Mutex>, msgs: &[(tracing::Level, &str)]) { + let actual = String::from_utf8(buf.try_lock().unwrap().to_vec()).unwrap(); + let mut expected_lines = msgs.iter(); + for line in actual.lines() { + let line = dbg!(line).trim(); + let (level, msg) = expected_lines + .next() + .unwrap_or_else(|| panic!("expected no more lines, but got: {:?}", line)); + let expected = format!("{} {}: {}", level, module_path!(), msg); + assert_eq!(line, expected.as_str()); + } + } + #[test] fn custom_writer_closure() { let buf = Arc::new(Mutex::new(Vec::new())); @@ -495,4 +713,67 @@ mod test { let msg = "my mutex writer error"; test_writer(make_writer, msg, &buf); } + + #[test] + fn level_filters() { + use tracing::Level; + + let info_buf = Arc::new(Mutex::new(Vec::new())); + let info = MockMakeWriter::new(info_buf.clone()); + + let debug_buf = Arc::new(Mutex::new(Vec::new())); + let debug = MockMakeWriter::new(debug_buf.clone()); + + let warn_buf = Arc::new(Mutex::new(Vec::new())); + let warn = MockMakeWriter::new(warn_buf.clone()); + + let err_buf = Arc::new(Mutex::new(Vec::new())); + let err = MockMakeWriter::new(err_buf.clone()); + + let make_writer = info + .with_max_level(Level::INFO) + .and(debug.with_max_level(Level::DEBUG)) + .and(warn.with_max_level(Level::WARN)) + .and(err.with_max_level(Level::ERROR)); + + let c = { + #[cfg(feature = "ansi")] + let f = Format::default().without_time().with_ansi(false); + #[cfg(not(feature = "ansi"))] + let f = Format::default().without_time(); + Collector::builder() + .event_format(f) + .with_writer(make_writer) + .with_max_level(Level::TRACE) + .finish() + }; + + let _s = tracing::collect::set_default(c); + + trace!("trace"); + debug!("debug"); + info!("info"); + warn!("warn"); + error!("error"); + + let all_lines = [ + (Level::TRACE, "trace"), + (Level::DEBUG, "debug"), + (Level::INFO, "info"), + (Level::WARN, "warn"), + (Level::ERROR, "error"), + ]; + + println!("max level debug"); + has_lines(&debug_buf, &all_lines[1..]); + + println!("max level info"); + has_lines(&info_buf, &all_lines[2..]); + + println!("max level warn"); + has_lines(&warn_buf, &all_lines[3..]); + + println!("max level error"); + has_lines(&err_buf, &all_lines[4..]); + } } From 20dee7136fa9df2ae5c3fb8c57afd157d6dcf64c Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 23 Jun 2021 16:53:23 -0700 Subject: [PATCH 2/8] wip docs Signed-off-by: Eliza Weisman --- tracing-subscriber/src/fmt/writer.rs | 334 ++++++++++++++++++++++++++- 1 file changed, 329 insertions(+), 5 deletions(-) diff --git a/tracing-subscriber/src/fmt/writer.rs b/tracing-subscriber/src/fmt/writer.rs index cd1fdceaf2..f444d1fedd 100644 --- a/tracing-subscriber/src/fmt/writer.rs +++ b/tracing-subscriber/src/fmt/writer.rs @@ -223,7 +223,34 @@ pub trait MakeWriter<'a> { } } +/// Extension trait adding combinators for working with types implementing +/// [`MakeWriter`]. +/// +/// This is not intended to be implemented directly for user-defined +/// [`MakeWriter`]s; instead, it should be imported when the desired methods are +/// used. pub trait MakeWriterExt<'a>: MakeWriter<'a> { + /// Wraps `self` and returns a [`MakeWriter`] that will only write output + /// for events at or below the provided verbosity [`Level`]. + /// + /// Events whose level is more verbose than `level` will be ignored, and no + /// output will be written. + /// + /// # Examples + /// + /// ``` + /// use tracing::Level; + /// use tracing_subscriber::fmt::writer::MakeWriterExt; + /// + /// // Construct a writer that outputs events to `stderr` only if the span or + /// // event's level is >= WARN (WARN and ERROR). + /// let mk_writer = std::io::stderr.with_max_level(Level::WARN); + /// + /// tracing_subscriber::fmt().with_writer(mk_writer).init(); + /// ``` + /// + /// [`Level`]: tracing_core::Level + /// [`io::Write`]: std::io::Write fn with_max_level(self, level: tracing_core::Level) -> WithMaxLevel where Self: Sized, @@ -231,6 +258,47 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { WithMaxLevel::new(self, level) } + /// Wraps `self` and returns a [`MakeWriter`] that will only write output + /// for events at or above the provided verbosity [`Level`]. + /// + /// Events whose level is less verbose than `level` will be ignored, and no + /// output will be written. + /// + /// # Examples + /// + /// ``` + /// use tracing::Level; + /// use tracing_subscriber::fmt::writer::MakeWriterExt; + /// + /// // Construct a writer that outputs events to `stdout` only if the span or + /// // event's level is <= DEBUG (DEBUG and TRACE). + /// let mk_writer = std::io::stdout.with_min_level(Level::DEBUG); + /// + /// tracing_subscriber::fmt().with_writer(mk_writer).init(); + /// ``` + /// [`Level`]: tracing_core::Level + /// [`io::Write`]: std::io::Write + fn with_min_level(self, level: tracing_core::Level) -> WithMinLevel + where + Self: Sized, + { + WithMinLevel::new(self, level) + } + + /// Combines `self` with another type implementing [`MakeWriter`], returning + /// a new [`MakeWriter`] that produces [writers] that write to *both* + /// outputs. + /// + /// # Examples + /// + /// ``` + /// use tracing_subscriber::fmt::writer::MakeWriterExt; + /// + /// // Construct a writer that outputs events to `stdout` *and* `stderr`. + /// let mk_writer = std::io::stdout.and(std::io::stderr); + /// + /// tracing_subscriber::fmt().with_writer(mk_writer).init(); + /// ``` fn and(self, other: B) -> Tee where Self: Sized, @@ -238,6 +306,34 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { { Tee::new(self, other) } + + /// Combines `self` with another type implementing [`MakeWriter`], returning + /// a new [`MakeWriter`] that calls `other`'s [`make_writer`] if `self`'s + /// `make_writer` returns [`OptionalWriter::none`]. + /// + /// # Examples + /// + /// ``` + /// use tracing::Level; + /// use tracing_subscriber::fmt::writer::MakeWriterExt; + /// + /// // Produces a writer that writes to `stderr` if the level is >= WARN, + /// // or returns `OptionalWriter::none()` otherwise. + /// let mk_writer = std::io::stderr.with_max_level(Level::WARN); + /// + /// // If the `stderr` `MakeWriter` is disabled by the max level filter, + /// // write to stdout instead: + /// let mk_writer = stderr.or_else(std::io::stdout); + /// + /// tracing_subscriber::fmt().with_writer(mk_writer).init(); + /// ``` + fn or_else(self, other: B) -> OrElse + where + Self: MakeWriter<'a, Writer = OptionalWriter> + Sized, + B: MakeWriter<'a> + Sized, + { + OrElse::new(self, other) + } } /// A type implementing [`io::Write`] for a [`MutexGuard`] where the type @@ -306,6 +402,7 @@ pub struct TestWriter { /// [`io::Write`]: std::io::Write pub struct BoxMakeWriter { inner: Box MakeWriter<'a, Writer = Box> + Send + Sync>, + name: &'static str, } /// A [writer] that is one of two types implementing [`io::Write`][writer]. @@ -322,14 +419,63 @@ pub enum EitherWriter { B(B), } +/// A [writer] which may or may not be enabled. +/// +/// This may be used by [`MakeWriter`] implementations that wish to +/// conditionally enable or disable the returned writer based on a span or +/// event's [`Metadata`]. +/// +/// [writer]: std::io::Write pub type OptionalWriter = EitherWriter; +/// A [`MakeWriter`] combinator that only returns an enabled [writer] for spans +/// and events with metadata at or below a specified verbosity [`Level`]. +/// +/// This is returned by the [`MakeWriterExt::with_max_level] method. See the +/// method documentation for details. +/// +/// [writer]: std::io::Write +/// [`Level`]: tracing_core::Level #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct WithMaxLevel { make: M, level: tracing_core::Level, } +/// A [`MakeWriter`] combinator that only returns an enabled [writer] for spans +/// and events with metadata at or above a specified verbosity [`Level`]. +/// +/// This is returned by the [`MakeWriterExt::with_min_level] method. See the +/// method documentation for details. +/// +/// [writer]: std::io::Write +/// [`Level`]: tracing_core::Level +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct WithMinLevel { + make: M, + level: tracing_core::Level, +} + +/// A [`MakeWriter`] combinator that only returns an enabled [writer] for spans +/// and events with metadata at or above a specified verbosity [`Level`]. +/// +/// This is returned by the [`MakeWriterExt::with_min_level] method. See the +/// method documentation for details. +/// +/// [writer]: std::io::Write +/// [`Level`]: tracing_core::Level +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct WithFilter { + make: M, + filter: F, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct OrElse { + inner: A, + or_else: B, +} + #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Tee { a: A, @@ -388,13 +534,16 @@ impl BoxMakeWriter { { Self { inner: Box::new(Boxed(make_writer)), + name: std::any::type_name::(), } } } impl Debug for BoxMakeWriter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.pad("BoxMakeWriter { ... }") + f.debug_tuple("BoxMakeWriter") + .field(&format_args!("<{}>", self.name)) + .finish() } } @@ -538,6 +687,7 @@ impl From> for OptionalWriter { } } } + // === impl WithMaxLevel === impl WithMaxLevel { @@ -564,6 +714,71 @@ impl<'a, M: MakeWriter<'a>> MakeWriter<'a> for WithMaxLevel { } } +// === impl WithMinLevel === + +impl WithMinLevel { + pub fn new(make: M, level: tracing_core::Level) -> Self { + Self { make, level } + } +} + +impl<'a, M: MakeWriter<'a>> MakeWriter<'a> for WithMinLevel { + type Writer = OptionalWriter; + + #[inline] + fn make_writer(&'a self) -> Self::Writer { + // If we don't know the level, assume it's disabled. + OptionalWriter::none() + } + + #[inline] + fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer { + if meta.level() >= &self.level { + return OptionalWriter::some(self.make.make_writer_for(meta)); + } + OptionalWriter::none() + } +} + +// ==== impl WithFilter === + +impl WithFilter { + /// Wraps `make` with the provided `filter`, returning a [`MakeWriter`] that + /// will call `make.make_writer_for()` when `filter` returns `true` for a + /// span or event's [`Metadata`], and returns a [`sink`] otherwise. + /// + /// [`Metadata`]: tracing_core::Metadata + /// [`sink`]: std::io::sink + pub fn new(make: M, filter: F) -> Self + where + F: Fn(&Metadata<'_>) -> bool, + { + Self { make, filter } + } +} + +impl<'a, M, F> MakeWriter<'a> for WithFilter +where + M: MakeWriter<'a>, + F: Fn(&Metadata<'_>) -> bool, +{ + type Writer = OptionalWriter; + + #[inline] + fn make_writer(&'a self) -> Self::Writer { + OptionalWriter::some(self.make.make_writer()) + } + + #[inline] + fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer { + if (self.filter)(meta) { + OptionalWriter::some(self.make.make_writer_for(meta)) + } else { + OptionalWriter::none() + } + } +} + // === impl Tee === impl Tee { @@ -636,6 +851,44 @@ where } } +// === impl OrElse === + +impl OrElse { + /// Combines + pub fn new<'a, W>(inner: A, or_else: B) -> Self + where + A: MakeWriter<'a, Writer = OptionalWriter>, + B: MakeWriter<'a>, + { + Self { inner, or_else } + } +} + +impl<'a, A, B, W> MakeWriter<'a> for OrElse +where + A: MakeWriter<'a, Writer = OptionalWriter>, + B: MakeWriter<'a>, + W: io::Write, +{ + type Writer = EitherWriter; + + #[inline] + fn make_writer(&'a self) -> Self::Writer { + match self.inner.make_writer() { + EitherWriter::A(writer) => EitherWriter::A(writer), + EitherWriter::B(_) => EitherWriter::B(self.or_else.make_writer()), + } + } + + #[inline] + fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer { + match self.inner.make_writer_for(meta) { + EitherWriter::A(writer) => EitherWriter::A(writer), + EitherWriter::B(_) => EitherWriter::B(self.or_else.make_writer_for(meta)), + } + } +} + // === blanket impls === impl<'a, M> MakeWriterExt<'a> for M where M: MakeWriter<'a> {} @@ -646,8 +899,9 @@ mod test { use crate::fmt::format::Format; use crate::fmt::test::{MockMakeWriter, MockWriter}; use crate::fmt::Collector; + use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; - use tracing::{debug, error, info, trace, warn}; + use tracing::{debug, error, info, trace, warn, Level}; use tracing_core::dispatch::{self, Dispatch}; fn test_writer(make_writer: T, msg: &str, buf: &Mutex>) @@ -715,9 +969,7 @@ mod test { } #[test] - fn level_filters() { - use tracing::Level; - + fn combinators_level_filters() { let info_buf = Arc::new(Mutex::new(Vec::new())); let info = MockMakeWriter::new(info_buf.clone()); @@ -776,4 +1028,76 @@ mod test { println!("max level error"); has_lines(&err_buf, &all_lines[4..]); } + + #[test] + fn combinators_or_else() { + let some_buf = Arc::new(Mutex::new(Vec::new())); + let some = MockMakeWriter::new(some_buf.clone()); + + let or_else_buf = Arc::new(Mutex::new(Vec::new())); + let or_else = MockMakeWriter::new(or_else_buf.clone()); + + let return_some = AtomicBool::new(true); + let make_writer = move || { + if return_some.swap(false, Ordering::Relaxed) { + OptionalWriter::some(some.make_writer()) + } else { + OptionalWriter::none() + } + }; + let make_writer = make_writer.or_else(or_else); + let c = { + #[cfg(feature = "ansi")] + let f = Format::default().without_time().with_ansi(false); + #[cfg(not(feature = "ansi"))] + let f = Format::default().without_time(); + Collector::builder() + .event_format(f) + .with_writer(make_writer) + .with_max_level(Level::TRACE) + .finish() + }; + + let _s = tracing::collect::set_default(c); + info!("hello"); + info!("world"); + info!("goodbye"); + + has_lines(&some_buf, &[(Level::INFO, "hello")]); + has_lines( + &or_else_buf, + &[(Level::INFO, "world"), (Level::INFO, "goodbye")], + ); + } + + #[test] + fn combinators_and() { + let a_buf = Arc::new(Mutex::new(Vec::new())); + let a = MockMakeWriter::new(a_buf.clone()); + + let b_buf = Arc::new(Mutex::new(Vec::new())); + let b = MockMakeWriter::new(b_buf.clone()); + + let lines = &[(Level::INFO, "world"), (Level::INFO, "goodbye")]; + + let make_writer = a.and(b); + let c = { + #[cfg(feature = "ansi")] + let f = Format::default().without_time().with_ansi(false); + #[cfg(not(feature = "ansi"))] + let f = Format::default().without_time(); + Collector::builder() + .event_format(f) + .with_writer(make_writer) + .with_max_level(Level::TRACE) + .finish() + }; + + let _s = tracing::collect::set_default(c); + info!("hello"); + info!("world"); + + has_lines(&a_buf, &lines[..]); + has_lines(&b_buf, &lines[..]); + } } From ed0ab24a615ba8b89d8593451e08a7561b341852 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Thu, 24 Jun 2021 11:04:07 -0700 Subject: [PATCH 3/8] test fixup Signed-off-by: Eliza Weisman --- tracing-subscriber/src/fmt/writer.rs | 250 ++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 6 deletions(-) diff --git a/tracing-subscriber/src/fmt/writer.rs b/tracing-subscriber/src/fmt/writer.rs index f444d1fedd..3e1a189e6c 100644 --- a/tracing-subscriber/src/fmt/writer.rs +++ b/tracing-subscriber/src/fmt/writer.rs @@ -249,6 +249,40 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { /// tracing_subscriber::fmt().with_writer(mk_writer).init(); /// ``` /// + /// Writing the `ERROR` and `WARN` levels to `stderr`, and everything else + /// to `stdout`: + /// + /// ``` + /// # use tracing::Level; + /// # use tracing_subscriber::fmt::writer::MakeWriterExt; + /// + /// let mk_writer = std::io::stderr + /// .with_max_level(Level::WARN) + /// .or_else(std::io::stdout); + /// + /// tracing_subscriber::fmt().with_writer(mk_writer).init(); + /// ``` + /// + /// Writing the `ERROR` level to `stderr`, the `INFO` and `WARN` levels to + /// `stdout`, and the `INFO` and DEBUG` levels to a file: + /// + /// ``` + /// use std::fs::File; + /// # // don't actually create the file when running the tests. + /// # fn docs() -> std::io::Result<()> { + /// let debug_log = File::create("debug.log")?; + /// + /// let mk_writer = std::io::stderr + /// .with_max_level(Level::ERROR) + /// .or_else(std::io::stdout + /// .with_max_level(Level::INFO) + /// .and(debug_log.with_max_level(Level::DEBUG)) + /// ); + /// + /// tracing_subscriber::fmt().with_writer(mk_writer).init(); + /// # Ok(()) } + /// ``` + /// /// [`Level`]: tracing_core::Level /// [`io::Write`]: std::io::Write fn with_max_level(self, level: tracing_core::Level) -> WithMaxLevel @@ -276,6 +310,21 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { /// /// tracing_subscriber::fmt().with_writer(mk_writer).init(); /// ``` + /// This can be combined with [`MakeWriterExt::with_max_level`] to write + /// only within a range of levels: + /// + /// ``` + /// # use tracing::Level; + /// # use tracing_subscriber::fmt::writer::MakeWriterExt; + /// // Only write the `DEBUG` and `INFO` levels to stdout. + /// let mk_writer = std::io::stdout + /// .with_max_level(Level::DEBUG) + /// .with_min_level(Level::INFO) + /// // Write the `WARN` and `ERROR` levels to stderr. + /// .and(std::io::stderr.with_min_level(Level::WARN)); + /// + /// tracing_subscriber::fmt().with_writer(mk_writer).init(); + /// ``` /// [`Level`]: tracing_core::Level /// [`io::Write`]: std::io::Write fn with_min_level(self, level: tracing_core::Level) -> WithMinLevel @@ -285,10 +334,89 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { WithMinLevel::new(self, level) } + /// Wraps `self` with a predicate that takes a span or event's [`Metadata`] + /// and returns a `bool`. The returned [`MakeWriter`]'s + /// [`MakeWriter::make_writer_for`][mwf] method will check the predicate to + /// determine if a writer should be produced for a given span or event. + /// + /// If the predicate returns `false`, the wrapped [`MakeWriter`]'s + /// [`make_writer_for`][mwf] will return [`OptionalWriter::none`]. + /// Otherwise, it calls the wrapped [`MakeWriter`]'s + /// [`make_writer_for`][mwf] method, and returns the produced writer. + /// + /// This can be used to filter an output based on arbitrary [`Metadata`] + /// parameters. + /// + /// # Examples + /// + /// Writing events with a specific target to an HTTP access log, and other + /// events to stdout: + /// + /// ``` + /// use tracing_subscriber::fmt::writer::MakeWriterExt; + /// use std::fs::File; + /// # // don't actually create the file when running the tests. + /// # fn docs() -> std::io::Result<()> { + /// let access_log = File::create("access.log")?; + /// + /// let mk_writer = access_log + /// // Only write events with the target "http::access_log" to the + /// // access log file. + /// .with_filter(|meta| meta.target() == "http::access_log") + /// // Write events with all other targets to stdout. + /// .or_else(std::io::stdout); + /// + /// tracing_subscriber::fmt().with_writer(mk_writer).init(); + /// # Ok(()) + /// # } + /// ``` + /// + /// Conditionally enabling or disabling a log file: + /// ``` + /// use tracing_subscriber::fmt::writer::MakeWriterExt; + /// use std::{sync::atomic::{AtomicBool, Ordering}, fs::File}; + /// + /// static DEBUG_LOG_ENABLED: AtomicBool = AtomicBool::new(false); + /// + /// # // don't actually create the file when running the tests. + /// # fn docs() -> std::io::Result<()> { + /// // Create the debug log file + /// let debug_file = File::create("debug.log")? + /// // Enable the debug log only if the flag is enabled. + /// .with_filter(|_| DEBUG_LOG_ENABLED.load(Ordering::Acquire)); + /// + /// // Always write to stdout + /// let mk_writer = std::io::stdout + /// // Write to the debug file if it's enabled + /// .and(debug_file); + /// + /// tracing_subscriber::fmt().with_writer(mk_writer).init(); + /// + /// // ... + /// + /// // Later, we can toggle on or off the debug log file. + /// DEBUG_LOG_ENABLED.store(true, Ordering::Release); + /// # Ok(()) + /// # } + /// + /// [`Metadata`]: tracing_core::Metadata + fn with_filter(self, filter: F) -> WithFilter + where + Self: Sized, + F: Fn(&Metadata<'_>) -> bool, + { + WithFilter::new(self, filter) + } + /// Combines `self` with another type implementing [`MakeWriter`], returning /// a new [`MakeWriter`] that produces [writers] that write to *both* /// outputs. /// + /// If writing to either writer returns an error, the returned writer will + /// return that error. However, both writers will still be written to before + /// the error is returned, so it is possible for one writer to fail while + /// the other is written to successfully. + /// /// # Examples /// /// ``` @@ -299,6 +427,9 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { /// /// tracing_subscriber::fmt().with_writer(mk_writer).init(); /// ``` + /// + /// `and` can be used in conjunction with filtering combinators. For + /// example, if we want to write to a number of outputs depending on fn and(self, other: B) -> Tee where Self: Sized, @@ -456,26 +587,38 @@ pub struct WithMinLevel { level: tracing_core::Level, } -/// A [`MakeWriter`] combinator that only returns an enabled [writer] for spans -/// and events with metadata at or above a specified verbosity [`Level`]. +/// A [`MakeWriter`] combinator that wraps a [`MakeWriter`] with a predicate for +/// span and event [`Metadata`], so that the [`MakeWriterExt::make_writer_for`] +/// method returns [`OptionalWriter::some`] when the predicate returns `true`, +/// and [`OptionalWriter::none`] when the predicate returns `false`. /// -/// This is returned by the [`MakeWriterExt::with_min_level] method. See the +/// This is returned by the [`MakeWriterExt::with_filter`] method. See the /// method documentation for details. /// -/// [writer]: std::io::Write -/// [`Level`]: tracing_core::Level +/// [`Metadata`]: tracing_core::Metadata #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct WithFilter { make: M, filter: F, } +/// Combines a [`MakeWriter`] that returns an [`OptionalWriter`] with another +/// [`MakeWriter`], so that the second [`MakeWriter`] is used when the first +/// [`MakeWriter`] returns [`OptionalWriter::none`]. +/// +/// This is returned by the [`MakeWriterExt::or_else] method. See the +/// method documentation for details. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct OrElse { inner: A, or_else: B, } +/// Combines two types implementing [`MakeWriter`] (or [`std::io::Write`]) to +/// produce a writer that writes to both [`MakeWriter`]'s returned writers. +/// +/// This is returned by the [`MakeWriterExt::and`] method. See the method +/// documentation for details. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Tee { a: A, @@ -494,6 +637,13 @@ where } } +impl<'a> MakeWriter<'a> for std::fs::File { + type Writer = &'a std::fs::File; + fn make_writer(&'a self) -> Self::Writer { + self + } +} + // === impl TestWriter === impl TestWriter { @@ -670,16 +820,29 @@ where } impl OptionalWriter { + /// Returns a [disabled writer]. + /// + /// Any bytes written to the returned writer are discarded. + /// + /// This is equivalent to returning [`Option::None`]. + /// + /// [disabled writer]: std::io::sink + #[inline] pub fn none() -> Self { EitherWriter::B(std::io::sink()) } + /// Returns an enabled writer of type `T`. + /// + /// This is equivalent to returning [`Option::Some`]. + #[inline] pub fn some(t: T) -> Self { EitherWriter::A(t) } } impl From> for OptionalWriter { + #[inline] fn from(opt: Option) -> Self { match opt { Some(writer) => Self::some(writer), @@ -691,6 +854,13 @@ impl From> for OptionalWriter { // === impl WithMaxLevel === impl WithMaxLevel { + /// Wraps the provided [`MakeWriter`] with a maximum [`Level`], so that it + /// returns [`OptionalWriter::none`] for spans and events whose level is + /// more verbose than the maximum level. + /// + /// See [`MakeWriterExt::with_max_level`] for details. + /// + /// [`Level`]: tracing_core::Level pub fn new(make: M, level: tracing_core::Level) -> Self { Self { make, level } } @@ -717,6 +887,13 @@ impl<'a, M: MakeWriter<'a>> MakeWriter<'a> for WithMaxLevel { // === impl WithMinLevel === impl WithMinLevel { + /// Wraps the provided [`MakeWriter`] with a minimum [`Level`], so that it + /// returns [`OptionalWriter::none`] for spans and events whose level is + /// less verbose than the maximum level. + /// + /// See [`MakeWriterExt::with_min_level`] for details. + /// + /// [`Level`]: tracing_core::Level pub fn new(make: M, level: tracing_core::Level) -> Self { Self { make, level } } @@ -747,6 +924,8 @@ impl WithFilter { /// will call `make.make_writer_for()` when `filter` returns `true` for a /// span or event's [`Metadata`], and returns a [`sink`] otherwise. /// + /// See [`MakeWriterExt::with_filter`] for details. + /// /// [`Metadata`]: tracing_core::Metadata /// [`sink`]: std::io::sink pub fn new(make: M, filter: F) -> Self @@ -782,6 +961,11 @@ where // === impl Tee === impl Tee { + /// Combines two types implementing [`MakeWriter`], returning + /// a new [`MakeWriter`] that produces [writers] that write to *both* + /// outputs. + /// + /// See the documentation for [`MakeWriterExt::and`] for details. pub fn new(a: A, b: B) -> Self { Self { a, b } } @@ -1070,6 +1254,60 @@ mod test { ); } + #[test] + fn combinators_or_else_chain() { + let info_buf = Arc::new(Mutex::new(Vec::new())); + let info = MockMakeWriter::new(info_buf.clone()); + + let debug_buf = Arc::new(Mutex::new(Vec::new())); + let debug = MockMakeWriter::new(debug_buf.clone()); + + let warn_buf = Arc::new(Mutex::new(Vec::new())); + let warn = MockMakeWriter::new(warn_buf.clone()); + + let err_buf = Arc::new(Mutex::new(Vec::new())); + let err = MockMakeWriter::new(err_buf.clone()); + + let make_writer = err.with_max_level(Level::ERROR).or_else( + warn.with_max_level(Level::WARN).or_else( + info.with_max_level(Level::INFO) + .or_else(debug.with_max_level(Level::DEBUG)), + ), + ); + + let c = { + #[cfg(feature = "ansi")] + let f = Format::default().without_time().with_ansi(false); + #[cfg(not(feature = "ansi"))] + let f = Format::default().without_time(); + Collector::builder() + .event_format(f) + .with_writer(make_writer) + .with_max_level(Level::TRACE) + .finish() + }; + + let _s = tracing::collect::set_default(c); + + trace!("trace"); + debug!("debug"); + info!("info"); + warn!("warn"); + error!("error"); + + println!("max level debug"); + has_lines(&debug_buf, &[(Level::DEBUG, "debug")]); + + println!("max level info"); + has_lines(&info_buf, &[(Level::INFO, "info")]); + + println!("max level warn"); + has_lines(&warn_buf, &[(Level::WARN, "warn")]); + + println!("max level error"); + has_lines(&err_buf, &[(Level::ERROR, "error")]); + } + #[test] fn combinators_and() { let a_buf = Arc::new(Mutex::new(Vec::new())); @@ -1078,7 +1316,7 @@ mod test { let b_buf = Arc::new(Mutex::new(Vec::new())); let b = MockMakeWriter::new(b_buf.clone()); - let lines = &[(Level::INFO, "world"), (Level::INFO, "goodbye")]; + let lines = &[(Level::INFO, "hello"), (Level::INFO, "world")]; let make_writer = a.and(b); let c = { From 29182f55c364770183da716f40694142aceeff8a Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Thu, 24 Jun 2021 11:06:15 -0700 Subject: [PATCH 4/8] fixup examples Signed-off-by: Eliza Weisman --- tracing-subscriber/src/fmt/writer.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tracing-subscriber/src/fmt/writer.rs b/tracing-subscriber/src/fmt/writer.rs index 3e1a189e6c..842ff9b3ee 100644 --- a/tracing-subscriber/src/fmt/writer.rs +++ b/tracing-subscriber/src/fmt/writer.rs @@ -267,6 +267,8 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { /// `stdout`, and the `INFO` and DEBUG` levels to a file: /// /// ``` + /// # use tracing::Level; + /// # use tracing_subscriber::fmt::writer::MakeWriterExt; /// use std::fs::File; /// # // don't actually create the file when running the tests. /// # fn docs() -> std::io::Result<()> { @@ -398,7 +400,7 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { /// DEBUG_LOG_ENABLED.store(true, Ordering::Release); /// # Ok(()) /// # } - /// + /// ``` /// [`Metadata`]: tracing_core::Metadata fn with_filter(self, filter: F) -> WithFilter where @@ -450,7 +452,7 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { /// /// // Produces a writer that writes to `stderr` if the level is >= WARN, /// // or returns `OptionalWriter::none()` otherwise. - /// let mk_writer = std::io::stderr.with_max_level(Level::WARN); + /// let stderr = std::io::stderr.with_max_level(Level::WARN); /// /// // If the `stderr` `MakeWriter` is disabled by the max level filter, /// // write to stdout instead: From cb49875f1328608a9356517eb0ccd94f84e656d3 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Thu, 24 Jun 2021 11:11:24 -0700 Subject: [PATCH 5/8] add bound that rustc 1.42.0 apparently needs Signed-off-by: Eliza Weisman --- tracing-subscriber/src/fmt/writer.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tracing-subscriber/src/fmt/writer.rs b/tracing-subscriber/src/fmt/writer.rs index 842ff9b3ee..ff8bc25363 100644 --- a/tracing-subscriber/src/fmt/writer.rs +++ b/tracing-subscriber/src/fmt/writer.rs @@ -464,6 +464,7 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { where Self: MakeWriter<'a, Writer = OptionalWriter> + Sized, B: MakeWriter<'a> + Sized, + W: Write, { OrElse::new(self, other) } From 129cd2540a8872f84b2abd614ac0dd5ac5972c2d Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Thu, 24 Jun 2021 11:36:23 -0700 Subject: [PATCH 6/8] apparently 1.42.0 can't infer this either Signed-off-by: Eliza Weisman --- tracing-subscriber/src/fmt/writer.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tracing-subscriber/src/fmt/writer.rs b/tracing-subscriber/src/fmt/writer.rs index ff8bc25363..9716916520 100644 --- a/tracing-subscriber/src/fmt/writer.rs +++ b/tracing-subscriber/src/fmt/writer.rs @@ -1046,6 +1046,7 @@ impl OrElse { where A: MakeWriter<'a, Writer = OptionalWriter>, B: MakeWriter<'a>, + W: Write, { Self { inner, or_else } } From c919f7ac1a611eee0af857b6ae97d07367d5fb46 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 25 Jun 2021 11:50:57 -0700 Subject: [PATCH 7/8] Update tracing-subscriber/src/fmt/writer.rs Co-authored-by: David Barsky --- tracing-subscriber/src/fmt/writer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tracing-subscriber/src/fmt/writer.rs b/tracing-subscriber/src/fmt/writer.rs index 9716916520..7f6b13cc34 100644 --- a/tracing-subscriber/src/fmt/writer.rs +++ b/tracing-subscriber/src/fmt/writer.rs @@ -231,7 +231,8 @@ pub trait MakeWriter<'a> { /// used. pub trait MakeWriterExt<'a>: MakeWriter<'a> { /// Wraps `self` and returns a [`MakeWriter`] that will only write output - /// for events at or below the provided verbosity [`Level`]. + /// for events at or below the provided verbosity [`Level`]. For instance, + /// `Level::TRACE` is considered to be _more verbose` than `Level::INFO`. /// /// Events whose level is more verbose than `level` will be ignored, and no /// output will be written. From e2bec0a9e000a87541fee918b8314f41c5957f33 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 25 Jun 2021 13:15:27 -0700 Subject: [PATCH 8/8] review feedback Signed-off-by: Eliza Weisman --- tracing-subscriber/src/fmt/writer.rs | 2 ++ tracing-subscriber/src/prelude.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/tracing-subscriber/src/fmt/writer.rs b/tracing-subscriber/src/fmt/writer.rs index 7f6b13cc34..85c9eda0e3 100644 --- a/tracing-subscriber/src/fmt/writer.rs +++ b/tracing-subscriber/src/fmt/writer.rs @@ -402,7 +402,9 @@ pub trait MakeWriterExt<'a>: MakeWriter<'a> { /// # Ok(()) /// # } /// ``` + /// /// [`Metadata`]: tracing_core::Metadata + /// [mwf]: MakeWriter::make_writer_for fn with_filter(self, filter: F) -> WithFilter where Self: Sized, diff --git a/tracing-subscriber/src/prelude.rs b/tracing-subscriber/src/prelude.rs index 643c59f05d..72570350e8 100644 --- a/tracing-subscriber/src/prelude.rs +++ b/tracing-subscriber/src/prelude.rs @@ -6,3 +6,7 @@ pub use crate::field::{MakeExt as _, RecordFields as _}; pub use crate::subscribe::{CollectExt as _, Subscribe as _}; pub use crate::util::SubscriberInitExt as _; + +#[cfg(feature = "fmt")] +#[cfg_attr(docsrs, doc(cfg(feature = "fmt")))] +pub use crate::fmt::writer::MakeWriterExt as _;