Skip to main content

rainier_telemetry/
logging.rs

1//! How log lines should look — [`LogFormat`].
2//!
3//! One setting, because the choice is really one question: *is a human reading
4//! this, or a log aggregator?* A human wants colour, alignment and one event
5//! per paragraph. Datadog, Loki and CloudWatch want one JSON object per line
6//! and nothing else, and they will happily index a pretty-printed line as an
7//! unparseable blob with the fields no longer fields.
8//!
9//! Getting this wrong is quiet in exactly the way that costs an incident: the
10//! logs are *there*, they simply cannot be searched by the field you need at
11//! the moment you need it.
12
13use rainier_support::setting_enum;
14
15setting_enum! {
16    /// The shape of a log line.
17    ///
18    /// ```
19    /// use rainier_support::Setting;
20    /// use rainier_telemetry::LogFormat;
21    ///
22    /// assert_eq!(LogFormat::parse("json").unwrap(), LogFormat::Json);
23    /// assert!(LogFormat::parse("jsn").is_err());
24    /// ```
25    pub enum LogFormat: "log format" {
26        /// JSON in production and staging, pretty everywhere else.
27        ///
28        /// The default, because it is right far more often than either fixed
29        /// answer, and because the failure it prevents — a production
30        /// deployment logging prose into an aggregator that wanted objects —
31        /// is one nobody notices until they are searching for something.
32        #[default]
33        Auto = "auto",
34
35        /// The human format: coloured, aligned, one event per line.
36        ///
37        /// `tracing_subscriber`'s default, which is what a Rainier
38        /// application has always printed — so this is the format nobody
39        /// asked for and everybody recognises.
40        Pretty = "pretty",
41
42        /// Denser than `pretty` and still readable. For `docker logs` and CI,
43        /// where the terminal is narrow and nothing is colouring anything.
44        Compact = "compact",
45
46        /// One JSON object per line. For anything that indexes fields.
47        Json = "json",
48    }
49}
50
51impl LogFormat {
52    /// What [`Auto`](Self::Auto) means in this environment.
53    ///
54    /// Production and staging get JSON; everything else gets `pretty`. Staging
55    /// is included deliberately — it exists to be production-shaped, and a
56    /// staging deployment whose logs are not searchable the same way is one
57    /// that cannot rehearse an incident.
58    ///
59    /// ```
60    /// use rainier_telemetry::LogFormat;
61    ///
62    /// assert_eq!(LogFormat::Auto.resolve("production"), LogFormat::Json);
63    /// assert_eq!(LogFormat::Auto.resolve("local"), LogFormat::Pretty);
64    /// // Anything explicit is left alone.
65    /// assert_eq!(LogFormat::Compact.resolve("production"), LogFormat::Compact);
66    /// ```
67    pub fn resolve(self, environment: &str) -> Self {
68        match self {
69            Self::Auto if matches!(environment, "production" | "prod" | "staging") => Self::Json,
70            Self::Auto => Self::Pretty,
71            explicit => explicit,
72        }
73    }
74
75    /// Whether lines in this format are meant to be parsed rather than read.
76    pub fn is_structured(self) -> bool {
77        matches!(self, Self::Json)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use rainier_support::Setting;
85
86    #[test]
87    fn auto_follows_the_environment() {
88        assert_eq!(LogFormat::Auto.resolve("production"), LogFormat::Json);
89        assert_eq!(LogFormat::Auto.resolve("prod"), LogFormat::Json);
90        assert_eq!(LogFormat::Auto.resolve("staging"), LogFormat::Json);
91        assert_eq!(LogFormat::Auto.resolve("local"), LogFormat::Pretty);
92        assert_eq!(LogFormat::Auto.resolve("testing"), LogFormat::Pretty);
93        assert_eq!(LogFormat::Auto.resolve(""), LogFormat::Pretty);
94    }
95
96    #[test]
97    fn an_explicit_format_is_never_second_guessed() {
98        // Somebody who wrote `LOG_FORMAT=pretty` on a production box is
99        // debugging, and having the setting quietly ignored is the worst
100        // possible answer.
101        for format in [LogFormat::Pretty, LogFormat::Compact, LogFormat::Json] {
102            assert_eq!(format.resolve("production"), format);
103            assert_eq!(format.resolve("local"), format);
104        }
105    }
106
107    #[test]
108    fn resolving_is_idempotent() {
109        // The installer resolves once; nothing should change if it happens
110        // twice.
111        let once = LogFormat::Auto.resolve("production");
112        assert_eq!(once.resolve("production"), once);
113    }
114
115    #[test]
116    fn only_json_is_for_machines() {
117        assert!(LogFormat::Json.is_structured());
118        assert!(!LogFormat::Pretty.is_structured());
119        assert!(!LogFormat::Compact.is_structured());
120        // `Auto` is not an answer yet, so it is not a structured one.
121        assert!(!LogFormat::Auto.is_structured());
122    }
123
124    #[test]
125    fn the_default_is_auto() {
126        assert_eq!(LogFormat::default(), LogFormat::Auto);
127        assert_eq!(LogFormat::parse("auto").unwrap(), LogFormat::Auto);
128    }
129}