Skip to main content

logged_stream/
logger.rs

1use crate::RecordKind;
2use crate::record::Record;
3use std::borrow::Cow;
4use std::collections;
5use std::fs;
6use std::io::Write;
7use std::io::{self};
8use std::path::Path;
9use std::str::FromStr;
10use std::sync::mpsc;
11
12//////////////////////////////////////////////////////////////////////////////////////////////////////////////
13// Trait
14//////////////////////////////////////////////////////////////////////////////////////////////////////////////
15
16/// Trait for processing log records in [`LoggedStream`].
17///
18/// This trait allows processing log records ([`Record`]) using the [`log`] method. It should be implemented for
19/// structures intended to be used as the logging component within [`LoggedStream`]. The [`log`] method is called
20/// by [`LoggedStream`] for further log record processing (e.g., writing to the console, memory, or database)
21/// after the log record message has been formatted by an implementation of [`BufferFormatter`] and filtered
22/// by an implementation of [`RecordFilter`].
23///
24/// [`log`]: Logger::log
25/// [`LoggedStream`]: crate::LoggedStream
26/// [`RecordFilter`]: crate::RecordFilter
27/// [`BufferFormatter`]: crate::BufferFormatter
28pub trait Logger: Send + 'static {
29    fn log(&mut self, record: Record);
30}
31
32impl Logger for Box<dyn Logger> {
33    fn log(&mut self, record: Record) {
34        (**self).log(record)
35    }
36}
37
38//////////////////////////////////////////////////////////////////////////////////////////////////////////////
39// ConsoleLogger
40//////////////////////////////////////////////////////////////////////////////////////////////////////////////
41
42/// Logger implementation that writes log records to the console.
43///
44/// This implementation of the [`Logger`] trait writes log records ([`Record`]) to the console using the provided
45/// [`log::Level`]. Log records with the [`Error`] kind ignore the provided [`log::Level`] and are always written
46/// with [`log::Level::Error`].
47///
48/// Optionally, a prefix can be configured via [`with_prefix`] or [`set_prefix`]. When set, it is printed
49/// verbatim at the beginning of every log line, before the record kind character. This is useful to
50/// disambiguate output when several [`LoggedStream`]s (for example one per connection) log to the same
51/// console. No prefix is configured by default.
52///
53/// [`Error`]: crate::RecordKind::Error
54/// [`with_prefix`]: ConsoleLogger::with_prefix
55/// [`set_prefix`]: ConsoleLogger::set_prefix
56/// [`LoggedStream`]: crate::LoggedStream
57#[derive(Debug, Clone)]
58pub struct ConsoleLogger {
59    level: log::Level,
60    prefix: Option<Cow<'static, str>>,
61}
62
63impl ConsoleLogger {
64    /// Construct a new instance of [`ConsoleLogger`] using the provided log level [`str`]. Returns an
65    /// [`Err`] if the provided log level is invalid. The constructed logger has no prefix; use
66    /// [`with_prefix`] or [`set_prefix`] to add one.
67    ///
68    /// [`with_prefix`]: ConsoleLogger::with_prefix
69    /// [`set_prefix`]: ConsoleLogger::set_prefix
70    pub fn new(level: &str) -> Result<Self, log::ParseLevelError> {
71        let level = log::Level::from_str(level)?;
72        Ok(Self {
73            level,
74            prefix: None,
75        })
76    }
77
78    /// Construct a new instance of [`ConsoleLogger`] using the provided log level [`str`]. Panics if the
79    /// provided log level is invalid.
80    pub fn new_unchecked(level: &str) -> Self {
81        Self::new(level).unwrap()
82    }
83
84    /// Set a prefix that will be printed at the beginning of every log line produced by this logger, and
85    /// return the modified logger. This is a chainable builder method.
86    ///
87    /// The prefix is rendered verbatim immediately before the record kind character — no separator is
88    /// inserted between them — so include any trailing separator you want yourself (for example a trailing
89    /// space or brackets). An empty prefix therefore produces the same output as no prefix at all.
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// use logged_stream::ConsoleLogger;
95    ///
96    /// let logger = ConsoleLogger::new_unchecked("debug").with_prefix("[conn 5] ");
97    /// assert_eq!(logger.prefix(), Some("[conn 5] "));
98    /// ```
99    pub fn with_prefix(mut self, prefix: impl Into<Cow<'static, str>>) -> Self {
100        self.prefix = Some(prefix.into());
101        self
102    }
103
104    /// Set or replace the prefix printed at the beginning of every log line produced by this logger, in
105    /// place. See [`with_prefix`] for details on how the prefix is rendered.
106    ///
107    /// [`with_prefix`]: ConsoleLogger::with_prefix
108    pub fn set_prefix(&mut self, prefix: impl Into<Cow<'static, str>>) {
109        self.prefix = Some(prefix.into());
110    }
111
112    /// Remove the configured prefix, so log lines are printed without any leading prefix again.
113    pub fn clear_prefix(&mut self) {
114        self.prefix = None;
115    }
116
117    /// Return the currently configured prefix, or [`None`] if no prefix is set.
118    #[inline]
119    pub fn prefix(&self) -> Option<&str> {
120        self.prefix.as_deref()
121    }
122}
123
124impl Logger for ConsoleLogger {
125    fn log(&mut self, record: Record) {
126        let level = match record.kind {
127            RecordKind::Error => log::Level::Error,
128            _ => self.level,
129        };
130        // Format the record straight into the `log::log!` arguments instead of building an
131        // intermediate `String`. The prefix-less path is byte-for-byte identical to the historical
132        // implementation and allocates nothing beyond what `log` itself does, and both paths keep
133        // formatting lazy so nothing is rendered when the level is disabled.
134        match self.prefix.as_deref() {
135            Some(prefix) => log::log!(level, "{}{} {}", prefix, record.kind, record.message),
136            None => log::log!(level, "{} {}", record.kind, record.message),
137        }
138    }
139}
140
141impl Logger for Box<ConsoleLogger> {
142    fn log(&mut self, record: Record) {
143        (**self).log(record)
144    }
145}
146
147//////////////////////////////////////////////////////////////////////////////////////////////////////////////
148// MemoryStorageLogger
149//////////////////////////////////////////////////////////////////////////////////////////////////////////////
150
151/// Logger implementation that writes log records to an inner [`VecDeque`] collection.
152///
153/// This implementation of the [`Logger`] trait writes log records ([`Record`]) into an inner collection
154/// ([`collections::VecDeque`]). The length of the inner collection is limited by a number provided during
155/// structure construction. You can retrieve accumulated log records from the inner collection using the
156/// [`get_log_records`] method and clear the inner collection using the [`clear_log_records`] method.
157///
158/// [`VecDeque`]: collections::VecDeque
159/// [`get_log_records`]: MemoryStorageLogger::get_log_records
160/// [`clear_log_records`]: MemoryStorageLogger::clear_log_records
161#[derive(Debug, Clone)]
162pub struct MemoryStorageLogger {
163    storage: collections::VecDeque<Record>,
164    max_length: usize,
165}
166
167impl MemoryStorageLogger {
168    /// Construct a new instance of [`MemoryStorageLogger`] using provided inner collection max length number,
169    pub fn new(max_length: usize) -> Self {
170        Self {
171            storage: collections::VecDeque::new(),
172            max_length,
173        }
174    }
175
176    /// Retrieve log records from inner collection.
177    #[inline]
178    pub fn get_log_records(&self) -> collections::VecDeque<Record> {
179        self.storage.clone()
180    }
181
182    /// Clear inner collection of log records.
183    #[inline]
184    pub fn clear_log_records(&mut self) {
185        self.storage.clear()
186    }
187}
188
189impl Logger for MemoryStorageLogger {
190    fn log(&mut self, record: Record) {
191        self.storage.push_back(record);
192        if self.storage.len() > self.max_length {
193            let _ = self.storage.pop_front();
194        }
195    }
196}
197
198impl Logger for Box<MemoryStorageLogger> {
199    fn log(&mut self, record: Record) {
200        (**self).log(record)
201    }
202}
203
204//////////////////////////////////////////////////////////////////////////////////////////////////////////////
205// ChannelLogger
206//////////////////////////////////////////////////////////////////////////////////////////////////////////////
207
208/// Logger implementation that sends log records via an asynchronous channel.
209///
210/// This implementation of the [`Logger`] trait sends log records ([`Record`]) using the sending-half of an underlying
211/// asynchronous channel. You can obtain the receiving-half of the channel using the [`take_receiver`] and
212/// [`take_receiver_unchecked`] methods.
213///
214/// [`take_receiver`]: ChannelLogger::take_receiver
215/// [`take_receiver_unchecked`]: ChannelLogger::take_receiver_unchecked
216#[derive(Debug)]
217pub struct ChannelLogger {
218    sender: mpsc::Sender<Record>,
219    receiver: Option<mpsc::Receiver<Record>>,
220}
221
222impl ChannelLogger {
223    /// Construct a new instance of [`ChannelLogger`].
224    pub fn new() -> Self {
225        let (sender, receiver) = mpsc::channel();
226        Self {
227            sender,
228            receiver: Some(receiver),
229        }
230    }
231
232    /// Take channel receiving-half. Returns [`None`] if it was already taken.
233    #[inline]
234    pub fn take_receiver(&mut self) -> Option<mpsc::Receiver<Record>> {
235        self.receiver.take()
236    }
237
238    /// Take channel receiving-half. Panics if it was already taken.
239    pub fn take_receiver_unchecked(&mut self) -> mpsc::Receiver<Record> {
240        self.take_receiver().unwrap()
241    }
242}
243
244impl Default for ChannelLogger {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250impl Logger for ChannelLogger {
251    fn log(&mut self, record: Record) {
252        let _ = self.sender.send(record);
253    }
254}
255
256impl Logger for Box<ChannelLogger> {
257    fn log(&mut self, record: Record) {
258        (**self).log(record)
259    }
260}
261
262//////////////////////////////////////////////////////////////////////////////////////////////////////////////
263// FileLogger
264//////////////////////////////////////////////////////////////////////////////////////////////////////////////
265
266/// Logger implementation that writes log records into the provided file.
267///
268/// This implementation of the [`Logger`] trait writes log records ([`Record`]) into a file, one line per
269/// record, in the form `[timestamp] {kind} {message}`.
270///
271/// Optionally, a prefix can be configured via [`with_prefix`] or [`set_prefix`]. When set, it is written
272/// verbatim immediately before the record kind character — that is, after the timestamp — which mirrors
273/// how [`ConsoleLogger`] renders its prefix relative to the timestamp emitted by the logging backend. This
274/// is useful to disambiguate output when several [`LoggedStream`]s (for example one per connection) write
275/// to the same file. No prefix is configured by default.
276///
277/// # Sharing one file between several loggers
278///
279/// Each record is rendered up front and written with a single [`write_all`] call, so concurrent loggers
280/// never interleave parts of a line. For that to hold, every logger must write to a file opened in
281/// **append** mode — either construct them with [`open`], or share one handle with
282/// [`fs::File::try_clone`]. Handing several loggers independently opened non-append files (for example
283/// from [`fs::File::create`]) gives each of them its own starting offset, and they will silently
284/// overwrite each other's records.
285///
286/// [`with_prefix`]: FileLogger::with_prefix
287/// [`set_prefix`]: FileLogger::set_prefix
288/// [`open`]: FileLogger::open
289/// [`write_all`]: io::Write::write_all
290/// [`ConsoleLogger`]: crate::ConsoleLogger
291/// [`LoggedStream`]: crate::LoggedStream
292#[derive(Debug)]
293pub struct FileLogger {
294    file: fs::File,
295    prefix: Option<Cow<'static, str>>,
296}
297
298impl FileLogger {
299    /// Construct a new instance of [`FileLogger`] using the provided file. The constructed logger has no
300    /// prefix; use [`with_prefix`] or [`set_prefix`] to add one.
301    ///
302    /// If the same file is going to be written by several loggers, it must be opened in append mode;
303    /// prefer [`open`], which does that for you.
304    ///
305    /// [`with_prefix`]: FileLogger::with_prefix
306    /// [`set_prefix`]: FileLogger::set_prefix
307    /// [`open`]: FileLogger::open
308    pub fn new(file: fs::File) -> Self {
309        Self { file, prefix: None }
310    }
311
312    /// Construct a new instance of [`FileLogger`] writing to the file at the provided path, creating the
313    /// file if it does not exist and opening it in append mode.
314    ///
315    /// Append mode is what makes it safe for several loggers — for example one per connection, each with
316    /// its own prefix — to write to the same file concurrently without overwriting each other. Returns an
317    /// [`Err`] if the file could not be opened.
318    ///
319    /// # Examples
320    ///
321    /// ```
322    /// use logged_stream::FileLogger;
323    ///
324    /// let path = std::env::temp_dir().join("logged-stream-open-doctest.log");
325    /// let logger = FileLogger::open(&path)?;
326    /// # std::fs::remove_file(&path)?;
327    /// # Ok::<(), std::io::Error>(())
328    /// ```
329    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
330        let file = fs::OpenOptions::new()
331            .create(true)
332            .append(true)
333            .open(path)?;
334        Ok(Self::new(file))
335    }
336
337    /// Set a prefix that will be written before the record kind character of every line produced by this
338    /// logger, and return the modified logger. This is a chainable builder method.
339    ///
340    /// The prefix is written verbatim between the timestamp and the record kind character — no separator
341    /// is inserted between the prefix and the kind — so include any trailing separator you want yourself
342    /// (for example a trailing space or brackets). An empty prefix therefore produces the same output as
343    /// no prefix at all.
344    ///
345    /// # Examples
346    ///
347    /// ```
348    /// use logged_stream::FileLogger;
349    ///
350    /// let path = std::env::temp_dir().join("logged-stream-with-prefix-doctest.log");
351    /// let logger = FileLogger::open(&path)?.with_prefix("[conn 5] ");
352    /// assert_eq!(logger.prefix(), Some("[conn 5] "));
353    /// # std::fs::remove_file(&path)?;
354    /// # Ok::<(), std::io::Error>(())
355    /// ```
356    pub fn with_prefix(mut self, prefix: impl Into<Cow<'static, str>>) -> Self {
357        self.prefix = Some(prefix.into());
358        self
359    }
360
361    /// Set or replace the prefix written before the record kind character of every line produced by this
362    /// logger, in place. See [`with_prefix`] for details on how the prefix is rendered.
363    ///
364    /// [`with_prefix`]: FileLogger::with_prefix
365    pub fn set_prefix(&mut self, prefix: impl Into<Cow<'static, str>>) {
366        self.prefix = Some(prefix.into());
367    }
368
369    /// Remove the configured prefix, so lines are written without any prefix again.
370    pub fn clear_prefix(&mut self) {
371        self.prefix = None;
372    }
373
374    /// Return the currently configured prefix, or [`None`] if no prefix is set.
375    #[inline]
376    pub fn prefix(&self) -> Option<&str> {
377        self.prefix.as_deref()
378    }
379}
380
381impl Logger for FileLogger {
382    fn log(&mut self, record: Record) {
383        // Render the whole line before touching the file, then hand it to a single `write_all`.
384        // `std::fs::File` is unbuffered, so writing through `writeln!` would issue one write call per
385        // format piece and let concurrent loggers sharing the file splice their lines into each other.
386        // This is deliberately the opposite trade-off from `ConsoleLogger`, which formats straight into
387        // `log::log!` arguments: there the logging backend does the buffering and locking, here nothing
388        // does. The line is rendered into a fresh `String` rather than a buffer reused across calls so
389        // that a single large record does not permanently retain its capacity.
390        let line = match self.prefix.as_deref() {
391            Some(prefix) => format!(
392                "[{}] {}{} {}\n",
393                record.time.format("%+"),
394                prefix,
395                record.kind,
396                record.message
397            ),
398            None => format!(
399                "[{}] {} {}\n",
400                record.time.format("%+"),
401                record.kind,
402                record.message
403            ),
404        };
405        let _ = self.file.write_all(line.as_bytes());
406    }
407}
408
409impl Logger for Box<FileLogger> {
410    fn log(&mut self, record: Record) {
411        (**self).log(record)
412    }
413}
414
415//////////////////////////////////////////////////////////////////////////////////////////////////////////////
416// Tests
417//////////////////////////////////////////////////////////////////////////////////////////////////////////////
418
419#[cfg(test)]
420mod tests {
421    use crate::logger::ChannelLogger;
422    use crate::logger::ConsoleLogger;
423    use crate::logger::FileLogger;
424    use crate::logger::Logger;
425    use crate::logger::MemoryStorageLogger;
426    use crate::record::Record;
427    use crate::record::RecordKind;
428    use std::cell::RefCell;
429    use std::fs;
430    use std::path::PathBuf;
431    use std::sync::Arc;
432    use std::sync::Barrier;
433    use std::sync::Once;
434    use std::sync::atomic::AtomicUsize;
435    use std::sync::atomic::Ordering;
436    use std::thread;
437
438    //////////////////////////////////////////////////////////////////////////////////////////////////////////
439    // ConsoleLogger
440    //////////////////////////////////////////////////////////////////////////////////////////////////////////
441
442    // A minimal `log::Log` implementation used to capture the exact level and line `ConsoleLogger`
443    // emits through the `log` facade. Captured records are stored per-thread, so tests running in
444    // parallel never observe each other's output.
445    thread_local! {
446        static CAPTURED: RefCell<Vec<(log::Level, String)>> = const { RefCell::new(Vec::new()) };
447    }
448
449    struct CapturingLogger;
450
451    impl log::Log for CapturingLogger {
452        fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
453            true
454        }
455
456        fn log(&self, record: &log::Record<'_>) {
457            CAPTURED.with(|captured| {
458                captured
459                    .borrow_mut()
460                    .push((record.level(), format!("{}", record.args())))
461            });
462        }
463
464        fn flush(&self) {}
465    }
466
467    static CAPTURING_LOGGER: CapturingLogger = CapturingLogger;
468    static INIT_CAPTURING_LOGGER: Once = Once::new();
469
470    // Install the capturing logger exactly once for the whole test binary, raise the max level so
471    // records are not filtered out, and clear this thread's captured lines to give the calling test
472    // a clean slate.
473    fn install_capturing_logger() {
474        INIT_CAPTURING_LOGGER.call_once(|| {
475            // `set_logger` only fails if a logger is already installed; the lib test binary installs
476            // none of its own, so this succeeds. Ignore the error defensively.
477            let _ = log::set_logger(&CAPTURING_LOGGER);
478            log::set_max_level(log::LevelFilter::Trace);
479        });
480        CAPTURED.with(|captured| captured.borrow_mut().clear());
481    }
482
483    fn captured_lines() -> Vec<String> {
484        CAPTURED.with(|captured| {
485            captured
486                .borrow()
487                .iter()
488                .map(|(_, msg)| msg.clone())
489                .collect()
490        })
491    }
492
493    fn captured_records() -> Vec<(log::Level, String)> {
494        CAPTURED.with(|captured| captured.borrow().clone())
495    }
496
497    #[test]
498    fn test_console_logger_prefix_default_none() {
499        assert_eq!(ConsoleLogger::new_unchecked("debug").prefix(), None);
500        assert_eq!(ConsoleLogger::new("info").unwrap().prefix(), None);
501    }
502
503    #[test]
504    fn test_console_logger_with_prefix() {
505        // Static string literal.
506        let logger = ConsoleLogger::new_unchecked("debug").with_prefix("[conn 5] ");
507        assert_eq!(logger.prefix(), Some("[conn 5] "));
508
509        // Owned runtime string (the typical case for a per-connection identifier).
510        let id = 42;
511        let logger = ConsoleLogger::new_unchecked("debug").with_prefix(format!("[conn {id}] "));
512        assert_eq!(logger.prefix(), Some("[conn 42] "));
513    }
514
515    #[test]
516    fn test_console_logger_set_and_clear_prefix() {
517        let mut logger = ConsoleLogger::new_unchecked("debug");
518        assert_eq!(logger.prefix(), None);
519
520        logger.set_prefix(String::from("[server] "));
521        assert_eq!(logger.prefix(), Some("[server] "));
522
523        logger.set_prefix("[client] ");
524        assert_eq!(logger.prefix(), Some("[client] "));
525
526        logger.clear_prefix();
527        assert_eq!(logger.prefix(), None);
528    }
529
530    #[test]
531    fn test_console_logger_logs_prefix_before_kind() {
532        install_capturing_logger();
533
534        let mut logger = ConsoleLogger::new_unchecked("debug");
535
536        // Without a prefix, the emitted line matches the historical `"{kind} {message}"` format.
537        logger.log(Record::new(RecordKind::Write, String::from("ab:cd")));
538
539        // With a prefix, it is prepended verbatim, before the record kind character.
540        logger.set_prefix("[conn 5] ");
541        logger.log(Record::new(RecordKind::Read, String::from("01:02")));
542
543        // After clearing, subsequent lines are emitted without any prefix again.
544        logger.clear_prefix();
545        logger.log(Record::new(
546            RecordKind::Shutdown,
547            String::from("Writer shutdown request."),
548        ));
549
550        assert_eq!(
551            captured_lines(),
552            vec![
553                String::from("> ab:cd"),
554                String::from("[conn 5] < 01:02"),
555                String::from("- Writer shutdown request."),
556            ]
557        );
558    }
559
560    #[test]
561    fn test_console_logger_forces_error_level() {
562        install_capturing_logger();
563
564        // The logger is configured at Debug, below Error. Non-error records are emitted at the
565        // configured level, but Error records are always forced to `log::Level::Error`.
566        let mut logger = ConsoleLogger::new_unchecked("debug");
567        logger.log(Record::new(RecordKind::Write, String::from("01:02")));
568        logger.log(Record::new(RecordKind::Error, String::from("boom")));
569
570        // A prefix does not change the forced Error level.
571        logger.set_prefix("[conn 5] ");
572        logger.log(Record::new(RecordKind::Error, String::from("kaboom")));
573
574        assert_eq!(
575            captured_records(),
576            vec![
577                (log::Level::Debug, String::from("> 01:02")),
578                (log::Level::Error, String::from("! boom")),
579                (log::Level::Error, String::from("[conn 5] ! kaboom")),
580            ]
581        );
582    }
583
584    #[test]
585    fn test_console_logger_empty_prefix_matches_no_prefix() {
586        install_capturing_logger();
587
588        let mut logger = ConsoleLogger::new_unchecked("debug");
589        // No prefix.
590        logger.log(Record::new(RecordKind::Write, String::from("01:02")));
591        // Empty prefix — documented to produce the same output as no prefix at all.
592        logger.set_prefix("");
593        logger.log(Record::new(RecordKind::Write, String::from("01:02")));
594
595        let lines = captured_lines();
596        assert_eq!(lines.len(), 2);
597        assert_eq!(lines[0], lines[1]);
598        assert_eq!(lines[0], "> 01:02");
599    }
600
601    //////////////////////////////////////////////////////////////////////////////////////////////////////////
602    // FileLogger
603    //////////////////////////////////////////////////////////////////////////////////////////////////////////
604
605    // Build a unique temporary file path for a test, so tests running in parallel never share a
606    // file. The loggers under test append, so any stale file from an earlier run is removed first.
607    fn temp_log_path(tag: &str) -> PathBuf {
608        static COUNTER: AtomicUsize = AtomicUsize::new(0);
609        let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
610        let path = std::env::temp_dir().join(format!(
611            "logged-stream-{}-{}-{}.log",
612            tag,
613            std::process::id(),
614            unique
615        ));
616        let _ = fs::remove_file(&path);
617        path
618    }
619
620    // Split a written line into its bracketed timestamp and everything after it.
621    fn split_timestamp(line: &str) -> (&str, &str) {
622        let close = line
623            .find("] ")
624            .expect("line should start with a bracketed timestamp");
625        (&line[1..close], &line[close + 2..])
626    }
627
628    #[test]
629    fn test_file_logger_prefix_default_none() {
630        let path = temp_log_path("prefix-default");
631        let logger = FileLogger::open(&path).unwrap();
632
633        assert_eq!(logger.prefix(), None);
634
635        drop(logger);
636        let _ = fs::remove_file(&path);
637    }
638
639    #[test]
640    fn test_file_logger_set_and_clear_prefix() {
641        let path = temp_log_path("prefix-set");
642        let mut logger = FileLogger::open(&path).unwrap();
643        assert_eq!(logger.prefix(), None);
644
645        logger.set_prefix(String::from("[server] "));
646        assert_eq!(logger.prefix(), Some("[server] "));
647
648        logger.set_prefix("[client] ");
649        assert_eq!(logger.prefix(), Some("[client] "));
650
651        logger.clear_prefix();
652        assert_eq!(logger.prefix(), None);
653
654        drop(logger);
655        let _ = fs::remove_file(&path);
656    }
657
658    #[test]
659    fn test_file_logger_writes_prefix_after_timestamp() {
660        let path = temp_log_path("prefix-placement");
661        let mut logger = FileLogger::open(&path).unwrap();
662
663        // Without a prefix, the line keeps the historical `[timestamp] {kind} {message}` shape.
664        logger.log(Record::new(RecordKind::Write, String::from("ab:cd")));
665
666        // With a prefix, it is written after the timestamp, immediately before the kind character.
667        logger.set_prefix("[conn 5] ");
668        logger.log(Record::new(RecordKind::Read, String::from("01:02")));
669
670        // After clearing, subsequent lines are written without any prefix again.
671        logger.clear_prefix();
672        logger.log(Record::new(
673            RecordKind::Shutdown,
674            String::from("Writer shutdown request."),
675        ));
676
677        drop(logger);
678
679        let content = fs::read_to_string(&path).unwrap();
680        let lines = content.lines().collect::<Vec<&str>>();
681        assert_eq!(lines.len(), 3);
682
683        let expected = ["> ab:cd", "[conn 5] < 01:02", "- Writer shutdown request."];
684        for (line, expected) in lines.iter().zip(expected) {
685            assert!(line.starts_with('['), "missing timestamp: {line}");
686            let (timestamp, rest) = split_timestamp(line);
687            // The part before the prefix must still be a real timestamp, which is what makes the
688            // written lines sortable and parseable by log tooling.
689            assert!(
690                chrono::DateTime::parse_from_rfc3339(timestamp).is_ok(),
691                "not a timestamp: {timestamp}"
692            );
693            assert_eq!(rest, expected);
694        }
695
696        let _ = fs::remove_file(&path);
697    }
698
699    #[test]
700    fn test_file_logger_concurrent_loggers_do_not_interleave_lines() {
701        const THREADS: usize = 8;
702        const RECORDS: usize = 150;
703
704        // A payload shaped like the ones this crate actually produces, long enough that rendering it
705        // through several small writes would let concurrent loggers splice their lines together.
706        let payload = ["ab"; 120].join(":");
707        let path = temp_log_path("concurrent");
708        let barrier = Arc::new(Barrier::new(THREADS));
709        let mut handles = Vec::new();
710
711        for thread_index in 0..THREADS {
712            let path = path.clone();
713            let payload = payload.clone();
714            let barrier = Arc::clone(&barrier);
715            handles.push(thread::spawn(move || {
716                // One logger per "connection", all appending to the same file.
717                let mut logger = FileLogger::open(&path)
718                    .unwrap()
719                    .with_prefix(format!("[conn {thread_index}] "));
720                barrier.wait();
721                for _ in 0..RECORDS {
722                    logger.log(Record::new(RecordKind::Write, payload.clone()));
723                }
724            }));
725        }
726        for handle in handles {
727            handle.join().unwrap();
728        }
729
730        let content = fs::read_to_string(&path).unwrap();
731        let lines = content.lines().collect::<Vec<&str>>();
732        assert_eq!(
733            lines.len(),
734            THREADS * RECORDS,
735            "records were lost or split across lines"
736        );
737        for line in lines {
738            // Any splicing of two concurrent writes breaks at least one of these invariants.
739            assert!(line.starts_with('['), "spliced line: {line}");
740            assert_eq!(line.matches("[conn ").count(), 1, "spliced line: {line}");
741            assert!(line.ends_with(&payload), "truncated line: {line}");
742        }
743
744        let _ = fs::remove_file(&path);
745    }
746
747    //////////////////////////////////////////////////////////////////////////////////////////////////////////
748    // Trait assertions
749    //////////////////////////////////////////////////////////////////////////////////////////////////////////
750
751    fn assert_unpin<T: Unpin>() {}
752
753    fn assert_send<T: Send>() {}
754
755    fn assert_logger<T: Logger>() {}
756
757    #[test]
758    fn test_unpin() {
759        assert_unpin::<ConsoleLogger>();
760        assert_unpin::<ChannelLogger>();
761        assert_unpin::<MemoryStorageLogger>();
762        assert_unpin::<FileLogger>();
763    }
764
765    #[test]
766    fn test_send() {
767        assert_send::<ConsoleLogger>();
768        assert_send::<MemoryStorageLogger>();
769        assert_send::<ChannelLogger>();
770        assert_send::<FileLogger>();
771
772        assert_send::<Box<dyn Logger>>();
773        assert_send::<Box<ConsoleLogger>>();
774        assert_send::<Box<MemoryStorageLogger>>();
775        assert_send::<Box<ChannelLogger>>();
776        assert_send::<Box<FileLogger>>();
777    }
778
779    #[test]
780    fn test_box() {
781        assert_logger::<Box<dyn Logger>>();
782        assert_logger::<Box<ConsoleLogger>>();
783        assert_logger::<Box<MemoryStorageLogger>>();
784        assert_logger::<Box<ChannelLogger>>();
785        assert_logger::<Box<FileLogger>>();
786    }
787
788    #[test]
789    fn test_trait_object_safety() {
790        // Assert trait object construct.
791        let mut console: Box<dyn Logger> = Box::new(ConsoleLogger::new_unchecked("debug"));
792        let mut memory: Box<dyn Logger> = Box::new(MemoryStorageLogger::new(100));
793        let mut channel: Box<dyn Logger> = Box::new(ChannelLogger::new());
794        let path = temp_log_path("object-safety");
795        let mut file: Box<dyn Logger> = Box::new(FileLogger::open(&path).unwrap());
796
797        let record = Record::new(RecordKind::Open, String::from("test log record"));
798
799        // Assert that trait object methods are dispatchable.
800        console.log(record.clone());
801        memory.log(record.clone());
802        channel.log(record.clone());
803        file.log(record);
804
805        drop(file);
806        let _ = fs::remove_file(&path);
807    }
808}