1#![forbid(unsafe_code)]
30
31use {
32 serde_json::{Map, Value},
33 std::fmt,
34 tracing::{
35 Event, Level, Subscriber,
36 field::{Field, Visit},
37 },
38 tracing_subscriber::{
39 Layer, Registry,
40 fmt::{
41 FmtContext, FormatEvent, FormatFields, MakeWriter,
42 format::Writer,
43 layer as fmt_layer,
44 time::{FormatTime, SystemTime},
45 },
46 layer::Filter,
47 registry::LookupSpan,
48 },
49};
50
51fn severity(level: Level) -> &'static str {
57 match level {
58 Level::ERROR => "ERROR",
59 Level::WARN => "WARNING",
60 Level::INFO => "INFO",
61 Level::DEBUG | Level::TRACE => "DEBUG",
62 }
63}
64
65#[derive(Default)]
68struct JsonVisitor {
69 fields: Map<String, Value>,
70}
71
72impl JsonVisitor {
73 fn insert(&mut self, field: &Field, value: Value) {
74 self.fields.insert(field.name().to_owned(), value);
75 }
76}
77
78impl Visit for JsonVisitor {
79 fn record_str(&mut self, field: &Field, value: &str) { self.insert(field, Value::from(value)); }
80
81 fn record_bool(&mut self, field: &Field, value: bool) {
82 self.insert(field, Value::from(value));
83 }
84
85 fn record_i64(&mut self, field: &Field, value: i64) { self.insert(field, Value::from(value)); }
86
87 fn record_u64(&mut self, field: &Field, value: u64) { self.insert(field, Value::from(value)); }
88
89 fn record_f64(&mut self, field: &Field, value: f64) { self.insert(field, Value::from(value)); }
90
91 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
92 self.insert(field, Value::from(format!("{value:?}")));
96 }
97}
98
99#[derive(Debug, Clone, Copy, Default)]
102pub struct CloudLogging;
103
104impl<S, N> FormatEvent<S, N> for CloudLogging
105where
106 S: Subscriber + for<'a> LookupSpan<'a>,
107 N: for<'a> FormatFields<'a> + 'static,
108{
109 fn format_event(
110 &self,
111 _ctx: &FmtContext<'_, S, N>,
112 mut writer: Writer<'_>,
113 event: &Event<'_>,
114 ) -> fmt::Result {
115 let meta = event.metadata();
116 let mut obj = Map::new();
117
118 let mut timestamp = String::new();
122 {
123 let mut tw = Writer::new(&mut timestamp);
124 SystemTime.format_time(&mut tw)?;
125 }
126 obj.insert("timestamp".to_owned(), Value::from(timestamp));
127 obj.insert("severity".to_owned(), Value::from(severity(*meta.level())));
128 obj.insert("target".to_owned(), Value::from(meta.target()));
129
130 let mut visitor = JsonVisitor::default();
134 event.record(&mut visitor);
135 for (key, value) in visitor.fields {
136 obj.entry(key).or_insert(value);
137 }
138
139 let line = serde_json::to_string(&Value::Object(obj)).map_err(|_| fmt::Error)?;
140 writeln!(writer, "{line}")
141 }
142}
143
144#[must_use]
157pub fn layer<W, F>(filter: F, make_writer: W) -> Box<dyn Layer<Registry> + Send + Sync>
158where
159 W: for<'writer> MakeWriter<'writer> + Send + Sync + 'static,
160 F: Filter<Registry> + Send + Sync + 'static,
161{
162 fmt_layer()
163 .event_format(CloudLogging)
164 .with_writer(make_writer)
165 .with_filter(filter)
166 .boxed()
167}
168
169#[cfg(test)]
170mod tests {
171 use {
172 super::*,
173 assert2::check,
174 std::{
175 io::{self, Write},
176 sync::{Arc, Mutex},
177 },
178 tracing::subscriber,
179 tracing_subscriber::layer::SubscriberExt as _,
180 };
181
182 #[derive(Clone)]
183 struct Buf(Arc<Mutex<Vec<u8>>>);
184
185 impl Write for Buf {
186 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
187 self.0.lock().unwrap().extend_from_slice(bytes);
188 Ok(bytes.len())
189 }
190
191 fn flush(&mut self) -> io::Result<()> { Ok(()) }
192 }
193
194 impl<'a> MakeWriter<'a> for Buf {
195 type Writer = Self;
196
197 fn make_writer(&'a self) -> Self::Writer { self.clone() }
198 }
199
200 fn capture(emit: impl FnOnce()) -> serde_json::Value {
202 let buf = Arc::new(Mutex::new(Vec::new()));
203 let subscriber = tracing_subscriber::registry().with(layer(
204 tracing_subscriber::EnvFilter::new("trace"),
205 Buf(Arc::clone(&buf)),
206 ));
207 subscriber::with_default(subscriber, emit);
208 let out = String::from_utf8(buf.lock().unwrap().clone()).expect("utf8 log output");
209 assert2::assert!(!out.contains('\u{1b}'));
211 let line = out.lines().next().expect("a log line");
212 serde_json::from_str(line).expect("each log line is valid JSON")
213 }
214
215 #[yare::parameterized(
216 severity = { "severity", serde_json::json!("INFO") },
217 message = { "message", serde_json::json!("connection opened") },
218 listener = { "listener", serde_json::json!("PLAIN") },
219 sasl = { "sasl", serde_json::json!(false) },
220 port = { "port", serde_json::json!(9092) },
221 )]
222 fn emits_cloud_logging_json(field: &str, want: serde_json::Value) {
223 let v = capture(|| {
224 tracing::info!(
225 listener = "PLAIN",
226 sasl = false,
227 port = 9092,
228 "connection opened"
229 );
230 });
231 assert2::assert!(v[field] == want);
233 check!(v["target"].is_string());
234 check!(v["timestamp"].as_str().is_some_and(|t| t.ends_with('Z')));
235 check!(v.get("level").is_none());
237 }
238
239 #[test]
240 fn maps_warn_to_warning_severity() {
241 let v = capture(|| tracing::warn!("disk almost full"));
242 assert2::assert!(&v["severity"] == &serde_json::json!("WARNING"));
243 assert2::assert!(&v["message"] == &serde_json::json!("disk almost full"));
244 }
245
246 #[test]
247 fn maps_trace_to_debug_severity() {
248 let v = capture(|| tracing::trace!("fine-grained detail"));
249 assert2::assert!(v["severity"] == "DEBUG");
250 }
251
252 #[test]
253 fn preserves_u64_and_f64_field_types() {
254 let v = capture(|| {
258 tracing::info!(offset = 42_u64, ratio = 0.5_f64, "metrics");
259 });
260 assert2::assert!(&v["offset"] == &serde_json::json!(42_u64));
261 assert2::assert!(&v["ratio"] == &serde_json::json!(0.5));
262 }
263}