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}