reduct_base/
logger.rs

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