//! Logging support for `hx`. use std::io::Write; use std::path::Path; use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; /// Minimal `log::Log` implementation: a level filter plus a single line-buffered sink. struct Logger { level: log::LevelFilter, sink: Mutex>, } impl log::Log for Logger { fn enabled(&self, metadata: &log::Metadata) -> bool { metadata.level() <= self.level } fn log(&self, record: &log::Record) { if !self.enabled(record.metadata()) { return; } if let Ok(mut sink) = self.sink.lock() { let _ = writeln!( sink, "{} {} [{}] {}", log_timestamp(), record.target(), record.level(), record.args() ); } } fn flush(&self) { if let Ok(mut sink) = self.sink.lock() { let _ = sink.flush(); } } } fn install( level: log::LevelFilter, sink: Box, ) -> Result<(), log::SetLoggerError> { log::set_boxed_logger(Box::new(Logger { level, sink: Mutex::new(sink), }))?; log::set_max_level(level); Ok(()) } /// Install the global logger writing to `path` (created if absent, appended to). pub fn init_file(level: log::LevelFilter, path: &Path) -> std::io::Result<()> { let file = std::fs::OpenOptions::new() .append(true) .create(true) .open(path)?; install(level, Box::new(file)).map_err(std::io::Error::other) } /// Install the global logger writing to stdout (used by integration tests). #[cfg(feature = "integration")] pub fn init_stdout(level: log::LevelFilter) { let _ = install(level, Box::new(std::io::stdout())); } /// RFC3339-style UTC timestamp for a log line: `YYYY-MM-DDTHH:MM:SS.mmm`. pub fn log_timestamp() -> String { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default(); format_timestamp(now.as_secs(), now.subsec_millis()) } fn format_timestamp(secs: u64, millis: u32) -> String { let days = (secs / 86_400) as i64; let tod = secs % 86_400; let (hour, min, sec) = (tod / 3600, (tod % 3600) / 60, tod % 60); let (year, month, day) = civil_from_days(days); format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}.{millis:03}") } /// Howard Hinnant's `civil_from_days`: days since the Unix epoch (1970-01-01, /// UTC) to `(year, month, day)`. Exact for the whole proleptic Gregorian range. fn civil_from_days(z: i64) -> (i64, u32, u32) { let z = z + 719_468; let era = if z >= 0 { z } else { z - 146_096 } / 146_097; let doe = z - era * 146_097; // [0, 146096] let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399] let year = yoe + era * 400; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] let mp = (5 * doy + 2) / 153; // [0, 11] let day = (doy - (153 * mp + 2) / 5 + 1) as u32; // [1, 31] let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32; // [1, 12] (if month <= 2 { year + 1 } else { year }, month, day) } #[cfg(test)] mod tests { use super::format_timestamp; #[test] fn timestamp_matches_known_instants() { // epoch assert_eq!(format_timestamp(0, 0), "1970-01-01T00:00:00.000"); // next day, and time-of-day + millis assert_eq!( format_timestamp(86_400 + 3661, 7), "1970-01-02T01:01:01.007" ); // a leap day: 2024-02-29T12:30:45.123 UTC == 1709209845 s assert_eq!( format_timestamp(1_709_209_845, 123), "2024-02-29T12:30:45.123" ); // 2000-03-01 (the algorithm's era boundary) assert_eq!(format_timestamp(951_868_800, 0), "2000-03-01T00:00:00.000"); } }