Skip to content

Commit eb16f13

Browse files
committed
Allow StdStreamSink via print macro, add a new feature gate test
1 parent c028239 commit eb16f13

File tree

5 files changed

+159
-47
lines changed

5 files changed

+159
-47
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
matrix:
6363
os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
6464
fn_features: ['', 'log native libsystemd multi-thread runtime-pattern serde serde_json sval']
65-
cfg_feature: ['', 'flexible-string', 'source-location']
65+
cfg_feature: ['', 'flexible-string', 'source-location', 'test']
6666
runs-on: ${{ matrix.os }}
6767
steps:
6868
- name: Checkout repository

spdlog/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ release-level-info = []
3333
release-level-debug = []
3434
release-level-trace = []
3535

36+
test = []
3637
source-location = []
3738
native = []
3839
libsystemd = ["dep:libsystemd-sys"]

spdlog/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@
217217
//!
218218
//! - `serde_json` enables [`formatter::JsonFormatter`].
219219
//!
220+
//! - `test` changes the default behavior of [`StdStreamSink`] to use print
221+
//! macros. See [`StdStreamSinkBuilder::via_print_macro`] for more details.
222+
//!
220223
//! # Supported Rust versions
221224
//!
222225
//! <!--
@@ -279,6 +282,7 @@
279282
//! [log crate]: https://crates.io/crates/log
280283
//! [`Formatter`]: crate::formatter::Formatter
281284
//! [`RuntimePattern`]: crate::formatter::RuntimePattern
285+
//! [`StdStreamSinkBuilder::via_print_macro`]: sink::StdStreamSinkBuilder::via_print_macro
282286
//! [`RotationPolicy::Daily`]: crate::sink::RotationPolicy::Daily
283287
//! [`RotationPolicy::Hourly`]: crate::sink::RotationPolicy::Hourly
284288

spdlog/src/sink/std_stream_sink.rs

Lines changed: 128 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ pub enum StdStream {
2121
Stderr,
2222
}
2323

24+
impl StdStream {
25+
fn via_write(&self) -> StdStreamDest<io::StdoutLock<'_>, io::StderrLock<'_>> {
26+
match self {
27+
Self::Stdout => StdStreamDest::Stdout(io::stdout().lock()),
28+
Self::Stderr => StdStreamDest::Stderr(io::stderr().lock()),
29+
}
30+
}
31+
32+
fn via_macro(&self) -> StdStreamDest<via_macro::Stdout, via_macro::Stderr> {
33+
match self {
34+
Self::Stdout => StdStreamDest::Stdout(via_macro::Stdout),
35+
Self::Stderr => StdStreamDest::Stderr(via_macro::Stderr),
36+
}
37+
}
38+
}
39+
2440
// `io::stdout()` and `io::stderr()` return different types, and
2541
// `Std***::lock()` is not in any trait, so we need this struct to abstract
2642
// them.
@@ -30,27 +46,12 @@ enum StdStreamDest<O, E> {
3046
Stderr(E),
3147
}
3248

33-
impl StdStreamDest<io::Stdout, io::Stderr> {
34-
#[must_use]
35-
fn new(stream: StdStream) -> Self {
36-
match stream {
37-
StdStream::Stdout => StdStreamDest::Stdout(io::stdout()),
38-
StdStream::Stderr => StdStreamDest::Stderr(io::stderr()),
39-
}
40-
}
41-
42-
#[must_use]
43-
fn lock(&self) -> StdStreamDest<io::StdoutLock<'_>, io::StderrLock<'_>> {
44-
match self {
45-
StdStreamDest::Stdout(stream) => StdStreamDest::Stdout(stream.lock()),
46-
StdStreamDest::Stderr(stream) => StdStreamDest::Stderr(stream.lock()),
47-
}
48-
}
49-
49+
impl<O, E> StdStreamDest<O, E> {
50+
#[allow(dead_code)]
5051
fn stream_type(&self) -> StdStream {
5152
match self {
52-
StdStreamDest::Stdout(_) => StdStream::Stdout,
53-
StdStreamDest::Stderr(_) => StdStream::Stderr,
53+
Self::Stdout(_) => StdStream::Stdout,
54+
Self::Stderr(_) => StdStream::Stderr,
5455
}
5556
}
5657
}
@@ -74,8 +75,42 @@ macro_rules! impl_write_for_dest {
7475
}
7576
};
7677
}
77-
impl_write_for_dest!(StdStreamDest<io::Stdout, io::Stderr>);
7878
impl_write_for_dest!(StdStreamDest<io::StdoutLock<'_>, io::StderrLock<'_>>);
79+
impl_write_for_dest!(StdStreamDest<via_macro::Stdout, via_macro::Stderr>);
80+
81+
mod via_macro {
82+
use super::*;
83+
84+
fn bytes_to_str(buf: &[u8]) -> io::Result<&str> {
85+
str::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
86+
}
87+
88+
pub(crate) struct Stdout;
89+
90+
impl Write for Stdout {
91+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
92+
print!("{}", bytes_to_str(buf)?);
93+
Ok(buf.len())
94+
}
95+
96+
fn flush(&mut self) -> io::Result<()> {
97+
io::stdout().flush()
98+
}
99+
}
100+
101+
pub(crate) struct Stderr;
102+
103+
impl Write for Stderr {
104+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
105+
eprint!("{}", bytes_to_str(buf)?);
106+
Ok(buf.len())
107+
}
108+
109+
fn flush(&mut self) -> io::Result<()> {
110+
io::stderr().flush()
111+
}
112+
}
113+
}
79114

