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    fn test_rlg_logger_enabled() {
170        let logger = RlgLogger::new(LogFormat::JSON);
171        // Default filter is 0 (ALL), so everything is enabled
172        let metadata = log::MetadataBuilder::new()
173            .level(log::Level::Trace)
174            .build();
175        assert!(log::Log::enabled(&logger, &metadata));
176
177        let metadata = log::MetadataBuilder::new()
178            .level(log::Level::Error)
179            .build();
180        assert!(log::Log::enabled(&logger, &metadata));
181    }
182
183    #[test]
184    #[cfg_attr(miri, ignore)]
185    fn test_rlg_logger_log_with_metadata() {
186        let logger = RlgLogger::new(LogFormat::JSON);
187
188        // Build a record with file/line/module metadata
189        let record = log::RecordBuilder::new()
190            .args(format_args!("test log message"))
191            .level(log::Level::Info)
192            .target("test_target")
193            .file(Some("test_file.rs"))
194            .line(Some(42))
195            .module_path(Some("test_module"))
196            .build();
197
198        log::Log::log(&logger, &record);
199    }
200
201    #[test]
202    #[cfg_attr(miri, ignore)]
203    fn test_rlg_logger_log_without_metadata() {
204        let logger = RlgLogger::new(LogFormat::MCP);
205
206        // Build a record without optional metadata
207        let record = log::RecordBuilder::new()
208            .args(format_args!("minimal message"))
209            .level(log::Level::Warn)
210            .target("minimal_target")
211            .build();
212
213        log::Log::log(&logger, &record);
214    }
215
216    #[test]
217    #[cfg_attr(miri, ignore)]
218    fn test_rlg_logger_log_all_levels() {
219        let logger = RlgLogger::new(LogFormat::JSON);
220
221        for level in &[
222            log::Level::Error,
223            log::Level::Warn,
224            log::Level::Info,
225            log::Level::Debug,
226            log::Level::Trace,
227        ] {
228            let record = log::RecordBuilder::new()
229                .args(format_args!("level test"))
230                .level(*level)
231                .target("level_test")
232                .build();
233            log::Log::log(&logger, &record);
234        }
235    }
236
237    #[test]
238    fn test_rlg_logger_flush() {
239        let logger = RlgLogger::new(LogFormat::JSON);
240        log::Log::flush(&logger); // Should be a no-op
241    }
242}