Skip to main content

logged_stream/
logger.rs

1use crate::RecordKind;
2use crate::record::Record;
3use std::borrow::Cow;
4use std::collections;
5use std::io::Write;
6use std::str::FromStr;
7use std::sync::mpsc;
8
9//////////////////////////////////////////////////////////////////////////////////////////////////////////////
10// Trait
11//////////////////////////////////////////////////////////////////////////////////////////////////////////////
12
13/// Trait for processing log records in [`LoggedStream`].
14///
15/// This trait allows processing log records ([`Record`]) using the [`log`] method. It should be implemented for
16/// structures intended to be used as the logging component within [`LoggedStream`]. The [`log`] method is called
17/// by [`LoggedStream`] for further log record processing (e.g., writing to the console, memory, or database)
18/// after the log record message has been formatted by an implementation of [`BufferFormatter`] and filtered
19/// by an implementation of [`RecordFilter`].
20///
21/// [`log`]: Logger::log
22/// [`LoggedStream`]: crate::LoggedStream
23/// [`RecordFilter`]: crate::RecordFilter
24/// [`BufferFormatter`]: crate::BufferFormatter
25pub trait Logger: Send + 'static {
26    fn log(&mut self, record: Record);
27}
28
29impl Logger for Box<dyn Logger> {
30    fn log(&mut self, record: Record) {
31        (**self).log(record)
32    }
33}
34
35//////////////////////////////////////////////////////////////////////////////////////////////////////////////
36// ConsoleLogger
37//////////////////////////////////////////////////////////////////////////////////////////////////////////////
38
39/// Logger implementation that writes log records to the console.
40///
41/// This implementation of the [`Logger`] trait writes log records ([`Record`]) to the console using the provided
42/// [`log::Level`]. Log records with the [`Error`] kind ignore the provided [`log::Level`] and are always written
43/// with [`log::Level::Error`].
44///
45/// Optionally, a prefix can be configured via [`with_prefix`] or [`set_prefix`]. When set, it is printed
46/// verbatim at the beginning of every log line, before the record kind character. This is useful to
47/// disambiguate output when several [`LoggedStream`]s (for example one per connection) log to the same
48/// console. No prefix is configured by default.
49///
50/// [`Error`]: crate::RecordKind::Error
51/// [`with_prefix`]: ConsoleLogger::with_prefix
52/// [`set_prefix`]: ConsoleLogger::set_prefix
53/// [`LoggedStream`]: crate::LoggedStream
54#[derive(Debug, Clone)]
55pub struct ConsoleLogger {
56    level: log::Level,
57    prefix: Option<Cow<'static, str>>,
58}
59
60impl ConsoleLogger {
61    /// Construct a new instance of [`ConsoleLogger`] using the provided log level [`str`]. Returns an
62    /// [`Err`] if the provided log level is invalid. The constructed logger has no prefix; use
63    /// [`with_prefix`] or [`set_prefix`] to add one.
64    ///
65    /// [`with_prefix`]: ConsoleLogger::with_prefix
66    /// [`set_prefix`]: ConsoleLogger::set_prefix
67    pub fn new(level: &str) -> Result<Self, log::ParseLevelError> {
68        let level = log::Level::from_str(level)?;
69        Ok(Self {
70            level,
71            prefix: None,
72        })
73    }
74
75    /// Construct a new instance of [`ConsoleLogger`] using the provided log level [`str`]. Panics if the
76    /// provided log level is invalid.
77    pub fn new_unchecked(level: &str) -> Self {
78        Self::new(level).unwrap()
79    }
80
81    /// Set a prefix that will be printed at the beginning of every log line produced by this logger, and
82    /// return the modified logger. This is a chainable builder method.
83    ///
84    /// The prefix is rendered verbatim immediately before the record kind character — no separator is
85    /// inserted between them — so include any trailing separator you want yourself (for example a trailing
86    /// space or brackets). An empty prefix therefore produces the same output as no prefix at all.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use logged_stream::ConsoleLogger;
92    ///
93    /// let logger = ConsoleLogger::new_unchecked("debug").with_prefix("[conn 5] ");
94    /// assert_eq!(logger.prefix(), Some("[conn 5] "));
95    /// ```
96    pub fn with_prefix(mut self, prefix: impl Into<Cow<'static, str>>) -> Self {
97        self.prefix = Some(prefix.into());
98        self
99    }
100
101    /// Set or replace the prefix printed at the beginning of every log line produced by this logger, in
102    /// place. See [`with_prefix`] for details on how the prefix is rendered.
103    ///
104    /// [`with_prefix`]: ConsoleLogger::with_prefix
105    pub fn set_prefix(&mut self, prefix: impl Into<Cow<'static, str>>) {
106        self.prefix = Some(prefix.into());
107    }
108
109    /// Remove the configured prefix, so log lines are printed without any leading prefix again.
110    pub fn clear_prefix(&mut self) {
111        self.prefix = None;
112    }
113
114    /// Return the currently configured prefix, or [`None`] if no prefix is set.
115    #[inline]
116    pub fn prefix(&self) -> Option<&str> {
117        self.prefix.as_deref()
118    }
119}
120
121impl Logger for ConsoleLogger {
122    fn log(&mut self, record: Record) {
123        let level = match record.kind {
124            RecordKind::Error => log::Level::Error,
125            _ => self.level,
126        };
127        // Format the record straight into the `log::log!` arguments instead of building an
128        // intermediate `String`. The prefix-less path is byte-for-byte identical to the historical
129        // implementation and allocates nothing beyond what `log` itself does, and both paths keep
130        // formatting lazy so nothing is rendered when the level is disabled.
131        match self.prefix.as_deref() {
132            Some(prefix) => log::log!(level, "{}{} {}", prefix, record.kind, record.message),
133            None => log::log!(level, "{} {}", record.kind, record.message),
134        }
135    }
136}
137
138impl Logger for Box<ConsoleLogger> {
139    fn log(&mut self, record: Record) {
140        (**self).log(record)
141    }
142}
143
144//////////////////////////////////////////////////////////////////////////////////////////////////////////////
145// MemoryStorageLogger
146//////////////////////////////////////////////////////////////////////////////////////////////////////////////
147
148/// Logger implementation that writes log records to an inner [`VecDeque`] collection.
149///
150/// This implementation of the [`Logger`] trait writes log records ([`Record`]) into an inner collection
151/// ([`collections::VecDeque`]). The length of the inner collection is limited by a number provided during
152/// structure construction. You can retrieve accumulated log records from the inner collection using the
153/// [`get_log_records`] method and clear the inner collection using the [`clear_log_records`] method.
154///
155/// [`VecDeque`]: collections::VecDeque
156/// [`get_log_records`]: MemoryStorageLogger::get_log_records
157/// [`clear_log_records`]: MemoryStorageLogger::clear_log_records
158#[derive(Debug, Clone)]
159pub struct MemoryStorageLogger {
160    storage: collections::VecDeque<Record>,
161    max_length: usize,
162}
163
164impl MemoryStorageLogger {
165    /// Construct a new instance of [`MemoryStorageLogger`] using provided inner collection max length number,
166    pub fn new(max_length: usize) -> Self {
167        Self {
168            storage: collections::VecDeque::new(),
169            max_length,
170        }
171    }
172
173    /// Retrieve log records from inner collection.
174    #[inline]
175    pub fn get_log_records(&self) -> collections::VecDeque<Record> {
176        self.storage.clone()
177    }
178
179    /// Clear inner collection of log records.
180    #[inline]
181    pub fn clear_log_records(&mut self) {
182        self.storage.clear()
183    }
184}
185
186impl Logger for MemoryStorageLogger {
187    fn log(&mut self, record: Record) {
188        self.storage.push_back(record);
189        if self.storage.len() > self.max_length {
190            let _ = self.storage.pop_front();
191        }
192    }
193}
194
195impl Logger for Box<MemoryStorageLogger> {
196    fn log(&mut self, record: Record) {
197        (**self).log(record)
198    }
199}
200
201//////////////////////////////////////////////////////////////////////////////////////////////////////////////
202// ChannelLogger
203//////////////////////////////////////////////////////////////////////////////////////////////////////////////
204
205/// Logger implementation that sends log records via an asynchronous channel.
206///
207/// This implementation of the [`Logger`] trait sends log records ([`Record`]) using the sending-half of an underlying
208/// asynchronous channel. You can obtain the receiving-half of the channel using the [`take_receiver`] and
209/// [`take_receiver_unchecked`] methods.
210///
211/// [`take_receiver`]: ChannelLogger::take_receiver
212/// [`take_receiver_unchecked`]: ChannelLogger::take_receiver_unchecked
213#[derive(Debug)]
214pub struct ChannelLogger {
215    sender: mpsc::Sender<Record>,
216    receiver: Option<mpsc::Receiver<Record>>,
217}
218
219impl ChannelLogger {
220    /// Construct a new instance of [`ChannelLogger`].
221    pub fn new() -> Self {
222        let (sender, receiver) = mpsc::channel();
223        Self {
224            sender,
225            receiver: Some(receiver),
226        }
227    }
228
229    /// Take channel receiving-half. Returns [`None`] if it was already taken.
230    #[inline]
231    pub fn take_receiver(&mut self) -> Option<mpsc::Receiver<Record>> {
232        self.receiver.take()
233    }
234
235    /// Take channel receiving-half. Panics if it was already taken.
236    pub fn take_receiver_unchecked(&mut self) -> mpsc::Receiver<Record> {
237        self.take_receiver().unwrap()
238    }
239}
240
241impl Default for ChannelLogger {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247impl Logger for ChannelLogger {
248    fn log(&mut self, record: Record) {
249        let _ = self.sender.send(record);
250    }
251}
252
253impl Logger for Box<ChannelLogger> {
254    fn log(&mut self, record: Record) {
255        (**self).log(record)
256    }
257}
258
259//////////////////////////////////////////////////////////////////////////////////////////////////////////////
260// FileLogger
261//////////////////////////////////////////////////////////////////////////////////////////////////////////////
262
263/// This implementation of [`Logger`] trait writes log records ([`Record`]) into provided file.
264pub struct FileLogger {
265    file: std::fs::File,
266}
267
268impl FileLogger {
269    /// Construct a new instance of [`FileLogger`] using provided file.
270    pub fn new(file: std::fs::File) -> Self {
271        Self { file }
272    }
273}
274
275impl Logger for FileLogger {
276    fn log(&mut self, record: Record) {
277        let _ = writeln!(
278            self.file,
279            "[{}] {} {}",
280            record.time.format("%+"),
281            record.kind,
282            record.message
283        );
284    }
285}
286
287impl Logger for Box<FileLogger> {
288    fn log(&mut self, record: Record) {
289        (**self).log(record)
290    }
291}
292
293//////////////////////////////////////////////////////////////////////////////////////////////////////////////
294// Tests
295//////////////////////////////////////////////////////////////////////////////////////////////////////////////
296
297#[cfg(test)]
298mod tests {
299    use crate::logger::ChannelLogger;
300    use crate::logger::ConsoleLogger;
301    use crate::logger::FileLogger;
302    use crate::logger::Logger;
303    use crate::logger::MemoryStorageLogger;
304    use crate::record::Record;
305    use crate::record::RecordKind;
306    use std::cell::RefCell;
307    use std::sync::Once;
308
309    // A minimal `log::Log` implementation used to capture the exact level and line `ConsoleLogger`
310    // emits through the `log` facade. Captured records are stored per-thread, so tests running in
311    // parallel never observe each other's output.
312    thread_local! {
313        static CAPTURED: RefCell<Vec<(log::Level, String)>> = const { RefCell::new(Vec::new()) };
314    }
315
316    struct CapturingLogger;
317
318    impl log::Log for CapturingLogger {
319        fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
320            true
321        }
322
323        fn log(&self, record: &log::Record<'_>) {
324            CAPTURED.with(|captured| {
325                captured
326                    .borrow_mut()
327                    .push((record.level(), format!("{}", record.args())))
328            });
329        }
330
331        fn flush(&self) {}
332    }
333
334    static CAPTURING_LOGGER: CapturingLogger = CapturingLogger;
335    static INIT_CAPTURING_LOGGER: Once = Once::new();
336
337    // Install the capturing logger exactly once for the whole test binary, raise the max level so
338    // records are not filtered out, and clear this thread's captured lines to give the calling test
339    // a clean slate.
340    fn install_capturing_logger() {
341        INIT_CAPTURING_LOGGER.call_once(|| {
342            // `set_logger` only fails if a logger is already installed; the lib test binary installs
343            // none of its own, so this succeeds. Ignore the error defensively.
344            let _ = log::set_logger(&CAPTURING_LOGGER);
345            log::set_max_level(log::LevelFilter::Trace);
346        });
347        CAPTURED.with(|captured| captured.borrow_mut().clear());
348    }
349
350    fn captured_lines() -> Vec<String> {
351        CAPTURED.with(|captured| {
352            captured
353                .borrow()
354                .iter()
355                .map(|(_, msg)| msg.clone())
356                .collect()
357        })
358    }
359
360    fn captured_records() -> Vec<(log::Level, String)> {
361        CAPTURED.with(|captured| captured.borrow().clone())
362    }
363
364    fn assert_unpin<T: Unpin>() {}
365
366    #[test]
367    fn test_unpin() {
368        assert_unpin::<ConsoleLogger>();
369        assert_unpin::<ChannelLogger>();
370        assert_unpin::<MemoryStorageLogger>();
371        assert_unpin::<FileLogger>();
372    }
373
374    #[test]
375    fn test_trait_object_safety() {
376        // Assert traint object construct.
377        let mut console: Box<dyn Logger> = Box::new(ConsoleLogger::new_unchecked("debug"));
378        let mut memory: Box<dyn Logger> = Box::new(MemoryStorageLogger::new(100));
379        let mut channel: Box<dyn Logger> = Box::new(ChannelLogger::new());
380
381        let record = Record::new(RecordKind::Open, String::from("test log record"));
382
383        // Assert that trait object methods are dispatchable.
384        console.log(record.clone());
385        memory.log(record.clone());
386        channel.log(record);
387    }
388
389    fn assert_logger<T: Logger>() {}
390
391    #[test]
392    fn test_box() {
393        assert_logger::<Box<dyn Logger>>();
394        assert_logger::<Box<ConsoleLogger>>();
395        assert_logger::<Box<MemoryStorageLogger>>();
396        assert_logger::<Box<ChannelLogger>>();
397        assert_logger::<Box<FileLogger>>();
398    }
399
400    #[test]
401    fn test_console_logger_prefix_default_none() {
402        assert_eq!(ConsoleLogger::new_unchecked("debug").prefix(), None);
403        assert_eq!(ConsoleLogger::new("info").unwrap().prefix(), None);
404    }
405
406    #[test]
407    fn test_console_logger_with_prefix() {
408        // Static string literal.
409        let logger = ConsoleLogger::new_unchecked("debug").with_prefix("[conn 5] ");
410        assert_eq!(logger.prefix(), Some("[conn 5] "));
411
412        // Owned runtime string (the typical case for a per-connection identifier).
413        let id = 42;
414        let logger = ConsoleLogger::new_unchecked("debug").with_prefix(format!("[conn {id}] "));
415        assert_eq!(logger.prefix(), Some("[conn 42] "));
416    }
417
418    #[test]
419    fn test_console_logger_set_and_clear_prefix() {
420        let mut logger = ConsoleLogger::new_unchecked("debug");
421        assert_eq!(logger.prefix(), None);
422
423        logger.set_prefix(String::from("[server] "));
424        assert_eq!(logger.prefix(), Some("[server] "));
425
426        logger.set_prefix("[client] ");
427        assert_eq!(logger.prefix(), Some("[client] "));
428
429        logger.clear_prefix();
430        assert_eq!(logger.prefix(), None);
431    }
432
433    #[test]
434    fn test_console_logger_logs_prefix_before_kind() {
435        install_capturing_logger();
436
437        let mut logger = ConsoleLogger::new_unchecked("debug");
438
439        // Without a prefix, the emitted line matches the historical `"{kind} {message}"` format.
440        logger.log(Record::new(RecordKind::Write, String::from("ab:cd")));
441
442        // With a prefix, it is prepended verbatim, before the record kind character.
443        logger.set_prefix("[conn 5] ");
444        logger.log(Record::new(RecordKind::Read, String::from("01:02")));
445
446        // After clearing, subsequent lines are emitted without any prefix again.
447        logger.clear_prefix();
448        logger.log(Record::new(
449            RecordKind::Shutdown,
450            String::from("Writer shutdown request."),
451        ));
452
453        assert_eq!(
454            captured_lines(),
455            vec![
456                String::from("> ab:cd"),
457                String::from("[conn 5] < 01:02"),
458                String::from("- Writer shutdown request."),
459            ]
460        );
461    }
462
463    #[test]
464    fn test_console_logger_forces_error_level() {
465        install_capturing_logger();
466
467        // The logger is configured at Debug, below Error. Non-error records are emitted at the
468        // configured level, but Error records are always forced to `log::Level::Error`.
469        let mut logger = ConsoleLogger::new_unchecked("debug");
470        logger.log(Record::new(RecordKind::Write, String::from("01:02")));
471        logger.log(Record::new(RecordKind::Error, String::from("boom")));
472
473        // A prefix does not change the forced Error level.
474        logger.set_prefix("[conn 5] ");
475        logger.log(Record::new(RecordKind::Error, String::from("kaboom")));
476
477        assert_eq!(
478            captured_records(),
479            vec![
480                (log::Level::Debug, String::from("> 01:02")),
481                (log::Level::Error, String::from("! boom")),
482                (log::Level::Error, String::from("[conn 5] ! kaboom")),
483            ]
484        );
485    }
486
487    #[test]
488    fn test_console_logger_empty_prefix_matches_no_prefix() {
489        install_capturing_logger();
490
491        let mut logger = ConsoleLogger::new_unchecked("debug");
492        // No prefix.
493        logger.log(Record::new(RecordKind::Write, String::from("01:02")));
494        // Empty prefix — documented to produce the same output as no prefix at all.
495        logger.set_prefix("");
496        logger.log(Record::new(RecordKind::Write, String::from("01:02")));
497
498        let lines = captured_lines();
499        assert_eq!(lines.len(), 2);
500        assert_eq!(lines[0], lines[1]);
501        assert_eq!(lines[0], "> 01:02");
502    }
503
504    fn assert_send<T: Send>() {}
505
506    #[test]
507    fn test_send() {
508        assert_send::<ConsoleLogger>();
509        assert_send::<MemoryStorageLogger>();
510        assert_send::<ChannelLogger>();
511        assert_send::<FileLogger>();
512
513        assert_send::<Box<dyn Logger>>();
514        assert_send::<Box<ConsoleLogger>>();
515        assert_send::<Box<MemoryStorageLogger>>();
516        assert_send::<Box<ChannelLogger>>();
517        assert_send::<Box<FileLogger>>();
518    }
519}