1use std::collections::BTreeMap;
8use std::time::SystemTime;
9
10use serde::Serialize;
11
12use crate::model::metric::Labels;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
19#[cfg_attr(feature = "config", derive(serde::Deserialize))]
20#[serde(rename_all = "lowercase")]
24pub enum Severity {
25 Trace,
27 Debug,
29 Info,
31 Warn,
33 Error,
35 Fatal,
37}
38
39impl Severity {
40 const fn rank(self) -> u8 {
46 match self {
47 Severity::Trace => 0,
48 Severity::Debug => 1,
49 Severity::Info => 2,
50 Severity::Warn => 3,
51 Severity::Error => 4,
52 Severity::Fatal => 5,
53 }
54 }
55}
56
57impl Ord for Severity {
58 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
59 self.rank().cmp(&other.rank())
60 }
61}
62
63impl PartialOrd for Severity {
64 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
65 Some(self.cmp(other))
66 }
67}
68
69#[derive(Debug, Clone)]
75pub struct LogEvent {
76 pub timestamp: SystemTime,
78 pub severity: Severity,
80 pub message: String,
82 pub labels: Labels,
84 pub fields: BTreeMap<String, String>,
86}
87
88impl LogEvent {
89 pub fn new(
98 severity: Severity,
99 message: String,
100 labels: Labels,
101 fields: BTreeMap<String, String>,
102 ) -> Self {
103 Self {
104 timestamp: SystemTime::now(),
105 severity,
106 message,
107 labels,
108 fields,
109 }
110 }
111
112 pub fn with_timestamp(
125 timestamp: SystemTime,
126 severity: Severity,
127 message: String,
128 labels: Labels,
129 fields: BTreeMap<String, String>,
130 ) -> Self {
131 Self {
132 timestamp,
133 severity,
134 message,
135 labels,
136 fields,
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use std::time::{Duration, UNIX_EPOCH};
144
145 use super::*;
146
147 #[test]
152 fn new_uses_current_timestamp() {
153 let before = SystemTime::now();
154 let event = LogEvent::new(
155 Severity::Info,
156 "hello".to_string(),
157 Labels::default(),
158 BTreeMap::new(),
159 );
160 let after = SystemTime::now();
161
162 assert!(
163 event.timestamp >= before,
164 "timestamp should not precede the call"
165 );
166 assert!(
167 event.timestamp <= after,
168 "timestamp should not exceed the call"
169 );
170 }
171
172 #[test]
173 fn new_stores_severity_message_and_fields() {
174 let mut fields = BTreeMap::new();
175 fields.insert("host".to_string(), "web-01".to_string());
176
177 let event = LogEvent::new(
178 Severity::Error,
179 "connection failed".to_string(),
180 Labels::default(),
181 fields,
182 );
183
184 assert_eq!(event.severity, Severity::Error);
185 assert_eq!(event.message, "connection failed");
186 assert_eq!(event.fields.get("host").map(String::as_str), Some("web-01"));
187 }
188
189 #[test]
190 fn new_with_empty_fields_succeeds() {
191 let event = LogEvent::new(
192 Severity::Debug,
193 "empty".to_string(),
194 Labels::default(),
195 BTreeMap::new(),
196 );
197 assert!(event.fields.is_empty());
198 }
199
200 #[test]
205 fn with_timestamp_uses_exact_provided_timestamp() {
206 let ts = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
207 let event = LogEvent::with_timestamp(
208 ts,
209 Severity::Warn,
210 "test message".to_string(),
211 Labels::default(),
212 BTreeMap::new(),
213 );
214
215 assert_eq!(
216 event.timestamp, ts,
217 "timestamp must be exactly the one provided"
218 );
219 }
220
221 #[test]
222 fn with_timestamp_stores_all_fields_correctly() {
223 let ts = UNIX_EPOCH + Duration::from_secs(42);
224 let mut fields = BTreeMap::new();
225 fields.insert("service".to_string(), "api".to_string());
226 fields.insert("region".to_string(), "us-east-1".to_string());
227
228 let event = LogEvent::with_timestamp(
229 ts,
230 Severity::Fatal,
231 "system crash".to_string(),
232 Labels::default(),
233 fields,
234 );
235
236 assert_eq!(event.timestamp, ts);
237 assert_eq!(event.severity, Severity::Fatal);
238 assert_eq!(event.message, "system crash");
239 assert_eq!(event.fields.get("service").map(String::as_str), Some("api"));
240 assert_eq!(
241 event.fields.get("region").map(String::as_str),
242 Some("us-east-1")
243 );
244 }
245
246 #[test]
247 fn with_timestamp_at_unix_epoch_is_valid() {
248 let event = LogEvent::with_timestamp(
249 UNIX_EPOCH,
250 Severity::Trace,
251 "epoch".to_string(),
252 Labels::default(),
253 BTreeMap::new(),
254 );
255 assert_eq!(event.timestamp, UNIX_EPOCH);
256 }
257
258 #[test]
263 fn fields_are_sorted_by_key() {
264 let mut fields = BTreeMap::new();
265 fields.insert("zebra".to_string(), "z".to_string());
266 fields.insert("alpha".to_string(), "a".to_string());
267 fields.insert("mango".to_string(), "m".to_string());
268
269 let event = LogEvent::new(
270 Severity::Info,
271 "sorted".to_string(),
272 Labels::default(),
273 fields,
274 );
275
276 let keys: Vec<&str> = event.fields.keys().map(String::as_str).collect();
277 assert_eq!(keys, vec!["alpha", "mango", "zebra"]);
278 }
279
280 #[test]
285 fn severity_trace_serializes_to_lowercase_json() {
286 let s = serde_json::to_string(&Severity::Trace).unwrap();
287 assert_eq!(s, r#""trace""#);
288 }
289
290 #[test]
291 fn severity_debug_serializes_to_lowercase_json() {
292 let s = serde_json::to_string(&Severity::Debug).unwrap();
293 assert_eq!(s, r#""debug""#);
294 }
295
296 #[test]
297 fn severity_info_serializes_to_lowercase_json() {
298 let s = serde_json::to_string(&Severity::Info).unwrap();
299 assert_eq!(s, r#""info""#);
300 }
301
302 #[test]
303 fn severity_warn_serializes_to_lowercase_json() {
304 let s = serde_json::to_string(&Severity::Warn).unwrap();
305 assert_eq!(s, r#""warn""#);
306 }
307
308 #[test]
309 fn severity_error_serializes_to_lowercase_json() {
310 let s = serde_json::to_string(&Severity::Error).unwrap();
311 assert_eq!(s, r#""error""#);
312 }
313
314 #[test]
315 fn severity_fatal_serializes_to_lowercase_json() {
316 let s = serde_json::to_string(&Severity::Fatal).unwrap();
317 assert_eq!(s, r#""fatal""#);
318 }
319
320 #[cfg(feature = "config")]
326 #[test]
327 fn severity_deserializes_from_lowercase_trace() {
328 let s: Severity = serde_json::from_str(r#""trace""#).unwrap();
329 assert_eq!(s, Severity::Trace);
330 }
331
332 #[cfg(feature = "config")]
333 #[test]
334 fn severity_deserializes_from_lowercase_debug() {
335 let s: Severity = serde_json::from_str(r#""debug""#).unwrap();
336 assert_eq!(s, Severity::Debug);
337 }
338
339 #[cfg(feature = "config")]
340 #[test]
341 fn severity_deserializes_from_lowercase_info() {
342 let s: Severity = serde_json::from_str(r#""info""#).unwrap();
343 assert_eq!(s, Severity::Info);
344 }
345
346 #[cfg(feature = "config")]
347 #[test]
348 fn severity_deserializes_from_lowercase_warn() {
349 let s: Severity = serde_json::from_str(r#""warn""#).unwrap();
350 assert_eq!(s, Severity::Warn);
351 }
352
353 #[cfg(feature = "config")]
354 #[test]
355 fn severity_deserializes_from_lowercase_error() {
356 let s: Severity = serde_json::from_str(r#""error""#).unwrap();
357 assert_eq!(s, Severity::Error);
358 }
359
360 #[cfg(feature = "config")]
361 #[test]
362 fn severity_deserializes_from_lowercase_fatal() {
363 let s: Severity = serde_json::from_str(r#""fatal""#).unwrap();
364 assert_eq!(s, Severity::Fatal);
365 }
366
367 #[cfg(feature = "config")]
368 #[test]
369 fn severity_rejects_uppercase_deserialization() {
370 let result: Result<Severity, _> = serde_json::from_str(r#""INFO""#);
371 assert!(
372 result.is_err(),
373 "uppercase severity string must be rejected"
374 );
375 }
376
377 #[cfg(feature = "config")]
378 #[test]
379 fn severity_rejects_unknown_variant() {
380 let result: Result<Severity, _> = serde_json::from_str(r#""critical""#);
381 assert!(result.is_err(), "unknown severity variant must be rejected");
382 }
383
384 #[cfg(feature = "config")]
389 #[test]
390 fn severity_info_serializes_to_lowercase_yaml() {
391 let s = serde_yaml_ng::to_string(&Severity::Info).unwrap();
392 assert!(s.trim() == "info", "expected 'info', got: {s}");
393 }
394
395 #[cfg(feature = "config")]
396 #[test]
397 fn severity_error_serializes_to_lowercase_yaml() {
398 let s = serde_yaml_ng::to_string(&Severity::Error).unwrap();
399 assert!(s.trim() == "error", "expected 'error', got: {s}");
400 }
401
402 #[test]
407 fn severity_is_send_and_sync() {
408 fn assert_send_sync<T: Send + Sync>() {}
409 assert_send_sync::<Severity>();
410 }
411
412 #[test]
417 fn severity_ordering_follows_severity_ladder() {
418 assert!(Severity::Trace < Severity::Debug);
419 assert!(Severity::Debug < Severity::Info);
420 assert!(Severity::Info < Severity::Warn);
421 assert!(Severity::Warn < Severity::Error);
422 assert!(Severity::Error < Severity::Fatal);
423 }
424
425 #[test]
426 fn severity_equal_variants_compare_as_equal() {
427 assert_eq!(
428 Severity::Info.cmp(&Severity::Info),
429 std::cmp::Ordering::Equal
430 );
431 assert_eq!(
432 Severity::Fatal.cmp(&Severity::Fatal),
433 std::cmp::Ordering::Equal
434 );
435 }
436
437 #[test]
438 fn severity_partial_ord_consistent_with_ord() {
439 assert_eq!(
440 Severity::Trace.partial_cmp(&Severity::Fatal),
441 Some(std::cmp::Ordering::Less)
442 );
443 assert_eq!(
444 Severity::Fatal.partial_cmp(&Severity::Trace),
445 Some(std::cmp::Ordering::Greater)
446 );
447 }
448
449 #[test]
450 fn severity_sort_produces_ascending_order() {
451 let mut levels = vec![
452 Severity::Fatal,
453 Severity::Trace,
454 Severity::Warn,
455 Severity::Debug,
456 Severity::Error,
457 Severity::Info,
458 ];
459 levels.sort();
460 assert_eq!(
461 levels,
462 vec![
463 Severity::Trace,
464 Severity::Debug,
465 Severity::Info,
466 Severity::Warn,
467 Severity::Error,
468 Severity::Fatal,
469 ]
470 );
471 }
472
473 #[test]
478 fn log_event_is_send_and_sync() {
479 fn assert_send_sync<T: Send + Sync>() {}
480 assert_send_sync::<LogEvent>();
481 }
482
483 #[test]
488 fn log_event_clone_is_independent() {
489 let ts = UNIX_EPOCH + Duration::from_secs(1000);
490 let mut fields = BTreeMap::new();
491 fields.insert("k".to_string(), "v".to_string());
492
493 let original = LogEvent::with_timestamp(
494 ts,
495 Severity::Info,
496 "msg".to_string(),
497 Labels::default(),
498 fields,
499 );
500 let mut cloned = original.clone();
501
502 cloned.message = "different".to_string();
503 cloned.fields.insert("k".to_string(), "changed".to_string());
504
505 assert_eq!(original.message, "msg");
506 assert_eq!(original.fields.get("k").map(String::as_str), Some("v"));
507 }
508
509 #[test]
514 fn new_stores_labels_correctly() {
515 let labels = Labels::from_pairs(&[("device", "wlan0"), ("hostname", "router-01")]).unwrap();
516 let event = LogEvent::new(Severity::Info, "test".to_string(), labels, BTreeMap::new());
517
518 assert_eq!(event.labels.len(), 2);
519 let label_pairs: Vec<(&str, &str)> = event.labels.iter().collect();
520 assert_eq!(label_pairs[0].0, "device");
521 assert_eq!(label_pairs[0].1, "wlan0");
522 assert_eq!(label_pairs[1].0, "hostname");
523 assert_eq!(label_pairs[1].1, "router-01");
524 }
525
526 #[test]
527 fn with_timestamp_stores_labels_correctly() {
528 let ts = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
529 let labels = Labels::from_pairs(&[("env", "staging"), ("region", "us_west")]).unwrap();
530 let event = LogEvent::with_timestamp(
531 ts,
532 Severity::Warn,
533 "warning event".to_string(),
534 labels,
535 BTreeMap::new(),
536 );
537
538 assert_eq!(event.labels.len(), 2);
539 let label_pairs: Vec<(&str, &str)> = event.labels.iter().collect();
540 assert_eq!(label_pairs[0].0, "env");
541 assert_eq!(label_pairs[0].1, "staging");
542 assert_eq!(label_pairs[1].0, "region");
543 assert_eq!(label_pairs[1].1, "us_west");
544 }
545
546 #[test]
547 fn log_event_clone_preserves_labels() {
548 let ts = UNIX_EPOCH + Duration::from_secs(1000);
549 let labels = Labels::from_pairs(&[("service", "api"), ("zone", "eu1")]).unwrap();
550 let original = LogEvent::with_timestamp(
551 ts,
552 Severity::Error,
553 "cloned".to_string(),
554 labels,
555 BTreeMap::new(),
556 );
557
558 let cloned = original.clone();
559
560 assert_eq!(cloned.labels.len(), 2);
561 let original_pairs: Vec<(&str, &str)> = original.labels.iter().collect();
562 let cloned_pairs: Vec<(&str, &str)> = cloned.labels.iter().collect();
563 assert_eq!(original_pairs, cloned_pairs);
564 }
565
566 #[test]
567 fn new_with_empty_labels_has_no_labels() {
568 let event = LogEvent::new(
569 Severity::Info,
570 "no labels".to_string(),
571 Labels::default(),
572 BTreeMap::new(),
573 );
574 assert!(event.labels.is_empty());
575 assert_eq!(event.labels.len(), 0);
576 }
577}