1use std::collections::BTreeMap;
21
22use serde::ser::SerializeMap;
23use serde::Serialize;
24
25use crate::model::log::LogEvent;
26use crate::model::metric::{Labels, MetricEvent};
27use crate::{EncoderError, SondaError};
28
29use super::Encoder;
30
31pub struct JsonLines {
43 precision: Option<u8>,
45}
46
47impl JsonLines {
48 pub fn new(precision: Option<u8>) -> Self {
53 Self { precision }
54 }
55}
56
57impl Default for JsonLines {
58 fn default() -> Self {
59 Self::new(None)
60 }
61}
62
63struct LabelsRef<'a>(&'a Labels);
70
71impl<'a> Serialize for LabelsRef<'a> {
72 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
73 let mut map = serializer.serialize_map(Some(self.0.len()))?;
74 for (k, v) in self.0.iter() {
75 map.serialize_entry(k, v)?;
76 }
77 map.end()
78 }
79}
80
81struct StringMapRef<'a>(&'a BTreeMap<String, String>);
86
87impl<'a> Serialize for StringMapRef<'a> {
88 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
89 let mut map = serializer.serialize_map(Some(self.0.len()))?;
90 for (k, v) in self.0.iter() {
91 map.serialize_entry(k.as_str(), v.as_str())?;
92 }
93 map.end()
94 }
95}
96
97#[derive(Serialize)]
105struct JsonMetric<'a> {
106 name: &'a str,
107 value: f64,
108 labels: LabelsRef<'a>,
109 timestamp: &'a str,
110}
111
112#[derive(Serialize)]
121struct JsonLog<'a> {
122 timestamp: &'a str,
123 severity: &'a str,
124 message: &'a str,
125 labels: LabelsRef<'a>,
126 fields: StringMapRef<'a>,
127}
128
129impl Encoder for JsonLines {
130 fn encode_metric(&self, event: &MetricEvent, buf: &mut Vec<u8>) -> Result<(), SondaError> {
135 let ts_bytes = super::format_rfc3339_millis_array(event.timestamp)?;
136 let timestamp =
138 std::str::from_utf8(&ts_bytes).expect("RFC 3339 timestamp is always valid UTF-8");
139
140 let value = match self.precision {
141 None => event.value,
142 Some(n) => {
143 let factor = 10f64.powi(n as i32);
144 (event.value * factor).round() / factor
145 }
146 };
147
148 let record = JsonMetric {
149 name: &event.name,
150 value,
151 labels: LabelsRef(&event.labels),
152 timestamp,
153 };
154
155 serde_json::to_writer(&mut *buf, &record)
156 .map_err(|e| SondaError::Encoder(EncoderError::SerializationFailed(e)))?;
157
158 buf.push(b'\n');
159
160 Ok(())
161 }
162
163 fn encode_log(&self, event: &LogEvent, buf: &mut Vec<u8>) -> Result<(), SondaError> {
169 let ts_bytes = super::format_rfc3339_millis_array(event.timestamp)?;
170 let timestamp =
171 std::str::from_utf8(&ts_bytes).expect("RFC 3339 timestamp is always valid UTF-8");
172
173 let severity_str = match event.severity {
175 crate::model::log::Severity::Trace => "trace",
176 crate::model::log::Severity::Debug => "debug",
177 crate::model::log::Severity::Info => "info",
178 crate::model::log::Severity::Warn => "warn",
179 crate::model::log::Severity::Error => "error",
180 crate::model::log::Severity::Fatal => "fatal",
181 };
182
183 let record = JsonLog {
184 timestamp,
185 severity: severity_str,
186 message: &event.message,
187 labels: LabelsRef(&event.labels),
188 fields: StringMapRef(&event.fields),
189 };
190
191 serde_json::to_writer(&mut *buf, &record)
192 .map_err(|e| SondaError::Encoder(EncoderError::SerializationFailed(e)))?;
193
194 buf.push(b'\n');
195
196 Ok(())
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::model::metric::{Labels, MetricEvent};
204 use std::time::{Duration, UNIX_EPOCH};
205
206 fn make_event(
208 name: &str,
209 value: f64,
210 labels: &[(&str, &str)],
211 timestamp: std::time::SystemTime,
212 ) -> MetricEvent {
213 let labels = Labels::from_pairs(labels).unwrap();
214 MetricEvent::with_timestamp(name.to_string(), value, labels, timestamp).unwrap()
215 }
216
217 #[test]
220 fn output_is_valid_json_parseable_by_serde_json() {
221 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
222 let event = make_event("cpu_usage", 0.75, &[("host", "srv1")], ts);
223 let encoder = JsonLines::new(None);
224 let mut buf = Vec::new();
225 encoder.encode_metric(&event, &mut buf).unwrap();
226
227 let line = String::from_utf8(buf).unwrap();
228 let line = line.trim_end_matches('\n');
229 let parsed: serde_json::Value = serde_json::from_str(line).expect("must be valid JSON");
230 assert!(parsed.is_object(), "output must be a JSON object");
231 }
232
233 #[test]
236 fn roundtrip_name_matches_original_event() {
237 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
238 let event = make_event("http_requests", 42.0, &[], ts);
239 let encoder = JsonLines::new(None);
240 let mut buf = Vec::new();
241 encoder.encode_metric(&event, &mut buf).unwrap();
242
243 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
244 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
245 assert_eq!(parsed["name"], "http_requests");
246 }
247
248 #[test]
249 fn roundtrip_value_matches_original_event() {
250 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
251 let event = make_event("latency", 3.14, &[], ts);
252 let encoder = JsonLines::new(None);
253 let mut buf = Vec::new();
254 encoder.encode_metric(&event, &mut buf).unwrap();
255
256 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
257 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
258 assert!((parsed["value"].as_f64().unwrap() - 3.14).abs() < f64::EPSILON);
259 }
260
261 #[test]
262 fn roundtrip_labels_match_original_event() {
263 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
264 let event = make_event("metric", 1.0, &[("env", "prod"), ("host", "srv1")], ts);
265 let encoder = JsonLines::new(None);
266 let mut buf = Vec::new();
267 encoder.encode_metric(&event, &mut buf).unwrap();
268
269 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
270 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
271 assert_eq!(parsed["labels"]["env"], "prod");
272 assert_eq!(parsed["labels"]["host"], "srv1");
273 }
274
275 #[test]
276 fn roundtrip_timestamp_matches_original_event() {
277 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
279 let event = make_event("up", 1.0, &[], ts);
280 let encoder = JsonLines::new(None);
281 let mut buf = Vec::new();
282 encoder.encode_metric(&event, &mut buf).unwrap();
283
284 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
285 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
286 assert_eq!(
287 parsed["timestamp"], "2023-11-14T22:13:20.000Z",
288 "timestamp must be RFC 3339 with millisecond precision"
289 );
290 }
291
292 #[test]
295 fn empty_labels_produces_empty_json_object() {
296 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
297 let event = make_event("up", 1.0, &[], ts);
298 let encoder = JsonLines::new(None);
299 let mut buf = Vec::new();
300 encoder.encode_metric(&event, &mut buf).unwrap();
301
302 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
303 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
304 assert_eq!(
305 parsed["labels"],
306 serde_json::json!({}),
307 "empty labels must be an empty JSON object"
308 );
309 }
310
311 #[test]
314 fn each_encoded_line_ends_with_newline() {
315 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
316 let event = make_event("up", 1.0, &[], ts);
317 let encoder = JsonLines::new(None);
318 let mut buf = Vec::new();
319 encoder.encode_metric(&event, &mut buf).unwrap();
320
321 assert_eq!(
322 *buf.last().unwrap(),
323 b'\n',
324 "line must terminate with newline"
325 );
326 }
327
328 #[test]
329 fn multiple_events_each_end_with_newline() {
330 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
331 let encoder = JsonLines::new(None);
332 let mut buf = Vec::new();
333 for i in 0..3u64 {
334 let event = make_event("up", i as f64, &[], ts + Duration::from_millis(i));
335 encoder.encode_metric(&event, &mut buf).unwrap();
336 }
337
338 let text = String::from_utf8(buf).unwrap();
339 let lines: Vec<&str> = text.lines().collect();
340 assert_eq!(lines.len(), 3, "must produce exactly 3 lines");
341 for line in &lines {
343 serde_json::from_str::<serde_json::Value>(line).expect("each line must be valid JSON");
344 }
345 }
346
347 #[test]
350 fn multiple_encodes_accumulate_in_same_buffer() {
351 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
352 let encoder = JsonLines::new(None);
353 let mut buf = Vec::new();
354
355 let event1 = make_event("metric_a", 1.0, &[], ts);
356 let event2 = make_event("metric_b", 2.0, &[], ts + Duration::from_millis(1));
357 encoder.encode_metric(&event1, &mut buf).unwrap();
358 encoder.encode_metric(&event2, &mut buf).unwrap();
359
360 let text = String::from_utf8(buf).unwrap();
361 assert!(
362 text.contains("metric_a"),
363 "buffer must contain first metric name"
364 );
365 assert!(
366 text.contains("metric_b"),
367 "buffer must contain second metric name"
368 );
369 }
370
371 #[test]
374 fn timestamp_uses_rfc3339_format_with_millisecond_precision() {
375 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_123);
376 let event = make_event("up", 1.0, &[], ts);
377 let encoder = JsonLines::new(None);
378 let mut buf = Vec::new();
379 encoder.encode_metric(&event, &mut buf).unwrap();
380
381 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
382 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
383 let ts_str = parsed["timestamp"].as_str().unwrap();
384
385 assert!(ts_str.ends_with('Z'), "timestamp must end with Z: {ts_str}");
387 assert!(
388 ts_str.contains('T'),
389 "timestamp must contain T separator: {ts_str}"
390 );
391 assert_eq!(
393 ts_str.len(),
394 24,
395 "timestamp must be exactly 24 chars: {ts_str}"
396 );
397 assert!(
398 ts_str.contains(".123"),
399 "milliseconds must be .123: {ts_str}"
400 );
401 }
402
403 #[test]
404 fn timestamp_at_unix_epoch_formats_correctly() {
405 let ts = UNIX_EPOCH;
406 let event = make_event("up", 1.0, &[], ts);
407 let encoder = JsonLines::new(None);
408 let mut buf = Vec::new();
409 encoder.encode_metric(&event, &mut buf).unwrap();
410
411 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
412 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
413 assert_eq!(parsed["timestamp"], "1970-01-01T00:00:00.000Z");
414 }
415
416 #[test]
417 fn timestamp_with_zero_milliseconds_shows_dot_zero_zero_zero() {
418 let ts = UNIX_EPOCH + Duration::from_secs(1);
420 let event = make_event("up", 1.0, &[], ts);
421 let encoder = JsonLines::new(None);
422 let mut buf = Vec::new();
423 encoder.encode_metric(&event, &mut buf).unwrap();
424
425 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
426 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
427 assert_eq!(parsed["timestamp"], "1970-01-01T00:00:01.000Z");
428 }
429
430 #[test]
433 fn regression_anchor_single_label_exact_output() {
434 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
436 let event = make_event("http_requests", 100.0, &[("endpoint", "api")], ts);
437 let encoder = JsonLines::new(None);
438 let mut buf = Vec::new();
439 encoder.encode_metric(&event, &mut buf).unwrap();
440
441 let output = String::from_utf8(buf).unwrap();
442 assert_eq!(
444 output,
445 "{\"name\":\"http_requests\",\"value\":100.0,\"labels\":{\"endpoint\":\"api\"},\"timestamp\":\"2026-03-20T12:00:00.000Z\"}\n"
446 );
447 }
448
449 #[test]
450 fn regression_anchor_no_labels_exact_output() {
451 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
453 let event = make_event("up", 1.0, &[], ts);
454 let encoder = JsonLines::new(None);
455 let mut buf = Vec::new();
456 encoder.encode_metric(&event, &mut buf).unwrap();
457
458 let output = String::from_utf8(buf).unwrap();
459 assert_eq!(
460 output,
461 "{\"name\":\"up\",\"value\":1.0,\"labels\":{},\"timestamp\":\"2023-11-14T22:13:20.000Z\"}\n"
462 );
463 }
464
465 #[test]
466 fn regression_anchor_multiple_labels_sorted_in_output() {
467 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
469 let event = make_event(
470 "cpu",
471 0.5,
472 &[("zone", "eu1"), ("host", "srv1"), ("env", "prod")],
473 ts,
474 );
475 let encoder = JsonLines::new(None);
476 let mut buf = Vec::new();
477 encoder.encode_metric(&event, &mut buf).unwrap();
478
479 let output = String::from_utf8(buf).unwrap();
480 assert_eq!(
481 output,
482 "{\"name\":\"cpu\",\"value\":0.5,\"labels\":{\"env\":\"prod\",\"host\":\"srv1\",\"zone\":\"eu1\"},\"timestamp\":\"2023-11-14T22:13:20.000Z\"}\n"
483 );
484 }
485
486 #[test]
489 fn json_fields_appear_in_consistent_order() {
490 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
491 let event = make_event("metric", 1.0, &[("k", "v")], ts);
492 let encoder = JsonLines::new(None);
493 let mut buf = Vec::new();
494 encoder.encode_metric(&event, &mut buf).unwrap();
495
496 let output = String::from_utf8(buf).unwrap();
497 let line = output.trim_end_matches('\n');
498
499 let name_pos = line.find("\"name\"").unwrap();
501 let value_pos = line.find("\"value\"").unwrap();
502 let labels_pos = line.find("\"labels\"").unwrap();
503 let timestamp_pos = line.find("\"timestamp\"").unwrap();
504
505 assert!(name_pos < value_pos, "name must come before value");
506 assert!(value_pos < labels_pos, "value must come before labels");
507 assert!(
508 labels_pos < timestamp_pos,
509 "labels must come before timestamp"
510 );
511 }
512
513 #[test]
516 fn json_lines_encoder_is_send_and_sync() {
517 fn assert_send_sync<T: Send + Sync>() {}
518 assert_send_sync::<JsonLines>();
519 }
520
521 #[test]
524 fn encoder_config_json_lines_creates_encoder_via_factory() {
525 use crate::encoder::{create_encoder, EncoderConfig};
526
527 let config = EncoderConfig::JsonLines { precision: None };
528 let encoder = create_encoder(&config).unwrap();
529
530 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
531 let event = make_event("up", 1.0, &[], ts);
532 let mut buf = Vec::new();
533 encoder.encode_metric(&event, &mut buf).unwrap();
534
535 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
536 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
537 assert_eq!(parsed["name"], "up");
538 }
539
540 fn fmt_ts(ts: std::time::SystemTime) -> String {
544 let mut buf = Vec::new();
545 super::super::format_rfc3339_millis(ts, &mut buf).unwrap();
546 String::from_utf8(buf).unwrap()
547 }
548
549 fn fmt_ts_array(ts: std::time::SystemTime) -> String {
551 let arr = super::super::format_rfc3339_millis_array(ts).unwrap();
552 std::str::from_utf8(&arr).unwrap().to_string()
553 }
554
555 #[test]
556 fn format_rfc3339_millis_epoch_returns_correct_string() {
557 assert_eq!(fmt_ts(UNIX_EPOCH), "1970-01-01T00:00:00.000Z");
558 }
559
560 #[test]
561 fn format_rfc3339_millis_known_timestamp_2026_03_20_returns_correct_string() {
562 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
564 assert_eq!(fmt_ts(ts), "2026-03-20T12:00:00.000Z");
565 }
566
567 #[test]
568 fn format_rfc3339_millis_preserves_milliseconds() {
569 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_456);
570 let result = fmt_ts(ts);
571 assert!(
572 result.ends_with(".456Z"),
573 "must end with .456Z but got: {result}"
574 );
575 }
576
577 #[test]
578 fn format_rfc3339_millis_midnight_boundary() {
579 let ts = UNIX_EPOCH + Duration::from_millis(86_399_999);
581 assert_eq!(fmt_ts(ts), "1970-01-01T23:59:59.999Z");
582 }
583
584 #[test]
585 fn format_rfc3339_millis_start_of_day_plus_one_second() {
586 let ts = UNIX_EPOCH + Duration::from_secs(86400); assert_eq!(fmt_ts(ts), "1970-01-02T00:00:00.000Z");
588 }
589
590 #[test]
591 fn format_rfc3339_millis_leap_year_feb_29() {
592 let ts = UNIX_EPOCH + Duration::from_secs(1_709_164_800);
596 assert_eq!(fmt_ts(ts), "2024-02-29T00:00:00.000Z");
597 }
598
599 #[test]
600 fn format_rfc3339_millis_end_of_year_dec_31() {
601 let ts = UNIX_EPOCH + Duration::from_millis(1_704_067_199_999);
603 assert_eq!(fmt_ts(ts), "2023-12-31T23:59:59.999Z");
604 }
605
606 #[test]
607 fn format_rfc3339_millis_array_matches_buffer_output() {
608 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_456);
610 assert_eq!(fmt_ts(ts), fmt_ts_array(ts));
611 }
612
613 #[test]
614 fn format_rfc3339_millis_array_is_valid_utf8() {
615 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
616 let arr = super::super::format_rfc3339_millis_array(ts).unwrap();
617 assert!(
618 std::str::from_utf8(&arr).is_ok(),
619 "array output must be valid UTF-8"
620 );
621 }
622
623 #[test]
624 fn format_rfc3339_millis_array_length_is_24() {
625 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
626 let arr = super::super::format_rfc3339_millis_array(ts).unwrap();
627 assert_eq!(
628 arr.len(),
629 24,
630 "RFC 3339 millis timestamp must be exactly 24 bytes"
631 );
632 }
633
634 fn make_log_event(
640 severity: crate::model::log::Severity,
641 message: &str,
642 fields: &[(&str, &str)],
643 ts: std::time::SystemTime,
644 ) -> crate::model::log::LogEvent {
645 let mut map = std::collections::BTreeMap::new();
646 for (k, v) in fields {
647 map.insert(k.to_string(), v.to_string());
648 }
649 crate::model::log::LogEvent::with_timestamp(
650 ts,
651 severity,
652 message.to_string(),
653 crate::model::metric::Labels::default(),
654 map,
655 )
656 }
657
658 #[test]
661 fn encode_log_produces_valid_json() {
662 use crate::model::log::Severity;
663 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
664 let event = make_log_event(Severity::Info, "hello world", &[], ts);
665 let encoder = JsonLines::new(None);
666 let mut buf = Vec::new();
667 encoder.encode_log(&event, &mut buf).unwrap();
668 let line = String::from_utf8(buf).unwrap();
669 let line = line.trim_end_matches('\n');
670 let parsed: serde_json::Value =
671 serde_json::from_str(line).expect("encode_log output must be valid JSON");
672 assert!(parsed.is_object(), "output must be a JSON object");
673 }
674
675 #[test]
678 fn encode_log_includes_timestamp_field() {
679 use crate::model::log::Severity;
680 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
681 let event = make_log_event(Severity::Info, "msg", &[], ts);
682 let encoder = JsonLines::new(None);
683 let mut buf = Vec::new();
684 encoder.encode_log(&event, &mut buf).unwrap();
685 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
686 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
687 assert!(
688 parsed.get("timestamp").is_some(),
689 "encode_log output must include 'timestamp' field"
690 );
691 }
692
693 #[test]
694 fn encode_log_includes_severity_field() {
695 use crate::model::log::Severity;
696 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
697 let event = make_log_event(Severity::Warn, "msg", &[], ts);
698 let encoder = JsonLines::new(None);
699 let mut buf = Vec::new();
700 encoder.encode_log(&event, &mut buf).unwrap();
701 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
702 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
703 assert!(
704 parsed.get("severity").is_some(),
705 "encode_log output must include 'severity' field"
706 );
707 }
708
709 #[test]
710 fn encode_log_includes_message_field() {
711 use crate::model::log::Severity;
712 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
713 let event = make_log_event(Severity::Info, "test message here", &[], ts);
714 let encoder = JsonLines::new(None);
715 let mut buf = Vec::new();
716 encoder.encode_log(&event, &mut buf).unwrap();
717 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
718 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
719 assert!(
720 parsed.get("message").is_some(),
721 "encode_log output must include 'message' field"
722 );
723 }
724
725 #[test]
726 fn encode_log_includes_fields_field() {
727 use crate::model::log::Severity;
728 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
729 let event = make_log_event(Severity::Info, "msg", &[("ip", "10.0.0.1")], ts);
730 let encoder = JsonLines::new(None);
731 let mut buf = Vec::new();
732 encoder.encode_log(&event, &mut buf).unwrap();
733 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
734 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
735 assert!(
736 parsed.get("fields").is_some(),
737 "encode_log output must include 'fields' field"
738 );
739 }
740
741 #[test]
744 fn encode_log_severity_info_is_lowercase() {
745 use crate::model::log::Severity;
746 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
747 let event = make_log_event(Severity::Info, "msg", &[], ts);
748 let encoder = JsonLines::new(None);
749 let mut buf = Vec::new();
750 encoder.encode_log(&event, &mut buf).unwrap();
751 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
752 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
753 assert_eq!(
754 parsed["severity"], "info",
755 "severity must be lowercase 'info'"
756 );
757 }
758
759 #[test]
760 fn encode_log_severity_error_is_lowercase() {
761 use crate::model::log::Severity;
762 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
763 let event = make_log_event(Severity::Error, "msg", &[], ts);
764 let encoder = JsonLines::new(None);
765 let mut buf = Vec::new();
766 encoder.encode_log(&event, &mut buf).unwrap();
767 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
768 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
769 assert_eq!(parsed["severity"], "error");
770 }
771
772 #[test]
773 fn encode_log_severity_warn_is_lowercase() {
774 use crate::model::log::Severity;
775 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
776 let event = make_log_event(Severity::Warn, "msg", &[], ts);
777 let encoder = JsonLines::new(None);
778 let mut buf = Vec::new();
779 encoder.encode_log(&event, &mut buf).unwrap();
780 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
781 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
782 assert_eq!(parsed["severity"], "warn");
783 }
784
785 #[test]
786 fn encode_log_severity_trace_is_lowercase() {
787 use crate::model::log::Severity;
788 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
789 let event = make_log_event(Severity::Trace, "msg", &[], ts);
790 let encoder = JsonLines::new(None);
791 let mut buf = Vec::new();
792 encoder.encode_log(&event, &mut buf).unwrap();
793 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
794 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
795 assert_eq!(parsed["severity"], "trace");
796 }
797
798 #[test]
799 fn encode_log_severity_debug_is_lowercase() {
800 use crate::model::log::Severity;
801 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
802 let event = make_log_event(Severity::Debug, "msg", &[], ts);
803 let encoder = JsonLines::new(None);
804 let mut buf = Vec::new();
805 encoder.encode_log(&event, &mut buf).unwrap();
806 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
807 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
808 assert_eq!(parsed["severity"], "debug");
809 }
810
811 #[test]
812 fn encode_log_severity_fatal_is_lowercase() {
813 use crate::model::log::Severity;
814 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
815 let event = make_log_event(Severity::Fatal, "msg", &[], ts);
816 let encoder = JsonLines::new(None);
817 let mut buf = Vec::new();
818 encoder.encode_log(&event, &mut buf).unwrap();
819 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
820 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
821 assert_eq!(parsed["severity"], "fatal");
822 }
823
824 #[test]
827 fn encode_log_roundtrip_message_matches_original() {
828 use crate::model::log::Severity;
829 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
830 let event = make_log_event(Severity::Info, "Request from 10.0.0.1", &[], ts);
831 let encoder = JsonLines::new(None);
832 let mut buf = Vec::new();
833 encoder.encode_log(&event, &mut buf).unwrap();
834 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
835 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
836 assert_eq!(parsed["message"], "Request from 10.0.0.1");
837 }
838
839 #[test]
840 fn encode_log_roundtrip_fields_match_original() {
841 use crate::model::log::Severity;
842 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
843 let event = make_log_event(
844 Severity::Info,
845 "req",
846 &[("ip", "10.0.0.1"), ("endpoint", "/api")],
847 ts,
848 );
849 let encoder = JsonLines::new(None);
850 let mut buf = Vec::new();
851 encoder.encode_log(&event, &mut buf).unwrap();
852 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
853 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
854 assert_eq!(parsed["fields"]["ip"], "10.0.0.1");
855 assert_eq!(parsed["fields"]["endpoint"], "/api");
856 }
857
858 #[test]
859 fn encode_log_roundtrip_timestamp_matches_original() {
860 use crate::model::log::Severity;
861 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
863 let event = make_log_event(Severity::Info, "msg", &[], ts);
864 let encoder = JsonLines::new(None);
865 let mut buf = Vec::new();
866 encoder.encode_log(&event, &mut buf).unwrap();
867 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
868 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
869 assert_eq!(
870 parsed["timestamp"], "2026-03-20T12:00:00.000Z",
871 "roundtrip timestamp must match"
872 );
873 }
874
875 #[test]
878 fn encode_log_empty_fields_produces_empty_json_object() {
879 use crate::model::log::Severity;
880 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
881 let event = make_log_event(Severity::Info, "msg", &[], ts);
882 let encoder = JsonLines::new(None);
883 let mut buf = Vec::new();
884 encoder.encode_log(&event, &mut buf).unwrap();
885 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
886 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
887 assert_eq!(
888 parsed["fields"],
889 serde_json::json!({}),
890 "empty fields must serialize as empty JSON object"
891 );
892 }
893
894 #[test]
897 fn encode_log_line_ends_with_newline() {
898 use crate::model::log::Severity;
899 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
900 let event = make_log_event(Severity::Info, "msg", &[], ts);
901 let encoder = JsonLines::new(None);
902 let mut buf = Vec::new();
903 encoder.encode_log(&event, &mut buf).unwrap();
904 assert_eq!(
905 *buf.last().unwrap(),
906 b'\n',
907 "encode_log line must end with newline"
908 );
909 }
910
911 #[test]
914 fn encode_log_fields_appear_in_spec_order() {
915 use crate::model::log::Severity;
917 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
918 let event = make_log_event(Severity::Info, "msg", &[("k", "v")], ts);
919 let encoder = JsonLines::new(None);
920 let mut buf = Vec::new();
921 encoder.encode_log(&event, &mut buf).unwrap();
922 let output = String::from_utf8(buf).unwrap();
923 let line = output.trim_end_matches('\n');
924 let ts_pos = line.find("\"timestamp\"").unwrap();
925 let sev_pos = line.find("\"severity\"").unwrap();
926 let msg_pos = line.find("\"message\"").unwrap();
927 let labels_pos = line.find("\"labels\"").unwrap();
928 let fields_pos = line.find("\"fields\"").unwrap();
929 assert!(ts_pos < sev_pos, "timestamp must come before severity");
930 assert!(sev_pos < msg_pos, "severity must come before message");
931 assert!(msg_pos < labels_pos, "message must come before labels");
932 assert!(labels_pos < fields_pos, "labels must come before fields");
933 }
934
935 #[test]
938 fn encode_log_regression_anchor_simple_info_event() {
939 use crate::model::log::Severity;
940 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
942 let event = make_log_event(Severity::Info, "Request from 10.0.0.1", &[], ts);
943 let encoder = JsonLines::new(None);
944 let mut buf = Vec::new();
945 encoder.encode_log(&event, &mut buf).unwrap();
946 let output = String::from_utf8(buf).unwrap();
947 assert_eq!(
948 output,
949 "{\"timestamp\":\"2026-03-20T12:00:00.000Z\",\"severity\":\"info\",\"message\":\"Request from 10.0.0.1\",\"labels\":{},\"fields\":{}}\n"
950 );
951 }
952
953 #[test]
954 fn encode_log_regression_anchor_with_fields() {
955 use crate::model::log::Severity;
956 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
958 let event = make_log_event(
959 Severity::Error,
960 "db timeout",
961 &[("endpoint", "/api"), ("ip", "10.0.0.1")],
962 ts,
963 );
964 let encoder = JsonLines::new(None);
965 let mut buf = Vec::new();
966 encoder.encode_log(&event, &mut buf).unwrap();
967 let output = String::from_utf8(buf).unwrap();
968 assert_eq!(
969 output,
970 "{\"timestamp\":\"2026-03-20T12:00:00.000Z\",\"severity\":\"error\",\"message\":\"db timeout\",\"labels\":{},\"fields\":{\"endpoint\":\"/api\",\"ip\":\"10.0.0.1\"}}\n"
971 );
972 }
973
974 fn make_log_event_with_labels(
980 severity: crate::model::log::Severity,
981 message: &str,
982 labels: &[(&str, &str)],
983 fields: &[(&str, &str)],
984 ts: std::time::SystemTime,
985 ) -> crate::model::log::LogEvent {
986 let mut field_map = std::collections::BTreeMap::new();
987 for (k, v) in fields {
988 field_map.insert(k.to_string(), v.to_string());
989 }
990 let label_set = crate::model::metric::Labels::from_pairs(labels).unwrap();
991 crate::model::log::LogEvent::with_timestamp(
992 ts,
993 severity,
994 message.to_string(),
995 label_set,
996 field_map,
997 )
998 }
999
1000 #[test]
1001 fn encode_log_with_labels_includes_labels_in_json() {
1002 use crate::model::log::Severity;
1003 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
1004 let event = make_log_event_with_labels(
1005 Severity::Info,
1006 "labeled event",
1007 &[("device", "wlan0"), ("hostname", "router_01")],
1008 &[],
1009 ts,
1010 );
1011 let encoder = JsonLines::new(None);
1012 let mut buf = Vec::new();
1013 encoder.encode_log(&event, &mut buf).unwrap();
1014 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
1015 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
1016 assert_eq!(parsed["labels"]["device"], "wlan0");
1017 assert_eq!(parsed["labels"]["hostname"], "router_01");
1018 }
1019
1020 #[test]
1021 fn encode_log_labels_are_sorted_by_key() {
1022 use crate::model::log::Severity;
1023 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
1024 let event = make_log_event_with_labels(
1026 Severity::Info,
1027 "sorted labels",
1028 &[("zone", "eu1"), ("env", "prod"), ("app", "sonda")],
1029 &[],
1030 ts,
1031 );
1032 let encoder = JsonLines::new(None);
1033 let mut buf = Vec::new();
1034 encoder.encode_log(&event, &mut buf).unwrap();
1035 let output = String::from_utf8(buf).unwrap();
1036 let line = output.trim_end_matches('\n');
1037
1038 let app_pos = line.find("\"app\"").unwrap();
1040 let env_pos = line.find("\"env\"").unwrap();
1041 let zone_pos = line.find("\"zone\"").unwrap();
1042 assert!(
1043 app_pos < env_pos,
1044 "app must come before env in sorted labels"
1045 );
1046 assert!(
1047 env_pos < zone_pos,
1048 "env must come before zone in sorted labels"
1049 );
1050 }
1051
1052 #[test]
1053 fn encode_log_with_empty_labels_produces_empty_labels_object() {
1054 use crate::model::log::Severity;
1055 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
1056 let event = make_log_event(Severity::Info, "no labels", &[], ts);
1057 let encoder = JsonLines::new(None);
1058 let mut buf = Vec::new();
1059 encoder.encode_log(&event, &mut buf).unwrap();
1060 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
1061 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
1062 assert_eq!(
1063 parsed["labels"],
1064 serde_json::json!({}),
1065 "empty labels must serialize as empty JSON object"
1066 );
1067 }
1068
1069 #[test]
1070 fn encode_log_regression_anchor_with_labels_exact_output() {
1071 use crate::model::log::Severity;
1072 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
1073 let event = make_log_event_with_labels(
1074 Severity::Info,
1075 "Request from 10.0.0.1",
1076 &[("device", "wlan0")],
1077 &[("ip", "10.0.0.1")],
1078 ts,
1079 );
1080 let encoder = JsonLines::new(None);
1081 let mut buf = Vec::new();
1082 encoder.encode_log(&event, &mut buf).unwrap();
1083 let output = String::from_utf8(buf).unwrap();
1084 assert_eq!(
1085 output,
1086 "{\"timestamp\":\"2026-03-20T12:00:00.000Z\",\"severity\":\"info\",\"message\":\"Request from 10.0.0.1\",\"labels\":{\"device\":\"wlan0\"},\"fields\":{\"ip\":\"10.0.0.1\"}}\n"
1087 );
1088 }
1089
1090 #[test]
1091 fn encode_log_with_labels_and_fields_both_present() {
1092 use crate::model::log::Severity;
1093 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
1094 let event = make_log_event_with_labels(
1095 Severity::Error,
1096 "timeout",
1097 &[("env", "prod")],
1098 &[("endpoint", "/api")],
1099 ts,
1100 );
1101 let encoder = JsonLines::new(None);
1102 let mut buf = Vec::new();
1103 encoder.encode_log(&event, &mut buf).unwrap();
1104 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
1105 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
1106 assert_eq!(parsed["labels"]["env"], "prod");
1108 assert_eq!(parsed["fields"]["endpoint"], "/api");
1109 }
1110
1111 #[test]
1114 fn prometheus_encoder_encode_log_still_returns_not_supported_after_slice_2_3() {
1115 use crate::encoder::{create_encoder, EncoderConfig};
1116 let encoder = create_encoder(&EncoderConfig::PrometheusText { precision: None }).unwrap();
1117 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
1118 let event = make_log_event(crate::model::log::Severity::Info, "should fail", &[], ts);
1119 let mut buf = Vec::new();
1120 let result = encoder.encode_log(&event, &mut buf);
1121 assert!(
1122 result.is_err(),
1123 "prometheus encoder must still return error for encode_log"
1124 );
1125 let msg = result.unwrap_err().to_string();
1126 assert!(
1127 msg.contains("not supported"),
1128 "error must mention 'not supported', got: {msg}"
1129 );
1130 assert!(buf.is_empty(), "buffer must remain empty on error");
1131 }
1132
1133 #[test]
1136 fn precision_two_rounds_json_value() {
1137 let encoder = JsonLines::new(Some(2));
1138 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
1139 let event = make_event("cpu", 99.60573, &[], ts);
1140 let mut buf = Vec::new();
1141 encoder.encode_metric(&event, &mut buf).unwrap();
1142 let line = String::from_utf8(buf).unwrap();
1143 let line = line.trim_end_matches('\n');
1144 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
1145 let value = parsed["value"].as_f64().unwrap();
1146 assert!((value - 99.61).abs() < 1e-10, "expected 99.61, got {value}");
1148 }
1149
1150 #[test]
1151 fn precision_none_preserves_full_value_in_json() {
1152 let encoder = JsonLines::new(None);
1153 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
1154 let event = make_event("cpu", 99.60573506572389, &[], ts);
1155 let mut buf = Vec::new();
1156 encoder.encode_metric(&event, &mut buf).unwrap();
1157 let line = String::from_utf8(buf).unwrap();
1158 let line = line.trim_end_matches('\n');
1159 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
1160 let value = parsed["value"].as_f64().unwrap();
1161 assert!(
1164 (value - 99.60573506572389).abs() < 1e-11,
1165 "full precision must be preserved: {value}"
1166 );
1167 }
1168
1169 #[test]
1170 fn precision_zero_rounds_to_whole_number_in_json() {
1171 let encoder = JsonLines::new(Some(0));
1172 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
1173 let event = make_event("up", 42.9, &[], ts);
1174 let mut buf = Vec::new();
1175 encoder.encode_metric(&event, &mut buf).unwrap();
1176 let line = String::from_utf8(buf).unwrap();
1177 let line = line.trim_end_matches('\n');
1178 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
1179 let value = parsed["value"].as_f64().unwrap();
1180 assert!((value - 43.0).abs() < 1e-10, "expected 43.0, got {value}");
1181 }
1182
1183 #[test]
1190 fn labels_ref_serializes_empty_labels_as_empty_object() {
1191 let labels = Labels::from_pairs(&[]).unwrap();
1192 let wrapper = super::LabelsRef(&labels);
1193 let json = serde_json::to_string(&wrapper).unwrap();
1194 assert_eq!(json, "{}");
1195 }
1196
1197 #[test]
1198 fn labels_ref_serializes_single_label() {
1199 let labels = Labels::from_pairs(&[("host", "srv1")]).unwrap();
1200 let wrapper = super::LabelsRef(&labels);
1201 let json = serde_json::to_string(&wrapper).unwrap();
1202 assert_eq!(json, r#"{"host":"srv1"}"#);
1203 }
1204
1205 #[test]
1206 fn labels_ref_serializes_multiple_labels_in_sorted_order() {
1207 let labels =
1208 Labels::from_pairs(&[("zone", "eu1"), ("env", "prod"), ("host", "srv1")]).unwrap();
1209 let wrapper = super::LabelsRef(&labels);
1210 let json = serde_json::to_string(&wrapper).unwrap();
1211 assert_eq!(json, r#"{"env":"prod","host":"srv1","zone":"eu1"}"#);
1212 }
1213
1214 #[test]
1215 fn labels_ref_handles_values_with_special_json_characters() {
1216 let labels = Labels::from_pairs(&[("msg", "hello \"world\"")]).unwrap();
1217 let wrapper = super::LabelsRef(&labels);
1218 let json = serde_json::to_string(&wrapper).unwrap();
1219 assert_eq!(json, r#"{"msg":"hello \"world\""}"#);
1221 }
1222
1223 #[test]
1226 fn string_map_ref_serializes_empty_map_as_empty_object() {
1227 let map = BTreeMap::new();
1228 let wrapper = super::StringMapRef(&map);
1229 let json = serde_json::to_string(&wrapper).unwrap();
1230 assert_eq!(json, "{}");
1231 }
1232
1233 #[test]
1234 fn string_map_ref_serializes_entries_in_sorted_order() {
1235 let mut map = BTreeMap::new();
1236 map.insert("z_key".to_string(), "last".to_string());
1237 map.insert("a_key".to_string(), "first".to_string());
1238 map.insert("m_key".to_string(), "middle".to_string());
1239 let wrapper = super::StringMapRef(&map);
1240 let json = serde_json::to_string(&wrapper).unwrap();
1241 assert_eq!(json, r#"{"a_key":"first","m_key":"middle","z_key":"last"}"#);
1242 }
1243
1244 #[test]
1247 fn encode_metric_many_labels_produces_sorted_json_object() {
1248 let pairs: Vec<(&str, &str)> = vec![
1250 ("j", "10"),
1251 ("i", "9"),
1252 ("h", "8"),
1253 ("g", "7"),
1254 ("f", "6"),
1255 ("e", "5"),
1256 ("d", "4"),
1257 ("c", "3"),
1258 ("b", "2"),
1259 ("a", "1"),
1260 ];
1261 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
1262 let event = make_event("multi", 1.0, &pairs, ts);
1263 let encoder = JsonLines::new(None);
1264 let mut buf = Vec::new();
1265 encoder.encode_metric(&event, &mut buf).unwrap();
1266 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
1267 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
1268 let labels = parsed["labels"].as_object().unwrap();
1269 let keys: Vec<&String> = labels.keys().collect();
1270 assert_eq!(
1271 keys,
1272 &["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
1273 "labels must be sorted alphabetically"
1274 );
1275 }
1276
1277 #[test]
1278 fn encode_log_many_fields_produces_sorted_json_object() {
1279 use crate::model::log::Severity;
1280 let ts = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
1281 let event = make_log_event(
1282 Severity::Info,
1283 "multi-field",
1284 &[
1285 ("z_field", "last"),
1286 ("a_field", "first"),
1287 ("m_field", "mid"),
1288 ],
1289 ts,
1290 );
1291 let encoder = JsonLines::new(None);
1292 let mut buf = Vec::new();
1293 encoder.encode_log(&event, &mut buf).unwrap();
1294 let line = std::str::from_utf8(&buf).unwrap().trim_end_matches('\n');
1295 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
1296 let fields = parsed["fields"].as_object().unwrap();
1297 let keys: Vec<&String> = fields.keys().collect();
1298 assert_eq!(
1299 keys,
1300 &["a_field", "m_field", "z_field"],
1301 "fields must be sorted alphabetically"
1302 );
1303 }
1304}