1use crate::Result;
2use std::fs::{File, OpenOptions};
3use std::io::Write;
4use std::path::Path;
5use std::sync::Mutex;
6use std::sync::atomic::{AtomicUsize, Ordering};
7use std::thread;
8
9use crate::{env, ui};
10use log::{Level, LevelFilter, Metadata, Record};
11use miette::IntoDiagnostic;
12use once_cell::sync::Lazy;
13
14static TERM_LEVEL: AtomicUsize = AtomicUsize::new(LevelFilter::Info as usize);
16static FILE_LEVEL: AtomicUsize = AtomicUsize::new(LevelFilter::Info as usize);
17
18fn usize_to_level_filter(n: usize) -> LevelFilter {
19 match n {
20 0 => LevelFilter::Off,
21 1 => LevelFilter::Error,
22 2 => LevelFilter::Warn,
23 3 => LevelFilter::Info,
24 4 => LevelFilter::Debug,
25 5 => LevelFilter::Trace,
26 _ => LevelFilter::Info, }
28}
29
30fn load_term_level() -> LevelFilter {
31 usize_to_level_filter(TERM_LEVEL.load(Ordering::Relaxed))
32}
33
34fn load_file_level() -> LevelFilter {
35 usize_to_level_filter(FILE_LEVEL.load(Ordering::Relaxed))
36}
37
38#[derive(Debug)]
39struct Logger {
40 log_file: Option<Mutex<File>>,
41}
42
43impl log::Log for Logger {
44 fn enabled(&self, metadata: &Metadata) -> bool {
45 let max_level = std::cmp::max(load_term_level(), load_file_level());
46 metadata.level() <= max_level
47 }
48
49 fn log(&self, record: &Record) {
50 let file_level = load_file_level();
51 let term_level = load_term_level();
52 if record.level() <= file_level
53 && let Some(log_file) = &self.log_file
54 {
55 let mut log_file = match log_file.lock() {
56 Ok(guard) => guard,
57 Err(poisoned) => poisoned.into_inner(),
58 };
59 let out = format!(
60 "{now} {level} {args}",
61 now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
62 level = self.styled_level(record.level()),
63 args = record.args()
64 );
65 let _ = writeln!(log_file, "{}", console::strip_ansi_codes(&out));
66 }
67 if record.level() <= term_level {
68 let out = self.render(record, term_level);
69 if !out.is_empty() {
70 clx::progress::pause();
71 eprintln!("{out}");
72 clx::progress::resume();
73 }
74 }
75 }
76
77 fn flush(&self) {}
78}
79
80static LOGGER: Lazy<Logger> = Lazy::new(Logger::init);
81
82impl Logger {
83 fn init() -> Self {
84 let term_level = *env::PITCHFORK_LOG;
85 let file_level = *env::PITCHFORK_LOG_FILE_LEVEL;
86
87 TERM_LEVEL.store(term_level as usize, Ordering::Relaxed);
89 FILE_LEVEL.store(file_level as usize, Ordering::Relaxed);
90
91 let mut logger = Logger { log_file: None };
92
93 let log_file = &*env::PITCHFORK_LOG_FILE;
94 if let Ok(log_file) = init_log_file(log_file) {
95 logger.log_file = Some(Mutex::new(log_file));
96 } else {
97 warn!("could not open log file: {log_file:?}");
98 }
99
100 logger
101 }
102
103 fn apply_settings_levels(&self) {
111 use std::sync::atomic::Ordering;
112 let s = crate::settings::settings();
113
114 let term_level: LevelFilter = s.general.log_level.parse().unwrap_or(LevelFilter::Info);
115 let file_level: LevelFilter = s.general.log_file_level.parse().unwrap_or(term_level);
116 let max_level = std::cmp::max(term_level, file_level);
117
118 TERM_LEVEL.store(term_level as usize, Ordering::Relaxed);
125 FILE_LEVEL.store(file_level as usize, Ordering::Relaxed);
126
127 log::set_max_level(max_level);
130 }
131
132 fn render(&self, record: &Record, level: LevelFilter) -> String {
133 match level {
134 LevelFilter::Off => "".to_string(),
135 LevelFilter::Trace => {
136 let file = record.file().unwrap_or("<unknown>");
137 let ignore_crates = ["/notify-debouncer-full-", "/notify-"];
138 if record.level() == Level::Trace && ignore_crates.iter().any(|c| file.contains(c))
139 {
140 return "".to_string();
141 }
142 let meta = ui::style::edim(format!(
143 "{thread_id:>2} [{file}:{line}]",
144 thread_id = thread_id(),
145 line = record.line().unwrap_or(0),
146 ));
147 format!(
148 "{level} {meta} {args}",
149 level = self.styled_level(record.level()),
150 args = record.args()
151 )
152 }
153 LevelFilter::Debug => format!(
154 "{level} {args}",
155 level = self.styled_level(record.level()),
156 args = record.args()
157 ),
158 _ => {
159 let pitchfork = match record.level() {
160 Level::Error => ui::style::ered("pitchfork"),
161 Level::Warn => ui::style::eyellow("pitchfork"),
162 _ => ui::style::edim("pitchfork"),
163 };
164 match record.level() {
165 Level::Info => format!("{pitchfork} {args}", args = record.args()),
166 _ => format!(
167 "{pitchfork} {level} {args}",
168 level = self.styled_level(record.level()),
169 args = record.args()
170 ),
171 }
172 }
173 }
174 }
175
176 fn styled_level(&self, level: Level) -> String {
177 let level = match level {
178 Level::Error => ui::style::ered("ERROR").to_string(),
179 Level::Warn => ui::style::eyellow("WARN").to_string(),
180 Level::Info => ui::style::ecyan("INFO").to_string(),
181 Level::Debug => ui::style::emagenta("DEBUG").to_string(),
182 Level::Trace => ui::style::edim("TRACE").to_string(),
183 };
184 console::pad_str(&level, 5, console::Alignment::Left, None).to_string()
185 }
186}
187
188pub fn thread_id() -> String {
189 let id = format!("{:?}", thread::current().id());
190 let id = id.replace("ThreadId(", "");
191 id.replace(")", "")
192}
193
194pub fn init() {
195 static INIT: std::sync::Once = std::sync::Once::new();
196 INIT.call_once(|| {
197 let max_level = std::cmp::max(load_term_level(), load_file_level());
198 if let Err(err) = log::set_logger(&*LOGGER).map(|()| log::set_max_level(max_level)) {
199 eprintln!("pitchfork: could not initialize logger: {err}");
200 }
201 });
202}
203
204pub fn apply_settings() {
209 LOGGER.apply_settings_levels();
210}
211
212fn init_log_file(log_file: &Path) -> Result<File> {
213 if let Some(log_dir) = log_file.parent() {
214 xx::file::mkdirp(log_dir)?;
215 }
216 OpenOptions::new()
217 .create(true)
218 .append(true)
219 .open(log_file)
220 .into_diagnostic()
221}