80115
/// A sink with a std stream as the target.
81116
///
@@ -85,35 +120,39 @@ impl_write_for_dest!(StdStreamDest<io::StdoutLock<'_>, io::StderrLock<'_>>);
85120
/// Note that this sink always flushes the buffer once with each logging.
86121
pub struct StdStreamSink {
87122
prop: SinkProp,
88-
dest: StdStreamDest<io::Stdout, io::Stderr>,
123+
via_print_macro: bool,
124+
std_stream: StdStream,
89125
should_render_style: bool,
90126
level_styles: LevelStyles,
91127
}
92128

93129
impl StdStreamSink {
94130
/// Gets a builder of `StdStreamSink` with default parameters:
95131
///
96-
/// | Parameter | Default Value |
97-
/// |-------------------|-----------------------------|
98-
/// | [level_filter] | `All` |
99-
/// | [formatter] | `FullFormatter` |
100-
/// | [error_handler] | [`ErrorHandler::default()`] |
101-
/// | | |
102-
/// | [std_stream] | *must be specified* |
103-
/// | [style_mode] | `Auto` |
132+
/// | Parameter | Default Value |
133+
/// |-------------------|------------------------------------------------------|
134+
/// | [level_filter] | `All` |
135+
/// | [formatter] | `FullFormatter` |
136+
/// | [error_handler] | [`ErrorHandler::default()`] |
137+
/// | | |
138+
/// | [std_stream] | *must be specified* |
139+
/// | [style_mode] | `Auto` |
140+
/// | [via_print_macro] | `false`, or `true` if feature gate `test` is enabled |
104141
///
105142
/// [level_filter]: StdStreamSinkBuilder::level_filter
106143
/// [formatter]: StdStreamSinkBuilder::formatter
107144
/// [error_handler]: StdStreamSinkBuilder::error_handler
108145
/// [`ErrorHandler::default()`]: crate::error::ErrorHandler::default()
109146
/// [std_stream]: StdStreamSinkBuilder::std_stream
110147
/// [style_mode]: StdStreamSinkBuilder::style_mode
148+
/// [via_print_macro]: StdStreamSinkBuilder::via_print_macro
111149
#[must_use]
112150
pub fn builder() -> StdStreamSinkBuilder<()> {
113151
StdStreamSinkBuilder {
114152
prop: SinkProp::default(),
115153
std_stream: (),
116154
style_mode: StyleMode::Auto,
155+
via_print_macro: cfg!(feature = "test"),
117156
}
118157
}
119158

@@ -138,7 +177,7 @@ impl StdStreamSink {
138177

139178
/// Sets the style mode.
140179
pub fn set_style_mode(&mut self, style_mode: StyleMode) {
141-
self.should_render_style = Self::should_render_style(style_mode, self.dest.stream_type());
180+
self.should_render_style = Self::should_render_style(style_mode, self.std_stream);
142181
}
143182

144183
#[must_use]
@@ -171,8 +210,34 @@ impl Sink for StdStreamSink {
171210
.formatter()
172211
.format(record, &mut string_buf, &mut ctx)?;
173212

174-
let mut dest = self.dest.lock();
213+
if !self.via_print_macro {
214+
self.log_write(record, &string_buf, &ctx, self.std_stream.via_write())
215+
} else {
216+
self.log_write(record, &string_buf, &ctx, self.std_stream.via_macro())
217+
}
218+
}
219+
220+
fn flush(&self) -> Result<()> {
221+
if !self.via_print_macro {
222+
self.std_stream.via_write().flush()
223+
} else {
224+
self.std_stream.via_macro().flush()
225+
}
226+
.map_err(Error::FlushBuffer)
227+
}
228+
}
175229

230+
impl StdStreamSink {
231+
fn log_write<O: Write, E: Write>(
232+
&self,
233+
record: &Record,
234+
string_buf: &StringBuf,
235+
ctx: &FormatterContext<'_>,
236+
mut dest: StdStreamDest<O, E>,
237+
) -> Result<()>
238+
where
239+
StdStreamDest<O, E>: Write,
240+
{
176241
(|| {
177242
// TODO: Simplify the if block when our MSRV reaches let-chain support.
178243
if self.should_render_style {
@@ -197,16 +262,12 @@ impl Sink for StdStreamSink {
197262

198263
// stderr is not buffered, so we don't need to flush it.
199264
// https://doc.rust-lang.org/std/io/fn.stderr.html
200-
if let StdStreamDest::Stdout(_) = dest {
265+
if let StdStream::Stdout = self.std_stream {
201266
dest.flush().map_err(Error::FlushBuffer)?;
202267
}
203268

204269
Ok(())
205270
}
206-
207-
fn flush(&self) -> Result<()> {
208-
self.dest.lock().flush().map_err(Error::FlushBuffer)
209-
}
210271
}
211272

212273
// --------------------------------------------------
@@ -217,6 +278,7 @@ pub struct StdStreamSinkBuilder<ArgSS> {
217278
prop: SinkProp,
218279
std_stream: ArgSS,
219280
style_mode: StyleMode,
281+
via_print_macro: bool,
220282
}
221283

222284
impl<ArgSS> StdStreamSinkBuilder<ArgSS> {
@@ -245,6 +307,7 @@ impl<ArgSS> StdStreamSinkBuilder<ArgSS> {
245307
prop: self.prop,
246308
std_stream,
247309
style_mode: self.style_mode,
310+
via_print_macro: self.via_print_macro,
248311
}
249312
}
250313

@@ -257,6 +320,33 @@ impl<ArgSS> StdStreamSinkBuilder<ArgSS> {
257320
self
258321
}
259322

323+
/// Specifies to use `print!` and `eprint!` macros for output.
324+
///
325+
/// If enabled, the sink will use [`print!`] and [`eprint!`] macros instead
326+
/// of [`io::Write`] trait with [`io::stdout`] and [`io::stderr`] to output
327+
/// logs. This is useful if you want the logs to be [captured] by `cargo
328+
/// test` and `cargo bench`.
329+
///
330+
/// This parameter is **optional**, and defaults to `false`, or defaults to
331+
/// `true` if feature gate `test` is enabled.
332+
///
333+
/// A convienient way to enable it for `cargo test` and `cargo bench` is to
334+
/// add the following lines to your `Cargo.toml`:
335+
///
336+
/// ```toml
337+
/// # Note that it's not [dependencies]
338+
///
339+
/// [dev-dependencies]
340+
/// spdlog-rs = { version = "...", features = ["test"] }
341+
/// ```
342+
///
343+
/// [captured]: https://doc.rust-lang.org/book/ch11-02-running-tests.html#showing-function-output
344+
#[must_use]
345+
pub fn via_print_macro(mut self) -> Self {
346+
self.via_print_macro = true;
347+
self
348+
}
349+
260350
// Prop
261351
//
262352

@@ -305,7 +395,8 @@ impl StdStreamSinkBuilder<StdStream> {
305395
pub fn build(self) -> Result<StdStreamSink> {
306396
Ok(StdStreamSink {
307397
prop: self.prop,
308-
dest: StdStreamDest::new(self.std_stream),
398+
via_print_macro: self.via_print_macro,
399+
std_stream: self.std_stream,
309400
should_render_style: StdStreamSink::should_render_style(
310401
self.style_mode,
311402
self.std_stream,

spdlog/tests/broken_stdio.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,33 @@
22
//
33
// Rust's print macros will panic if a write fails, we should avoid using
44
// print macros internally.
5+
6+
#[cfg(target_os = "linux")]
57
fn main() {
6-
#[cfg(target_family = "unix")]
7-
{
8-
let dev_full = std::ffi::CString::new("/dev/full").unwrap();
9-
unsafe {
10-
let fd = libc::open(dev_full.as_ptr(), libc::O_WRONLY);
11-
libc::dup2(fd, libc::STDOUT_FILENO);
12-
libc::dup2(fd, libc::STDERR_FILENO);
8+
#[cfg(not(feature = "test"))]
9+
run(); // Should not panic
10+
11+
// Expect this test to panic when the "test" feature is enabled, because we
12+
// intentionally use print macros in `StdStreamSink` for capturing output
13+
// for `cargo test`.
14+
#[cfg(feature = "test")]
15+
assert!(std::panic::catch_unwind(run).is_err());
16+
17+
fn run() {
18+
{
19+
let dev_full = std::ffi::CString::new("/dev/full").unwrap();
20+
unsafe {
21+
let fd = libc::open(dev_full.as_ptr(), libc::O_WRONLY);
22+
libc::dup2(fd, libc::STDOUT_FILENO);
23+
libc::dup2(fd, libc::STDERR_FILENO);
24+
}
1325
}
26+
spdlog::info!("will panic if print macros are used internally");
27+
spdlog::error!("will panic if print macros are used internally");
1428
}
29+
}
30+
31+
#[cfg(not(target_os = "linux"))]
32+
fn main() {
1533
// TODO: Other platforms?
16-
spdlog::info!("will panic if print macros are used internally");
17-
spdlog::error!("will panic if print macros are used internally");
1834
}

0 commit comments

Comments
 (0)