Skip to content

Commit b3af7e1

Browse files
authored
subscriber: add MakeWriter::make_writer_for (#1141)
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 b15b119 commit b3af7e1

File tree

2 files changed

+189
-6
lines changed

2 files changed

+189
-6
lines changed

tracing-subscriber/src/fmt/fmt_subscriber.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ where
692692

693693
let ctx = self.make_ctx(ctx);
694694
if self.fmt_event.format_event(&ctx, &mut buf, event).is_ok() {
695-
let mut writer = self.make_writer.make_writer();
695+
let mut writer = self.make_writer.make_writer_for(event.metadata());
696696
let _ = io::Write::write_all(&mut writer, buf.as_bytes());
697697
}
698698

@@ -858,11 +858,12 @@ impl Timings {
858858

859859
#[cfg(test)]
860860
mod test {
861+
use super::*;
861862
use crate::fmt::{
862863
self,
863864
format::{self, test::MockTime, Format},
864865
subscribe::Subscribe as _,
865-
test::MockMakeWriter,
866+
test::{MockMakeWriter, MockWriter},
866867
time,
867868
};
868869
use crate::Registry;
@@ -1035,4 +1036,66 @@ mod test {
10351036
actual.as_str()
10361037
);
10371038
}
1039+
1040+
#[test]
1041+
fn make_writer_based_on_meta() {
1042+
struct MakeByTarget {
1043+
make_writer1: MockMakeWriter,
1044+
make_writer2: MockMakeWriter,
1045+
}
1046+
1047+
impl<'a> MakeWriter<'a> for MakeByTarget {
1048+
type Writer = MockWriter;
1049+
1050+
fn make_writer(&'a self) -> Self::Writer {
1051+
self.make_writer1.make_writer()
1052+
}
1053+
1054+
fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
1055+
if meta.target() == "writer2" {
1056+
return self.make_writer2.make_writer();
1057+
}
1058+
self.make_writer()
1059+
}
1060+
}
1061+
1062+
let make_writer1 = MockMakeWriter::default();
1063+
let make_writer2 = MockMakeWriter::default();
1064+
1065+
let make_writer = MakeByTarget {
1066+
make_writer1: make_writer1.clone(),
1067+
make_writer2: make_writer2.clone(),
1068+
};
1069+
1070+
let subscriber = crate::fmt::Collector::builder()
1071+
.with_writer(make_writer)
1072+
.with_level(false)
1073+
.with_target(false)
1074+
.with_ansi(false)
1075+
.with_timer(MockTime)
1076+
.with_span_events(FmtSpan::CLOSE)
1077+
.finish();
1078+
1079+
with_default(subscriber, || {
1080+
let span1 = tracing::info_span!("writer1_span", x = 42);
1081+
let _e = span1.enter();
1082+
tracing::info!(target: "writer2", "hello writer2!");
1083+
let span2 = tracing::info_span!(target: "writer2", "writer2_span");
1084+
let _e = span2.enter();
1085+
tracing::warn!(target: "writer1", "hello writer1!");
1086+
});
1087+
1088+
let actual = sanitize_timings(make_writer1.get_string());
1089+
assert_eq!(
1090+
"fake time writer1_span{x=42}:writer2_span: hello writer1!\n\
1091+
fake time writer1_span{x=42}: close timing timing\n",
1092+
actual.as_str()
1093+
);
1094+
let actual = sanitize_timings(make_writer2.get_string());
1095+
assert_eq!(
1096+
"fake time writer1_span{x=42}: hello writer2!\n\
1097+
fake time writer1_span{x=42}:writer2_span: close timing timing\n",
1098+
actual.as_str()
1099+
);
1100+
}
10381101
}

tracing-subscriber/src/fmt/writer.rs

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77
io::{self, Write},
88
sync::{Mutex, MutexGuard},
99
};
10+
use tracing_core::Metadata;
1011

