Skip to main content

tracing_systemd/
output.rs

1//! The destination a [`SystemdLayer`](crate::SystemdLayer) writes to.
2//!
3//! Use [`Output::stdout`] / [`Output::stderr`] for the typical case, or
4//! [`Output::writer`] to capture into any [`io::Write`] (handy in tests).
5
6use std::fmt;
7use std::io::{self, IsTerminal, Write};
8use std::sync::Mutex;
9
10/// Where a layer writes formatted lines.
11pub struct Output {
12    sink: Sink,
13    is_terminal: bool,
14}
15
16enum Sink {
17    Stdout,
18    Stderr,
19    Custom(Mutex<Box<dyn Write + Send>>),
20}
21
22impl Output {
23    /// Write to standard output. TTY-detected at construction.
24    #[must_use]
25    pub fn stdout() -> Self {
26        Self {
27            sink: Sink::Stdout,
28            is_terminal: io::stdout().is_terminal(),
29        }
30    }
31
32    /// Write to standard error. TTY-detected at construction.
33    #[must_use]
34    pub fn stderr() -> Self {
35        Self {
36            sink: Sink::Stderr,
37            is_terminal: io::stderr().is_terminal(),
38        }
39    }
40
41    /// Write to any [`io::Write`]. Treated as non-TTY (no auto-color).
42    ///
43    /// Useful for testing, or to send formatted lines to a file.
44    /// The writer is held behind a mutex so the layer can write from any thread.
45    pub fn writer<W>(writer: W) -> Self
46    where
47        W: Write + Send + 'static,
48    {
49        Self {
50            sink: Sink::Custom(Mutex::new(Box::new(writer))),
51            is_terminal: false,
52        }
53    }
54
55    /// Whether the output is connected to a terminal. Used for color auto-detection.
56    #[must_use]
57    pub fn is_terminal(&self) -> bool {
58        self.is_terminal
59    }
60
61    /// Write a single line. Errors are silently dropped — a logging layer
62    /// should never crash the program because stdout/stderr/writer failed.
63    pub(crate) fn write_line(&self, line: &str) {
64        match &self.sink {
65            Sink::Stdout => {
66                let mut guard = io::stdout().lock();
67                let _ = writeln!(guard, "{line}");
68            }
69            Sink::Stderr => {
70                let mut guard = io::stderr().lock();
71                let _ = writeln!(guard, "{line}");
72            }
73            Sink::Custom(m) => {
74                // Recover from poisoning rather than panicking in a logger.
75                let mut guard = m.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
76                let _ = writeln!(guard, "{line}");
77            }
78        }
79    }
80}
81
82impl fmt::Debug for Output {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        let kind = match &self.sink {
85            Sink::Stdout => "Stdout",
86            Sink::Stderr => "Stderr",
87            Sink::Custom(_) => "Custom",
88        };
89        f.debug_struct("Output")
90            .field("sink", &kind)
91            .field("is_terminal", &self.is_terminal)
92            .finish()
93    }
94}
95
96impl Default for Output {
97    fn default() -> Self {
98        Self::stdout()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use std::sync::{Arc, Mutex};
106
107    #[derive(Clone, Default)]
108    struct Buf(Arc<Mutex<Vec<u8>>>);
109    impl Write for Buf {
110        fn write(&mut self, b: &[u8]) -> io::Result<usize> {
111            self.0.lock().unwrap().extend_from_slice(b);
112            Ok(b.len())
113        }
114        fn flush(&mut self) -> io::Result<()> {
115            Ok(())
116        }
117    }
118
119    #[test]
120    fn writer_captures_lines() {
121        let buf = Buf::default();
122        let captured = buf.0.clone();
123        let out = Output::writer(buf);
124
125        out.write_line("hello");
126        out.write_line("world");
127
128        let bytes = captured.lock().unwrap();
129        assert_eq!(std::str::from_utf8(&bytes).unwrap(), "hello\nworld\n");
130    }
131
132    #[test]
133    fn writer_is_not_a_terminal() {
134        let buf = Buf::default();
135        let out = Output::writer(buf);
136        assert!(!out.is_terminal());
137    }
138
139    #[test]
140    fn debug_does_not_panic() {
141        let _ = format!("{:?}", Output::stdout());
142        let _ = format!("{:?}", Output::stderr());
143        let _ = format!("{:?}", Output::writer(Vec::<u8>::new()));
144    }
145}