Skip to main content

tracing_systemd/
layer.rs

1//! [`SystemdLayer`]: a `tracing-subscriber` Layer that pretty-prints
2//! span chains, with optional ANSI colors and timestamps.
3
4use std::borrow::Cow;
5use std::fmt;
6
7use tracing::{Event, Subscriber};
8use tracing_subscriber::Layer;
9use tracing_subscriber::layer::Context;
10use tracing_subscriber::registry::LookupSpan;
11
12use crate::format::event::{EventInput, render_event};
13use crate::format::span_chain::SpanLink;
14use crate::format::{FormatConfig, TimestampFormat, syslog_prefix};
15use crate::output::Output;
16use crate::visit::{FieldMap, FieldStorage, FieldVisitor};
17
18#[cfg(feature = "colors")]
19use crate::format::color::{ColorMode, ColorTheme};
20
21/// A `tracing-subscriber` Layer that emits a pretty span-chain line per event.
22///
23/// Construct with [`SystemdLayer::stdout`], then chain `with_*` methods to
24/// configure formatting, and pass it to a `tracing-subscriber::registry()`.
25///
26/// ```no_run
27/// use tracing_subscriber::prelude::*;
28/// use tracing_systemd::SystemdLayer;
29///
30/// tracing_subscriber::registry()
31///     .with(SystemdLayer::stdout().with_target(true))
32///     .init();
33/// ```
34pub struct SystemdLayer {
35    config: FormatConfig,
36    output: Output,
37    #[cfg(feature = "colors")]
38    color_mode: ColorMode,
39    #[cfg(feature = "colors")]
40    color_theme: ColorTheme,
41}
42
43impl fmt::Debug for SystemdLayer {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        let mut d = f.debug_struct("SystemdLayer");
46        d.field("config", &self.config).field("output", &self.output);
47        #[cfg(feature = "colors")]
48        d.field("color_mode", &self.color_mode)
49            .field("color_theme", &self.color_theme);
50        d.finish()
51    }
52}
53
54impl Default for SystemdLayer {
55    fn default() -> Self {
56        Self::stdout()
57    }
58}
59
60// ---------- Constructors ----------
61
62impl SystemdLayer {
63    /// Construct a layer that writes formatted lines to standard output.
64    /// All formatting options have sensible defaults; chain `with_*`
65    /// methods to override them.
66    #[must_use]
67    pub fn stdout() -> Self {
68        Self {
69            config: FormatConfig::default(),
70            output: Output::stdout(),
71            #[cfg(feature = "colors")]
72            color_mode: ColorMode::default(),
73            #[cfg(feature = "colors")]
74            color_theme: ColorTheme::default(),
75        }
76    }
77
78    /// Construct a layer that writes formatted lines to standard error.
79    #[must_use]
80    pub fn stderr() -> Self {
81        Self {
82            config: FormatConfig::default(),
83            output: Output::stderr(),
84            #[cfg(feature = "colors")]
85            color_mode: ColorMode::default(),
86            #[cfg(feature = "colors")]
87            color_theme: ColorTheme::default(),
88        }
89    }
90
91    /// Construct a layer that writes formatted lines to a journald-friendly
92    /// stream — i.e. **stdout with syslog-priority prefixes** (`<3>`–`<7>`).
93    /// `journalctl` understands these prefixes when no native journal field
94    /// is supplied, which is what happens for processes managed by a
95    /// systemd unit (their stdout is already routed to the journal).
96    ///
97    /// For direct journal-protocol writes (without going through stdout),
98    /// enable the `journald` feature and use [`crate::journald::layer`].
99    #[must_use]
100    pub fn unit_stdout() -> Self {
101        Self {
102            config: FormatConfig {
103                use_level_prefix: true,
104                ..FormatConfig::default()
105            },
106            output: Output::stdout(),
107            #[cfg(feature = "colors")]
108            color_mode: ColorMode::Never,
109            #[cfg(feature = "colors")]
110            color_theme: ColorTheme::monochrome(),
111        }
112    }
113}
114
115// ---------- Builder methods ----------
116
117impl SystemdLayer {
118    /// Override the destination. Useful for `Output::stderr()` or
119    /// `Output::writer(buf)` (test capture).
120    #[must_use]
121    pub fn with_output(mut self, output: Output) -> Self {
122        self.output = output;
123        self
124    }
125
126    /// Show the event's `target` (typically the module path) before the span chain.
127    /// Default: `false`.
128    #[must_use]
129    pub fn with_target(mut self, show: bool) -> Self {
130        self.config.show_target = show;
131        self
132    }
133
134    /// Include the OS thread id in each line. Default: `false`.
135    #[must_use]
136    pub fn with_thread_ids(mut self, show: bool) -> Self {
137        self.config.show_thread_id = show;
138        self
139    }
140
141    /// Show a timestamp at the start of each line. Default: `false`.
142    /// See also [`SystemdLayer::with_timestamp_format`].
143    #[must_use]
144    pub fn with_timestamps(mut self, show: bool) -> Self {
145        self.config.show_timestamp = show;
146        self
147    }
148
149    /// Choose the timestamp format. Implies `with_timestamps(true)` for any
150    /// non-`None` value. Default: [`TimestampFormat::None`].
151    #[must_use]
152    pub fn with_timestamp_format(mut self, format: TimestampFormat) -> Self {
153        self.config.timestamp_format = format;
154        if format != TimestampFormat::None {
155            self.config.show_timestamp = true;
156        }
157        self
158    }
159
160    /// Emit a syslog-priority prefix (`<3>` – `<7>`) before each line. This
161    /// is what `journalctl` uses to assign a `PRIORITY` when ingesting plain
162    /// stdout from a systemd unit. Default: `true`.
163    #[must_use]
164    pub fn with_level_prefix(mut self, use_prefix: bool) -> Self {
165        self.config.use_level_prefix = use_prefix;
166        self
167    }
168
169    /// Separator between spans in the chain. Default: `"::"`.
170    #[must_use]
171    pub fn with_span_separator(mut self, sep: impl Into<Cow<'static, str>>) -> Self {
172        self.config.span_separator = sep.into();
173        self
174    }
175
176    /// Separator between the level/span chain and the event message. Default: `": "`.
177    #[must_use]
178    pub fn with_message_separator(mut self, sep: impl Into<Cow<'static, str>>) -> Self {
179        self.config.message_separator = sep.into();
180        self
181    }
182
183    /// Separator after the level. Default: `" "`.
184    #[must_use]
185    pub fn with_level_separator(mut self, sep: impl Into<Cow<'static, str>>) -> Self {
186        self.config.level_separator = sep.into();
187        self
188    }
189
190    /// String wrapping the *opening* of a span argument list. Default: `"("`.
191    #[must_use]
192    pub fn with_function_bracket_left(mut self, s: impl Into<Cow<'static, str>>) -> Self {
193        self.config.function_bracket_left = s.into();
194        self
195    }
196
197    /// String wrapping the *closing* of a span argument list. Default: `")"`.
198    #[must_use]
199    pub fn with_function_bracket_right(mut self, s: impl Into<Cow<'static, str>>) -> Self {
200        self.config.function_bracket_right = s.into();
201        self
202    }
203
204    /// String between an argument name and its value. Default: `": "`.
205    #[must_use]
206    pub fn with_arguments_equality(mut self, s: impl Into<Cow<'static, str>>) -> Self {
207        self.config.arguments_equality = s.into();
208        self
209    }
210
211    /// String between consecutive arguments. Default: `", "`.
212    #[must_use]
213    pub fn with_arguments_separator(mut self, s: impl Into<Cow<'static, str>>) -> Self {
214        self.config.arguments_separator = s.into();
215        self
216    }
217
218    /// Prefix for the thread id when `with_thread_ids(true)`. Default: `"["`.
219    #[must_use]
220    pub fn with_thread_id_prefix(mut self, s: impl Into<Cow<'static, str>>) -> Self {
221        self.config.thread_id_prefix = s.into();
222        self
223    }
224
225    /// Suffix for the thread id when `with_thread_ids(true)`. Default: `"] "`.
226    #[must_use]
227    pub fn with_thread_id_suffix(mut self, s: impl Into<Cow<'static, str>>) -> Self {
228        self.config.thread_id_suffix = s.into();
229        self
230    }
231
232    /// Set the [`ColorMode`]. Default: [`ColorMode::Auto`] (color iff TTY
233    /// and `NO_COLOR` is unset).
234    #[cfg(feature = "colors")]
235    #[cfg_attr(docsrs, doc(cfg(feature = "colors")))]
236    #[must_use]
237    pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
238        self.color_mode = mode;
239        self
240    }
241
242    /// Set the [`ColorTheme`]. Default: [`ColorTheme::default`] (matches 0.1).
243    #[cfg(feature = "colors")]
244    #[cfg_attr(docsrs, doc(cfg(feature = "colors")))]
245    #[must_use]
246    pub fn with_color_theme(mut self, theme: ColorTheme) -> Self {
247        self.color_theme = theme;
248        self
249    }
250}
251
252// ---------- Layer impl ----------
253
254impl<S> Layer<S> for SystemdLayer
255where
256    S: Subscriber + for<'a> LookupSpan<'a>,
257{
258    fn on_new_span(
259        &self,
260        attrs: &tracing::span::Attributes<'_>,
261        id: &tracing::span::Id,
262        ctx: Context<'_, S>,
263    ) {
264        let mut fields = FieldMap::new();
265        attrs.record(&mut FieldVisitor::new(&mut fields));
266
267        if let Some(span) = ctx.span(id) {
268            span.extensions_mut().insert(FieldStorage(fields));
269        }
270    }
271
272    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
273        // Walk parent spans and copy their fields out.
274        // The leaf span (if any) is the *last* element; everything before
275        // it is "parents" in the chain.
276        let mut chain: Vec<SpanLink> = Vec::new();
277        if let Some(scope) = ctx.event_scope(event) {
278            for span in scope.from_root() {
279                let exts = span.extensions();
280                let fields = exts
281                    .get::<FieldStorage>()
282                    .map(|s| s.0.clone())
283                    .unwrap_or_default();
284                chain.push(SpanLink {
285                    name: span.name(),
286                    fields,
287                });
288            }
289        }
290        let leaf = chain.last().cloned();
291        let parents: &[SpanLink] = if chain.is_empty() {
292            &[]
293        } else {
294            &chain[..chain.len() - 1]
295        };
296
297        // Visit the event's own fields.
298        let mut event_fields = FieldMap::new();
299        event.record(&mut FieldVisitor::new(&mut event_fields));
300
301        let metadata = event.metadata();
302        let level = *metadata.level();
303
304        let input = EventInput {
305            level,
306            target: metadata.target(),
307            parents,
308            leaf: leaf.as_ref(),
309            fields: &event_fields,
310        };
311
312        // Pick a theme based on color mode + tty status.
313        #[cfg(feature = "colors")]
314        let line = {
315            let use_color = self.color_mode.resolve_now(self.output.is_terminal());
316            let theme = if use_color { Some(&self.color_theme) } else { None };
317            render_event(&self.config, &input, theme)
318        };
319        #[cfg(not(feature = "colors"))]
320        let line = render_event(&self.config, &input);
321
322        if self.config.use_level_prefix {
323            self.output.write_line(&format!("{}{}", syslog_prefix(level), line));
324        } else {
325            self.output.write_line(&line);
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use std::io::Write;
334    use std::sync::{Arc, Mutex};
335    use tracing::{Level, info, info_span, warn};
336    use tracing_subscriber::prelude::*;
337
338    #[derive(Clone, Default)]
339    struct Buf(Arc<Mutex<Vec<u8>>>);
340    impl Write for Buf {
341        fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
342            self.0.lock().unwrap().extend_from_slice(b);
343            Ok(b.len())
344        }
345        fn flush(&mut self) -> std::io::Result<()> {
346            Ok(())
347        }
348    }
349
350    fn capture<F: FnOnce()>(layer: SystemdLayer, body: F) -> String {
351        let buf = Buf::default();
352        let captured = buf.0.clone();
353        let layer = layer.with_output(Output::writer(buf));
354        tracing::subscriber::with_default(tracing_subscriber::registry().with(layer), body);
355        let bytes = captured.lock().unwrap().clone();
356        String::from_utf8(bytes).expect("utf-8 output")
357    }
358
359    #[test]
360    fn bare_info_event() {
361        let layer = SystemdLayer::stdout().with_level_prefix(false);
362        let out = capture(layer, || {
363            info!("hello");
364        });
365        // Target depends on test crate name; just check the suffix.
366        assert!(out.ends_with(": hello\n"), "got {out:?}");
367        assert!(out.starts_with("INFO "), "got {out:?}");
368        let _ = Level::INFO; // silence unused
369    }
370
371    #[test]
372    fn span_arguments_appear_in_output() {
373        let layer = SystemdLayer::stdout().with_level_prefix(false);
374        let out = capture(layer, || {
375            let span = info_span!("worker", id = 7u64);
376            let _g = span.enter();
377            warn!("done");
378        });
379        assert!(out.contains("worker(id: 7)"), "got {out:?}");
380        assert!(out.contains("done"), "got {out:?}");
381    }
382
383    #[test]
384    fn level_prefix_emits_syslog_marker() {
385        let layer = SystemdLayer::stdout().with_level_prefix(true);
386        let out = capture(layer, || {
387            info!("p");
388        });
389        assert!(out.starts_with("<5>INFO"), "got {out:?}");
390    }
391}