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