Skip to main content

rustdv_sim/
log.rs

1//! Sim-time-stamped logging in the book's format: `  2.00ns INFO ...`
2//! (design-doc OQ-8: the `tracing` mapping is deferred; this minimal
3//! zero-dependency logger reproduces the output format the book teaches).
4
5use std::cell::Cell;
6
7use crate::time::sim_time_ns;
8
9#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
10pub enum Level {
11    Debug = 0,
12    Info = 1,
13    Warning = 2,
14    Error = 3,
15    Critical = 4,
16    /// Above every real level, so nothing passes the threshold — the port
17    /// of pyuvm's `disable_logging()`.
18    Off = 5,
19}
20
21impl Level {
22    fn as_str(self) -> &'static str {
23        match self {
24            Level::Debug => "DEBUG",
25            Level::Info => "INFO",
26            Level::Warning => "WARNING",
27            Level::Error => "ERROR",
28            Level::Critical => "CRITICAL",
29            Level::Off => "OFF",
30        }
31    }
32}
33
34thread_local! {
35    static THRESHOLD: Cell<Level> = const { Cell::new(Level::Info) };
36}
37
38pub fn set_level(l: Level) {
39    THRESHOLD.with(|t| t.set(l));
40}
41
42pub fn log(level: Level, msg: &str) {
43    let enabled = THRESHOLD.with(|t| level >= t.get());
44    if enabled {
45        // Through `emit`, so a global log file captures un-pathed messages
46        // too. Subtree handlers do not apply: this message has no path.
47        emit(&format!(
48            "{:>10.2}ns {:<8} {}",
49            sim_time_ns(),
50            level.as_str(),
51            msg
52        ));
53    }
54}
55
56pub fn debug(msg: &str) {
57    log(Level::Debug, msg);
58}
59pub fn info(msg: &str) {
60    log(Level::Info, msg);
61}
62pub fn warning(msg: &str) {
63    log(Level::Warning, msg);
64}
65pub fn error(msg: &str) {
66    log(Level::Error, msg);
67}
68
69pub fn critical(msg: &str) {
70    log(Level::Critical, msg);
71}
72
73// ---------------------------------------------------------------------------
74// Hierarchical targets and handlers (the pyuvm logging surface, ported)
75// ---------------------------------------------------------------------------
76
77use std::cell::RefCell;
78use std::io::Write;
79use std::rc::Rc;
80
81/// Does `path` name this component or one below it? `"a.b"` is under
82/// `"a"`, but `"ab"` is not — the dot matters.
83fn under(path: &str, prefix: &str) -> bool {
84    crate::path::str_is_under(path, prefix)
85}
86
87thread_local! {
88    /// Per-target levels: (path prefix, level). Longest prefix wins;
89    /// the global THRESHOLD is the fallback — pyuvm's per-component
90    /// logger levels with hierarchy-wide setting (set_logging_level_hier).
91    static TARGET_LEVELS: RefCell<Vec<(String, Level)>> = const { RefCell::new(Vec::new()) };
92    /// Optional global log file (pyuvm's FileHandler on the root).
93    static LOG_FILE: RefCell<Option<std::fs::File>> = const { RefCell::new(None) };
94    /// Per-subtree file handlers (add_logging_handler_hier): every message
95    /// from a path under the prefix is also written here.
96    static TARGET_FILES: RefCell<Vec<(String, Rc<RefCell<std::fs::File>>)>> =
97        const { RefCell::new(Vec::new()) };
98    /// Per-subtree console suppression (remove_streaming_handler_hier).
99    /// Longest matching prefix wins; absent means "console on".
100    static TARGET_CONSOLE: RefCell<Vec<(String, bool)>> = const { RefCell::new(Vec::new()) };
101}
102
103/// Set the level for a component path and everything under it
104/// (port of set_logging_level_hier; a leaf path is set_logging_level).
105pub fn set_level_for(path_prefix: &str, l: Level) {
106    TARGET_LEVELS.with(|t| {
107        let mut v = t.borrow_mut();
108        v.retain(|(p, _)| p != path_prefix);
109        v.push((path_prefix.to_string(), l));
110    });
111}
112
113/// Also write every printed message to `path` (port of logging.FileHandler;
114/// mode "w"). Pass append=true for mode "a".
115pub fn log_to_file(path: &str, append: bool) -> std::io::Result<()> {
116    let file = open_log(path, append)?;
117    LOG_FILE.with(|f| *f.borrow_mut() = Some(file));
118    Ok(())
119}
120
121/// Stop writing to the global log file (port of remove_logging_handler).
122pub fn remove_log_file() {
123    LOG_FILE.with(|f| *f.borrow_mut() = None);
124}
125
126fn open_log(path: &str, append: bool) -> std::io::Result<std::fs::File> {
127    std::fs::OpenOptions::new()
128        .create(true)
129        .write(true)
130        .append(append)
131        .truncate(!append)
132        .open(path)
133}
134
135/// Send this component's subtree to a file as well — the port of
136/// `add_logging_handler_hier(logging.FileHandler(...))`.
137pub fn add_file_for(path_prefix: &str, path: &str, append: bool) -> std::io::Result<()> {
138    let file = Rc::new(RefCell::new(open_log(path, append)?));
139    TARGET_FILES.with(|t| t.borrow_mut().push((path_prefix.to_string(), file)));
140    Ok(())
141}
142
143/// Turn console output on or off for a subtree — the port of
144/// `remove_streaming_handler_hier()` (and its undo).
145pub fn set_console_for(path_prefix: &str, enabled: bool) {
146    TARGET_CONSOLE.with(|t| {
147        let mut v = t.borrow_mut();
148        v.retain(|(p, _)| p != path_prefix);
149        v.push((path_prefix.to_string(), enabled));
150    });
151}
152
153/// Drop all logging configuration: levels, handlers, console suppression.
154///
155/// The runner calls this before every test, so configuration set by one
156/// test cannot leak into the next — pyuvm's `run_test` does the same via
157/// `set_default_logging_level(INFO)`. Without it, a chapter that logs to a
158/// file or disables logging would silently reshape every later test.
159pub fn reset_config() {
160    THRESHOLD.with(|t| t.set(Level::Info));
161    TARGET_LEVELS.with(|t| t.borrow_mut().clear());
162    TARGET_FILES.with(|t| t.borrow_mut().clear());
163    TARGET_CONSOLE.with(|t| t.borrow_mut().clear());
164    LOG_FILE.with(|f| *f.borrow_mut() = None);
165}
166
167fn console_enabled_for(path: &str) -> bool {
168    TARGET_CONSOLE
169        .with(|t| {
170            t.borrow()
171                .iter()
172                .filter(|(p, _)| under(path, p))
173                .max_by_key(|(p, _)| p.len())
174                .map(|(_, on)| *on)
175        })
176        .unwrap_or(true)
177}
178
179/// Emit a line attributed to `path`: console unless this subtree's console
180/// was removed, plus every file handler covering the path.
181fn emit_for(path: &str, line: &str) {
182    if console_enabled_for(path) {
183        println!("{line}");
184    }
185    TARGET_FILES.with(|t| {
186        for (prefix, file) in t.borrow().iter() {
187            if under(path, prefix) {
188                let _ = writeln!(file.borrow_mut(), "{line}");
189            }
190        }
191    });
192    LOG_FILE.with(|f| {
193        if let Some(file) = f.borrow_mut().as_mut() {
194            let _ = writeln!(file, "{line}");
195        }
196    });
197}
198
199fn emit(line: &str) {
200    println!("{line}");
201    LOG_FILE.with(|f| {
202        if let Some(file) = f.borrow_mut().as_mut() {
203            let _ = writeln!(file, "{line}");
204        }
205    });
206}
207
208/// A named logger: the pyuvm `self.logger`. The path is **not** typed by
209/// hand — [`crate::log`] users get one from `RustdvCtx`, which carries the
210/// path the phase walk derived (D7), so it cannot go stale when a
211/// component moves.
212#[derive(Clone)]
213pub struct Logger {
214    /// The component's position in the tree, as segments (D7). A type rather
215    /// than a `String` so it cannot be fabricated by hand — see
216    /// [`crate::path::RustdvPath`].
217    path: crate::path::RustdvPath,
218}
219
220impl Logger {
221    /// Build a logger from a dotted path. Retained for the framework's own
222    /// use and for the free-function logging path; component contexts derive
223    /// their loggers with [`Logger::at`] instead.
224    pub fn new(path: &str) -> Logger {
225        let mut p = crate::path::RustdvPath::empty();
226        if !path.is_empty() {
227            for seg in path.split('.') {
228                p = p.child(seg);
229            }
230        }
231        Logger { path: p }
232    }
233
234    /// Build a logger at a path the walk derived.
235    pub fn at(path: crate::path::RustdvPath) -> Logger {
236        Logger { path }
237    }
238
239    pub fn path(&self) -> &str {
240        self.path.as_str()
241    }
242
243    /// This logger's path as segments — what the walk and the connection
244    /// registry address components by.
245    pub fn rustdv_path(&self) -> &crate::path::RustdvPath {
246        &self.path
247    }
248
249    fn enabled(&self, level: Level) -> bool {
250        let per_target = TARGET_LEVELS.with(|t| {
251            t.borrow()
252                .iter()
253                .filter(|(p, _)| under(self.path.as_str(), p))
254                .max_by_key(|(p, _)| p.len())
255                .map(|(_, l)| *l)
256        });
257        match per_target {
258            Some(l) => level >= l,
259            None => THRESHOLD.with(|t| level >= t.get()),
260        }
261    }
262
263    pub fn log(&self, level: Level, msg: &str) {
264        if self.enabled(level) {
265            emit_for(
266                self.path.as_str(),
267                &format!(
268                    "{:>10.2}ns {:<8} [{}]: {}",
269                    sim_time_ns(),
270                    level.as_str(),
271                    self.path,
272                    msg
273                ),
274            );
275        }
276    }
277
278    pub fn debug(&self, msg: &str) {
279        self.log(Level::Debug, msg);
280    }
281    pub fn info(&self, msg: &str) {
282        self.log(Level::Info, msg);
283    }
284    pub fn warning(&self, msg: &str) {
285        self.log(Level::Warning, msg);
286    }
287    pub fn error(&self, msg: &str) {
288        self.log(Level::Error, msg);
289    }
290    pub fn critical(&self, msg: &str) {
291        self.log(Level::Critical, msg);
292    }
293}