1use std::fs::{File, OpenOptions, create_dir_all};
4use std::io::Write;
5use std::path::Path;
6use std::sync::Mutex;
7use std::sync::atomic::{AtomicU8, Ordering};
8use std::time::{Duration, Instant};
9
10use super::error::CoppError;
11
12#[derive(Default, Eq, PartialEq, Clone, Debug)]
16pub enum VerbosityOutput {
17 #[default]
27 Println,
28 Log,
49 File(String),
60}
61
62const VERBOSITY_MODE_PRINTLN: u8 = 0;
63const VERBOSITY_MODE_LOG: u8 = 1;
64const VERBOSITY_MODE_FILE: u8 = 2;
65
66static VERBOSITY_OUTPUT: AtomicU8 = AtomicU8::new(VERBOSITY_MODE_PRINTLN);
67static VERBOSITY_FILE: Mutex<Option<File>> = Mutex::new(None);
68static VERBOSITY_FILE_PATH: Mutex<Option<String>> = Mutex::new(None);
69
70pub fn set_verbosity_output(output: VerbosityOutput) -> Result<(), CoppError> {
74 match output {
75 VerbosityOutput::Println => {
76 VERBOSITY_OUTPUT.store(VERBOSITY_MODE_PRINTLN, Ordering::Relaxed);
77 Ok(())
78 }
79 VerbosityOutput::Log => {
80 VERBOSITY_OUTPUT.store(VERBOSITY_MODE_LOG, Ordering::Relaxed);
81 Ok(())
82 }
83 VerbosityOutput::File(path) => set_verbosity_log_file(path),
84 }
85}
86
87pub fn verbosity_output() -> VerbosityOutput {
89 match VERBOSITY_OUTPUT.load(Ordering::Relaxed) {
90 VERBOSITY_MODE_LOG => VerbosityOutput::Log,
91 VERBOSITY_MODE_FILE => {
92 let path = VERBOSITY_FILE_PATH
93 .lock()
94 .expect("verbosity_output: mutex poisoned")
95 .clone()
96 .unwrap_or_else(|| "<unknown>".to_string());
97 VerbosityOutput::File(path)
98 }
99 _ => VerbosityOutput::Println,
100 }
101}
102
103pub fn set_verbosity_log_file(path: impl AsRef<Path>) -> Result<(), CoppError> {
108 let path = path.as_ref();
109 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
110 create_dir_all(parent)?;
111 }
112 let file = OpenOptions::new().create(true).append(true).open(path)?;
113 let path_string = path.to_string_lossy().to_string();
114 let mut file_slot = VERBOSITY_FILE
115 .lock()
116 .expect("set_verbosity_log_file: mutex poisoned");
117 let mut path_slot = VERBOSITY_FILE_PATH
118 .lock()
119 .expect("set_verbosity_log_file(path): mutex poisoned");
120 *file_slot = Some(file);
121 *path_slot = Some(path_string);
122 VERBOSITY_OUTPUT.store(VERBOSITY_MODE_FILE, Ordering::Relaxed);
123 Ok(())
124}
125
126#[repr(u8)]
130#[derive(Default, Eq, PartialEq, PartialOrd, Ord, Clone, Copy)]
131pub enum Verbosity {
132 #[default]
134 Silent = 0,
135 Summary = 1,
140 Debug = 2,
143 Trace = 3,
146}
147
148pub fn emit_verbosity_line(level: Verbosity, message: impl AsRef<str>) {
150 let message = message.as_ref();
151 match verbosity_output() {
152 VerbosityOutput::Println => println!("{message}"),
153 VerbosityOutput::Log => match level {
154 Verbosity::Silent => {}
155 Verbosity::Summary => log::info!("{message}"),
156 Verbosity::Debug => log::debug!("{message}"),
157 Verbosity::Trace => log::trace!("{message}"),
158 },
159 VerbosityOutput::File(_) => {
160 if matches!(level, Verbosity::Silent) {
161 return;
162 }
163 let mut file_slot = VERBOSITY_FILE
164 .lock()
165 .expect("emit_verbosity_line: mutex poisoned");
166 if let Some(file) = file_slot.as_mut() {
167 let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
168 let level_tag = match level {
169 Verbosity::Silent => "SILENT",
170 Verbosity::Summary => "INFO",
171 Verbosity::Debug => "DEBUG",
172 Verbosity::Trace => "TRACE",
173 };
174 let _ = writeln!(file, "[{now}] [{level_tag}] {message}");
175 let _ = file.flush();
176 } else {
177 println!("{message}");
178 }
179 }
180 }
181}
182
183#[macro_export]
195macro_rules! verbosity_log {
196 ($level:expr, $($arg:tt)*) => {
197 $crate::diag::emit_verbosity_line($level, format!($($arg)*))
198 };
199}
200
201pub(crate) trait Verboser {
202 const LEVEL: Verbosity;
204 #[inline(always)]
206 fn is_enabled(&self, level: Verbosity) -> bool {
207 Self::LEVEL >= level
208 }
209 fn record_start_time(&mut self);
210 fn elapsed(&self) -> Duration;
211}
212
213pub(crate) struct SilentVerboser;
214
215impl Verboser for SilentVerboser {
216 const LEVEL: Verbosity = Verbosity::Silent;
217 #[inline(always)]
218 fn record_start_time(&mut self) {}
219 #[inline(always)]
220 fn elapsed(&self) -> Duration {
221 Duration::ZERO
222 }
223}
224pub(crate) struct SummaryVerboser {
225 start_time: Instant,
226}
227impl SummaryVerboser {
228 pub fn new() -> Self {
229 Self {
230 start_time: Instant::now(),
231 }
232 }
233}
234impl Verboser for SummaryVerboser {
235 const LEVEL: Verbosity = Verbosity::Summary;
236 #[inline(always)]
237 fn record_start_time(&mut self) {
238 self.start_time = Instant::now();
239 }
240 #[inline(always)]
241 fn elapsed(&self) -> Duration {
242 self.start_time.elapsed()
243 }
244}
245pub(crate) struct DebugVerboser {
246 start_time: Instant,
247}
248impl DebugVerboser {
249 pub fn new() -> Self {
250 Self {
251 start_time: Instant::now(),
252 }
253 }
254}
255impl Verboser for DebugVerboser {
256 const LEVEL: Verbosity = Verbosity::Debug;
257 #[inline(always)]
258 fn record_start_time(&mut self) {
259 self.start_time = Instant::now();
260 }
261 #[inline(always)]
262 fn elapsed(&self) -> Duration {
263 self.start_time.elapsed()
264 }
265}
266pub(crate) struct TraceVerboser {
267 start_time: Instant,
268}
269impl TraceVerboser {
270 pub fn new() -> Self {
271 Self {
272 start_time: Instant::now(),
273 }
274 }
275}
276impl Verboser for TraceVerboser {
277 const LEVEL: Verbosity = Verbosity::Trace;
278 #[inline(always)]
279 fn record_start_time(&mut self) {
280 self.start_time = Instant::now();
281 }
282 #[inline(always)]
283 fn elapsed(&self) -> Duration {
284 self.start_time.elapsed()
285 }
286}
287
288pub(crate) fn format_duration_human(d: Duration) -> String {
290 let secs = d.as_secs_f64();
291
292 fn fmt(v: f64, unit: &str) -> String {
293 if v >= 100.0 {
294 format!("{v:.0} {unit}")
295 } else if v >= 10.0 {
296 format!("{v:.1} {unit}")
297 } else {
298 format!("{v:.3} {unit}")
299 }
300 }
301
302 if secs < 1e-6 {
303 fmt(secs * 1e9, "ns")
304 } else if secs < 1e-3 {
305 fmt(secs * 1e6, "us")
306 } else if secs < 1.0 {
307 fmt(secs * 1e3, "ms")
308 } else if secs < 60.0 {
309 fmt(secs, "s")
310 } else if secs < 3600.0 {
311 fmt(secs / 60.0, "min")
312 } else {
313 fmt(secs / 3600.0, "h")
314 }
315}