use std::time::{SystemTime, UNIX_EPOCH};
#[doc(hidden)]
#[must_use]
pub fn __format_timestamp() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
__format_timestamp_from_duration(now.as_secs(), now.subsec_millis())
}
#[doc(hidden)]
#[must_use]
#[allow(clippy::similar_names)] pub fn __format_timestamp_from_duration(secs: u64, millis: u32) -> String {
use crate::constants::{SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
let days = secs / SECONDS_PER_DAY;
let remaining = secs % SECONDS_PER_DAY;
let hours = remaining / SECONDS_PER_HOUR;
let remaining = remaining % SECONDS_PER_HOUR;
let minutes = remaining / SECONDS_PER_MINUTE;
let seconds = remaining % SECONDS_PER_MINUTE;
let z = days + 719468;
let era = z / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if m <= 2 { y + 1 } else { y };
format!("{year:04}-{m:02}-{d:02}T{hours:02}:{minutes:02}:{seconds:02}.{millis:03}Z")
}
#[doc(hidden)]
#[must_use]
pub fn __escape_json(s: &str) -> String {
let estimated_capacity = s.len() + (s.len() / 10).max(8);
let mut result = String::with_capacity(estimated_capacity);
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c if c.is_control() => {
use std::fmt::Write;
let _ = write!(result, "\\u{:04x}", c as u32);
},
c => result.push(c),
}
}
result
}
#[doc(hidden)]
pub fn __write_simple_log(level: &str, msg: &str) {
use std::io::Write;
let timestamp = __format_timestamp();
let escaped = __escape_json(msg);
let _ = writeln!(
std::io::stderr(),
r#"{{"level":"{level}","msg":"{escaped}","ts":"{timestamp}"}}"#,
);
}
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => { $crate::log::__write_simple_log("info", &format!($($arg)*)) };
}
#[macro_export]
macro_rules! log_warn {
($($arg:tt)*) => { $crate::log::__write_simple_log("warn", &format!($($arg)*)) };
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => { $crate::log::__write_simple_log("error", &format!($($arg)*)) };
}
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => {{
#[cfg(debug_assertions)]
$crate::log::__write_simple_log("debug", &format!($($arg)*));
}};
}
pub use log_debug as debug;
pub use log_error as error;
pub use log_info as info;
pub use log_warn as warn;
#[doc(hidden)]
#[must_use]
pub fn __build_structured_log(level: &str, msg: &str, fields: &[(&str, &str)]) -> String {
use crate::time::now_iso;
let estimated_capacity = 50 + msg.len() * 2 + fields.len() * 30;
let mut output = String::with_capacity(estimated_capacity);
output.push_str(r#"{"level":""#);
output.push_str(level);
output.push_str(r#"","msg":""#);
output.push_str(&__escape_json(msg));
output.push('"');
for (key, value) in fields {
output.push_str(r#",""#);
output.push_str(&__escape_json(key));
output.push_str(r#"":""#);
output.push_str(&__escape_json(value));
output.push('"');
}
output.push_str(r#","ts":""#);
output.push_str(&now_iso());
output.push_str(r#""}"#);
output
}
#[macro_export]
macro_rules! log {
($level:ident, $msg:expr $(, $key:ident : $value:expr)* $(,)?) => {{
use std::io::Write;
let fields: &[(&str, &str)] = &[
$( (stringify!($key), &format!("{}", $value)) ),*
];
let log_line = $crate::log::__build_structured_log(stringify!($level), $msg, fields);
let _ = writeln!(std::io::stderr(), "{}", log_line);
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_json_simple() {
assert_eq!(__escape_json("hello"), "hello");
}
#[test]
fn test_escape_json_quotes() {
assert_eq!(__escape_json(r#"say "hello""#), r#"say \"hello\""#);
}
#[test]
fn test_escape_json_backslash() {
assert_eq!(__escape_json(r"path\to\file"), r"path\\to\\file");
}
#[test]
fn test_escape_json_newlines() {
assert_eq!(__escape_json("line1\nline2"), "line1\\nline2");
}
#[test]
fn test_escape_json_tabs() {
assert_eq!(__escape_json("col1\tcol2"), "col1\\tcol2");
}
#[test]
fn test_escape_json_carriage_return() {
assert_eq!(__escape_json("line1\rline2"), "line1\\rline2");
}
#[test]
fn test_escape_json_control_chars() {
let input = "\x00\x01\x02\x1F";
let escaped = __escape_json(input);
assert!(escaped.contains("\\u0000"));
assert!(escaped.contains("\\u0001"));
assert!(escaped.contains("\\u0002"));
assert!(escaped.contains("\\u001f"));
}
#[test]
fn test_escape_json_mixed() {
let input = "Hello \"World\"\nLine2\t\x00end";
let escaped = __escape_json(input);
assert!(escaped.contains("\\\""));
assert!(escaped.contains("\\n"));
assert!(escaped.contains("\\t"));
assert!(escaped.contains("\\u0000"));
}
#[test]
fn test_escape_json_empty() {
assert_eq!(__escape_json(""), "");
}
#[test]
fn test_escape_json_unicode() {
assert_eq!(__escape_json("日本語"), "日本語");
assert_eq!(__escape_json("emoji: 🎉"), "emoji: 🎉");
}
#[test]
fn test_timestamp_format() {
let ts = __format_timestamp();
assert_eq!(ts.len(), 24);
assert_eq!(ts.chars().nth(4), Some('-'));
assert_eq!(ts.chars().nth(7), Some('-'));
assert_eq!(ts.chars().nth(10), Some('T'));
assert_eq!(ts.chars().nth(13), Some(':'));
assert_eq!(ts.chars().nth(16), Some(':'));
assert_eq!(ts.chars().nth(19), Some('.'));
assert_eq!(ts.chars().last(), Some('Z'));
}
#[test]
fn test_timestamp_valid_date_parts() {
let ts = __format_timestamp();
let year: u32 = ts[0..4].parse().expect("valid year");
assert!((1970..=3000).contains(&year));
let month: u32 = ts[5..7].parse().expect("valid month");
assert!((1..=12).contains(&month));
let day: u32 = ts[8..10].parse().expect("valid day");
assert!((1..=31).contains(&day));
let hour: u32 = ts[11..13].parse().expect("valid hour");
assert!(hour <= 23);
let minute: u32 = ts[14..16].parse().expect("valid minute");
assert!(minute <= 59);
let second: u32 = ts[17..19].parse().expect("valid second");
assert!(second <= 59);
let millis: u32 = ts[20..23].parse().expect("valid milliseconds");
assert!(millis <= 999);
}
#[test]
fn test_timestamp_changes_over_time() {
let ts1 = __format_timestamp();
std::thread::sleep(std::time::Duration::from_millis(2));
let ts2 = __format_timestamp();
assert_eq!(ts1.len(), 24);
assert_eq!(ts2.len(), 24);
}
#[test]
fn test_timestamp_epoch() {
assert_eq!(
__format_timestamp_from_duration(0, 0),
"1970-01-01T00:00:00.000Z"
);
}
#[test]
fn test_timestamp_known_date() {
assert_eq!(
__format_timestamp_from_duration(1737024600, 0),
"2025-01-16T10:50:00.000Z"
);
}
#[test]
fn test_timestamp_with_millis() {
assert_eq!(
__format_timestamp_from_duration(1737024600, 123),
"2025-01-16T10:50:00.123Z"
);
}
#[test]
fn test_timestamp_leap_year() {
assert_eq!(
__format_timestamp_from_duration(1709208000, 0),
"2024-02-29T12:00:00.000Z"
);
}
#[test]
fn test_timestamp_end_of_year() {
assert_eq!(
__format_timestamp_from_duration(1735689599, 999),
"2024-12-31T23:59:59.999Z"
);
}
#[test]
fn test_timestamp_start_of_year() {
assert_eq!(
__format_timestamp_from_duration(1704067200, 0),
"2024-01-01T00:00:00.000Z"
);
}
#[test]
fn test_timestamp_y2k() {
assert_eq!(
__format_timestamp_from_duration(946684800, 0),
"2000-01-01T00:00:00.000Z"
);
}
#[test]
fn test_timestamp_far_future() {
assert_eq!(
__format_timestamp_from_duration(4133980799, 0),
"2100-12-31T23:59:59.000Z"
);
}
#[test]
fn test_timestamp_hour_minute_second_boundaries() {
assert_eq!(
__format_timestamp_from_duration(86399, 0),
"1970-01-01T23:59:59.000Z"
);
assert_eq!(
__format_timestamp_from_duration(45045, 0),
"1970-01-01T12:30:45.000Z"
);
}
#[test]
fn test_timestamp_day_boundary() {
assert_eq!(
__format_timestamp_from_duration(86400, 0),
"1970-01-02T00:00:00.000Z"
);
}
#[test]
fn test_timestamp_month_boundaries() {
assert_eq!(
__format_timestamp_from_duration(31 * 86400, 0),
"1970-02-01T00:00:00.000Z"
);
assert_eq!(
__format_timestamp_from_duration(59 * 86400, 0),
"1970-03-01T00:00:00.000Z"
);
}
#[test]
fn test_timestamp_century_boundary() {
assert_eq!(
__format_timestamp_from_duration(951782400, 0),
"2000-02-29T00:00:00.000Z"
);
}
fn build_log_without_ts(level: &str, msg: &str, fields: &[(&str, &str)]) -> String {
let mut output = String::new();
output.push_str(r#"{"level":""#);
output.push_str(level);
output.push_str(r#"","msg":""#);
output.push_str(&__escape_json(msg));
output.push('"');
for (key, value) in fields {
output.push_str(r#",""#);
output.push_str(&__escape_json(key));
output.push_str(r#"":""#);
output.push_str(&__escape_json(value));
output.push('"');
}
output
}
#[test]
fn test_structured_log_basic() {
let output = build_log_without_ts("info", "user created", &[]);
assert_eq!(output, r#"{"level":"info","msg":"user created""#);
}
#[test]
fn test_structured_log_with_single_field() {
let output = build_log_without_ts("info", "user created", &[("id", "123")]);
assert_eq!(output, r#"{"level":"info","msg":"user created","id":"123""#);
}
#[test]
fn test_structured_log_with_multiple_fields() {
let output = build_log_without_ts(
"info",
"user created",
&[("id", "123"), ("email", "alice@example.com")],
);
assert_eq!(
output,
r#"{"level":"info","msg":"user created","id":"123","email":"alice@example.com""#
);
}
#[test]
fn test_structured_log_error_level() {
let output = build_log_without_ts(
"error",
"failed to fetch",
&[("url", "https://api.example.com"), ("status", "500")],
);
assert_eq!(
output,
r#"{"level":"error","msg":"failed to fetch","url":"https://api.example.com","status":"500""#
);
}
#[test]
fn test_structured_log_warn_level() {
let output = build_log_without_ts("warn", "rate limit approaching", &[("remaining", "5")]);
assert_eq!(
output,
r#"{"level":"warn","msg":"rate limit approaching","remaining":"5""#
);
}
#[test]
fn test_structured_log_debug_level() {
let output = build_log_without_ts(
"debug",
"request parsed",
&[("method", "GET"), ("path", "/users")],
);
assert_eq!(
output,
r#"{"level":"debug","msg":"request parsed","method":"GET","path":"/users""#
);
}
#[test]
fn test_structured_log_escapes_message() {
let output = build_log_without_ts("info", "message with \"quotes\"", &[]);
assert_eq!(output, r#"{"level":"info","msg":"message with \"quotes\"""#);
}
#[test]
fn test_structured_log_escapes_field_values() {
let output = build_log_without_ts("info", "test", &[("data", "line1\nline2")]);
assert_eq!(
output,
r#"{"level":"info","msg":"test","data":"line1\nline2""#
);
}
#[test]
fn test_structured_log_full_output_format() {
let output = __build_structured_log("info", "test message", &[("key", "value")]);
assert!(output.starts_with(r#"{"level":"info""#));
assert!(output.contains(r#""msg":"test message""#));
assert!(output.contains(r#""key":"value""#));
assert!(output.contains(r#","ts":"20"#)); assert!(output.ends_with(r#"Z"}"#));
}
#[test]
fn test_structured_log_timestamp_is_valid_iso() {
let output = __build_structured_log("info", "test", &[]);
let ts_start = output.find(r#""ts":""#).expect("should have ts field") + 6;
let ts_end = output[ts_start..].find('"').expect("should close ts") + ts_start;
let ts = &output[ts_start..ts_end];
assert!(ts.ends_with('Z'), "timestamp should end with Z");
assert!(ts.contains('T'), "timestamp should contain T separator");
assert!(
ts.len() == 20 || ts.len() == 24,
"timestamp should be 20 or 24 chars"
);
}
}