1use std::time::{SystemTime, UNIX_EPOCH};
68
69#[doc(hidden)]
72#[must_use]
73pub fn __format_timestamp() -> String {
74 let now = SystemTime::now()
75 .duration_since(UNIX_EPOCH)
76 .unwrap_or_default();
77
78 __format_timestamp_from_duration(now.as_secs(), now.subsec_millis())
79}
80
81#[doc(hidden)]
92#[must_use]
93#[allow(clippy::similar_names)] pub fn __format_timestamp_from_duration(secs: u64, millis: u32) -> String {
95 use crate::constants::{SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
96
97 let days = secs / SECONDS_PER_DAY;
99 let remaining = secs % SECONDS_PER_DAY;
100
101 let hours = remaining / SECONDS_PER_HOUR;
103 let remaining = remaining % SECONDS_PER_HOUR;
104 let minutes = remaining / SECONDS_PER_MINUTE;
105 let seconds = remaining % SECONDS_PER_MINUTE;
106
107 let z = days + 719468;
115
116 let era = z / 146097;
119
120 let doe = z - era * 146097;
122
123 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
129
130 let y = yoe + era * 400;
132
133 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
136
137 let mp = (5 * doy + 2) / 153;
140
141 let d = doy - (153 * mp + 2) / 5 + 1;
144
145 let m = if mp < 10 { mp + 3 } else { mp - 9 };
147
148 let year = if m <= 2 { y + 1 } else { y };
150
151 format!("{year:04}-{m:02}-{d:02}T{hours:02}:{minutes:02}:{seconds:02}.{millis:03}Z")
152}
153
154#[doc(hidden)]
168#[must_use]
169pub fn __escape_json(s: &str) -> String {
170 let estimated_capacity = s.len() + (s.len() / 10).max(8);
174 let mut result = String::with_capacity(estimated_capacity);
175 for c in s.chars() {
176 match c {
177 '"' => result.push_str("\\\""),
178 '\\' => result.push_str("\\\\"),
179 '\n' => result.push_str("\\n"),
180 '\r' => result.push_str("\\r"),
181 '\t' => result.push_str("\\t"),
182 c if c.is_control() => {
183 use std::fmt::Write;
185 let _ = write!(result, "\\u{:04x}", c as u32);
186 },
187 c => result.push(c),
188 }
189 }
190 result
191}
192
193#[doc(hidden)]
198pub fn __write_simple_log(level: &str, msg: &str) {
199 use std::io::Write;
200 let timestamp = __format_timestamp();
201 let escaped = __escape_json(msg);
202 let _ = writeln!(
203 std::io::stderr(),
204 r#"{{"level":"{level}","msg":"{escaped}","ts":"{timestamp}"}}"#,
205 );
206}
207
208#[macro_export]
218macro_rules! log_info {
219 ($($arg:tt)*) => { $crate::log::__write_simple_log("info", &format!($($arg)*)) };
220}
221
222#[macro_export]
232macro_rules! log_warn {
233 ($($arg:tt)*) => { $crate::log::__write_simple_log("warn", &format!($($arg)*)) };
234}
235
236#[macro_export]
246macro_rules! log_error {
247 ($($arg:tt)*) => { $crate::log::__write_simple_log("error", &format!($($arg)*)) };
248}
249
250#[macro_export]
262macro_rules! log_debug {
263 ($($arg:tt)*) => {{
264 #[cfg(debug_assertions)]
265 $crate::log::__write_simple_log("debug", &format!($($arg)*));
266 }};
267}
268
269pub use log_debug as debug;
271pub use log_error as error;
272pub use log_info as info;
273pub use log_warn as warn;
274
275#[doc(hidden)]
283#[must_use]
284pub fn __build_structured_log(level: &str, msg: &str, fields: &[(&str, &str)]) -> String {
285 use crate::time::now_iso;
286
287 let estimated_capacity = 50 + msg.len() * 2 + fields.len() * 30;
290 let mut output = String::with_capacity(estimated_capacity);
291
292 output.push_str(r#"{"level":""#);
293 output.push_str(level);
294 output.push_str(r#"","msg":""#);
295 output.push_str(&__escape_json(msg));
296 output.push('"');
297
298 for (key, value) in fields {
299 output.push_str(r#",""#);
300 output.push_str(&__escape_json(key));
301 output.push_str(r#"":""#);
302 output.push_str(&__escape_json(value));
303 output.push('"');
304 }
305
306 output.push_str(r#","ts":""#);
307 output.push_str(&now_iso());
308 output.push_str(r#""}"#);
309
310 output
311}
312
313#[macro_export]
343macro_rules! log {
344 ($level:ident, $msg:expr $(, $key:ident : $value:expr)* $(,)?) => {{
346 use std::io::Write;
347 let fields: &[(&str, &str)] = &[
348 $( (stringify!($key), &format!("{}", $value)) ),*
349 ];
350 let log_line = $crate::log::__build_structured_log(stringify!($level), $msg, fields);
351 let _ = writeln!(std::io::stderr(), "{}", log_line);
352 }};
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn test_escape_json_simple() {
361 assert_eq!(__escape_json("hello"), "hello");
362 }
363
364 #[test]
365 fn test_escape_json_quotes() {
366 assert_eq!(__escape_json(r#"say "hello""#), r#"say \"hello\""#);
367 }
368
369 #[test]
370 fn test_escape_json_backslash() {
371 assert_eq!(__escape_json(r"path\to\file"), r"path\\to\\file");
372 }
373
374 #[test]
375 fn test_escape_json_newlines() {
376 assert_eq!(__escape_json("line1\nline2"), "line1\\nline2");
377 }
378
379 #[test]
380 fn test_escape_json_tabs() {
381 assert_eq!(__escape_json("col1\tcol2"), "col1\\tcol2");
382 }
383
384 #[test]
385 fn test_escape_json_carriage_return() {
386 assert_eq!(__escape_json("line1\rline2"), "line1\\rline2");
387 }
388
389 #[test]
390 fn test_escape_json_control_chars() {
391 let input = "\x00\x01\x02\x1F";
393 let escaped = __escape_json(input);
394 assert!(escaped.contains("\\u0000"));
395 assert!(escaped.contains("\\u0001"));
396 assert!(escaped.contains("\\u0002"));
397 assert!(escaped.contains("\\u001f"));
398 }
399
400 #[test]
401 fn test_escape_json_mixed() {
402 let input = "Hello \"World\"\nLine2\t\x00end";
403 let escaped = __escape_json(input);
404 assert!(escaped.contains("\\\""));
405 assert!(escaped.contains("\\n"));
406 assert!(escaped.contains("\\t"));
407 assert!(escaped.contains("\\u0000"));
408 }
409
410 #[test]
411 fn test_escape_json_empty() {
412 assert_eq!(__escape_json(""), "");
413 }
414
415 #[test]
416 fn test_escape_json_unicode() {
417 assert_eq!(__escape_json("日本語"), "日本語");
419 assert_eq!(__escape_json("emoji: 🎉"), "emoji: 🎉");
420 }
421
422 #[test]
423 fn test_timestamp_format() {
424 let ts = __format_timestamp();
425 assert_eq!(ts.len(), 24);
427 assert_eq!(ts.chars().nth(4), Some('-'));
428 assert_eq!(ts.chars().nth(7), Some('-'));
429 assert_eq!(ts.chars().nth(10), Some('T'));
430 assert_eq!(ts.chars().nth(13), Some(':'));
431 assert_eq!(ts.chars().nth(16), Some(':'));
432 assert_eq!(ts.chars().nth(19), Some('.'));
433 assert_eq!(ts.chars().last(), Some('Z'));
434 }
435
436 #[test]
437 fn test_timestamp_valid_date_parts() {
438 let ts = __format_timestamp();
439 let year: u32 = ts[0..4].parse().expect("valid year");
441 assert!((1970..=3000).contains(&year));
442
443 let month: u32 = ts[5..7].parse().expect("valid month");
445 assert!((1..=12).contains(&month));
446
447 let day: u32 = ts[8..10].parse().expect("valid day");
449 assert!((1..=31).contains(&day));
450
451 let hour: u32 = ts[11..13].parse().expect("valid hour");
453 assert!(hour <= 23);
454
455 let minute: u32 = ts[14..16].parse().expect("valid minute");
457 assert!(minute <= 59);
458
459 let second: u32 = ts[17..19].parse().expect("valid second");
461 assert!(second <= 59);
462
463 let millis: u32 = ts[20..23].parse().expect("valid milliseconds");
465 assert!(millis <= 999);
466 }
467
468 #[test]
469 fn test_timestamp_changes_over_time() {
470 let ts1 = __format_timestamp();
471 std::thread::sleep(std::time::Duration::from_millis(2));
473 let ts2 = __format_timestamp();
474
475 assert_eq!(ts1.len(), 24);
478 assert_eq!(ts2.len(), 24);
479 }
480
481 #[test]
483 fn test_timestamp_epoch() {
484 assert_eq!(
486 __format_timestamp_from_duration(0, 0),
487 "1970-01-01T00:00:00.000Z"
488 );
489 }
490
491 #[test]
492 fn test_timestamp_known_date() {
493 assert_eq!(
495 __format_timestamp_from_duration(1737024600, 0),
496 "2025-01-16T10:50:00.000Z"
497 );
498 }
499
500 #[test]
501 fn test_timestamp_with_millis() {
502 assert_eq!(
504 __format_timestamp_from_duration(1737024600, 123),
505 "2025-01-16T10:50:00.123Z"
506 );
507 }
508
509 #[test]
510 fn test_timestamp_leap_year() {
511 assert_eq!(
513 __format_timestamp_from_duration(1709208000, 0),
514 "2024-02-29T12:00:00.000Z"
515 );
516 }
517
518 #[test]
519 fn test_timestamp_end_of_year() {
520 assert_eq!(
522 __format_timestamp_from_duration(1735689599, 999),
523 "2024-12-31T23:59:59.999Z"
524 );
525 }
526
527 #[test]
528 fn test_timestamp_start_of_year() {
529 assert_eq!(
531 __format_timestamp_from_duration(1704067200, 0),
532 "2024-01-01T00:00:00.000Z"
533 );
534 }
535
536 #[test]
537 fn test_timestamp_y2k() {
538 assert_eq!(
540 __format_timestamp_from_duration(946684800, 0),
541 "2000-01-01T00:00:00.000Z"
542 );
543 }
544
545 #[test]
546 fn test_timestamp_far_future() {
547 assert_eq!(
549 __format_timestamp_from_duration(4133980799, 0),
550 "2100-12-31T23:59:59.000Z"
551 );
552 }
553
554 #[test]
555 fn test_timestamp_hour_minute_second_boundaries() {
556 assert_eq!(
559 __format_timestamp_from_duration(86399, 0),
560 "1970-01-01T23:59:59.000Z"
561 );
562
563 assert_eq!(
566 __format_timestamp_from_duration(45045, 0),
567 "1970-01-01T12:30:45.000Z"
568 );
569 }
570
571 #[test]
572 fn test_timestamp_day_boundary() {
573 assert_eq!(
575 __format_timestamp_from_duration(86400, 0),
576 "1970-01-02T00:00:00.000Z"
577 );
578 }
579
580 #[test]
581 fn test_timestamp_month_boundaries() {
582 assert_eq!(
584 __format_timestamp_from_duration(31 * 86400, 0),
585 "1970-02-01T00:00:00.000Z"
586 );
587
588 assert_eq!(
590 __format_timestamp_from_duration(59 * 86400, 0),
591 "1970-03-01T00:00:00.000Z"
592 );
593 }
594
595 #[test]
596 fn test_timestamp_century_boundary() {
597 assert_eq!(
600 __format_timestamp_from_duration(951782400, 0),
601 "2000-02-29T00:00:00.000Z"
602 );
603
604 }
607
608 fn build_log_without_ts(level: &str, msg: &str, fields: &[(&str, &str)]) -> String {
614 let mut output = String::new();
615 output.push_str(r#"{"level":""#);
616 output.push_str(level);
617 output.push_str(r#"","msg":""#);
618 output.push_str(&__escape_json(msg));
619 output.push('"');
620
621 for (key, value) in fields {
622 output.push_str(r#",""#);
623 output.push_str(&__escape_json(key));
624 output.push_str(r#"":""#);
625 output.push_str(&__escape_json(value));
626 output.push('"');
627 }
628
629 output
630 }
631
632 #[test]
633 fn test_structured_log_basic() {
634 let output = build_log_without_ts("info", "user created", &[]);
635 assert_eq!(output, r#"{"level":"info","msg":"user created""#);
636 }
637
638 #[test]
639 fn test_structured_log_with_single_field() {
640 let output = build_log_without_ts("info", "user created", &[("id", "123")]);
641 assert_eq!(output, r#"{"level":"info","msg":"user created","id":"123""#);
642 }
643
644 #[test]
645 fn test_structured_log_with_multiple_fields() {
646 let output = build_log_without_ts(
647 "info",
648 "user created",
649 &[("id", "123"), ("email", "alice@example.com")],
650 );
651 assert_eq!(
652 output,
653 r#"{"level":"info","msg":"user created","id":"123","email":"alice@example.com""#
654 );
655 }
656
657 #[test]
658 fn test_structured_log_error_level() {
659 let output = build_log_without_ts(
660 "error",
661 "failed to fetch",
662 &[("url", "https://api.example.com"), ("status", "500")],
663 );
664 assert_eq!(
665 output,
666 r#"{"level":"error","msg":"failed to fetch","url":"https://api.example.com","status":"500""#
667 );
668 }
669
670 #[test]
671 fn test_structured_log_warn_level() {
672 let output = build_log_without_ts("warn", "rate limit approaching", &[("remaining", "5")]);
673 assert_eq!(
674 output,
675 r#"{"level":"warn","msg":"rate limit approaching","remaining":"5""#
676 );
677 }
678
679 #[test]
680 fn test_structured_log_debug_level() {
681 let output = build_log_without_ts(
682 "debug",
683 "request parsed",
684 &[("method", "GET"), ("path", "/users")],
685 );
686 assert_eq!(
687 output,
688 r#"{"level":"debug","msg":"request parsed","method":"GET","path":"/users""#
689 );
690 }
691
692 #[test]
693 fn test_structured_log_escapes_message() {
694 let output = build_log_without_ts("info", "message with \"quotes\"", &[]);
695 assert_eq!(output, r#"{"level":"info","msg":"message with \"quotes\"""#);
696 }
697
698 #[test]
699 fn test_structured_log_escapes_field_values() {
700 let output = build_log_without_ts("info", "test", &[("data", "line1\nline2")]);
701 assert_eq!(
702 output,
703 r#"{"level":"info","msg":"test","data":"line1\nline2""#
704 );
705 }
706
707 #[test]
708 fn test_structured_log_full_output_format() {
709 let output = __build_structured_log("info", "test message", &[("key", "value")]);
711
712 assert!(output.starts_with(r#"{"level":"info""#));
714
715 assert!(output.contains(r#""msg":"test message""#));
717
718 assert!(output.contains(r#""key":"value""#));
720
721 assert!(output.contains(r#","ts":"20"#)); assert!(output.ends_with(r#"Z"}"#));
724 }
725
726 #[test]
727 fn test_structured_log_timestamp_is_valid_iso() {
728 let output = __build_structured_log("info", "test", &[]);
729
730 let ts_start = output.find(r#""ts":""#).expect("should have ts field") + 6;
732 let ts_end = output[ts_start..].find('"').expect("should close ts") + ts_start;
733 let ts = &output[ts_start..ts_end];
734
735 assert!(ts.ends_with('Z'), "timestamp should end with Z");
737 assert!(ts.contains('T'), "timestamp should contain T separator");
738 assert!(
739 ts.len() == 20 || ts.len() == 24,
740 "timestamp should be 20 or 24 chars"
741 );
742 }
743}