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}