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}