Skip to content

Commit a21a514

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

File tree

5 files changed

+160
-47
lines changed

5 files changed

+160
-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: 129 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::{
44
convert::Infallible,
55
io::{self, Write},
6+
str,
67
};
78

89
use crate::{
@@ -21,6 +22,22 @@ pub enum StdStream {
2122
Stderr,
2223
}
2324

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

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-
50+
impl<O, E> StdStreamDest<O, E> {
51+
#[allow(dead_code)]
5052
fn stream_type(&self) -> StdStream {
5153
match self {
52-
StdStreamDest::Stdout(_) => StdStream::Stdout,
53-
StdStreamDest::Stderr(_) => StdStream::Stderr,
54+
Self::Stdout(_) => StdStream::Stdout,
55+
Self::Stderr(_) => StdStream::Stderr,
5456
}
5557
}
5658
}
@@ -74,8 +76,42 @@ macro_rules! impl_write_for_dest {
7476
}
7577
};
7678
}
77-
impl_write_for_dest!(StdStreamDest<io::Stdout, io::Stderr>);
7879
impl_write_for_dest!(StdStreamDest<io::StdoutLock<'_>, io::StderrLock<'_>>);
80+
impl_write_for_dest!(StdStreamDest<via_macro::Stdout, via_macro::Stderr>);
81+
82+
mod via_macro {
83+
use super::*;
84+
85+
fn bytes_to_str(buf: &[u8]) -> io::Result<&str> {
86+
str::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
87+
}
88+
89+
pub(crate) struct Stdout;
90+
91+
impl Write for Stdout {
92+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
93+
print!("{}", bytes_to_str(buf)?);
94+
Ok(buf.len())
95+
}
96+
97+
fn flush(&mut self) -> io::Result<()> {
98+
io::stdout().flush()
99+
}
100+
}
101+
102+
pub(crate) struct Stderr;
103+
104+
impl Write for Stderr {
105+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
106+
eprint!("{}", bytes_to_str(buf)?);
107+
Ok(buf.len())
108+
}
109+
110+
fn flush(&mut self) -> io::Result<()> {
111+
io::stderr().flush()
112+
}
113+
}
114+
}
79115

80116
/// A sink with a std stream as the target.
81117
///
@@ -85,35 +121,39 @@ impl_write_for_dest!(StdStreamDest<io::StdoutLock<'_>, io::StderrLock<'_>>);
85121
/// Note that this sink always flushes the buffer once with each logging.
86122
pub struct StdStreamSink {
87123
prop: SinkProp,
88-
dest: StdStreamDest<io::Stdout, io::Stderr>,
124+
via_print_macro: bool,
125+
std_stream: StdStream,
89126
should_render_style: bool,
90127
level_styles: LevelStyles,
91128
}
92129

93130
impl StdStreamSink {
94131
/// Gets a builder of `StdStreamSink` with default parameters:
95132
///
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` |
133+
/// | Parameter | Default Value |
134+
/// |-------------------|------------------------------------------------------|
135+
/// | [level_filter] | `All` |
136+
/// | [formatter] | `FullFormatter` |
137+
/// | [error_handler] | [`ErrorHandler::default()`] |
138+
/// | | |
139+
/// | [std_stream] | *must be specified* |
140+
/// | [style_mode] | `Auto` |
141+
/// | [via_print_macro] | `false`, or `true` if feature gate `test` is enabled |
104142
///
105143
/// [level_filter]: StdStreamSinkBuilder::level_filter
106144
/// [formatter]: StdStreamSinkBuilder::formatter
107145
/// [error_handler]: StdStreamSinkBuilder::error_handler
108146
/// [`ErrorHandler::default()`]: crate::error::ErrorHandler::default()
109147
/// [std_stream]: StdStreamSinkBuilder::std_stream
110148
/// [style_mode]: StdStreamSinkBuilder::style_mode
149+
/// [via_print_macro]: StdStreamSinkBuilder::via_print_macro
111150
#[must_use]
112151
pub fn builder() -> StdStreamSinkBuilder<()> {
113152
StdStreamSinkBuilder {
114153
prop: SinkProp::default(),
115154
std_stream: (),
116155
style_mode: StyleMode::Auto,
156+
via_print_macro: cfg!(feature = "test"),
117157
}
118158
}
119159

@@ -138,7 +178,7 @@ impl StdStreamSink {
138178

139179
/// Sets the style mode.
140180
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());
181+
self.should_render_style = Self::should_render_style(style_mode, self.std_stream);
142182
}
143183

144184
#[must_use]
@@ -171,8 +211,34 @@ impl Sink for StdStreamSink {
171211
.formatter()
172212
.format(record, &mut string_buf, &mut ctx)?;
173213

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

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

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

204270
Ok(())
205271
}
206-
207-
fn flush(&self) -> Result<()> {
208-
self.dest.lock().flush().map_err(Error::FlushBuffer)
209-
}
210272
}
211273

212274
// --------------------------------------------------
@@ -217,6 +279,7 @@ pub struct StdStreamSinkBuilder<ArgSS> {
217279
prop: SinkProp,
218280
std_stream: ArgSS,
219281
style_mode: StyleMode,
282+
via_print_macro: bool,
220283
}
221284

222285
impl<ArgSS> StdStreamSinkBuilder<ArgSS> {
@@ -245,6 +308,7 @@ impl<ArgSS> StdStreamSinkBuilder<ArgSS> {
245308
prop: self.prop,
246309
std_stream,
247310
style_mode: self.style_mode,
311+
via_print_macro: self.via_print_macro,
248312
}
249313
}
250314

@@ -257,6 +321,33 @@ impl<ArgSS> StdStreamSinkBuilder<ArgSS> {
257321
self
258322
}
259323

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

@@ -305,7 +396,8 @@ impl StdStreamSinkBuilder<StdStream> {
305396
pub fn build(self) -> Result<StdStreamSink> {
306397
Ok(StdStreamSink {
307398
prop: self.prop,
308-
dest: StdStreamDest::new(self.std_stream),
399+
via_print_macro: self.via_print_macro,
400+
std_stream: self.std_stream,
309401
should_render_style: StdStreamSink::should_render_style(
310402
self.style_mode,
311403
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)