Skip to main content

data_beans/aux/
logging.rs

1//! Shared logger setup for the workspace.
2//!
3//! The progress-bar style and the single [`MULTI_PROGRESS`] now live in
4//! [`legume_numeric::matrix::progress`] — the lowest common dependency, so `data-beans`
5//! and friends can share them without a dependency cycle (this crate depends
6//! *on* `data-beans`, so the primitive can't live here). This module
7//! re-exports [`new_progress_bar`], [`new_spinner`], and [`MULTI_PROGRESS`] so
8//! every existing caller (`data_beans::aux::logging::new_progress_bar`, senna,
9//! graph-embedding-util) keeps working unchanged, and adds [`init_logger`],
10//! which wraps `env_logger` in `indicatif_log_bridge` so `log` output renders
11//! above the bars instead of corrupting them, and [`hold_logs`], which keeps
12//! records back while a full-screen view owns the terminal.
13
14use std::sync::Mutex;
15
16pub use legume_numeric::matrix::progress::{new_progress_bar, new_spinner, MULTI_PROGRESS};
17
18/// Install `env_logger` wrapped in `indicatif_log_bridge::LogWrapper` so log
19/// messages render above any active progress bar. `verbose` selects between
20/// `legume_numeric::matrix::common_io::{VERBOSE,QUIET}_LOG_FILTER`; an external `RUST_LOG`
21/// still overrides the default filter.
22pub fn init_logger(verbose: bool) {
23    let default_filter = if verbose {
24        legume_numeric::matrix::common_io::VERBOSE_LOG_FILTER
25    } else {
26        legume_numeric::matrix::common_io::QUIET_LOG_FILTER
27    };
28    let logger =
29        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_filter))
30            .build();
31    let max_level = logger.filter();
32    let wrapped = indicatif_log_bridge::LogWrapper::new(MULTI_PROGRESS.clone(), logger);
33    let _ =
34        log::set_boxed_logger(Box::new(Holding(wrapped))).map(|()| log::set_max_level(max_level));
35}
36
37/// Records held back while a full-screen view owns the terminal: level,
38/// target and message. `None` when not holding.
39static HELD: Mutex<Option<Vec<(log::Level, String, String)>>> = Mutex::new(None);
40
41/// The logger [`init_logger`] installs: forwards records, or keeps them in
42/// [`HELD`] while [`hold_logs`] is on.
43struct Holding<L>(L);
44
45impl<L: log::Log> log::Log for Holding<L> {
46    fn enabled(&self, metadata: &log::Metadata) -> bool {
47        self.0.enabled(metadata)
48    }
49
50    fn log(&self, record: &log::Record) {
51        if let Some(held) = HELD.lock().unwrap_or_else(|e| e.into_inner()).as_mut() {
52            if self.0.enabled(record.metadata()) {
53                let entry = (
54                    record.level(),
55                    record.target().into(),
56                    record.args().to_string(),
57                );
58                held.push(entry);
59            }
60            return;
61        }
62        self.0.log(record);
63    }
64
65    fn flush(&self) {
66        self.0.flush();
67    }
68}
69
70/// Hold log records back (`true`) while a full-screen view owns the
71/// terminal, where a printed line would scroll it and tear the picture; on
72/// `false`, write what was held, in order. Only affects the logger that
73/// [`init_logger`] installed.
74pub fn hold_logs(on: bool) {
75    let held = {
76        let mut slot = HELD.lock().unwrap_or_else(|e| e.into_inner());
77        if on {
78            slot.get_or_insert_with(Vec::new);
79            return;
80        }
81        slot.take()
82    };
83    for (level, target, message) in held.into_iter().flatten() {
84        log::logger().log(
85            &log::Record::builder()
86                .level(level)
87                .target(&target)
88                .args(format_args!("{message}"))
89                .build(),
90        );
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use log::Log;
98    use std::sync::atomic::{AtomicUsize, Ordering};
99
100    struct Counting(AtomicUsize);
101
102    impl Log for Counting {
103        fn enabled(&self, _: &log::Metadata) -> bool {
104            true
105        }
106        fn log(&self, _: &log::Record) {
107            self.0.fetch_add(1, Ordering::Relaxed);
108        }
109        fn flush(&self) {}
110    }
111
112    #[test]
113    fn held_records_wait_for_release() {
114        let logger = Holding(Counting(AtomicUsize::new(0)));
115        let record = |msg| {
116            logger.log(
117                &log::Record::builder()
118                    .level(log::Level::Warn)
119                    .args(format_args!("{msg}"))
120                    .build(),
121            )
122        };
123        hold_logs(true);
124        record("held");
125        assert_eq!(logger.0 .0.load(Ordering::Relaxed), 0, "nothing written");
126        let held = HELD.lock().unwrap().as_ref().map(|h| h.len());
127        assert_eq!(held, Some(1));
128        hold_logs(false);
129        assert!(HELD.lock().unwrap().is_none(), "released");
130        record("through");
131        assert_eq!(logger.0 .0.load(Ordering::Relaxed), 1);
132    }
133}