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}