1112
/// A type that can create [`io::Write`] instances.
1213
///
@@ -19,6 +20,18 @@ use std::{
1920
/// [`std::sync::Mutex`][mutex] when the type inside the mutex implements
2021
/// [`io::Write`].
2122
///
23+
/// The [`MakeWriter::make_writer_for`] method takes [`Metadata`] describing a
24+
/// span or event and returns a writer. `MakeWriter`s can optionally provide
25+
/// implementations of this method with behaviors that differ based on the span
26+
/// or event being written. For example, events at different [levels] might be
27+
/// written to different output streams, or data from different [targets] might
28+
/// be written to separate log files. When the `MakeWriter` has no custom
29+
/// behavior based on metadata, the default implementation of `make_writer_for`
30+
/// simply calls `self.make_writer()`, ignoring the metadata. Therefore, when
31+
/// metadata _is_ available, callers should prefer to call `make_writer_for`,
32+
/// passing in that metadata, so that the `MakeWriter` implementation can choose
33+
/// the appropriate behavior.
34+
///
2235
/// # Examples
2336
///
2437
/// The simplest usage is to pass in a named function that returns a writer. For
@@ -89,6 +102,10 @@ use std::{
89102
/// [`io::stdout`]: std::io::stdout()
90103
/// [`io::stderr`]: std::io::stderr()
91104
/// [mutex]: std::sync::Mutex
105+
/// [`MakeWriter::make_writer_for`]: MakeWriter::make_writer_for
106+
/// [`Metadata`]: tracing_core::Metadata
107+
/// [levels]: tracing_core::Level
108+
/// [targets]: tracing_core::Metadata::target
92109
pub trait MakeWriter<'a> {
93110
/// The concrete [`io::Write`] implementation returned by [`make_writer`].
94111
///
@@ -100,16 +117,110 @@ pub trait MakeWriter<'a> {
100117
///
101118
/// # Implementer notes
102119
///
103-
/// [`fmt::Subscriber`] or [`fmt::Collector`] will call this method each time an event is recorded. Ensure any state
104-
/// that must be saved across writes is not lost when the [`Writer`] instance is dropped. If
105-
/// creating a [`io::Write`] instance is expensive, be sure to cache it when implementing
106-
/// [`MakeWriter`] to improve performance.
120+
/// [`fmt::Subscriber`] or [`fmt::Collector`] will call this method each
121+
/// time an event is recorded. Ensure any state that must be saved across
122+
/// writes is not lost when the [`Writer`] instance is dropped. If creating
123+
/// a [`io::Write`] instance is expensive, be sure to cache it when
124+
/// implementing [`MakeWriter`] to improve performance.
107125
///
108126
/// [`Writer`]: MakeWriter::Writer
109127
/// [`fmt::Subscriber`]: super::super::fmt::Subscriber
110128
/// [`fmt::Collector`]: super::super::fmt::Collector
111129
/// [`io::Write`]: std::io::Write
112130
fn make_writer(&'a self) -> Self::Writer;
131+
132+
/// Returns a [`Writer`] for writing data from the span or event described
133+
/// by the provided [`Metadata`].
134+
///
135+
/// By default, this calls [`self.make_writer()`][make_writer], ignoring
136+
/// the provided metadata, but implementations can override this to provide
137+
/// metadata-specific behaviors.
138+
///
139+
/// This method allows `MakeWriter` implementations to implement different
140+
/// behaviors based on the span or event being written. The `MakeWriter`
141+
/// type might return different writers based on the provided metadata, or
142+
/// might write some values to the writer before or after providing it to
143+
/// the caller.
144+
///
145+
/// For example, we might want to write data from spans and events at the
146+
/// [`ERROR`] and [`WARN`] levels to `stderr`, and data from spans or events
147+
/// at lower levels to stdout:
148+
///
149+
/// ```
150+
/// use std::io::{self, Stdout, Stderr, StdoutLock, StderrLock};
151+
/// use tracing_subscriber::fmt::writer::MakeWriter;
152+
/// use tracing_core::{Metadata, Level};
153+
///
154+
/// pub struct MyMakeWriter {
155+
/// stdout: Stdout,
156+
/// stderr: Stderr,
157+
/// }
158+
///
159+
/// /// A lock on either stdout or stderr, depending on the verbosity level
160+
/// /// of the event being written.
161+
/// pub enum StdioLock<'a> {
162+
/// Stdout(StdoutLock<'a>),
163+
/// Stderr(StderrLock<'a>),
164+
/// }
165+
///
166+
/// impl<'a> io::Write for StdioLock<'a> {
167+
/// fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
168+
/// match self {
169+
/// StdioLock::Stdout(lock) => lock.write(buf),
170+
/// StdioLock::Stderr(lock) => lock.write(buf),
171+
/// }
172+
/// }
173+
///
174+
/// fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
175+
/// // ...
176+
/// # match self {
177+
/// # StdioLock::Stdout(lock) => lock.write_all(buf),
178+
/// # StdioLock::Stderr(lock) => lock.write_all(buf),
179+
/// # }
180+
/// }
181+
///
182+
/// fn flush(&mut self) -> io::Result<()> {
183+
/// // ...
184+
/// # match self {
185+
/// # StdioLock::Stdout(lock) => lock.flush(),
186+
/// # StdioLock::Stderr(lock) => lock.flush(),
187+
/// # }
188+
/// }
189+
/// }
190+
///
191+
/// impl<'a> MakeWriter<'a> for MyMakeWriter {
192+
/// type Writer = StdioLock<'a>;
193+
///
194+
/// fn make_writer(&'a self) -> Self::Writer {
195+
/// // We must have an implementation of `make_writer` that makes
196+
/// // a "default" writer without any configuring metadata. Let's
197+
/// // just return stdout in that case.
198+
/// StdioLock::Stdout(self.stdout.lock())
199+
/// }
200+
///
201+
/// fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
202+
/// // Here's where we can implement our special behavior. We'll
203+
/// // check if the metadata's verbosity level is WARN or ERROR,
204+
/// // and return stderr in that case.
205+
/// if meta.level() <= &Level::WARN {
206+
/// return StdioLock::Stderr(self.stderr.lock());
207+
/// }
208+
///
209+
/// // Otherwise, we'll return stdout.
210+
/// StdioLock::Stdout(self.stdout.lock())
211+
/// }
212+
/// }
213+
/// ```
214+
///
215+
/// [`Writer`]: MakeWriter::Writer
216+
/// [`Metadata`]: tracing_core::Metadata
217+
/// [make_writer]: MakeWriter::make_writer
218+
/// [`WARN`]: tracing_core::Level::WARN
219+
/// [`ERROR`]: tracing_core::Level::ERROR
220+
fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
221+
let _ = meta;
222+
self.make_writer()
223+
}
113224
}
114225

115226
/// A type implementing [`io::Write`] for a [`MutexGuard`] where the type
@@ -248,6 +359,10 @@ impl<'a> MakeWriter<'a> for BoxMakeWriter {
248359
fn make_writer(&'a self) -> Self::Writer {
249360
self.inner.make_writer()
250361
}
362+
363+
fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
364+
self.inner.make_writer_for(meta)
365+
}
251366
}
252367

253368
struct Boxed<M>(M);
@@ -262,6 +377,11 @@ where
262377
let w = self.0.make_writer();
263378
Box::new(w)
264379
}
380+
381+
fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
382+
let w = self.0.make_writer_for(meta);
383+
Box::new(w)
384+
}
265385
}
266386

267387
// === impl Mutex/MutexGuardWriter ===

0 commit comments

Comments
 (0)