customlog/
console.rs

1use chrono::Local;
2use log::{Level, LevelFilter, Log, Metadata, Record};
3
4use super::{FormatArgs, translate_args};
5
6#[derive(Debug, Clone, Copy)]
7pub enum Console {
8    Stdout,
9    Stderr,
10}
11
12pub struct ConsoleLogger {
13    level: LevelFilter,
14    console: Console,
15    color: bool,
16    target_filter: Box<dyn Fn(&str) -> bool + Send + Sync>,
17    formatter: Box<dyn Fn(FormatArgs) -> String + Send + Sync>,
18}
19
20impl ConsoleLogger {
21    pub fn new_boxed<F1, F2>(
22        console: Console,
23        level: LevelFilter,
24        target_filter: F1,
25        formatter: F2,
26    ) -> Box<dyn Log>
27    where
28        F1: Fn(&str) -> bool + Send + Sync + 'static,
29        F2: Fn(FormatArgs) -> String + Send + Sync + 'static,
30    {
31        let color = is_console(console);
32        Box::new(Self {
33            level,
34            console,
35            color,
36            target_filter: Box::new(target_filter),
37            formatter: Box::new(formatter),
38        })
39    }
40}
41
42impl Log for ConsoleLogger {
43    fn enabled(&self, metadata: &Metadata) -> bool {
44        metadata.level() <= self.level && (self.target_filter)(metadata.target())
45    }
46
47    fn log(&self, record: &Record) {
48        if !self.enabled(record.metadata()) {
49            return;
50        }
51
52        const COL_RED: &str = "\x1b[31m";
53        const COL_YELLOW: &str = "\x1b[33m";
54        const COL_GREEN: &str = "\x1b[32m";
55        const COL_PURPLE: &str = "\x1b[35m";
56        const COL_RESET: &str = "\x1b[0m";
57
58        let timestamp = Local::now();
59
60        let level = record.level();
61        let level_str = if self.color {
62            match level {
63                Level::Error => format!("{COL_RED}[{level:5}]{COL_RESET}"),
64                Level::Warn => format!("{COL_YELLOW}[{level:5}]{COL_RESET}"),
65                Level::Info => format!("{COL_GREEN}[{level:5}]{COL_RESET}"),
66                Level::Debug => format!("{COL_PURPLE}[{level:5}]{COL_RESET}"),
67                _ => format!("[{level:5}]"),
68            }
69        } else {
70            format!("[{level:5}]")
71        };
72        let mut args = translate_args(record, timestamp);
73        args.level_str = level_str;
74
75        let output = self.formatter.as_ref()(args);
76        match self.console {
77            Console::Stdout => {
78                println!("{output}");
79            }
80            Console::Stderr => {
81                eprintln!("{output}");
82            }
83        }
84    }
85
86    fn flush(&self) {}
87}
88
89/// Stdout/Stderr is redirected?
90///
91/// If not, returns true. (colored output will be enabled)
92fn is_console(console: Console) -> bool {
93    let fd = match console {
94        Console::Stdout => libc::STDOUT_FILENO,
95        Console::Stderr => libc::STDERR_FILENO,
96    };
97    let ret = unsafe { libc::isatty(fd) };
98
99    ret != 0
100}