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