Skip to content

Commit da976f2

Browse files
authored
Allow StdStreamSink via print macro, add a new feature gate test (#106)
1 parent c028239 commit da976f2

File tree

5 files changed

+164
-47
lines changed

5 files changed

+164
-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: 133 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
use std::{
44
convert::Infallible,
55
io::{self, Write},
6+
// Import `str` module for function `std::str::from_utf8`, because method `str::from_utf8` is
7+
// stabilized since Rust 1.87.
8+
//
9+
// TODO: Remove this import when our MSRV reaches Rust 1.87.
10+
str,
611
};
712

813
use crate::{
@@ -21,6 +26,22 @@ pub enum StdStream {
2126
Stderr,
2227
}
2328

29+
impl StdStream {
30+
fn via_write(&self) -> StdStreamDest<io::StdoutLock<'_>, io::StderrLock<'_>> {
31+
match self {
32+
Self::Stdout => StdStreamDest::Stdout(io::stdout().lock()),
33+
Self::Stderr => StdStreamDest::Stderr(io::stderr().lock()),
34+
}
35+
}
36+
37+
fn via_macro(&self) -> StdStreamDest<via_macro::Stdout, via_macro::Stderr> {
38+
match self {
39+
Self::Stdout => StdStreamDest::Stdout(via_macro::Stdout),
40+
Self::Stderr => StdStreamDest::Stderr(via_macro::Stderr),
41+
}
42+
}
43+
}
44+
2445
// `io::stdout()` and `io::stderr()` return different types, and
2546
// `Std***::lock()` is not in any trait, so we need this struct to abstract
2647
// them.
@@ -30,27 +51,12 @@ enum StdStreamDest<O, E> {
3051
Stderr(E),
3152
}
3253

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-
54+
impl<O, E> StdStreamDest<O, E> {
55+
#[allow(dead_code)]
5056
fn stream_type(&self) -> StdStream {
5157
match self {
52-
StdStreamDest::Stdout(_) => StdStream::Stdout,
53-
StdStreamDest::Stderr(_) => StdStream::Stderr,
58+
Self::Stdout(_) => StdStream::Stdout,
59+
Self::Stderr(_) => StdStream::Stderr,
5460
}
5561
}
5662
}
@@ -74,8 +80,42 @@ macro_rules! impl_write_for_dest {
7480
}
7581
};
7682
}
77-
impl_write_for_dest!(StdStreamDest<io::Stdout, io::Stderr>);
7883
impl_write_for_dest!(StdStreamDest<io::StdoutLock<'_>, io::StderrLock<'_>>);
84+
impl_write_for_dest!(StdStreamDest<via_macro::Stdout, via_macro::Stderr>);
85+
86+
mod via_macro {
87+
use super::*;
88+
89+
fn bytes_to_str(buf: &[u8]) -> io::Result<&str> {
90+
str::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
91+
}
92+
93+
pub(crate) struct Stdout;
94+
95+
impl Write for Stdout {
96+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
97+
print!("{}", bytes_to_str(buf)?);
98+
Ok(buf.len())
99+
}
100+
101+
fn flush(&mut self) -> io::Result<()> {
102+
io::stdout().flush()
103+
}
104+
}
105+
106+
pub(crate) struct Stderr;
107+
108+
impl Write for Stderr {
109+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
110+
eprint!("{}", bytes_to_str(buf)?);
111+
Ok(buf.len())
112+
}
113+
114+
fn flush(&mut self) -> io::Result<()> {
115+
io::stderr().flush()
116+
}
117+
}
118+
}
79119

80120
/// A sink with a std stream as the target.
81121
///
@@ -85,35 +125,39 @@ impl_write_for_dest!(StdStreamDest<io::StdoutLock<'_>, io::StderrLock<'_>>);
85125
/// Note that this sink always flushes the buffer once with each logging.
86126
pub struct StdStreamSink {
87127
prop: SinkProp,
88-
dest: StdStreamDest<io::Stdout, io::Stderr>,
128+
via_print_macro: bool,
129+
std_stream: StdStream,
89130
should_render_style: bool,
90131
level_styles: LevelStyles,
91132
}
92133

