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