Skip to main content

rlg/
logger.rs

1// logger.rs
2// Copyright © 2024-2026 RustLogs (RLG). All rights reserved.
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6//! Bridge from the [`log`](https://docs.rs/log) crate facade into the RLG engine.
7//!
8//! Installed automatically by [`rlg::init()`](crate::init::init) unless
9//! you call `.without_log()` on the builder.
10
11use crate::engine::ENGINE;
12use crate::log::Log;
13use crate::log_format::LogFormat;
14use crate::log_level::LogLevel;
15
16/// Convert a [`log::Level`] to the corresponding [`LogLevel`].
17#[must_use]
18pub const fn map_log_level(level: log::Level) -> LogLevel {
19    match level {
20        log::Level::Error => LogLevel::ERROR,
21        log::Level::Warn => LogLevel::WARN,
22        log::Level::Info => LogLevel::INFO,
23        log::Level::Debug => LogLevel::DEBUG,
24        log::Level::Trace => LogLevel::TRACE,
25    }
26}
27
28/// Convert an RLG [`LogLevel`] to a [`log::LevelFilter`].
29#[must_use]
30pub const fn to_log_level_filter(level: LogLevel) -> log::LevelFilter {
31    match level {
32        LogLevel::ALL | LogLevel::TRACE => log::LevelFilter::Trace,
33        LogLevel::DEBUG => log::LevelFilter::Debug,
34        LogLevel::VERBOSE | LogLevel::INFO => log::LevelFilter::Info,
35        LogLevel::WARN => log::LevelFilter::Warn,
36        LogLevel::ERROR | LogLevel::FATAL | LogLevel::CRITICAL => {
37            log::LevelFilter::Error
38        }
39        LogLevel::NONE | LogLevel::DISABLED => log::LevelFilter::Off,
40    }
41}
42
43/// [`log::Log`] implementation that routes records into the RLG ring buffer.
44#[derive(Debug, Clone, Copy)]
45pub struct RlgLogger {
46    format: LogFormat,
47}
48
49impl RlgLogger {
50    /// Create an `RlgLogger` that formats output in the given format.
51    #[must_use]
52    pub const fn new(format: LogFormat) -> Self {
53        Self { format }
54    }
55}
56
57impl log::Log for RlgLogger {
58    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
59        map_log_level(metadata.level()).to_numeric()
60            >= ENGINE.filter_level()
61    }
62
63    fn log(&self, record: &log::Record<'_>) {
64        if !self.enabled(record.metadata()) {
65            return;
66        }
67
68        let level = map_log_level(record.level());
69        let mut entry = Log::build(level, &record.args().to_string());
70        entry.component =
71            std::borrow::Cow::Owned(record.target().to_string());
72        entry.format = self.format;
73
74        if let Some(file) = record.file() {
75            entry = entry.with("file", file);
76        }
77        if let Some(line) = record.line() {
78            entry = entry.with("line", line);
79        }
80        if let Some(module) = record.module_path() {
81            entry = entry.with("module", module);
82        }
83
84        entry.fire();
85    }
86
87    fn flush(&self) {
88        // The background flusher thread handles I/O.
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_map_log_level_all_variants() {
98        assert_eq!(map_log_level(log::Level::Error), LogLevel::ERROR);
99        assert_eq!(map_log_level(log::Level::Warn), LogLevel::WARN);
100        assert_eq!(map_log_level(log::Level::Info), LogLevel::INFO);
101        assert_eq!(map_log_level(log::Level::Debug), LogLevel::DEBUG);
102        assert_eq!(map_log_level(log::Level::Trace), LogLevel::TRACE);
103    }
104
105    #[test]
106    fn test_to_log_level_filter_all_variants() {
107        assert_eq!(
108            to_log_level_filter(LogLevel::ALL),
109            log::LevelFilter::Trace
110        );
111        assert_eq!(
112            to_log_level_filter(LogLevel::TRACE),
113            log::LevelFilter::Trace
114        );
115        assert_eq!(
116            to_log_level_filter(LogLevel::DEBUG),
117            log::LevelFilter::Debug
118        );
119        assert_eq!(
120            to_log_level_filter(LogLevel::VERBOSE),
121            log::LevelFilter::Info
122        );
123        assert_eq!(
124            to_log_level_filter(LogLevel::INFO),
125            log::LevelFilter::Info
126        );
127        assert_eq!(
128            to_log_level_filter(LogLevel::WARN),
129            log::LevelFilter::Warn
130        );
131        assert_eq!(
132            to_log_level_filter(LogLevel::ERROR),
133            log::LevelFilter::Error
134        );
135        assert_eq!(
136            to_log_level_filter(LogLevel::FATAL),
137            log::LevelFilter::Error
138        );
139        assert_eq!(
140            to_log_level_filter(LogLevel::CRITICAL),
141            log::LevelFilter::Error
142        );
143        assert_eq!(
144            to_log_level_filter(LogLevel::NONE),
145            log::LevelFilter::Off
146        );
147        assert_eq!(
148            to_log_level_filter(LogLevel::DISABLED),
149            log::LevelFilter::Off
150        );
151    }
152
153    #[test]
154    fn test_rlg_logger_new() {
155        let logger = RlgLogger::new(LogFormat::JSON);
156        assert_eq!(format!("{logger:?}"), "RlgLogger { format: JSON }");
157    }
158
159    #[test]
160    fn test_rlg_logger_clone_copy() {
161        let logger = RlgLogger::new(LogFormat::MCP);
162        let cloned = logger;
163        // Both are valid since RlgLogger is Copy
164        let _ = format!("{logger:?}");
165        let _ = format!("{cloned:?}");
166    }
167
168    #[test]
169    #[serial_test::serial(engine_filter)]
170    fn test_rlg_logger_enabled() {
171        // ENGINE.filter_level is process-wide state; serialise with
172        // tests that mutate it (below) to prevent test-ordering races
173        // under tarpaulin's threading.
174        let prior = ENGINE.filter_level();
175        ENGINE.set_filter(LogLevel::ALL.to_numeric());
176
177        let logger = RlgLogger::new(LogFormat::JSON);
178        let metadata = log::MetadataBuilder::new()
179            .level(log::Level::Trace)
180            .build();
181        assert!(log::Log::enabled(&logger, &metadata));
182
183        let metadata = log::MetadataBuilder::new()
184            .level(log::Level::Error)
185            .build();
186        assert!(log::Log::enabled(&logger, &metadata));
187
188        ENGINE.set_filter(prior);
189    }
190
191    #[test]
192    #[cfg_attr(miri, ignore)]
193    #[serial_test::serial(engine_filter)]
194    fn test_rlg_logger_log_below_filter_short_circuits() {
195        // Raise the global filter so that Info records are rejected,
196        // forcing the `!self.enabled(...)` early return.
197        let prior = ENGINE.filter_level();
198        ENGINE.set_filter(LogLevel::ERROR.to_numeric());
199        let logger = RlgLogger::new(LogFormat::JSON);
200        let record = log::RecordBuilder::new()
201            .args(format_args!("filtered"))
202            .level(log::Level::Info)
203            .target("filter_test")
204            .build();
205        log::Log::log(&logger, &record);
206        // Restore prior filter so we don't break other tests.
207        ENGINE.set_filter(prior);
208    }
209
210    #[test]
211    #[cfg_attr(miri, ignore)]
212    fn test_rlg_logger_log_with_metadata() {
213        let logger = RlgLogger::new(LogFormat::JSON);
214
215        // Build a record with file/line/module metadata
216        let record = log::RecordBuilder::new()
217            .args(format_args!("test log message"))
218            .level(log::Level::Info)
219            .target("test_target")
220            .file(Some("test_file.rs"))
221            .line(Some(42))
222            .module_path(Some("test_module"))
223            .build();
224
225        log::Log::log(&logger, &record);
226    }
227
228    #[test]
229    #[cfg_attr(miri, ignore)]
230    fn test_rlg_logger_log_without_metadata() {
231        let logger = RlgLogger::new(LogFormat::MCP);
232
233        // Build a record without optional metadata
234        let record = log::RecordBuilder::new()
235            .args(format_args!("minimal message"))
236            .level(log::Level::Warn)
237            .target("minimal_target")
238            .build();
239
240        log::Log::log(&logger, &record);
241    }
242
243    #[test]
244    #[cfg_attr(miri, ignore)]
245    fn test_rlg_logger_log_all_levels() {
246        let logger = RlgLogger::new(LogFormat::JSON);
247
248        for level in &[
249            log::Level::Error,
250            log::Level::Warn,
251            log::Level::Info,
252            log::Level::Debug,
253            log::Level::Trace,
254        ] {
255            let record = log::RecordBuilder::new()
256                .args(format_args!("level test"))
257                .level(*level)
258                .target("level_test")
259                .build();
260            log::Log::log(&logger, &record);
261        }
262    }
263
264    #[test]
265    fn test_rlg_logger_flush() {
266        let logger = RlgLogger::new(LogFormat::JSON);
267        log::Log::flush(&logger); // Should be a no-op
268    }
269}