1use std::collections::HashMap;
7use std::io::{self, Write};
8
9use serde::Serialize;
10use serde_json::Value;
11
12use crate::WideEventRecord;
13
14pub trait WideEventFormatter: Send + Sync {
20 fn write_record(
26 &self,
27 w: &mut dyn Write,
28 system: Option<&str>,
29 timestamp: &str,
30 record: &WideEventRecord,
31 ) -> io::Result<()>;
32}
33
34pub struct JsonFormatter;
36
37impl WideEventFormatter for JsonFormatter {
38 fn write_record(
39 &self,
40 w: &mut dyn Write,
41 system: Option<&str>,
42 timestamp: &str,
43 record: &WideEventRecord,
44 ) -> io::Result<()> {
45 #[derive(Serialize)]
46 struct Output<'a> {
47 #[serde(skip_serializing_if = "Option::is_none")]
48 system: Option<&'a str>,
49 subsystem: &'a str,
50 timestamp: &'a str,
51 duration_ns: f64,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 trace_id: &'a Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 span_id: &'a Option<String>,
56 #[serde(flatten)]
57 fields: &'a HashMap<&'static str, Value>,
58 }
59
60 let output = Output {
61 system,
62 subsystem: record.subsystem,
63 timestamp,
64 #[allow(
65 clippy::cast_precision_loss,
66 reason = "acceptable for duration display"
67 )]
68 duration_ns: record.duration.as_nanos() as f64,
69 trace_id: &record.trace_id,
70 span_id: &record.span_id,
71 fields: &record.fields,
72 };
73
74 serde_json::to_writer(w, &output).map_err(io::Error::other)
75 }
76}
77
78pub struct LogfmtFormatter;
80
81impl WideEventFormatter for LogfmtFormatter {
82 fn write_record(
83 &self,
84 w: &mut dyn Write,
85 system: Option<&str>,
86 timestamp: &str,
87 record: &WideEventRecord,
88 ) -> io::Result<()> {
89 let mut first = true;
90 let mut pair = |w: &mut dyn Write, key: &str, val: &str| -> io::Result<()> {
91 if !first {
92 write!(w, " ")?;
93 }
94 first = false;
95 if val.contains(' ') || val.contains('"') || val.contains('=') || val.is_empty() {
96 write!(
97 w,
98 "{key}=\"{}\"",
99 val.replace('\\', "\\\\").replace('"', "\\\"")
100 )
101 } else {
102 write!(w, "{key}={val}")
103 }
104 };
105
106 if let Some(system) = system {
107 pair(w, "system", system)?;
108 }
109 pair(w, "subsystem", record.subsystem)?;
110 pair(w, "timestamp", timestamp)?;
111 #[allow(
112 clippy::cast_precision_loss,
113 reason = "acceptable for duration display"
114 )]
115 let duration_ns = record.duration.as_nanos() as f64;
116 pair(w, "duration_ns", &duration_ns.to_string())?;
117
118 if let Some(ref tid) = record.trace_id {
119 pair(w, "trace_id", tid)?;
120 }
121 if let Some(ref sid) = record.span_id {
122 pair(w, "span_id", sid)?;
123 }
124
125 for (k, v) in &record.fields {
126 let val_str = match v {
127 Value::String(s) => s.as_str().into(),
128 Value::Number(n) => n.to_string(),
129 Value::Bool(b) => b.to_string(),
130 Value::Null => "null".into(),
131 other => serde_json::to_string(other).unwrap_or_default(),
132 };
133 pair(w, k, &val_str)?;
134 }
135
136 Ok(())
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn test_record() -> WideEventRecord {
145 let mut fields = HashMap::new();
146 fields.insert("method", Value::String("GET".into()));
147 fields.insert("status", Value::Number(200.into()));
148
149 WideEventRecord {
150 subsystem: "http",
151 duration: std::time::Duration::from_nanos(1_500_000),
152 fields,
153 trace_id: None,
154 span_id: None,
155 }
156 }
157
158 #[test]
159 fn json_basic() {
160 let mut buf = Vec::new();
161 JsonFormatter
162 .write_record(
163 &mut buf,
164 Some("myapp"),
165 "2024-01-15T14:30:00Z",
166 &test_record(),
167 )
168 .unwrap();
169
170 let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
171 assert_eq!(parsed["system"], "myapp");
172 assert_eq!(parsed["subsystem"], "http");
173 assert_eq!(parsed["timestamp"], "2024-01-15T14:30:00Z");
174 assert_eq!(parsed["method"], "GET");
175 assert_eq!(parsed["status"], 200);
176 assert_eq!(parsed["duration_ns"], 1_500_000.0);
177 }
178
179 #[test]
180 fn json_no_system() {
181 let mut buf = Vec::new();
182 JsonFormatter
183 .write_record(&mut buf, None, "2024-01-15T14:30:00Z", &test_record())
184 .unwrap();
185
186 let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
187 assert!(parsed.get("system").is_none());
188 }
189
190 #[test]
191 fn json_with_otel() {
192 let mut rec = test_record();
193 rec.trace_id = Some("abc123".into());
194 rec.span_id = Some("def456".into());
195
196 let mut buf = Vec::new();
197 JsonFormatter
198 .write_record(&mut buf, None, "2024-01-15T14:30:00Z", &rec)
199 .unwrap();
200
201 let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
202 assert_eq!(parsed["trace_id"], "abc123");
203 assert_eq!(parsed["span_id"], "def456");
204 }
205
206 #[test]
207 fn logfmt_basic() {
208 let mut buf = Vec::new();
209 LogfmtFormatter
210 .write_record(
211 &mut buf,
212 Some("myapp"),
213 "2024-01-15T14:30:00Z",
214 &test_record(),
215 )
216 .unwrap();
217
218 let output = String::from_utf8(buf).unwrap();
219 assert!(output.contains("system=myapp"));
220 assert!(output.contains("subsystem=http"));
221 assert!(output.contains("timestamp=2024-01-15T14:30:00Z"));
222 assert!(output.contains("duration_ns=1500000"));
223 }
224
225 #[test]
226 fn logfmt_quotes_spaces() {
227 let mut rec = test_record();
228 rec.fields
229 .insert("msg", Value::String("hello world".into()));
230
231 let mut buf = Vec::new();
232 LogfmtFormatter
233 .write_record(&mut buf, None, "now", &rec)
234 .unwrap();
235
236 let output = String::from_utf8(buf).unwrap();
237 assert!(output.contains("msg=\"hello world\""));
238 }
239}