Skip to main content

pitchfork_cli/
logger.rs

1use crate::Result;
2use std::fs::{File, OpenOptions};
3use std::io::Write;
4use std::path::Path;
5use std::sync::Mutex;
6use std::thread;
7
8use crate::{env, ui};
9use log::{Level, LevelFilter, Metadata, Record};
10use miette::IntoDiagnostic;
11use once_cell::sync::Lazy;
12
13#[derive(Debug)]
14struct Logger {
15    level: LevelFilter,
16    term_level: LevelFilter,
17    file_level: LevelFilter,
18    log_file: Option<Mutex<File>>,
19}
20
21impl log::Log for Logger {
22    fn enabled(&self, metadata: &Metadata) -> bool {
23        metadata.level() <= self.level
24    }
25
26    fn log(&self, record: &Record) {
27        if record.level() <= self.file_level
28            && let Some(log_file) = &self.log_file
29        {
30            let mut log_file = match log_file.lock() {
31                Ok(guard) => guard,
32                Err(poisoned) => poisoned.into_inner(),
33            };
34            let out = format!(
35                "{now} {level} {args}",
36                now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
37                level = self.styled_level(record.level()),
38                args = record.args()
39            );
40            let _ = writeln!(log_file, "{}", console::strip_ansi_codes(&out));
41        }
42        if record.level() <= self.term_level {
43            let out = self.render(record, self.term_level);
44            if !out.is_empty() {
45                eprintln!("{out}");
46            }
47        }
48    }
49
50    fn flush(&self) {}
51}
52
53static LOGGER: Lazy<Logger> = Lazy::new(Logger::init);
54
55impl Logger {
56    fn init() -> Self {
57        let term_level = *env::PITCHFORK_LOG;
58        let file_level = *env::PITCHFORK_LOG_FILE_LEVEL;
59
60        let mut logger = Logger {
61            level: std::cmp::max(term_level, file_level),
62            file_level,
63            term_level,
64            log_file: None,
65        };
66
67        let log_file = &*env::PITCHFORK_LOG_FILE;
68        if let Ok(log_file) = init_log_file(log_file) {
69            logger.log_file = Some(Mutex::new(log_file));
70        } else {
71            warn!("could not open log file: {log_file:?}");
72        }
73
74        logger
75    }
76
77    fn render(&self, record: &Record, level: LevelFilter) -> String {
78        match level {
79            LevelFilter::Off => "".to_string(),
80            LevelFilter::Trace => {
81                let file = record.file().unwrap_or("<unknown>");
82                let ignore_crates = ["/notify-debouncer-full-", "/notify-"];
83                if record.level() == Level::Trace && ignore_crates.iter().any(|c| file.contains(c))
84                {
85                    return "".to_string();
86                }
87                let meta = ui::style::edim(format!(
88                    "{thread_id:>2} [{file}:{line}]",
89                    thread_id = thread_id(),
90                    line = record.line().unwrap_or(0),
91                ));
92                format!(
93                    "{level} {meta} {args}",
94                    level = self.styled_level(record.level()),
95                    args = record.args()
96                )
97            }
98            LevelFilter::Debug => format!(
99                "{level} {args}",
100                level = self.styled_level(record.level()),
101                args = record.args()
102            ),
103            _ => {
104                let pitchfork = match record.level() {
105                    Level::Error => ui::style::ered("pitchfork"),
106                    Level::Warn => ui::style::eyellow("pitchfork"),
107                    _ => ui::style::edim("pitchfork"),
108                };
109                match record.level() {
110                    Level::Info => format!("{pitchfork} {args}", args = record.args()),
111                    _ => format!(
112                        "{pitchfork} {level} {args}",
113                        level = self.styled_level(record.level()),
114                        args = record.args()
115                    ),
116                }
117            }
118        }
119    }
120
121    fn styled_level(&self, level: Level) -> String {
122        let level = match level {
123            Level::Error => ui::style::ered("ERROR").to_string(),
124            Level::Warn => ui::style::eyellow("WARN").to_string(),
125            Level::Info => ui::style::ecyan("INFO").to_string(),
126            Level::Debug => ui::style::emagenta("DEBUG").to_string(),
127            Level::Trace => ui::style::edim("TRACE").to_string(),
128        };
129        console::pad_str(&level, 5, console::Alignment::Left, None).to_string()
130    }
131}
132
133pub fn thread_id() -> String {
134    let id = format!("{:?}", thread::current().id());
135    let id = id.replace("ThreadId(", "");
136    id.replace(")", "")
137}
138
139pub fn init() {
140    static INIT: std::sync::Once = std::sync::Once::new();
141    INIT.call_once(|| {
142        if let Err(err) = log::set_logger(&*LOGGER).map(|()| log::set_max_level(LOGGER.level)) {
143            eprintln!("mise: could not initialize logger: {err}");
144        }
145    });
146}
147
148fn init_log_file(log_file: &Path) -> Result<File> {
149    if let Some(log_dir) = log_file.parent() {
150        xx::file::mkdirp(log_dir)?;
151    }
152    OpenOptions::new()
153        .create(true)
154        .append(true)
155        .open(log_file)
156        .into_diagnostic()
157}