Skip to main content

tracing_systemd/format/
mod.rs

1//! Formatting types and rendering helpers.
2
3use std::borrow::Cow;
4
5#[cfg(feature = "colors")]
6pub(crate) mod color;
7pub(crate) mod event;
8pub(crate) mod span_chain;
9
10#[cfg(feature = "colors")]
11pub use color::{ColorMode, ColorTheme};
12
13/// How (or whether) to render a timestamp at the start of each stdout line.
14///
15/// Journald entries always carry their own native timestamp, so this
16/// applies only to the stdout layer.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub enum TimestampFormat {
19    /// No timestamp prefix. Default.
20    #[default]
21    None,
22    /// Seconds since [`UNIX_EPOCH`](std::time::UNIX_EPOCH), with millisecond
23    /// fraction (e.g. `1714867200.123`). Free of any external date crate.
24    UnixSeconds,
25    /// Seconds since this process started (e.g. `12.345`). Useful for tests
26    /// and short-lived programs.
27    Uptime,
28}
29
30/// Static configuration shared by both the stdout and journald renderers.
31///
32/// Held by `SystemdLayer` and not exposed publicly — users configure via
33/// builder methods on [`SystemdLayer`](crate::SystemdLayer) instead.
34#[derive(Debug, Clone)]
35#[allow(clippy::struct_excessive_bools)] // Independent toggles, not state.
36pub(crate) struct FormatConfig {
37    pub show_target: bool,
38    pub show_thread_id: bool,
39    pub show_timestamp: bool,
40    pub timestamp_format: TimestampFormat,
41    pub use_level_prefix: bool,
42    pub span_separator: Cow<'static, str>,
43    pub message_separator: Cow<'static, str>,
44    pub level_separator: Cow<'static, str>,
45    pub function_bracket_left: Cow<'static, str>,
46    pub function_bracket_right: Cow<'static, str>,
47    pub arguments_equality: Cow<'static, str>,
48    pub arguments_separator: Cow<'static, str>,
49    pub thread_id_prefix: Cow<'static, str>,
50    pub thread_id_suffix: Cow<'static, str>,
51}
52
53impl Default for FormatConfig {
54    fn default() -> Self {
55        Self {
56            show_target: false,
57            show_thread_id: false,
58            show_timestamp: false,
59            timestamp_format: TimestampFormat::None,
60            use_level_prefix: true,
61            span_separator: Cow::Borrowed("::"),
62            message_separator: Cow::Borrowed(": "),
63            level_separator: Cow::Borrowed(" "),
64            function_bracket_left: Cow::Borrowed("("),
65            function_bracket_right: Cow::Borrowed(")"),
66            arguments_equality: Cow::Borrowed(": "),
67            arguments_separator: Cow::Borrowed(", "),
68            thread_id_prefix: Cow::Borrowed("["),
69            thread_id_suffix: Cow::Borrowed("] "),
70        }
71    }
72}
73
74/// Map a tracing [`Level`](tracing::Level) to its syslog priority prefix
75/// (`<3>`–`<7>`), as understood by `journalctl` when no native field is
76/// supplied.
77pub(crate) fn syslog_prefix(level: tracing::Level) -> &'static str {
78    match level {
79        tracing::Level::ERROR => "<3>",
80        tracing::Level::WARN => "<4>",
81        tracing::Level::INFO => "<5>",
82        tracing::Level::DEBUG => "<6>",
83        tracing::Level::TRACE => "<7>",
84    }
85}
86
87/// Read the current thread's id as a `u64`. tracing-systemd 0.1 parsed
88/// the `Debug` output of [`ThreadId`](std::thread::ThreadId) by string
89/// splitting; we do the same here, but defensively. Stable Rust does not
90/// (yet) expose the integer directly outside nightly's `as_u64`.
91pub(crate) fn current_thread_id_int() -> String {
92    let id = format!("{:?}", std::thread::current().id());
93    // ThreadId's Debug is e.g. `ThreadId(2)`. Pull out the digits.
94    id.split_once('(')
95        .and_then(|(_, rest)| rest.split_once(')'))
96        .map_or_else(|| id.clone(), |(digits, _)| digits.to_owned())
97}
98
99/// Format a timestamp prefix according to `format`. Returns an empty string
100/// for [`TimestampFormat::None`].
101pub(crate) fn format_timestamp(format: TimestampFormat) -> String {
102    use std::time::{SystemTime, UNIX_EPOCH};
103
104    match format {
105        TimestampFormat::None => String::new(),
106        TimestampFormat::UnixSeconds => SystemTime::now()
107            .duration_since(UNIX_EPOCH)
108            .map_or_else(|_| String::from("0.000"), |d| {
109                format!("{}.{:03}", d.as_secs(), d.subsec_millis())
110            }),
111        TimestampFormat::Uptime => {
112            let elapsed = process_start().elapsed();
113            format!("{}.{:03}", elapsed.as_secs(), elapsed.subsec_millis())
114        }
115    }
116}
117
118fn process_start() -> std::time::Instant {
119    use std::sync::OnceLock;
120    static START: OnceLock<std::time::Instant> = OnceLock::new();
121    *START.get_or_init(std::time::Instant::now)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn syslog_prefix_maps_levels() {
130        assert_eq!(syslog_prefix(tracing::Level::ERROR), "<3>");
131        assert_eq!(syslog_prefix(tracing::Level::WARN), "<4>");
132        assert_eq!(syslog_prefix(tracing::Level::INFO), "<5>");
133        assert_eq!(syslog_prefix(tracing::Level::DEBUG), "<6>");
134        assert_eq!(syslog_prefix(tracing::Level::TRACE), "<7>");
135    }
136
137    #[test]
138    fn current_thread_id_int_is_numeric() {
139        let s = current_thread_id_int();
140        // Should be parseable as some kind of integer. ThreadId's Debug
141        // format isn't strictly stable, so we tolerate a fallback string,
142        // but on real Rust we expect digits.
143        if let Ok(n) = s.parse::<u64>() {
144            // Test threads have positive ids.
145            assert!(n > 0);
146        }
147    }
148
149    #[test]
150    fn format_timestamp_none_is_empty() {
151        assert_eq!(format_timestamp(TimestampFormat::None), "");
152    }
153
154    #[test]
155    fn format_timestamp_unix_has_dot() {
156        let s = format_timestamp(TimestampFormat::UnixSeconds);
157        assert!(s.contains('.'), "unexpected {s:?}");
158    }
159
160    #[test]
161    fn format_timestamp_uptime_has_dot() {
162        let s = format_timestamp(TimestampFormat::Uptime);
163        assert!(s.contains('.'), "unexpected {s:?}");
164    }
165}