Skip to content

Commit 1bfe02b

Browse files
committed
subscriber: add MakeWriter::make_writer_for (#1141)
This backports PR #1141 from `master`. subscriber: add `MakeWriter::make_writer_for` ## Motivation In some cases, it might be desirable to configure the writer used for writing out trace data based on the metadata of the span or event being written. For example, we might want to send different levels to different outputs, write logs from different targets to separate files, or wrap formatted output in ANSI color codes based on levels. Currently, it's not possible for the `MakeWriter` trait to model this kind of behavior --- it has one method, `make_writer`, which is completely unaware of *where* the data being written came from. In particular, this came up in PR #1137, when discussing a proposal that writing to syslog could be implemented as a `MakeWriter` implementation rather than as a `Subscribe` implementation, so that all the formatting logic from `tracing_subscriber::fmt` could be reused. See [here][1] for details. ## Solution This branch adds a new `make_writer_for` method to `MakeWriter`, taking a `Metadata`. Implementations can opt in to metadata-specific behavior by implementing this method. The method has a default implementation that just calls `self.make_writer()` and ignores the metadata, so it's only necessary to implement this when per-metadata behavior is required. This isn't a breaking change to existing implementations. There are a couple downsides to this approach: it's possible for callers to skip the metadata-specific behavior by calling `make_writer` rather than `make_writer_for`, and the impls for closures can't easily provide metadata-specific behavior. Since the upcoming release is going to be a breaking change anyway, we may want to just make the breaking change of having `MakeWriter::make_writer` _always_ take a `Metadata`, which solves these problems. However, that can't be backported to v0.1.x as easily. Additionally, that would mean that functions like `io::stdout` no longer implement `MakeWriter`; they would have to be wrapped in a wrapper type or closure that ignores metadata. [1]: #1137 (comment) Signed-off-by: Eliza Weisman <eliza@buoyant.io>
1 parent 4dbe03c commit 1bfe02b

File tree

3 files changed

+252
-16
lines changed

3 files changed

+252
-16
lines changed

tracing-subscriber/src/fmt/fmt_layer.rs

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ where
740740

741741
let ctx = self.make_ctx(ctx);
742742
if self.fmt_event.format_event(&ctx, &mut buf, event).is_ok() {
743-
let mut writer = self.make_writer.make_writer();
743+
let mut writer = self.make_writer.make_writer_for(event.metadata());
744744
let _ = io::Write::write_all(&mut writer, buf.as_bytes());
745745
}
746746

@@ -913,11 +913,12 @@ impl Timings {
913913

914914
#[cfg(test)]
915915
mod test {
916+
use super::*;
916917
use crate::fmt::{
917918
self,
918919
format::{self, test::MockTime, Format},
919920
layer::Layer as _,
920-
test::MockWriter,
921+
test::{MockMakeWriter, MockWriter},
921922
time,
922923
};
923924
use crate::Registry;
@@ -1110,4 +1111,70 @@ mod test {
11101111
actual.as_str()
11111112
);
11121113
}
1114+
1115+
#[test]
1116+
fn make_writer_based_on_meta() {
1117+
lazy_static! {
1118+
static ref BUF1: Mutex<Vec<u8>> = Mutex::new(vec![]);
1119+
static ref BUF2: Mutex<Vec<u8>> = Mutex::new(vec![]);
1120+
}
1121+
struct MakeByTarget<'a> {
1122+
make_writer1: MockMakeWriter<'a>,
1123+
make_writer2: MockMakeWriter<'a>,
1124+
}
1125+
1126+
impl<'a> MakeWriter for MakeByTarget<'a> {
1127+
type Writer = MockWriter<'a>;
1128+
1129+
fn make_writer(&self) -> Self::Writer {
1130+
self.make_writer1.make_writer()
1131+
}
1132+
1133+
fn make_writer_for(&self, meta: &Metadata<'_>) -> Self::Writer {
1134+
if meta.target() == "writer2" {
1135+
return self.make_writer2.make_writer();
1136+
}
1137+
self.make_writer()
1138+
}
1139+
}
1140+
1141+
let make_writer1 = MockMakeWriter::new(&BUF1);
1142+
let make_writer2 = MockMakeWriter::new(&BUF2);
1143+
1144+
let make_writer = MakeByTarget {
1145+
make_writer1: make_writer1.clone(),
1146+
make_writer2: make_writer2.clone(),
1147+
};
1148+
1149+
let subscriber = crate::fmt::Subscriber::builder()
1150+
.with_writer(make_writer)
1151+
.with_level(false)
1152+
.with_target(false)
1153+
.with_ansi(false)
1154+
.with_timer(MockTime)
1155+
.with_span_events(FmtSpan::CLOSE)
1156+
.finish();
1157+
1158+
with_default(subscriber, || {
1159+
let span1 = tracing::info_span!("writer1_span", x = 42);
1160+
let _e = span1.enter();
1161+
tracing::info!(target: "writer2", "hello writer2!");
1162+
let span2 = tracing::info_span!(target: "writer2", "writer2_span");
1163+
let _e = span2.enter();
1164+
tracing::warn!(target: "writer1", "hello writer1!");
1165+
});
1166+
1167+
let actual = sanitize_timings(make_writer1.get_string());
1168+
assert_eq!(
1169+
"fake time writer1_span{x=42}:writer2_span: hello writer1!\n\
1170+
fake time writer1_span{x=42}: close timing timing\n",
1171+
actual.as_str()
1172+
);
1173+
let actual = sanitize_timings(make_writer2.get_string());
1174+
assert_eq!(
1175+
"fake time writer1_span{x=42}: hello writer2!\n\
1176+
fake time writer1_span{x=42}:writer2_span: close timing timing\n",
1177+
actual.as_str()
1178+
);
1179+
}
11131180
}

