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;
8#[cfg(feature = "json")]
9pub(crate) mod json;
10pub(crate) mod span_chain;
11
12#[cfg(feature = "colors")]
13pub use color::{ColorMode, ColorTheme};
14
15/// How (or whether) to render a timestamp at the start of each stdout line.
16///
17/// Journald entries always carry their own native timestamp, so this
18/// applies only to the stdout layer.
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
20pub enum TimestampFormat {
21    /// No timestamp prefix. Default.
22    #[default]
23    None,
24    /// Seconds since [`UNIX_EPOCH`](std::time::UNIX_EPOCH), with millisecond
25    /// fraction (e.g. `1714867200.123`). Free of any external date crate.
26    UnixSeconds,
27    /// Seconds since this process started (e.g. `12.345`). Useful for tests
28    /// and short-lived programs.
29    Uptime,
30    /// RFC 3339 / ISO 8601 in UTC with millisecond precision and a trailing
31    /// `Z` (e.g. `2026-05-05T14:23:45.123Z`). Hand-formatted from
32    /// [`SystemTime`](std::time::SystemTime); no external date crate.
33    /// Default for [`SystemdLayer::json`](crate::SystemdLayer::json).
34    Rfc3339,
35}
36
37/// Which renderer the layer uses to turn an event into a line.
38///
39/// `Pretty` produces the human-readable span-chain form. `Json` produces a
40/// single-line JSON object per event (see [`SystemdLayer::json`](crate::SystemdLayer::json)).
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42pub(crate) enum RenderMode {
43    #[default]
44    Pretty,
45    #[cfg(feature = "json")]
46    Json,
47}
48
49/// Static configuration shared by both the stdout and journald renderers.
50///
51/// Held by `SystemdLayer` and not exposed publicly — users configure via
52/// builder methods on [`SystemdLayer`](crate::SystemdLayer) instead.
53#[derive(Debug, Clone)]
54#[allow(clippy::struct_excessive_bools)] // Independent toggles, not state.
55pub(crate) struct FormatConfig {
56    #[cfg_attr(not(feature = "json"), allow(dead_code))]
57    pub mode: RenderMode,
58    pub show_target: bool,
59    pub show_thread_id: bool,
60    pub show_timestamp: bool,
61    pub timestamp_format: TimestampFormat,
62    pub use_level_prefix: bool,
63    pub span_separator: Cow<'static, str>,
64    pub message_separator: Cow<'static, str>,
65    pub level_separator: Cow<'static, str>,
66    pub function_bracket_left: Cow<'static, str>,
67    pub function_bracket_right: Cow<'static, str>,
68    pub arguments_equality: Cow<'static, str>,
69    pub arguments_separator: Cow<'static, str>,
70    pub thread_id_prefix: Cow<'static, str>,
71    pub thread_id_suffix: Cow<'static, str>,
72}
73
74impl Default for FormatConfig {
75    fn default() -> Self {
76        Self {
77            mode: RenderMode::Pretty,
78            show_target: false,
79            show_thread_id: false,
80            show_timestamp: false,
81            timestamp_format: TimestampFormat::None,
82            use_level_prefix: true,
83            span_separator: Cow::Borrowed("::"),
84            message_separator: Cow::Borrowed(": "),
85            level_separator: Cow::Borrowed(" "),
86            function_bracket_left: Cow::Borrowed("("),
87            function_bracket_right: Cow::Borrowed(")"),
88            arguments_equality: Cow::Borrowed(": "),
89            arguments_separator: Cow::Borrowed(", "),
90            thread_id_prefix: Cow::Borrowed("["),
91            thread_id_suffix: Cow::Borrowed("] "),
92        }
93    }
94}
95
96/// Map a tracing [`Level`](tracing::Level) to its syslog priority prefix
97/// (`<3>`–`<7>`), as understood by `journalctl` when no native field is
98/// supplied.
99pub(crate) fn syslog_prefix(level: tracing::Level) -> &'static str {
100    match level {
101        tracing::Level::ERROR => "<3>",
102        tracing::Level::WARN => "<4>",
103        tracing::Level::INFO => "<5>",
104        tracing::Level::DEBUG => "<6>",
105        tracing::Level::TRACE => "<7>",
106    }
107}
108
109/// Read the current thread's id as a `u64`. tracing-systemd 0.1 parsed
110/// the `Debug` output of [`ThreadId`](std::thread::ThreadId) by string
111/// splitting; we do the same here, but defensively. Stable Rust does not
112/// (yet) expose the integer directly outside nightly's `as_u64`.
113pub(crate) fn current_thread_id_int() -> String {
114    let id = format!("{:?}", std::thread::current().id());
115    // ThreadId's Debug is e.g. `ThreadId(2)`. Pull out the digits.
116    id.split_once('(')
117        .and_then(|(_, rest)| rest.split_once(')'))
118        .map_or_else(|| id.clone(), |(digits, _)| digits.to_owned())
119}
120
121/// Format a timestamp prefix according to `format`. Returns an empty string
122/// for [`TimestampFormat::None`].
123pub(crate) fn format_timestamp(format: TimestampFormat) -> String {
124    use std::time::{SystemTime, UNIX_EPOCH};
125
126    match format {
127        TimestampFormat::None => String::new(),
128        TimestampFormat::UnixSeconds => SystemTime::now()
129            .duration_since(UNIX_EPOCH)
130            .map_or_else(|_| String::from("0.000"), |d| {
131                format!("{}.{:03}", d.as_secs(), d.subsec_millis())
132            }),
133        TimestampFormat::Uptime => {
134            let elapsed = process_start().elapsed();
135            format!("{}.{:03}", elapsed.as_secs(), elapsed.subsec_millis())
136        }
137        TimestampFormat::Rfc3339 => format_rfc3339(SystemTime::now()),
138    }
139}
140
141/// RFC 3339 / ISO 8601 in UTC with millisecond precision and a trailing `Z`.
142/// Hand-formatted from `SystemTime` using Howard Hinnant's date algorithm,
143/// so no external date crate is needed.
144fn format_rfc3339(now: std::time::SystemTime) -> String {
145    use std::time::UNIX_EPOCH;
146    let Ok(dur) = now.duration_since(UNIX_EPOCH) else {
147        return String::from("1970-01-01T00:00:00.000Z");
148    };
149    let total_secs = dur.as_secs();
150    let millis = dur.subsec_millis();
151    // Days since 1970-01-01 fit in i64 for any plausible system clock.
152    #[allow(clippy::cast_possible_wrap)]
153    let days = (total_secs / 86_400) as i64;
154    let sod = total_secs % 86_400;
155    let hour = sod / 3_600;
156    let minute = (sod % 3_600) / 60;
157    let second = sod % 60;
158    let (year, month, day) = civil_from_days(days);
159    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
160}
161
162/// Howard Hinnant's `civil_from_days`:
163/// <https://howardhinnant.github.io/date_algorithms.html#civil_from_days>.
164/// Converts days-since-1970-01-01 into a `(year, month, day)` tuple in the
165/// proleptic Gregorian calendar.
166#[allow(
167    clippy::cast_possible_wrap,
168    clippy::cast_sign_loss,
169    clippy::cast_possible_truncation
170)]
171fn civil_from_days(z: i64) -> (i64, u32, u32) {
172    let z = z + 719_468;
173    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
174    let doe = (z - era * 146_097) as u64; // [0, 146096]
175    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
176    let y = yoe as i64 + era * 400;
177    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
178    let mp = (5 * doy + 2) / 153; // [0, 11]
179    let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
180    let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
181    let year = if m <= 2 { y + 1 } else { y };
182    (year, m as u32, d as u32)
183}
184
185fn process_start() -> std::time::Instant {
186    use std::sync::OnceLock;
187    static START: OnceLock<std::time::Instant> = OnceLock::new();
188    *START.get_or_init(std::time::Instant::now)
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn syslog_prefix_maps_levels() {
197        assert_eq!(syslog_prefix(tracing::Level::ERROR), "<3>");
198        assert_eq!(syslog_prefix(tracing::Level::WARN), "<4>");
199        assert_eq!(syslog_prefix(tracing::Level::INFO), "<5>");
200        assert_eq!(syslog_prefix(tracing::Level::DEBUG), "<6>");
201        assert_eq!(syslog_prefix(tracing::Level::TRACE), "<7>");
202    }
203
204    #[test]
205    fn current_thread_id_int_is_numeric() {
206        let s = current_thread_id_int();
207        // Should be parseable as some kind of integer. ThreadId's Debug
208        // format isn't strictly stable, so we tolerate a fallback string,
209        // but on real Rust we expect digits.
210        if let Ok(n) = s.parse::<u64>() {
211            // Test threads have positive ids.
212            assert!(n > 0);
213        }
214    }
215
216    #[test]
217    fn format_timestamp_none_is_empty() {
218        assert_eq!(format_timestamp(TimestampFormat::None), "");
219    }
220
221    #[test]
222    fn format_timestamp_unix_has_dot() {
223        let s = format_timestamp(TimestampFormat::UnixSeconds);
224        assert!(s.contains('.'), "unexpected {s:?}");
225    }
226
227    #[test]
228    fn format_timestamp_uptime_has_dot() {
229        let s = format_timestamp(TimestampFormat::Uptime);
230        assert!(s.contains('.'), "unexpected {s:?}");
231    }
232
233    #[test]
234    fn rfc3339_known_epochs() {
235        use std::time::{Duration, UNIX_EPOCH};
236        // Epoch
237        assert_eq!(
238            format_rfc3339(UNIX_EPOCH),
239            "1970-01-01T00:00:00.000Z"
240        );
241        // 2026-05-05T14:23:45.123Z
242        // 56 years × 365 + 14 leap days = 20454 days through 2025-12-31;
243        // + 124 days through 2026-05-05 = 20578 days.
244        // 20578 × 86400 + 14×3600 + 23×60 + 45 = 1_777_991_025 s.
245        let t = UNIX_EPOCH + Duration::new(1_777_991_025, 123_000_000);
246        assert_eq!(format_rfc3339(t), "2026-05-05T14:23:45.123Z");
247        // A leap year: 2000-02-29
248        // `date -u -d '2000-02-29T00:00:00Z' +%s` -> 951782400
249        let t = UNIX_EPOCH + Duration::new(951_782_400, 0);
250        assert_eq!(format_rfc3339(t), "2000-02-29T00:00:00.000Z");
251        // 1999-12-31T23:59:59.999Z
252        let t = UNIX_EPOCH + Duration::new(946_684_799, 999_000_000);
253        assert_eq!(format_rfc3339(t), "1999-12-31T23:59:59.999Z");
254    }
255
256    #[test]
257    fn format_timestamp_rfc3339_shape() {
258        let s = format_timestamp(TimestampFormat::Rfc3339);
259        // Shape: YYYY-MM-DDTHH:MM:SS.mmmZ → length 24, ends with Z, has 'T'.
260        assert_eq!(s.len(), 24, "got {s:?}");
261        assert!(s.ends_with('Z'), "got {s:?}");
262        assert!(s.contains('T'), "got {s:?}");
263    }
264}