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 eprintln!("{out}");
71 }
72 }
73 }
74
75 fn flush(&self) {}
76}
77
78static LOGGER: Lazy<Logger> = Lazy::new(Logger::init);
79
80impl Logger {
81 fn init() -> Self {
82 let term_level = *env::PITCHFORK_LOG;
83 let file_level = *env::PITCHFORK_LOG_FILE_LEVEL;
84
85 TERM_LEVEL.store(term_level as usize, Ordering::Relaxed);
87 FILE_LEVEL.store(file_level as usize, Ordering::Relaxed);
88
89 let mut logger = Logger { log_file: None };
90
91 let log_file = &*env::PITCHFORK_LOG_FILE;
92 if let Ok(log_file) = init_log_file(log_file) {
93 logger.log_file = Some(Mutex::new(log_file));
94 } else {
95 warn!("could not open log file: {log_file:?}");
96 }
97
98 logger
99 }
100
101 fn apply_settings_levels(&self) {
109 use std::sync::atomic::Ordering;
110 let s = crate::settings::settings();
111
112 let term_level: LevelFilter = s.general.log_level.parse().unwrap_or(LevelFilter::Info);
113 let file_level: LevelFilter = s.general.log_file_level.parse().unwrap_or(term_level);
114 let max_level = std::cmp::max(term_level, file_level);
115
116 TERM_LEVEL.store(term_level as usize, Ordering::Relaxed);
123 FILE_LEVEL.store(file_level as usize, Ordering::Relaxed);
124
125 log::set_max_level(max_level);
128 }
129
130 fn render(&self, record: &Record, level: LevelFilter) -> String {
131 match level {
132 LevelFilter::Off => "".to_string(),
133 LevelFilter::Trace => {
134 let file = record.file().unwrap_or("<unknown>");
135 let ignore_crates = ["/notify-debouncer-full-", "/notify-"];
136 if record.level() == Level::Trace && ignore_crates.iter().any(|c| file.contains(c))
137 {
138 return "".to_string();
139 }
140 let meta = ui::style::edim(format!(
141 "{thread_id:>2} [{file}:{line}]",
142 thread_id = thread_id(),
143 line = record.line().unwrap_or(0),
144 ));
145 format!(
146 "{level} {meta} {args}",
147 level = self.styled_level(record.level()),
148 args = record.args()
149 )
150 }
151 LevelFilter::Debug => format!(
152 "{level} {args}",
153 level = self.styled_level(record.level()),
154 args = record.args()
155 ),
156 _ => {
157 let pitchfork = match record.level() {
158 Level::Error => ui::style::ered("pitchfork"),
159 Level::Warn => ui::style::eyellow("pitchfork"),
160 _ => ui::style::edim("pitchfork"),
161 };
162 match record.level() {
163 Level::Info => format!("{pitchfork} {args}", args = record.args()),
164 _ => format!(
165 "{pitchfork} {level} {args}",
166 level = self.styled_level(record.level()),
167 args = record.args()
168 ),
169 }
170 }
171 }
172 }
173
174 fn styled_level(&self, level: Level) -> String {
175 let level = match level {
176 Level::Error => ui::style::ered("ERROR").to_string(),
177 Level::Warn => ui::style::eyellow("WARN").to_string(),
178 Level::Info => ui::style::ecyan("INFO").to_string(),
179 Level::Debug => ui::style::emagenta("DEBUG").to_string(),
180 Level::Trace => ui::style::edim("TRACE").to_string(),
181 };
182 console::pad_str(&level, 5, console::Alignment::Left, None).to_string()
183 }
184}
185
186pub fn thread_id() -> String {
187 let id = format!("{:?}", thread::current().id());
188 let id = id.replace("ThreadId(", "");
189 id.replace(")", "")
190}
191
192pub fn init() {
193 static INIT: std::sync::Once = std::sync::Once::new();
194 INIT.call_once(|| {
195 let max_level = std::cmp::max(load_term_level(), load_file_level());
196 if let Err(err) = log::set_logger(&*LOGGER).map(|()| log::set_max_level(max_level)) {
197 eprintln!("pitchfork: could not initialize logger: {err}");
198 }
199 });
200}
201
202pub fn apply_settings() {
207 LOGGER.apply_settings_levels();
208}
209
210fn init_log_file(log_file: &Path) -> Result<File> {
211 if let Some(log_dir) = log_file.parent() {
212 xx::file::mkdirp(log_dir)?;
213 }
214 OpenOptions::new()
215 .create(true)
216 .append(true)
217 .open(log_file)
218 .into_diagnostic()
219}