evobench_tools/utillib/
logging.rs

1//! Simple logging infrastructure
2
3use std::{
4    fmt::Display,
5    io::{BufWriter, StderrLock, Write, stderr},
6    str::FromStr,
7    sync::atomic::{AtomicU8, Ordering},
8};
9
10use anyhow::{Result, bail};
11use strum::VariantNames;
12use strum_macros::EnumVariantNames;
13
14use crate::serde_types::date_and_time::DateTimeWithOffset;
15
16pub fn write_time(file: &str, line: u32, column: u32) -> BufWriter<StderrLock<'static>> {
17    // Costs an allocation.
18    let t_str = DateTimeWithOffset::now(None);
19    let mut lock = BufWriter::new(stderr().lock());
20    _ = write!(&mut lock, "{t_str}\t{file}:{line}:{column}\t");
21    lock
22}
23
24/// Logging in the medium verbosity level (warn < *info* < debug), if
25/// the expression in the first argument evaluates to true
26#[macro_export]
27macro_rules! info_if {
28    { $verbose:expr, $($arg:tt)* } => {
29        if $verbose {
30            use std::io::Write;
31            let mut lock = $crate::utillib::logging::write_time(file!(), line!(), column!());
32            _ = writeln!(&mut lock, $($arg)*);
33        }
34    }
35}
36
37// Do *not* make the fields public here to force going through
38// `From`/`Into`, OK? Also, do not add Clone to force evaluation
39// before doing anything further, OK?
40#[derive(Debug, clap::Args)]
41pub struct LogLevelOpts {
42    /// Disable warnings. Conflicts with `--verbose` and `--debug`
43    /// (decreases log-level from 'warn' to 'quiet'--only errors
44    /// interrupting processing are shown)
45    #[clap(short, long)]
46    quiet: bool,
47
48    /// Show what is being done (increases log-level from 'warn' to
49    /// 'info')
50    #[clap(short, long)]
51    verbose: bool,
52
53    /// Show information that helps debug this program (implies
54    /// `--verbose`) (increases log-level from 'warn' to 'debug')
55    #[clap(short, long)]
56    debug: bool,
57}
58
59impl LogLevelOpts {
60    /// Complain if both options in self and `opt_log_level` are
61    /// given. Returns None if neither kind of options were given (you
62    /// could `unwrap_or_default()`).
63    pub fn xor_log_level(self, opt_log_level: Option<LogLevel>) -> Result<Option<LogLevel>> {
64        if let Some(level) = TryInto::<Option<LogLevel>>::try_into(self)? {
65            if let Some(expected_log_level) = opt_log_level {
66                if level != expected_log_level {
67                    bail!(
68                        "both the {} option and log-level {} were given, please \
69                         only either give one of the options --quiet / --verbose / --debug \
70                         or a log-level",
71                        level
72                            .option_name()
73                            .expect("if TryInto gave a value then option_name will give one, too"),
74                        expected_log_level
75                    )
76                }
77            }
78            Ok(Some(level))
79        } else {
80            Ok(opt_log_level)
81        }
82    }
83}
84
85impl TryFrom<LogLevelOpts> for LogLevel {
86    type Error = anyhow::Error;
87
88    fn try_from(value: LogLevelOpts) -> Result<Self> {
89        match value {
90            LogLevelOpts {
91                verbose: false,
92                debug: false,
93                quiet: false,
94            } => Ok(LogLevel::Warn),
95            LogLevelOpts {
96                verbose: true,
97                debug: false,
98                quiet: false,
99            } => Ok(LogLevel::Info),
100            LogLevelOpts {
101                verbose: _,
102                debug: true,
103                quiet: false,
104            } => Ok(LogLevel::Debug),
105            LogLevelOpts {
106                verbose: false,
107                debug: false,
108                quiet: true,
109            } => Ok(LogLevel::Quiet),
110            LogLevelOpts {
111                verbose: _,
112                debug: _,
113                quiet: true,
114            } => bail!("option `--quiet` conflicts with the options `--verbose` and `--debug`"),
115        }
116    }
117}
118
119/// Like `TryFrom<LogLevelOpts> for LogLevel` but returns None if none
120/// of the 3 options were given.
121impl TryFrom<LogLevelOpts> for Option<LogLevel> {
122    type Error = anyhow::Error;
123
124    fn try_from(value: LogLevelOpts) -> std::result::Result<Self, Self::Error> {
125        match value {
126            LogLevelOpts {
127                verbose: false,
128                debug: false,
129                quiet: false,
130            } => Ok(None),
131            _ => Ok(Some(value.try_into()?)),
132        }
133    }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumVariantNames)]
137#[strum(serialize_all = "snake_case")]
138pub enum LogLevel {
139    /// Do not log anything
140    Quiet,
141    /// The default, only "warn!" statements are outputting anything.
142    Warn,
143    /// Verbose execution, not for debugging this program but for
144    /// giving the user information about what is going on
145    Info,
146    /// Highest amount of log statement, for debugging this program
147    Debug,
148}
149
150impl LogLevel {
151    pub const MAX: LogLevel = LogLevel::Debug;
152
153    // Not public api, only for sorting or comparisons! / level
154    // setting API.
155
156    fn level(self) -> u8 {
157        self as u8
158    }
159
160    fn from_level(level: u8) -> Option<Self> {
161        {
162            // Reminder
163            match LogLevel::Quiet {
164                LogLevel::Quiet => (),
165                LogLevel::Warn => (),
166                LogLevel::Info => (),
167                LogLevel::Debug => (),
168            }
169        }
170        match level {
171            0 => Some(LogLevel::Quiet),
172            1 => Some(LogLevel::Warn),
173            2 => Some(LogLevel::Info),
174            3 => Some(LogLevel::Debug),
175            _ => None,
176        }
177    }
178
179    /// The name of the log-level (matching what parsing accepts for
180    /// the given value)
181    fn to_str(self) -> &'static str {
182        match self {
183            LogLevel::Quiet => "quiet",
184            LogLevel::Warn => "warn",
185            LogLevel::Info => "info",
186            LogLevel::Debug => "debug",
187        }
188    }
189
190    /// The name of the (predominant) option that yields this
191    /// log-level
192    fn option_name(self) -> Option<&'static str> {
193        match self {
194            LogLevel::Quiet => Some("--quiet"),
195            LogLevel::Warn => None,
196            LogLevel::Info => Some("--verbose"),
197            LogLevel::Debug => Some("--debug"),
198        }
199    }
200}
201
202impl Default for LogLevel {
203    fn default() -> Self {
204        Self::Warn
205    }
206}
207
208#[test]
209fn t_default() {
210    assert_eq!(
211        LogLevel::default(),
212        LogLevelOpts {
213            verbose: false,
214            debug: false,
215            quiet: false
216        }
217        .try_into()
218        .expect("no conflicts")
219    );
220    assert_eq!(LogLevel::default().option_name(), None);
221}
222
223impl Display for LogLevel {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        f.write_str(self.to_str())
226    }
227}
228
229#[test]
230fn t_display() -> Result<()> {
231    for level_str in LogLevel::VARIANTS {
232        let level = LogLevel::from_str(level_str)?;
233        assert_eq!(level.to_str(), *level_str);
234    }
235    Ok(())
236}
237
238// Sigh, strum_macros::EnumString is useless as it does not show the
239// variants in its error message. (Clap 4 has its own macro instead?)
240impl FromStr for LogLevel {
241    type Err = anyhow::Error;
242
243    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
244        {
245            // Reminder
246            match LogLevel::Quiet {
247                LogLevel::Quiet => (),
248                LogLevel::Warn => (),
249                LogLevel::Info => (),
250                LogLevel::Debug => (),
251            }
252        }
253        match s {
254            "quiet" => Ok(LogLevel::Quiet),
255            "warn" => Ok(LogLevel::Warn),
256            "info" => Ok(LogLevel::Info),
257            "debug" => Ok(LogLevel::Debug),
258            _ => bail!(
259                "invalid log level name {s:?}, valid are: {}",
260                LogLevel::VARIANTS.join(", ")
261            ),
262        }
263    }
264}
265
266#[test]
267fn t_levels() -> Result<()> {
268    use std::str::FromStr;
269
270    for i in 0..=LogLevel::MAX.level() {
271        let lvl = LogLevel::from_level(i).expect("valid");
272        assert_eq!(lvl.level(), i);
273        let s = lvl.to_string();
274        assert_eq!(LogLevel::from_str(&s).unwrap(), lvl);
275    }
276    assert_eq!(LogLevel::from_level(LogLevel::MAX.level() + 1), None);
277    let lvl = LogLevel::from_str("info")?;
278    assert_eq!(lvl.level(), 2);
279    assert!(LogLevel::from_str("Info").is_err());
280    assert_eq!(lvl.to_string(), "info");
281    Ok(())
282}
283
284impl PartialOrd for LogLevel {
285    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
286        Some(self.cmp(other))
287    }
288}
289
290impl Ord for LogLevel {
291    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
292        self.level().cmp(&other.level())
293    }
294}
295
296pub static LOG_LEVEL: AtomicU8 = AtomicU8::new(1);
297
298/// Set the desired logging level (only messages with at least that
299/// importance will be logged). Changes the level for all threads.
300pub fn set_log_level(val: LogLevel) {
301    LOG_LEVEL.store(val.level(), Ordering::SeqCst);
302}
303
304/// Get the current logging level.
305#[inline]
306pub fn log_level() -> LogLevel {
307    let level = LOG_LEVEL.load(Ordering::Relaxed);
308    LogLevel::from_level(level).expect("no possibility to store invalid u8")
309}
310
311/// Logging in the least verbose level (*warn* < info < debug)
312#[macro_export]
313macro_rules! warn {
314    { $($arg:tt)* } => {
315        if $crate::utillib::logging::log_level() >= $crate::utillib::logging::LogLevel::Warn {
316            use std::io::Write;
317            let mut lock = $crate::utillib::logging::write_time(file!(), line!(), column!());
318            _ = writeln!(&mut lock, $($arg)*);
319        }
320    }
321}
322
323/// Logging in the medium verbosity level (warn < *info* < debug)
324#[macro_export]
325macro_rules! info {
326    { $($arg:tt)* } => {
327        if $crate::utillib::logging::log_level() >= $crate::utillib::logging::LogLevel::Info {
328            use std::io::Write;
329            let mut lock = $crate::utillib::logging::write_time(file!(), line!(), column!());
330            _ = writeln!(&mut lock, $($arg)*);
331        }
332    }
333}
334
335/// Logging in the most verbose level (warn < info < *debug*)
336#[macro_export]
337macro_rules! debug {
338    { $($arg:tt)* } => {
339        if $crate::utillib::logging::log_level() >= $crate::utillib::logging::LogLevel::Debug {
340            use std::io::Write;
341            let mut lock = $crate::utillib::logging::write_time(file!(), line!(), column!());
342            _ = writeln!(&mut lock, $($arg)*);
343        }
344    }
345}
346
347// -----------------------------------------------------------------------------
348
349/// Logging like `warn!` but prepending the message with `WARNING: unfinished`
350#[macro_export]
351macro_rules! unfinished {
352    { } => {
353        if $crate::utillib::logging::log_level() >= $crate::utillib::logging::LogLevel::Warn {
354            use std::io::Write;
355            let mut lock = $crate::utillib::logging::write_time(file!(), line!(), column!());
356            _ = writeln!(&mut lock, "WARNING: unfinished!");
357        }
358    };
359    { $fmt:tt $($arg:tt)* } => {
360        if $crate::utillib::logging::log_level() >= $crate::utillib::logging::LogLevel::Warn {
361            use std::io::Write;
362            let mut lock = $crate::utillib::logging::write_time(file!(), line!(), column!());
363            _ = lock.write_all("WARNING: unfinished!: ".as_bytes());
364            _ = writeln!(&mut lock, $fmt $($arg)*);
365        }
366    }
367}
368
369/// Logging like `warn!` but prepending the message with `WARNING: untested`
370#[macro_export]
371macro_rules! untested {
372    { } => {
373        if $crate::utillib::logging::log_level() >= $crate::utillib::logging::LogLevel::Warn {
374            use std::io::Write;
375            let mut lock = $crate::utillib::logging::write_time(file!(), line!(), column!());
376            _ = writeln!(&mut lock, "WARNING: untested!");
377        }
378    };
379    { $fmt:tt $($arg:tt)* } => {
380        if $crate::utillib::logging::log_level() >= $crate::utillib::logging::LogLevel::Warn {
381            use std::io::Write;
382            let mut lock = $crate::utillib::logging::write_time(file!(), line!(), column!());
383            _ = lock.write_all("WARNING: untested!: ".as_bytes());
384            _ = writeln!(&mut lock, $fmt $($arg)*);
385        }
386    }
387}