1#![forbid(missing_docs, unsafe_code)]
2use core::fmt::Arguments;
5use std::io::{self, IsTerminal, Write};
6use std::path::Path;
7use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
8use std::sync::{Mutex as StdMutex, OnceLock};
9use std::time::Instant;
10
11pub mod local;
13
14#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
16#[repr(u8)]
17pub enum Level {
18 Trace = 0,
20 Debug,
22 Info,
24 Warn,
26 Error,
28 Fatal,
30}
31
32#[cfg(debug_assertions)]
35const CT_MIN: Level = Level::Trace;
36#[cfg(not(debug_assertions))]
37const CT_MIN: Level = Level::Info;
38static RUNTIME_LEVEL: AtomicU8 = AtomicU8::new(Level::Info as u8);
39static SHOW_TID: AtomicBool = AtomicBool::new(cfg!(feature = "thread-id"));
40static SHOW_TIME: AtomicBool = AtomicBool::new(cfg!(feature = "timestamp"));
41static SHOW_GROUP: AtomicBool = AtomicBool::new(true);
42static SHOW_FILE_LINE: AtomicBool = AtomicBool::new(cfg!(feature = "file-line"));
43
44#[derive(Copy, Clone, Eq, PartialEq, Debug)]
46#[repr(u8)]
47pub enum ColorMode {
48 Auto,
50 Always,
52 Never,
54}
55static COLOR_MODE: AtomicU8 = AtomicU8::new(ColorMode::Auto as u8);
56#[inline]
57const fn level_from_u8(x: u8) -> Level {
58 match x {
59 0 => Level::Trace,
60 1 => Level::Debug,
61 3 => Level::Warn,
62 4 => Level::Error,
63 5 => Level::Fatal,
64 _ => Level::Info, }
66}
67#[inline]
68const fn color_mode_from_u8(x: u8) -> ColorMode {
69 match x {
70 1 => ColorMode::Always,
71 2 => ColorMode::Never,
72 _ => ColorMode::Auto,
73 }
74}
75#[inline]
76fn color_mode() -> ColorMode {
77 color_mode_from_u8(COLOR_MODE.load(Ordering::Relaxed))
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct ParseColorModeError;
83
84impl core::str::FromStr for ColorMode {
85 type Err = ParseColorModeError;
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 if s.eq_ignore_ascii_case("always") {
88 Ok(Self::Always)
89 } else if s.eq_ignore_ascii_case("never") {
90 Ok(Self::Never)
91 } else if s.is_empty() || s.eq_ignore_ascii_case("auto") {
92 Ok(Self::Auto)
93 } else {
94 Err(ParseColorModeError)
95 }
96 }
97}
98
99impl core::convert::TryFrom<&str> for ColorMode {
100 type Error = ParseColorModeError;
101 fn try_from(s: &str) -> Result<Self, Self::Error> {
102 s.parse()
103 }
104}
105
106#[derive(Copy, Clone)]
108pub enum Target {
109 Stdout,
111 Stderr,
113 Writer,
115}
116static TARGET: OnceLock<Target> = OnceLock::new();
117static WRITER: OnceLock<StdMutex<Box<dyn Write + Send>>> = OnceLock::new();
118pub fn set_target(t: Target) {
121 let _ = TARGET.set(t);
122}
123pub fn set_writer(w: Box<dyn Write + Send>) {
127 let _ = WRITER.set(StdMutex::new(w));
128 let _ = TARGET.set(Target::Writer);
130}
131pub fn set_file(path: impl AsRef<Path>) -> io::Result<()> {
135 let f = std::fs::OpenOptions::new()
136 .create(true)
137 .append(true)
138 .open(path)?;
139 set_writer(Box::new(f));
140 set_target(Target::Writer);
141 Ok(())
142}
143#[inline]
144fn target() -> Target {
145 *TARGET.get_or_init(|| Target::Stderr)
146}
147
148static EMIT_LOCK: StdMutex<()> = StdMutex::new(());
149
150#[inline]
152#[must_use]
153pub const fn ct_enabled(l: Level) -> bool {
154 (l as u8) >= (CT_MIN as u8)
155}
156#[inline]
157fn rt_enabled(l: Level) -> bool {
158 (l as u8) >= RUNTIME_LEVEL.load(Ordering::Relaxed)
159}
160
161#[cfg(feature = "color")]
162mod color {
163 pub const RST: &str = "\x1b[0m";
164 pub const BOLD: &str = "\x1b[1m";
165 pub const TRACE: &str = "\x1b[90m"; pub const DEBUG: &str = "\x1b[36m"; pub const INFO: &str = "\x1b[32m"; pub const WARN: &str = "\x1b[33m"; pub const ERROR: &str = "\x1b[31m"; pub const FATAL: &str = "\x1b[35m"; }
172#[cfg(feature = "color")]
174#[inline]
175const fn level_color(l: Level) -> &'static str {
176 use color::{DEBUG, ERROR, FATAL, INFO, TRACE, WARN};
177 match l {
178 Level::Trace => TRACE,
179 Level::Debug => DEBUG,
180 Level::Info => INFO,
181 Level::Warn => WARN,
182 Level::Error => ERROR,
183 Level::Fatal => FATAL,
184 }
185}
186
187#[inline]
189const fn level_name(l: Level) -> &'static str {
190 match l {
191 Level::Trace => "TRACE",
192 Level::Debug => "DEBUG",
193 Level::Info => "INFO",
194 Level::Warn => "WARN",
195 Level::Error => "ERROR",
196 Level::Fatal => "FATAL",
197 }
198}
199
200fn use_color() -> bool {
201 #[cfg(not(feature = "color"))]
202 {
203 false
204 }
205 #[cfg(feature = "color")]
206 {
207 match color_mode() {
208 ColorMode::Always => true,
209 ColorMode::Never => false,
210 ColorMode::Auto => match target() {
211 Target::Stdout => io::stdout().is_terminal(),
212 Target::Stderr => io::stderr().is_terminal(),
213 Target::Writer => false, },
215 }
216 }
217}
218
219#[inline]
221pub fn level() -> Level {
222 level_from_u8(RUNTIME_LEVEL.load(Ordering::Relaxed))
223}
224pub fn set_level(l: Level) {
226 RUNTIME_LEVEL.store(l as u8, Ordering::Relaxed);
227}
228pub fn set_show_thread_id(on: bool) {
230 SHOW_TID.store(on, Ordering::Relaxed);
231}
232pub fn set_show_time(on: bool) {
234 SHOW_TIME.store(on, Ordering::Relaxed);
235}
236pub fn set_show_file_line(on: bool) {
238 SHOW_FILE_LINE.store(on, Ordering::Relaxed);
239}
240pub fn set_show_group(on: bool) {
242 SHOW_GROUP.store(on, Ordering::Relaxed);
243}
244pub fn set_color_mode(mode: ColorMode) {
246 COLOR_MODE.store(mode as u8, Ordering::Relaxed);
247}
248pub fn init_from_env() {
250 if let Ok(s) = std::env::var("RUST_LOG_LEVEL") {
251 let l = match s.to_lowercase().as_str() {
252 "trace" => Level::Trace,
253 "debug" => Level::Debug,
254 "info" => Level::Info,
255 "warn" => Level::Warn,
256 "error" => Level::Error,
257 "fatal" => Level::Fatal,
258 _ => level(),
259 };
260 set_level(l);
261 }
262 if let Ok(s) = std::env::var("RUST_LOG_COLOR") {
263 set_color_mode(s.parse().unwrap_or(ColorMode::Auto));
264 }
265 if let Ok(s) = std::env::var("RUST_LOG_SHOW_TID") {
266 set_show_thread_id(s == "1" || s.eq_ignore_ascii_case("true"));
267 }
268 if let Ok(s) = std::env::var("RUST_LOG_SHOW_TIME") {
269 set_show_time(s == "1" || s.eq_ignore_ascii_case("true"));
270 }
271}
272
273#[inline]
275#[allow(dead_code)]
276const fn civil_from_days_utc(days_since_unix_epoch: i64) -> (i32, u32, u32) {
277 let z = days_since_unix_epoch + 719_468; let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
280 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let yd = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * yd + 2) / 153; let d = yd - (153 * mp + 2) / 5 + 1; let m = mp + 3 - 12 * (mp / 10); let y = 400 * era + yoe + (m <= 2) as i64; #[allow(clippy::cast_possible_truncation)]
288 #[allow(clippy::cast_sign_loss)]
289 (y as i32, m as u32, d as u32)
290}
291#[inline]
292fn write_timestamp(mut w: impl Write) {
293 #[cfg(all(feature = "timestamp", not(feature = "localtime")))]
294 {
295 use std::time::{SystemTime, UNIX_EPOCH};
296 let now = SystemTime::now()
297 .duration_since(UNIX_EPOCH)
298 .unwrap_or_default();
299 let secs = now.as_secs() as i64;
300 let ms = now.subsec_millis();
301
302 let days = secs.div_euclid(86_400);
303 let sod = secs.rem_euclid(86_400);
304 let h = (sod / 3_600) as i64;
305 let m = (sod % 3_600 / 60) as i64;
306 let s = (sod % 60) as i64;
307
308 let (year, month, day) = civil_from_days_utc(days);
309 let _ = write!(
310 w,
311 "{year:04}-{month:02}-{day:02} {h:02}:{m:02}:{s:02}.{ms:03}Z "
312 );
313 }
314 #[cfg(all(feature = "timestamp", feature = "localtime"))]
315 {
316 static TS_FMT: OnceLock<Vec<time::format_description::FormatItem<'static>>> =
318 OnceLock::new();
319 let fmt = TS_FMT.get_or_init(|| {
320 time::format_description::parse(
321 "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]",
322 )
323 .expect("valid timestamp format description")
324 });
325
326 let now = std::time::SystemTime::now();
327 let now: time::OffsetDateTime = now.into();
328 let now =
329 now.to_offset(time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC));
330 let _ = write!(w, "{} ", now.format(fmt).unwrap());
331 }
332}
333
334#[inline]
335fn write_tid(mut w: impl Write) {
336 if SHOW_TID.load(Ordering::Relaxed) {
337 #[cfg(feature = "thread-id")]
338 let _ = write!(w, " [{:?}]", std::thread::current().id());
339 }
340}
341
342#[inline]
343fn write_level(mut w: impl Write, l: Level, use_color: bool) {
344 #[cfg(feature = "color")]
345 if use_color {
346 let _ = write!(w, "{}{:<5}{}", level_color(l), level_name(l), color::RST);
347 return;
348 }
349 let _ = write!(w, "{:<5}", level_name(l));
350}
351
352fn emit_raw_bytes(bytes: &[u8]) {
353 let _g = EMIT_LOCK.lock().unwrap();
354 match target() {
355 Target::Stdout => {
356 let _ = io::stdout().lock().write_all(bytes);
357 }
358 Target::Stderr => {
359 let _ = io::stderr().lock().write_all(bytes);
360 }
361 Target::Writer => {
362 if let Some(m) = WRITER.get() {
363 let mut w = m.lock().unwrap();
364 let _ = w.write_all(bytes);
365 }
366 }
367 }
368}
369
370#[inline]
372pub fn emit(
373 l: Level,
374 group: Option<&'static str>,
375 file: &'static str,
376 line_no: u32,
377 args: Arguments,
378) {
379 if !rt_enabled(l) {
380 return;
381 }
382 let use_color = use_color();
383 let mut buf = Vec::<u8>::new();
384
385 if SHOW_TIME.load(Ordering::Relaxed) {
386 write_timestamp(&mut buf);
387 }
388 write_level(&mut buf, l, use_color);
389 write_tid(&mut buf);
390 if SHOW_FILE_LINE.load(Ordering::Relaxed) {
391 let _ = write!(&mut buf, " <{file}:{line_no}>");
392 }
393 if SHOW_GROUP.load(Ordering::Relaxed) {
394 if let Some(g) = group {
395 #[cfg(feature = "color")]
396 if use_color {
397 let _ = write!(
398 &mut buf,
399 " [{}{}{}{}]",
400 color::BOLD,
401 level_color(l),
402 g,
403 color::RST
404 );
405 } else {
406 let _ = write!(&mut buf, " [{g}]");
407 }
408 #[cfg(not(feature = "color"))]
409 {
410 let _ = write!(&mut buf, " [{g}]");
411 }
412 }
413 }
414 let _ = buf.write_all(b" ");
415 let _ = buf.write_fmt(args);
416 let _ = buf.write_all(b"\n");
417 emit_raw_bytes(&buf);
418}
419
420#[macro_export]
422macro_rules! __rustlog_log { ($lvl:expr, $grp:expr, $($t:tt)+) => {{ if $crate::ct_enabled($lvl) { $crate::emit($lvl, $grp, file!(), line!(), format_args!($($t)+)) } }} }
423#[macro_export]
425macro_rules! trace { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Trace, None, $($t)+) } }
426#[macro_export]
428macro_rules! debug { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Debug, None, $($t)+) } }
429#[macro_export]
431macro_rules! info { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Info, None, $($t)+) } }
432#[macro_export]
434macro_rules! warn { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Warn, None, $($t)+) } }
435#[macro_export]
437macro_rules! error { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Error, None, $($t)+) } }
438#[macro_export]
440macro_rules! fatal { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Fatal, None, $($t)+) } }
441#[macro_export]
443macro_rules! trace_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Trace, Some($grp), $($t)+) } }
444#[macro_export]
446macro_rules! debug_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Debug, Some($grp), $($t)+) } }
447#[macro_export]
449macro_rules! info_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Info, Some($grp), $($t)+) } }
450#[macro_export]
452macro_rules! warn_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Warn, Some($grp), $($t)+) } }
453#[macro_export]
455macro_rules! error_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Error, Some($grp), $($t)+) } }
456#[macro_export]
458macro_rules! fatal_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Fatal, Some($grp), $($t)+) } }
459#[macro_export]
461macro_rules! scope_time {
462 ($label:expr) => {
463 let _scope_time_guard = $crate::TimerGuard::new_at($label, file!(), line!());
464 };
465 ($label:expr, $body:block) => {{
466 let _scope_time_guard = $crate::TimerGuard::new_at($label, file!(), line!());
467 $body
468 }};
469}
470pub struct HumanDuration(pub std::time::Duration);
472impl core::fmt::Display for HumanDuration {
473 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
474 let d = self.0;
475 let secs = d.as_secs();
476 let nanos = d.subsec_nanos();
477 if secs == 0 {
478 if nanos < 1_000 {
479 write!(formatter, "{nanos} ns")
480 } else if nanos < 1_000_000 {
481 write!(formatter, "{} us", nanos / 1_000)
482 } else {
483 let ms = nanos / 1_000_000;
484 let us = (nanos / 1_000) % 1_000;
485 write!(formatter, "{ms}.{us:03} ms")
486 }
487 } else if secs < 60 {
488 let ms = nanos / 1_000_000;
489 write!(formatter, "{secs}.{ms:03} s")
490 } else if secs < 3_600 {
491 let m = secs / 60;
492 let s = secs % 60;
493 let ms = nanos / 1_000_000;
494 write!(formatter, "{m}m{s:02}.{ms:03}s")
495 } else if secs < 86_400 {
496 let h = secs / 3_600;
497 let m = (secs % 3_600) / 60;
498 let s = secs % 60;
499 let ms = nanos / 1_000_000;
500 write!(formatter, "{h}h{m:02}m{s:02}.{ms:03}s")
501 } else {
502 let days = secs / 86_400;
503 let rem = secs % 86_400;
504 let h = rem / 3_600;
505 let m = (rem % 3_600) / 60;
506 let s = rem % 60;
507 let ms = nanos / 1_000_000;
508 write!(formatter, "{days}d {h:02}h{m:02}m{s:02}.{ms:03}s")
509 }
510 }
511}
512impl From<std::time::Duration> for HumanDuration {
513 fn from(d: std::time::Duration) -> Self {
514 Self(d)
515 }
516}
517
518pub struct TimerGuard {
520 label: &'static str,
521 start: Instant,
522 file: &'static str,
523 line: u32,
524}
525impl TimerGuard {
526 #[inline]
528 #[must_use]
529 pub fn new_at(label: &'static str, file: &'static str, line: u32) -> Self {
530 Self {
531 label,
532 start: Instant::now(),
533 file,
534 line,
535 }
536 }
537}
538impl Drop for TimerGuard {
539 fn drop(&mut self) {
540 let elapsed = self.start.elapsed();
541 emit(
542 Level::Info,
543 Some(self.label),
544 self.file,
545 self.line,
546 format_args!("took {}", HumanDuration(elapsed)),
547 );
548 }
549}
550
551#[inline]
553pub fn banner_with(name: &str, version: &str) {
554 emit_raw_bytes(name.as_bytes());
555 emit_raw_bytes(b" v");
556 emit_raw_bytes(version.as_bytes());
557 emit_raw_bytes(b"\n");
558}
559
560#[macro_export]
561macro_rules! banner {
563 () => {
564 $crate::banner_with(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
565 };
566 ($name:expr, $version:expr) => {
567 $crate::banner_with($name, $version)
568 };
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use core::time::Duration as StdDuration;
575
576 #[test]
577 fn human_duration_formats_all_ranges() {
578 assert_eq!(
579 format!("{}", HumanDuration(StdDuration::from_nanos(500))),
580 "500 ns"
581 );
582 assert_eq!(
583 format!("{}", HumanDuration(StdDuration::from_nanos(1_500))),
584 "1 us"
585 );
586 assert_eq!(
587 format!("{}", HumanDuration(StdDuration::from_nanos(1_234_000))),
588 "1.234 ms"
589 );
590 assert_eq!(
591 format!("{}", HumanDuration(StdDuration::from_millis(1_234))),
592 "1.234 s"
593 );
594 assert_eq!(
595 format!("{}", HumanDuration(StdDuration::from_secs(65))),
596 "1m05.000s"
597 );
598 assert_eq!(
599 format!(
600 "{}",
601 HumanDuration(StdDuration::from_secs(3 * 3600 + 7 * 60 + 5))
602 ),
603 "3h07m05.000s"
604 );
605 assert_eq!(
606 format!("{}", HumanDuration(StdDuration::from_secs(2 * 86_400 + 5))),
607 "2d 00h00m05.000s"
608 );
609 }
610}