1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
20#[serde(rename_all = "UPPERCASE")]
21pub enum Severity {
22 Trace,
24 Debug,
26 Info,
28 Warn,
30 Error,
32 Fatal,
34 #[serde(untagged)]
36 Other(String),
37}
38
39impl<'de> Deserialize<'de> for Severity {
40 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41 where
42 D: serde::Deserializer<'de>,
43 {
44 let s = String::deserialize(deserializer)?;
45 Ok(parse_severity(&s))
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct LogEntry {
56 pub timestamp: i64,
58
59 pub message: String,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub severity: Option<Severity>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub source: Option<String>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub service: Option<String>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub id: Option<String>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub attributes: Option<BTreeMap<String, String>>,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub resource: Option<BTreeMap<String, String>>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub trace_id: Option<String>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub span_id: Option<String>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub extensions: Option<BTreeMap<String, serde_json::Value>>,
97}
98
99pub fn parse_severity(raw: &str) -> Severity {
104 match raw.trim().to_lowercase().as_str() {
105 "trace" => Severity::Trace,
106 "debug" | "7" => Severity::Debug,
107 "info" | "informational" | "notice" | "5" | "6" => Severity::Info,
108 "warn" | "warning" | "4" => Severity::Warn,
109 "error" | "err" | "3" => Severity::Error,
110 "fatal" | "critical" | "crit" | "alert" | "emergency" | "emerg" | "0" | "1" | "2" => {
111 Severity::Fatal
112 }
113 _ => Severity::Other(raw.trim().to_string()),
114 }
115}
116
117pub fn severity_from_otel_number(n: u32) -> Option<Severity> {
125 match n {
126 1..=4 => Some(Severity::Trace),
127 5..=8 => Some(Severity::Debug),
128 9..=12 => Some(Severity::Info),
129 13..=16 => Some(Severity::Warn),
130 17..=20 => Some(Severity::Error),
131 21..=24 => Some(Severity::Fatal),
132 _ => None,
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_parse_severity_standard() {
142 assert_eq!(parse_severity("trace"), Severity::Trace);
143 assert_eq!(parse_severity("TRACE"), Severity::Trace);
144 assert_eq!(parse_severity("debug"), Severity::Debug);
145 assert_eq!(parse_severity("info"), Severity::Info);
146 assert_eq!(parse_severity("warn"), Severity::Warn);
147 assert_eq!(parse_severity("error"), Severity::Error);
148 assert_eq!(parse_severity("fatal"), Severity::Fatal);
149 }
150
151 #[test]
152 fn test_parse_severity_aliases() {
153 assert_eq!(parse_severity("warning"), Severity::Warn);
154 assert_eq!(parse_severity("err"), Severity::Error);
155 assert_eq!(parse_severity("critical"), Severity::Fatal);
156 assert_eq!(parse_severity("crit"), Severity::Fatal);
157 assert_eq!(parse_severity("emerg"), Severity::Fatal);
158 assert_eq!(parse_severity("informational"), Severity::Info);
159 assert_eq!(parse_severity("notice"), Severity::Info);
160 }
161
162 #[test]
163 fn test_parse_severity_syslog_numeric() {
164 assert_eq!(parse_severity("0"), Severity::Fatal);
165 assert_eq!(parse_severity("3"), Severity::Error);
166 assert_eq!(parse_severity("4"), Severity::Warn);
167 assert_eq!(parse_severity("6"), Severity::Info);
168 assert_eq!(parse_severity("7"), Severity::Debug);
169 }
170
171 #[test]
172 fn test_parse_severity_unknown() {
173 assert_eq!(
174 parse_severity("custom_level"),
175 Severity::Other("custom_level".to_string())
176 );
177 assert_eq!(
178 parse_severity(" verbose "),
179 Severity::Other("verbose".to_string())
180 );
181 }
182
183 #[test]
184 fn test_parse_severity_trims_whitespace_in_other() {
185 assert_eq!(
187 parse_severity(" custom_level "),
188 Severity::Other("custom_level".to_string())
189 );
190 }
191
192 #[test]
193 fn test_parse_severity_trims_whitespace() {
194 assert_eq!(parse_severity(" error "), Severity::Error);
195 assert_eq!(parse_severity(" WARN"), Severity::Warn);
196 assert_eq!(parse_severity("info "), Severity::Info);
197 }
198
199 #[test]
200 fn test_severity_from_otel_number_buckets() {
201 assert_eq!(severity_from_otel_number(1), Some(Severity::Trace));
202 assert_eq!(severity_from_otel_number(4), Some(Severity::Trace));
203 assert_eq!(severity_from_otel_number(5), Some(Severity::Debug));
204 assert_eq!(severity_from_otel_number(8), Some(Severity::Debug));
205 assert_eq!(severity_from_otel_number(9), Some(Severity::Info));
206 assert_eq!(severity_from_otel_number(12), Some(Severity::Info));
207 assert_eq!(severity_from_otel_number(13), Some(Severity::Warn));
208 assert_eq!(severity_from_otel_number(16), Some(Severity::Warn));
209 assert_eq!(severity_from_otel_number(17), Some(Severity::Error));
210 assert_eq!(severity_from_otel_number(20), Some(Severity::Error));
211 assert_eq!(severity_from_otel_number(21), Some(Severity::Fatal));
212 assert_eq!(severity_from_otel_number(24), Some(Severity::Fatal));
213 }
214
215 #[test]
216 fn test_severity_from_otel_number_unspecified() {
217 assert_eq!(severity_from_otel_number(0), None);
218 }
219
220 #[test]
221 fn test_severity_from_otel_number_out_of_range() {
222 assert_eq!(severity_from_otel_number(25), None);
223 assert_eq!(severity_from_otel_number(100), None);
224 }
225
226 #[test]
227 fn test_severity_json_serialization() {
228 assert_eq!(
229 serde_json::to_string(&Severity::Error).unwrap(),
230 r#""ERROR""#
231 );
232 assert_eq!(serde_json::to_string(&Severity::Warn).unwrap(), r#""WARN""#);
233 assert_eq!(
234 serde_json::to_string(&Severity::Other("verbose".into())).unwrap(),
235 r#""verbose""#
236 );
237 }
238
239 #[test]
240 fn test_severity_json_deserialization_case_insensitive() {
241 let s: Severity = serde_json::from_str(r#""ERROR""#).unwrap();
243 assert_eq!(s, Severity::Error);
244
245 let s: Severity = serde_json::from_str(r#""error""#).unwrap();
247 assert_eq!(s, Severity::Error);
248
249 let s: Severity = serde_json::from_str(r#""Error""#).unwrap();
251 assert_eq!(s, Severity::Error);
252
253 assert_eq!(
255 serde_json::from_str::<Severity>(r#""trace""#).unwrap(),
256 Severity::Trace
257 );
258 assert_eq!(
259 serde_json::from_str::<Severity>(r#""debug""#).unwrap(),
260 Severity::Debug
261 );
262 assert_eq!(
263 serde_json::from_str::<Severity>(r#""info""#).unwrap(),
264 Severity::Info
265 );
266 assert_eq!(
267 serde_json::from_str::<Severity>(r#""warn""#).unwrap(),
268 Severity::Warn
269 );
270 assert_eq!(
271 serde_json::from_str::<Severity>(r#""fatal""#).unwrap(),
272 Severity::Fatal
273 );
274
275 assert_eq!(
277 serde_json::from_str::<Severity>(r#""warning""#).unwrap(),
278 Severity::Warn
279 );
280 assert_eq!(
281 serde_json::from_str::<Severity>(r#""critical""#).unwrap(),
282 Severity::Fatal
283 );
284
285 assert_eq!(
287 serde_json::from_str::<Severity>(r#""verbose""#).unwrap(),
288 Severity::Other("verbose".to_string())
289 );
290 }
291
292 #[test]
293 fn test_severity_json_roundtrip() {
294 for severity in [
296 Severity::Trace,
297 Severity::Debug,
298 Severity::Info,
299 Severity::Warn,
300 Severity::Error,
301 Severity::Fatal,
302 ] {
303 let json = serde_json::to_string(&severity).unwrap();
304 let back: Severity = serde_json::from_str(&json).unwrap();
305 assert_eq!(back, severity, "roundtrip failed for {json}");
306 }
307
308 let other = Severity::Other("verbose".to_string());
310 let json = serde_json::to_string(&other).unwrap();
311 let back: Severity = serde_json::from_str(&json).unwrap();
312 assert_eq!(back, other);
313 }
314
315 #[test]
316 fn test_log_entry_serialization() {
317 let entry = LogEntry {
318 timestamp: 1711266323,
319 message: "Connection refused".to_string(),
320 severity: Some(Severity::Error),
321 source: Some("172.16.1.100".to_string()),
322 service: Some("api-gateway".to_string()),
323 id: None,
324 attributes: None,
325 resource: None,
326 trace_id: None,
327 span_id: None,
328 extensions: None,
329 };
330
331 let json = serde_json::to_value(&entry).unwrap();
332 assert_eq!(json["severity"], "ERROR");
333 assert!(json.get("id").is_none());
334 }
335}