93134
impl StdStreamSink {
94135
/// Gets a builder of `StdStreamSink` with default parameters:
95136
///
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` |
137+
/// | Parameter | Default Value |
138+
/// |-------------------|------------------------------------------------------|
139+
/// | [level_filter] | `All` |
140+
/// | [formatter] | `FullFormatter` |
141+
/// | [error_handler] | [`ErrorHandler::default()`] |
142+
/// | | |
143+
/// | [std_stream] | *must be specified* |
144+
/// | [style_mode] | `Auto` |
145+
/// | [via_print_macro] | `false`, or `true` if feature gate `test` is enabled |
104146
///
105147
/// [level_filter]: StdStreamSinkBuilder::level_filter
106148
/// [formatter]: StdStreamSinkBuilder::formatter
107149
/// [error_handler]: StdStreamSinkBuilder::error_handler
108150
/// [`ErrorHandler::default()`]: crate::error::ErrorHandler::default()
109151
/// [std_stream]: StdStreamSinkBuilder::std_stream
110152
/// [style_mode]: StdStreamSinkBuilder::style_mode
153+
/// [via_print_macro]: StdStreamSinkBuilder::via_print_macro
111154
#[must_use]
112155
pub fn builder() -> StdStreamSinkBuilder<()> {
113156
StdStreamSinkBuilder {
114157
prop: SinkProp::default(),
115158
std_stream: (),
116159
style_mode: StyleMode::Auto,
160+
via_print_macro: cfg!(feature = "test"),
117161
}
118162
}
119163

@@ -138,7 +182,7 @@ impl StdStreamSink {
138182

139183
/// Sets the style mode.
140184
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());
185+
self.should_render_style = Self::should_render_style(style_mode, self.std_stream);
142186
}
143187

144188
#[must_use]
@@ -171,8 +215,34 @@ impl Sink for StdStreamSink {
171215
.formatter()
172216
.format(record, &mut string_buf, &mut ctx)?;
173217

174-
let mut dest = self.dest.lock();
218+
if !self.via_print_macro {
219+
self.log_write(record, &string_buf, &ctx, self.std_stream.via_write())
220+
} else {
221+
self.log_write(record, &string_buf, &ctx, self.std_stream.via_macro())
222+
}
223+
}
175224

225+
fn flush(&self) -> Result<()> {
226+
if !self.via_print_macro {
227+
self.std_stream.via_write().flush()
228+
} else {
229+
self.std_stream.via_macro().flush()
230+
}
231+
.map_err(Error::FlushBuffer)
232+
}
233+
}
234+
235+
impl StdStreamSink {
236+
fn log_write<O: Write, E: Write>(
237+
&self,
238+
record: &Record,
239+
string_buf: &StringBuf,
240+
ctx: &FormatterContext<'_>,
241+
mut dest: StdStreamDest<O, E>,
242+
) -> Result<()>
243+
where
244+
StdStreamDest<O, E>: Write,
245+
{
176246
(|| {
177247
// TODO: Simplify the if block when our MSRV reaches let-chain support.
178248
if self.should_render_style {
@@ -197,16 +267,12 @@ impl Sink for StdStreamSink {
197267

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

204274
Ok(())
205275
}
206-
207-
fn flush(&self) -> Result<()> {
208-
self.dest.lock().flush().map_err(Error::FlushBuffer)
209-
}
210276
}
211277

212278
// --------------------------------------------------
@@ -217,6 +283,7 @@ pub struct StdStreamSinkBuilder<ArgSS> {
217283
prop: SinkProp,
218284
std_stream: ArgSS,
219285
style_mode: StyleMode,
286+
via_print_macro: bool,
220287
}
221288

222289
impl<ArgSS> StdStreamSinkBuilder<ArgSS> {
@@ -245,6 +312,7 @@ impl<ArgSS> StdStreamSinkBuilder<ArgSS> {
245312
prop: self.prop,
246313
std_stream,
247314
style_mode: self.style_mode,
315+
via_print_macro: self.via_print_macro,
248316
}
249317
}
250318

@@ -257,6 +325,33 @@ impl<ArgSS> StdStreamSinkBuilder<ArgSS> {
257325
self
258326
}
259327

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

@@ -305,7 +400,8 @@ impl StdStreamSinkBuilder<StdStream> {
305400
pub fn build(self) -> Result<StdStreamSink> {
306401
Ok(StdStreamSink {
307402
prop: self.prop,
308-
dest: StdStreamDest::new(self.std_stream),
403+
via_print_macro: self.via_print_macro,
404+
std_stream: self.std_stream,
309405
should_render_style: StdStreamSink::should_render_style(
310406
self.style_mode,
311407
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)