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, Default)]
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    Default,
47    /// Modern structured syslog per RFC 5424.
48    Rfc5424,
49    /// BSD-style syslog per RFC 3164.
50    ///
51    /// The user-facing brief originally said "RFC 3124" - that is "The
52    /// Congestion Manager" and is unrelated to logging. We treat it as
53    /// a typo for RFC 3164 and implement the BSD syslog shape here.
54    Rfc3164,
55    /// Newline-delimited JSON, one event per line. Selected by either
56    /// `json` or `ndjson`.
57    Json,
58}
59
60impl LogFormat {
61    /// Parse a configuration / CLI value into a `LogFormat`.
62    ///
63    /// The match is case-insensitive. Empty input maps to
64    /// [`LogFormat::Default`] so a YAML value of `""` (or no value at
65    /// all) selects the default. The aliases `json` and `ndjson` both
66    /// map to [`LogFormat::Json`].
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use dynomite::core::log::LogFormat;
72    /// assert_eq!(LogFormat::parse("").unwrap(), LogFormat::Default);
73    /// assert_eq!(LogFormat::parse("json").unwrap(), LogFormat::Json);
74    /// assert_eq!(LogFormat::parse("ndjson").unwrap(), LogFormat::Json);
75    /// assert!(LogFormat::parse("xml").is_err());
76    /// ```
77    pub fn parse(s: &str) -> Result<Self, LogFormatParseError> {
78        let lower = s.trim().to_ascii_lowercase();
79        match lower.as_str() {
80            "" | "default" => Ok(Self::Default),
81            "rfc5424" => Ok(Self::Rfc5424),
82            "rfc3164" => Ok(Self::Rfc3164),
83            "json" | "ndjson" => Ok(Self::Json),
84            _ => Err(LogFormatParseError {
85                input: s.to_string(),
86            }),
87        }
88    }
89
90    /// Stable canonical name used by the CLI / config / docs.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use dynomite::core::log::LogFormat;
96    /// assert_eq!(LogFormat::Json.as_str(), "json");
97    /// assert_eq!(LogFormat::Rfc5424.as_str(), "rfc5424");
98    /// ```
99    pub fn as_str(self) -> &'static str {
100        match self {
101            Self::Default => "default",
102            Self::Rfc5424 => "rfc5424",
103            Self::Rfc3164 => "rfc3164",
104            Self::Json => "json",
105        }
106    }
107}
108
109impl fmt::Display for LogFormat {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.write_str(self.as_str())
112    }
113}
114
115/// Returned by [`LogFormat::parse`] for a value that does not match any
116/// of the four supported names.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct LogFormatParseError {
119    /// The unrecognised input as supplied by the operator.
120    pub input: String,
121}
122
123impl fmt::Display for LogFormatParseError {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(
126            f,
127            "unknown log_format '{}': expected one of default, rfc5424, rfc3164, json, ndjson",
128            self.input
129        )
130    }
131}
132
133impl std::error::Error for LogFormatParseError {}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn parse_known_values() {
141        for (input, expected) in [
142            ("default", LogFormat::Default),
143            ("DEFAULT", LogFormat::Default),
144            ("", LogFormat::Default),
145            ("  rfc5424  ", LogFormat::Rfc5424),
146            ("RFC3164", LogFormat::Rfc3164),
147            ("json", LogFormat::Json),
148            ("ndjson", LogFormat::Json),
149        ] {
150            assert_eq!(LogFormat::parse(input).unwrap(), expected, "input: {input}");
151        }
152    }
153
154    #[test]
155    fn parse_unknown_rejected() {
156        let err = LogFormat::parse("yaml").unwrap_err();
157        assert_eq!(err.input, "yaml");
158        assert!(err.to_string().contains("yaml"));
159    }
160
161    #[test]
162    fn default_trait_matches_default_variant() {
163        assert_eq!(LogFormat::default(), LogFormat::Default);
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}