Skip to main content

dynomite/core/log/
format.rs

1//! Output-format selector for the global tracing subscriber.
2//!
3//! `dynomited` ships with four selectable wire shapes for log records:
4//! the existing human-readable text format ([`LogFormat::Default`]),
5//! IETF syslog ([`LogFormat::Rfc5424`]), BSD syslog
6//! ([`LogFormat::Rfc3164`]), and newline-delimited JSON
7//! ([`LogFormat::Json`], also accepting the alias `ndjson`). The choice
8//! is exposed both through the YAML configuration (`log_format:` on the
9//! pool) and through a CLI override (`--log-format`).
10//!
11//! When neither knob is set, the default value reproduces the
12//! pre-existing behavior byte-for-byte: a `tracing_subscriber::fmt()`
13//! line with target enabled. This module is intentionally
14//! cheap to embed - all formats are dispatched at install time so the
15//! per-event hot path costs the same as the original implementation.
16//!
17//! # Examples
18//!
19//! ```
20//! use dynomite::core::log::LogFormat;
21//!
22//! assert_eq!(LogFormat::parse("default").unwrap(), LogFormat::Default);
23//! assert_eq!(LogFormat::parse("RFC5424").unwrap(), LogFormat::Rfc5424);
24//! assert_eq!(LogFormat::parse("rfc3164").unwrap(), LogFormat::Rfc3164);
25//! assert_eq!(LogFormat::parse("ndjson").unwrap(), LogFormat::Json);
26//! assert!(LogFormat::parse("yaml").is_err());
27//! ```
28
29use std::fmt;
30
31/// Selectable on-disk / on-stderr shape for emitted tracing events.
32///
33/// # Examples
34///
35/// ```
36/// use dynomite::core::log::LogFormat;
37/// assert_eq!(LogFormat::default(), LogFormat::Default);
38/// ```
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum LogFormat {
41    /// Human-readable text via `tracing_subscriber::fmt()`.
42    /// This is the historical default and is what `dynomited` emits
43    /// when neither the configuration nor the CLI request another
44    /// shape.
45    Default,
46    /// Modern structured syslog per RFC 5424.
47    Rfc5424,
48    /// BSD-style syslog per RFC 3164.
49    ///
50    /// The user-facing brief originally said "RFC 3124" - that is "The
51    /// Congestion Manager" and is unrelated to logging. We treat it as
52    /// a typo for RFC 3164 and implement the BSD syslog shape here.
53    Rfc3164,
54    /// Newline-delimited JSON, one event per line. Selected by either
55    /// `json` or `ndjson`.
56    Json,
57}
58
59impl Default for LogFormat {
60    fn default() -> Self {
61        Self::Default
62    }
63}
64
65impl LogFormat {
66    /// Parse a configuration / CLI value into a `LogFormat`.
67    ///
68    /// The match is case-insensitive. Empty input maps to
69    /// [`LogFormat::Default`] so a YAML value of `""` (or no value at
70    /// all) selects the default. The aliases `json` and `ndjson` both
71    /// map to [`LogFormat::Json`].
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use dynomite::core::log::LogFormat;
77    /// assert_eq!(LogFormat::parse("").unwrap(), LogFormat::Default);
78    /// assert_eq!(LogFormat::parse("json").unwrap(), LogFormat::Json);
79    /// assert_eq!(LogFormat::parse("ndjson").unwrap(), LogFormat::Json);
80    /// assert!(LogFormat::parse("xml").is_err());
81    /// ```
82    pub fn parse(s: &str) -> Result<Self, LogFormatParseError> {
83        let lower = s.trim().to_ascii_lowercase();
84        match lower.as_str() {
85            "" | "default" => Ok(Self::Default),
86            "rfc5424" => Ok(Self::Rfc5424),
87            "rfc3164" => Ok(Self::Rfc3164),
88            "json" | "ndjson" => Ok(Self::Json),
89            _ => Err(LogFormatParseError {
90                input: s.to_string(),
91            }),
92        }
93    }
94
95    /// Stable canonical name used by the CLI / config / docs.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use dynomite::core::log::LogFormat;
101    /// assert_eq!(LogFormat::Json.as_str(), "json");
102    /// assert_eq!(LogFormat::Rfc5424.as_str(), "rfc5424");
103    /// ```
104    pub fn as_str(self) -> &'static str {
105        match self {
106            Self::Default => "default",
107            Self::Rfc5424 => "rfc5424",
108            Self::Rfc3164 => "rfc3164",
109            Self::Json => "json",
110        }
111    }
112}
113
114impl fmt::Display for LogFormat {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        f.write_str(self.as_str())
117    }
118}
119
120/// Returned by [`LogFormat::parse`] for a value that does not match any
121/// of the four supported names.
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct LogFormatParseError {
124    /// The unrecognised input as supplied by the operator.
125    pub input: String,
126}
127
128impl fmt::Display for LogFormatParseError {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(
131            f,
132            "unknown log_format '{}': expected one of default, rfc5424, rfc3164, json, ndjson",
133            self.input
134        )
135    }
136}
137
138impl std::error::Error for LogFormatParseError {}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn parse_known_values() {
146        for (input, expected) in [
147            ("default", LogFormat::Default),
148            ("DEFAULT", LogFormat::Default),
149            ("", LogFormat::Default),
150            ("  rfc5424  ", LogFormat::Rfc5424),
151            ("RFC3164", LogFormat::Rfc3164),
152            ("json", LogFormat::Json),
153            ("ndjson", LogFormat::Json),
154        ] {
155            assert_eq!(LogFormat::parse(input).unwrap(), expected, "input: {input}");
156        }
157    }
158
159    #[test]
160    fn parse_unknown_rejected() {
161        let err = LogFormat::parse("yaml").unwrap_err();
162        assert_eq!(err.input, "yaml");
163        assert!(err.to_string().contains("yaml"));
164    }
165
166    #[test]
167    fn display_and_as_str_match() {
168        for variant in [
169            LogFormat::Default,
170            LogFormat::Rfc5424,
171            LogFormat::Rfc3164,
172            LogFormat::Json,
173        ] {
174            assert_eq!(variant.to_string(), variant.as_str());
175            assert_eq!(LogFormat::parse(variant.as_str()).unwrap(), variant);
176        }
177    }
178}