|
25 | 25 | //! completely eliminated by the compiler. |
26 | 26 |
|
27 | 27 | use std::io::Write; |
| 28 | +use std::time::SystemTime; |
28 | 29 |
|
29 | | -/// Trait for structured logging with zero-cost when disabled. |
30 | | -pub trait Logger { |
31 | | - fn log(&mut self, level: &str, msg: &str); |
| 30 | +// ==================================================================== |
| 31 | +// Log level filter |
| 32 | +// ==================================================================== |
| 33 | + |
| 34 | +/// Bitmask of allowed log levels for a stream. |
| 35 | +/// |
| 36 | +/// Combine levels with `|`: |
| 37 | +/// ``` |
| 38 | +/// use mmlogger::LevelFilter; |
| 39 | +/// let filter = LevelFilter::INFO | LevelFilter::WARN | LevelFilter::ERROR; |
| 40 | +/// ``` |
| 41 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 42 | +pub struct LevelFilter(u8); |
| 43 | + |
| 44 | +impl LevelFilter { |
| 45 | + pub const NONE: LevelFilter = LevelFilter(0b0000); |
| 46 | + pub const DEBUG: LevelFilter = LevelFilter(0b0001); |
| 47 | + pub const INFO: LevelFilter = LevelFilter(0b0010); |
| 48 | + // TODO: Add "PROGRESS" level between INFO and WARN. |
| 49 | + pub const WARN: LevelFilter = LevelFilter(0b0100); |
| 50 | + pub const ERROR: LevelFilter = LevelFilter(0b1000); |
| 51 | + pub const ALL: LevelFilter = LevelFilter(0b1111); |
| 52 | + |
| 53 | + pub fn allows(self, level: &str) -> bool { |
| 54 | + let bit: u8 = match level { |
| 55 | + "DEBUG" => 0b0001, |
| 56 | + "INFO" => 0b0010, |
| 57 | + "WARN" => 0b0100, |
| 58 | + "ERROR" => 0b1000, |
| 59 | + _ => 0, |
| 60 | + }; |
| 61 | + self.0 & bit != 0 |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +impl std::ops::BitOr for LevelFilter { |
| 66 | + type Output = Self; |
| 67 | + fn bitor(self, rhs: Self) -> Self { |
| 68 | + LevelFilter(self.0 | rhs.0) |
| 69 | + } |
32 | 70 | } |
33 | 71 |
|
34 | | -/// Logger that writes to any `Write` implementor. |
35 | | -pub struct BaseLogger<W: Write> { |
36 | | - writer: W, |
| 72 | +// ==================================================================== |
| 73 | +// Log format |
| 74 | +// ==================================================================== |
| 75 | + |
| 76 | +/// Output format for a log stream. |
| 77 | +pub enum LogFormat { |
| 78 | + /// Plain message with no prefix: `Message here` |
| 79 | + /// |
| 80 | + /// WARN and ERROR levels are prefixed with `Warning: ` and `Error: ` |
| 81 | + /// respectively. |
| 82 | + Plain, |
| 83 | + /// Level-prefixed: `[LEVEL] Message here` |
| 84 | + LevelPrefix, |
| 85 | + /// Timestamped: `YYYY-MM-DD HH:MM:SS [LEVEL] Message here` |
| 86 | + Timestamp, |
37 | 87 | } |
38 | 88 |
|
39 | | -impl<W: Write> BaseLogger<W> { |
40 | | - pub fn new(writer: W) -> Self { |
41 | | - Self { writer } |
| 89 | +// ==================================================================== |
| 90 | +// Timestamp helpers (stdlib only, no extra dependencies) |
| 91 | +// ==================================================================== |
| 92 | + |
| 93 | +/// Format a Unix timestamp (seconds since epoch) as `YYYY-MM-DD HH:MM:SS`. |
| 94 | +/// |
| 95 | +/// Uses the Gregorian calendar proleptic algorithm (valid for all |
| 96 | +/// positive Unix timestamps). |
| 97 | +fn format_unix_timestamp(secs: u64, buf: &mut [u8; 19]) { |
| 98 | + // Time-of-day components. |
| 99 | + let time_of_day = secs % 86400; |
| 100 | + let hh = time_of_day / 3600; |
| 101 | + let mm = (time_of_day % 3600) / 60; |
| 102 | + let ss = time_of_day % 60; |
| 103 | + |
| 104 | + // Date components via the civil calendar algorithm. |
| 105 | + // |
| 106 | + // http://howardhinnant.github.io/date_algorithms.html |
| 107 | + let days = (secs / 86400) as i64 + 719468; // shift epoch to 0000-03-01 |
| 108 | + let era = if days >= 0 { days } else { days - 146096 } / 146097; |
| 109 | + let doe = days - era * 146097; // day of era [0, 146096] |
| 110 | + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399] |
| 111 | + let y = yoe + era * 400; |
| 112 | + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] |
| 113 | + let mp = (5 * doy + 2) / 153; // [0, 11] |
| 114 | + let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31] |
| 115 | + let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12] |
| 116 | + let y = if m <= 2 { y + 1 } else { y }; |
| 117 | + |
| 118 | + // Write directly into the fixed-size buffer. |
| 119 | + // |
| 120 | + // Format: YYYY-MM-DD HH:MM:SS (19 bytes) |
| 121 | + let write2 = |buf: &mut [u8], offset: usize, v: u64| { |
| 122 | + buf[offset] = b'0' + (v / 10) as u8; |
| 123 | + buf[offset + 1] = b'0' + (v % 10) as u8; |
| 124 | + }; |
| 125 | + let write4 = |buf: &mut [u8], offset: usize, v: i64| { |
| 126 | + buf[offset] = b'0' + (v / 1000) as u8; |
| 127 | + buf[offset + 1] = b'0' + (v / 100 % 10) as u8; |
| 128 | + buf[offset + 2] = b'0' + (v / 10 % 10) as u8; |
| 129 | + buf[offset + 3] = b'0' + (v % 10) as u8; |
| 130 | + }; |
| 131 | + |
| 132 | + write4(buf, 0, y); |
| 133 | + buf[4] = b'-'; |
| 134 | + write2(buf, 5, m as u64); |
| 135 | + buf[7] = b'-'; |
| 136 | + write2(buf, 8, d as u64); |
| 137 | + buf[10] = b' '; |
| 138 | + write2(buf, 11, hh); |
| 139 | + buf[13] = b':'; |
| 140 | + write2(buf, 14, mm); |
| 141 | + buf[16] = b':'; |
| 142 | + write2(buf, 17, ss); |
| 143 | +} |
| 144 | + |
| 145 | +fn write_formatted<W: Write>( |
| 146 | + writer: &mut W, |
| 147 | + format: &LogFormat, |
| 148 | + level: &str, |
| 149 | + msg: &str, |
| 150 | +) { |
| 151 | + match format { |
| 152 | + LogFormat::Plain => match level { |
| 153 | + "WARN" => { |
| 154 | + let _ = writeln!(writer, "Warning: {}", msg); |
| 155 | + } |
| 156 | + "ERROR" => { |
| 157 | + let _ = writeln!(writer, "Error: {}", msg); |
| 158 | + } |
| 159 | + _ => { |
| 160 | + let _ = writeln!(writer, "{}", msg); |
| 161 | + } |
| 162 | + }, |
| 163 | + LogFormat::LevelPrefix => { |
| 164 | + let _ = writeln!(writer, "[{}] {}", level, msg); |
| 165 | + } |
| 166 | + LogFormat::Timestamp => { |
| 167 | + let secs = SystemTime::now() |
| 168 | + .duration_since(SystemTime::UNIX_EPOCH) |
| 169 | + .unwrap_or_default() |
| 170 | + .as_secs(); |
| 171 | + let mut buf = [0u8; 19]; |
| 172 | + format_unix_timestamp(secs, &mut buf); |
| 173 | + let ts = std::str::from_utf8(&buf).unwrap_or("0000-00-00 00:00:00"); |
| 174 | + let _ = writeln!(writer, "[{}] [{}] {}", ts, level, msg); |
| 175 | + } |
42 | 176 | } |
43 | 177 | } |
44 | 178 |
|
45 | | -impl<W: Write> Logger for BaseLogger<W> { |
46 | | - fn log(&mut self, level: &str, msg: &str) { |
47 | | - let _ = writeln!(self.writer, "[{}] {}", level, msg); |
| 179 | +// ==================================================================== |
| 180 | +// Logger trait |
| 181 | +// ==================================================================== |
| 182 | + |
| 183 | +/// Trait for structured logging with zero-cost when disabled. |
| 184 | +pub trait Logger { |
| 185 | + // TODO: Get rid of this general method and force people to either |
| 186 | + // create a new level, or reuse an existing one. We need to have a |
| 187 | + // trade-off between flexibility and performance and simplicity, |
| 188 | + // and I think the `log` method is simply too flexible and adds |
| 189 | + // the cost of a string rather than a well defined enum that we |
| 190 | + // can exhaustively match and ensure all cases are covered. |
| 191 | + fn log(&mut self, level: &str, msg: &str); |
| 192 | + |
| 193 | + fn info(&mut self, msg: &str) { |
| 194 | + self.log("INFO", msg); |
| 195 | + } |
| 196 | + |
| 197 | + // TODO: Add "progress" method between info and warn. |
| 198 | + |
| 199 | + fn warn(&mut self, msg: &str) { |
| 200 | + self.log("WARN", msg); |
| 201 | + } |
| 202 | + |
| 203 | + fn error(&mut self, msg: &str) { |
| 204 | + self.log("ERROR", msg); |
| 205 | + } |
| 206 | + |
| 207 | + fn debug(&mut self, msg: &str) { |
| 208 | + self.log("DEBUG", msg); |
48 | 209 | } |
49 | 210 | } |
50 | 211 |
|
51 | | -/// Logger that writes to two outputs simultaneously. |
52 | | -pub struct TeeLogger<W1: Write, W2: Write> { |
| 212 | +// ==================================================================== |
| 213 | +// DualStreamLogger |
| 214 | +// ==================================================================== |
| 215 | + |
| 216 | +/// Logger that writes to two outputs simultaneously, with independent |
| 217 | +/// level filtering and output formatting per stream. |
| 218 | +/// |
| 219 | +/// Default construction via [`DualStreamLogger::new`] uses: |
| 220 | +/// - Stream 1: plain format (`Message here`), all levels allowed. |
| 221 | +/// - Stream 2: timestamped format (`YYYY-MM-DD HH:MM:SS [LEVEL] Message here`), |
| 222 | +/// all levels allowed. |
| 223 | +/// |
| 224 | +pub struct DualStreamLogger<W1: Write, W2: Write> { |
53 | 225 | writer1: W1, |
| 226 | + format1: LogFormat, |
| 227 | + levels1: LevelFilter, |
54 | 228 | writer2: W2, |
| 229 | + format2: LogFormat, |
| 230 | + levels2: LevelFilter, |
55 | 231 | } |
56 | 232 |
|
57 | | -impl<W1: Write, W2: Write> TeeLogger<W1, W2> { |
58 | | - pub fn new(writer1: W1, writer2: W2) -> Self { |
59 | | - Self { writer1, writer2 } |
| 233 | +impl<W1: Write, W2: Write> DualStreamLogger<W1, W2> { |
| 234 | + /// Create a `DualStreamLogger` with explicit format and level |
| 235 | + /// configuration for each stream. |
| 236 | + pub fn new( |
| 237 | + writer1: W1, |
| 238 | + format1: LogFormat, |
| 239 | + levels1: LevelFilter, |
| 240 | + writer2: W2, |
| 241 | + format2: LogFormat, |
| 242 | + levels2: LevelFilter, |
| 243 | + ) -> Self { |
| 244 | + Self { |
| 245 | + writer1, |
| 246 | + format1, |
| 247 | + levels1, |
| 248 | + writer2, |
| 249 | + format2, |
| 250 | + levels2, |
| 251 | + } |
60 | 252 | } |
61 | 253 | } |
62 | 254 |
|
63 | | -impl<W1: Write, W2: Write> Logger for TeeLogger<W1, W2> { |
| 255 | +impl<W1: Write, W2: Write> Logger for DualStreamLogger<W1, W2> { |
64 | 256 | fn log(&mut self, level: &str, msg: &str) { |
65 | | - let _ = writeln!(self.writer1, "[{}] {}", level, msg); |
66 | | - let _ = writeln!(self.writer2, "[{}] {}", level, msg); |
| 257 | + if self.levels1.allows(level) { |
| 258 | + write_formatted(&mut self.writer1, &self.format1, level, msg); |
| 259 | + } |
| 260 | + if self.levels2.allows(level) { |
| 261 | + write_formatted(&mut self.writer2, &self.format2, level, msg); |
| 262 | + } |
67 | 263 | } |
68 | 264 | } |
69 | 265 |
|
| 266 | +// ==================================================================== |
| 267 | +// NoOpLogger |
| 268 | +// ==================================================================== |
| 269 | + |
70 | 270 | /// No-op logger with zero runtime cost. |
71 | 271 | pub struct NoOpLogger; |
72 | 272 |
|
73 | 273 | impl Logger for NoOpLogger { |
74 | 274 | #[inline(always)] |
75 | 275 | fn log(&mut self, _level: &str, _msg: &str) {} |
76 | 276 | } |
| 277 | + |
| 278 | +// ==================================================================== |
| 279 | +// Macros |
| 280 | +// ==================================================================== |
| 281 | + |
| 282 | +/// Debug print macro that expands to `if DEBUG { eprintln!(...) }`. |
| 283 | +/// |
| 284 | +/// Requires a `const DEBUG: bool` in the calling scope. When `DEBUG` |
| 285 | +/// is `false`, the compiler optimizes the branch away entirely. |
| 286 | +#[macro_export] |
| 287 | +macro_rules! mm_debug_eprintln { |
| 288 | + ($($arg:tt)*) => { |
| 289 | + if DEBUG { |
| 290 | + eprintln!($($arg)*); |
| 291 | + } |
| 292 | + }; |
| 293 | +} |
| 294 | + |
| 295 | +/// Debug log macro that routes through a logger instead of eprintln. |
| 296 | +/// |
| 297 | +/// Requires a `const DEBUG: bool` in the calling scope and a mutable |
| 298 | +/// logger as the first argument. When `DEBUG` is `false`, the compiler |
| 299 | +/// optimizes the entire call away. |
| 300 | +#[macro_export] |
| 301 | +macro_rules! mm_debug_log { |
| 302 | + ($logger:expr, $msg:literal) => { |
| 303 | + if DEBUG { |
| 304 | + $logger.debug($msg); |
| 305 | + } |
| 306 | + }; |
| 307 | + ($logger:expr, $fmt:literal, $($arg:tt)*) => { |
| 308 | + if DEBUG { |
| 309 | + $logger.debug(&format!($fmt, $($arg)*)); |
| 310 | + } |
| 311 | + }; |
| 312 | +} |
| 313 | + |
| 314 | +/// Info log macro — avoids the `&format!(...)` boilerplate. |
| 315 | +#[macro_export] |
| 316 | +macro_rules! mm_info_log { |
| 317 | + ($logger:expr, $msg:literal) => { |
| 318 | + $logger.info($msg); |
| 319 | + }; |
| 320 | + ($logger:expr, $fmt:literal, $($arg:tt)*) => { |
| 321 | + $logger.info(&format!($fmt, $($arg)*)); |
| 322 | + }; |
| 323 | +} |
| 324 | + |
| 325 | +/// Warn log macro — avoids the `&format!(...)` boilerplate. |
| 326 | +#[macro_export] |
| 327 | +macro_rules! mm_warn_log { |
| 328 | + ($logger:expr, $msg:literal) => { |
| 329 | + $logger.warn($msg); |
| 330 | + }; |
| 331 | + ($logger:expr, $fmt:literal, $($arg:tt)*) => { |
| 332 | + $logger.warn(&format!($fmt, $($arg)*)); |
| 333 | + }; |
| 334 | +} |
| 335 | + |
| 336 | +/// Error log macro — avoids the `&format!(...)` boilerplate. |
| 337 | +#[macro_export] |
| 338 | +macro_rules! mm_error_log { |
| 339 | + ($logger:expr, $msg:literal) => { |
| 340 | + $logger.error($msg); |
| 341 | + }; |
| 342 | + ($logger:expr, $fmt:literal, $($arg:tt)*) => { |
| 343 | + $logger.error(&format!($fmt, $($arg)*)); |
| 344 | + }; |
| 345 | +} |
0 commit comments