tracing-subscriber/src/fmt/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,11 @@ mod test {
11891189
pub(crate) fn buf(&self) -> MutexGuard<'a, Vec<u8>> {
11901190
self.buf.lock().unwrap()
11911191
}
1192+
1193+
pub(crate) fn get_string(&self) -> String {
1194+
String::from_utf8(self.buf.lock().unwrap().clone())
1195+
.expect("subscriber must write valid UTF-8")
1196+
}
11921197
}
11931198

11941199
impl<'a> MakeWriter for MockMakeWriter<'a> {

tracing-subscriber/src/fmt/writer.rs

Lines changed: 178 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
//! Abstractions for creating [`io::Write`] instances.
22
//!
33
//! [`io::Write`]: https://doc.rust-lang.org/std/io/trait.Write.html
4-
5-
use io::Write;
6-
use std::{fmt::Debug, io};
4+
use std::{
5+
fmt::Debug,
6+
io::{self, Write},
7+
};
8+
use tracing_core::Metadata;
79

810
/// A type that can create [`io::Write`] instances.
911
///
@@ -13,12 +15,75 @@ use std::{fmt::Debug, io};
1315
/// This trait is already implemented for function pointers and immutably-borrowing closures that
1416
/// return an instance of [`io::Write`], such as [`io::stdout`] and [`io::stderr`].
1517
///
16-
/// [`io::Write`]: https://doc.rust-lang.org/std/io/trait.Write.html
17-
/// [`fmt::Subscriber`]: ../../fmt/struct.Subscriber.html
18-
/// [`fmt::Layer`]: ../../fmt/struct.Layer.html
19-
/// [`Event`]: https://docs.rs/tracing-core/0.1.5/tracing_core/event/struct.Event.html
20-
/// [`io::stdout`]: https://doc.rust-lang.org/std/io/fn.stdout.html
21-
/// [`io::stderr`]: https://doc.rust-lang.org/std/io/fn.stderr.html
18+
/// The [`MakeWriter::make_writer_for`] method takes [`Metadata`] describing a
19+
/// span or event and returns a writer. `MakeWriter`s can optionally provide
20+
/// implementations of this method with behaviors that differ based on the span
21+
/// or event being written. For example, events at different [levels] might be
22+
/// written to different output streams, or data from different [targets] might
23+
/// be written to separate log files. When the `MakeWriter` has no custom
24+
/// behavior based on metadata, the default implementation of `make_writer_for`
25+
/// simply calls `self.make_writer()`, ignoring the metadata. Therefore, when
26+
/// metadata _is_ available, callers should prefer to call `make_writer_for`,
27+
/// passing in that metadata, so that the `MakeWriter` implementation can choose
28+
/// the appropriate behavior.
29+
///
30+
/// # Examples
31+
///
32+
/// The simplest usage is to pass in a named function that returns a writer. For
33+
/// example, to log all events to stderr, we could write:
34+
/// ```
35+
/// let subscriber = tracing_subscriber::fmt()
36+
/// .with_writer(std::io::stderr)
37+
/// .finish();
38+
/// # drop(subscriber);
39+
/// ```
40+
///
41+
/// Any function that returns a writer can be used:
42+
///
43+
/// ```
44+
/// fn make_my_great_writer() -> impl std::io::Write {
45+
/// // ...
46+
/// # std::io::stdout()
47+
/// }
48+
///
49+
/// let subscriber = tracing_subscriber::fmt()
50+
/// .with_writer(make_my_great_writer)
51+
/// .finish();
52+
/// # drop(subscriber);
53+
/// ```
54+
///
55+
/// A closure can be used to introduce arbitrary logic into how the writer is
56+
/// created. Consider the (admittedly rather silly) example of sending every 5th
57+
/// event to stderr, and all other events to stdout:
58+
///
59+
/// ```
60+
/// use std::io;
61+
/// use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
62+
///
63+
/// let n = AtomicUsize::new(0);
64+
/// let subscriber = tracing_subscriber::fmt()
65+
/// .with_writer(move || -> Box<dyn io::Write> {
66+
/// if n.fetch_add(1, Relaxed) % 5 == 0 {
67+
/// Box::new(io::stderr())
68+
/// } else {
69+
/// Box::new(io::stdout())
70+
/// }
71+
/// })
72+
/// .finish();
73+
/// # drop(subscriber);
74+
/// ```
75+
///
76+
/// [`io::Write`]: std::io::Write
77+
/// [`fmt::Collector`]: super::super::fmt::Collector
78+
/// [`fmt::Subscriber`]: super::super::fmt::Subscriber
79+
/// [`Event`]: tracing_core::event::Event
80+
/// [`io::stdout`]: std::io::stdout()
81+
/// [`io::stderr`]: std::io::stderr()
82+
/// [mutex]: std::sync::Mutex
83+
/// [`MakeWriter::make_writer_for`]: MakeWriter::make_writer_for
84+
/// [`Metadata`]: tracing_core::Metadata
85+
/// [levels]: tracing_core::Level
86+
/// [targets]: tracing_core::Metadata::target
2287
pub trait MakeWriter {
2388
/// The concrete [`io::Write`] implementation returned by [`make_writer`].
2489
///
@@ -36,11 +101,100 @@ pub trait MakeWriter {
36101
/// [`MakeWriter`] to improve performance.
37102
///
38103
/// [`Writer`]: #associatedtype.Writer
39-
/// [`fmt::Layer`]: ../../fmt/struct.Layer.html
40-
/// [`fmt::Subscriber`]: ../../fmt/struct.Subscriber.html
41-
/// [`io::Write`]: https://doc.rust-lang.org/std/io/trait.Write.html
42-
/// [`MakeWriter`]: trait.MakeWriter.html
104+
/// [`fmt::Layer`]: crate::fmt::Layer
105+
/// [`fmt::Subscriber`]: crate::fmt::Subscriber
106+
/// [`io::Write`]: std::io::Write
43107
fn make_writer(&self) -> Self::Writer;
108+
109+
/// Returns a [`Writer`] for writing data from the span or event described
110+
/// by the provided [`Metadata`].
111+
///
112+
/// By default, this calls [`self.make_writer()`][make_writer], ignoring
113+
/// the provided metadata, but implementations can override this to provide
114+
/// metadata-specific behaviors.
115+
///
116+
/// This method allows `MakeWriter` implementations to implement different
117+
/// behaviors based on the span or event being written. The `MakeWriter`
118+
/// type might return different writers based on the provided metadata, or
119+
/// might write some values to the writer before or after providing it to
120+
/// the caller.
121+
///
122+
/// For example, we might want to write data from spans and events at the
123+
/// [`ERROR`] and [`WARN`] levels to `stderr`, and data from spans or events
124+
/// at lower levels to stdout:
125+
///
126+
/// ```
127+
/// use std::io::{self, Stdout, Stderr};
128+
/// use tracing_subscriber::fmt::writer::MakeWriter;
129+
/// use tracing_core::{Metadata, Level};
130+
///
131+
/// pub struct MyMakeWriter {}
132+
///
133+
/// /// A lock on either stdout or stderr, depending on the verbosity level
134+
/// /// of the event being written.
135+
/// pub enum Stdio {
136+
/// Stdout(Stdout),
137+
/// Stderr(Stderr),
138+
/// }
139+
///
140+
/// impl io::Write for Stdio {
141+
/// fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
142+
/// match self {
143+
/// Stdio::Stdout(io) => io.write(buf),
144+
/// Stdio::Stderr(io) => io.write(buf),
145+
/// }
146+
/// }
147+
///
148+
/// fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
149+
/// // ...
150+
/// # match self {
151+
/// # Stdio::Stdout(io) => io.write_all(buf),
152+
/// # Stdio::Stderr(io) => io.write_all(buf),
153+
/// # }
154+
/// }
155+
///
156+
/// fn flush(&mut self) -> io::Result<()> {
157+
/// // ...
158+
/// # match self {
159+
/// # Stdio::Stdout(io) => io.flush(),
160+
/// # Stdio::Stderr(io) => io.flush(),
161+
/// # }
162+
/// }
163+
/// }
164+
///
165+
/// impl MakeWriter for MyMakeWriter {
166+
/// type Writer = Stdio;
167+
///
168+
/// fn make_writer(&self) -> Self::Writer {
169+
/// // We must have an implementation of `make_writer` that makes
170+
/// // a "default" writer without any configuring metadata. Let's
171+
/// // just return stdout in that case.
172+
/// Stdio::Stdout(io::stdout())
173+
/// }
174+
///
175+
/// fn make_writer_for(&self, meta: &Metadata<'_>) -> Self::Writer {
176+
/// // Here's where we can implement our special behavior. We'll
177+
/// // check if the metadata's verbosity level is WARN or ERROR,
178+
/// // and return stderr in that case.
179+
/// if meta.level() <= &Level::WARN {
180+
/// return Stdio::Stderr(io::stderr());
181+
/// }
182+
///
183+
/// // Otherwise, we'll return stdout.
184+
/// Stdio::Stdout(io::stdout())
185+
/// }
186+
/// }
187+
/// ```
188+
///
189+
/// [`Writer`]: MakeWriter::Writer
190+
/// [`Metadata`]: tracing_core::Metadata
191+
/// [make_writer]: MakeWriter::make_writer
192+
/// [`WARN`]: tracing_core::Level::WARN
193+
/// [`ERROR`]: tracing_core::Level::ERROR
194+
fn make_writer_for(&self, meta: &Metadata<'_>) -> Self::Writer {
195+
let _ = meta;
196+
self.make_writer()
197+
}
44198
}
45199

46200
impl<F, W> MakeWriter for F
@@ -162,6 +316,10 @@ impl MakeWriter for BoxMakeWriter {
162316
fn make_writer(&self) -> Self::Writer {
163317
self.inner.make_writer()
164318
}
319+
320+
fn make_writer_for(&self, meta: &Metadata<'_>) -> Self::Writer {
321+
self.inner.make_writer_for(meta)
322+
}
165323
}
166324

167325
struct Boxed<M>(M);
@@ -174,7 +332,13 @@ where
174332
type Writer = Box<dyn Write>;
175333

176334
fn make_writer(&self) -> Self::Writer {
177-
Box::new(self.0.make_writer())
335+
let w = self.0.make_writer();
336+
Box::new(w)
337+
}
338+
339+
fn make_writer_for(&self, meta: &Metadata<'_>) -> Self::Writer {
340+
let w = self.0.make_writer_for(meta);
341+
Box::new(w)
178342
}
179343
}
180344

0 commit comments

Comments
 (0)