Skip to main content

reduct_base/
logger.rs

1// Copyright 2021-2026 ReductSoftware UG
2// Licensed under the Apache License, Version 2.0
3
4use chrono::prelude::{DateTime, Utc};
5use log::{Level, Log, Metadata, Record};
6use std::collections::BTreeMap;
7use std::sync::{LazyLock, RwLock};
8use thread_id;
9
10static LOGGER: Logger = Logger;
11
12pub struct Logger;
13static PATHS: LazyLock<RwLock<BTreeMap<String, Level>>> =
14    LazyLock::new(|| RwLock::new(BTreeMap::new()));
15impl Log for Logger {
16    fn enabled(&self, metadata: &Metadata) -> bool {
17        let paths = PATHS.read().unwrap();
18        // Check paths in reverse order (most specific first)
19        for (path, level) in paths.iter().rev() {
20            if path.is_empty() {
21                return metadata.level() <= *level;
22            }
23            if metadata.target().replace("::", "/").starts_with(path) {
24                return metadata.level() <= *level;
25            }
26        }
27        false
28    }
29
30    fn log(&self, record: &Record) {
31        if self.enabled(record.metadata()) {
32            let now: DateTime<Utc> = Utc::now();
33
34            let file = if let Some(file) = record.file() {
35                // Absolute path to crate, remove path to registry
36                match file.split_once("src/") {
37                    Some((_, file)) => file,
38                    None => file,
39                }
40            } else {
41                "(unknown)"
42            };
43
44            let package_name = if let Some(package_name) = record.target().split_once(':') {
45                package_name.0
46            } else {
47                record.target()
48            };
49
50            println!(
51                "{} ({:>5}) [{}] -- {:}/{:}:{:} {:?}",
52                now.format("%Y-%m-%d %H:%M:%S.%3f"),
53                thread_id::get() % 100000,
54                record.level(),
55                package_name,
56                file,
57                record.line().unwrap(),
58                record.args(),
59            );
60        }
61    }
62
63    fn flush(&self) {}
64}
65
66impl Logger {
67    /// Initialize the logger.
68    ///
69    /// # Arguments
70    ///
71    /// * `level` - The log level to use. Can be one of TRACE, DEBUG, INFO, WARN, ERROR.
72    pub fn init(levels: &str) {
73        let mut max_level = Level::Trace;
74        {
75            let mut paths = PATHS.write().unwrap();
76            paths.clear();
77            paths.insert("".to_string(), Level::Info); // default level
78        }
79        for level in levels.split(',') {
80            let mut parts = level.splitn(2, '=');
81            let mut path = parts.next().unwrap().trim();
82            let level = if let Some(lvl) = parts.next() {
83                lvl.trim()
84            } else {
85                // for case INFO,path=DEBUG
86                let lvl = path;
87                path = ""; // root
88                lvl
89            };
90
91            let level = match level.to_uppercase().as_str() {
92                "TRACE" => Level::Trace,
93                "DEBUG" => Level::Debug,
94                "INFO" => Level::Info,
95                "WARN" => Level::Warn,
96                "ERROR" => Level::Error,
97                _ => {
98                    eprintln!("Invalid log level: {}, defaulting to INFO", level);
99                    Level::Info
100                }
101            };
102
103            max_level = std::cmp::max(max_level, level);
104            PATHS.write().unwrap().insert(path.to_string(), level);
105        }
106
107        log::set_logger(&LOGGER).ok();
108        log::set_max_level(max_level.to_level_filter());
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use serial_test::serial;
116
117    #[test]
118    #[serial]
119    fn it_works() {
120        Logger::init("INFO");
121        log::info!("Hello, world!");
122    }
123
124    #[test]
125    #[serial]
126    fn test_log_levels() {
127        Logger::init("DEBUG,path=TRACE,crate/module=ERROR");
128        assert_eq!(
129            LOGGER.enabled(
130                &Metadata::builder()
131                    .level(Level::Info)
132                    .target("crate")
133                    .build()
134            ),
135            true
136        );
137        assert_eq!(
138            LOGGER.enabled(
139                &Metadata::builder()
140                    .level(Level::Debug)
141                    .target("crate")
142                    .build()
143            ),
144            true
145        );
146        assert_eq!(
147            LOGGER.enabled(
148                &Metadata::builder()
149                    .level(Level::Trace)
150                    .target("crate")
151                    .build()
152            ),
153            false
154        );
155        assert_eq!(
156            LOGGER.enabled(
157                &Metadata::builder()
158                    .level(Level::Error)
159                    .target("crate/module")
160                    .build()
161            ),
162            true
163        );
164        assert_eq!(
165            LOGGER.enabled(
166                &Metadata::builder()
167                    .level(Level::Warn)
168                    .target("crate/module")
169                    .build()
170            ),
171            false
172        );
173        assert_eq!(
174            LOGGER.enabled(
175                &Metadata::builder()
176                    .level(Level::Info)
177                    .target("other")
178                    .build()
179            ),
180            true
181        );
182        assert_eq!(
183            LOGGER.enabled(
184                &Metadata::builder()
185                    .level(Level::Debug)
186                    .target("other")
187                    .build()
188            ),
189            true
190        );
191        assert_eq!(
192            LOGGER.enabled(
193                &Metadata::builder()
194                    .level(Level::Trace)
195                    .target("other")
196                    .build()
197            ),
198            false
199        );
200    }
201
202    #[test]
203    #[serial]
204    fn test_log_wrong_level() {
205        Logger::init("WRONG");
206        assert_eq!(
207            LOGGER.enabled(
208                &Metadata::builder()
209                    .level(Level::Info)
210                    .target("crate")
211                    .build()
212            ),
213            true,
214            "Default level is INFO"
215        );
216        assert_eq!(
217            LOGGER.enabled(
218                &Metadata::builder()
219                    .level(Level::Debug)
220                    .target("crate")
221                    .build()
222            ),
223            false
224        );
225    }
226}