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}