use crate::RecordKind;
use crate::record::Record;
use std::borrow::Cow;
use std::collections;
use std::fs;
use std::io::Write;
use std::io::{self};
use std::path::Path;
use std::str::FromStr;
use std::sync::mpsc;
pub trait Logger: Send + 'static {
fn log(&mut self, record: Record);
}
impl Logger for Box<dyn Logger> {
fn log(&mut self, record: Record) {
(**self).log(record)
}
}
#[derive(Debug, Clone)]
pub struct ConsoleLogger {
level: log::Level,
prefix: Option<Cow<'static, str>>,
}
impl ConsoleLogger {
pub fn new(level: &str) -> Result<Self, log::ParseLevelError> {
let level = log::Level::from_str(level)?;
Ok(Self {
level,
prefix: None,
})
}
pub fn new_unchecked(level: &str) -> Self {
Self::new(level).unwrap()
}
pub fn with_prefix(mut self, prefix: impl Into<Cow<'static, str>>) -> Self {
self.prefix = Some(prefix.into());
self
}
pub fn set_prefix(&mut self, prefix: impl Into<Cow<'static, str>>) {
self.prefix = Some(prefix.into());
}
pub fn clear_prefix(&mut self) {
self.prefix = None;
}
#[inline]
pub fn prefix(&self) -> Option<&str> {
self.prefix.as_deref()
}
}
impl Logger for ConsoleLogger {
fn log(&mut self, record: Record) {
let level = match record.kind {
RecordKind::Error => log::Level::Error,
_ => self.level,
};
match self.prefix.as_deref() {
Some(prefix) => log::log!(level, "{}{} {}", prefix, record.kind, record.message),
None => log::log!(level, "{} {}", record.kind, record.message),
}
}
}
impl Logger for Box<ConsoleLogger> {
fn log(&mut self, record: Record) {
(**self).log(record)
}
}
#[derive(Debug, Clone)]
pub struct MemoryStorageLogger {
storage: collections::VecDeque<Record>,
max_length: usize,
}
impl MemoryStorageLogger {
pub fn new(max_length: usize) -> Self {
Self {
storage: collections::VecDeque::new(),
max_length,
}
}
#[inline]
pub fn get_log_records(&self) -> collections::VecDeque<Record> {
self.storage.clone()
}
#[inline]
pub fn clear_log_records(&mut self) {
self.storage.clear()
}
}
impl Logger for MemoryStorageLogger {
fn log(&mut self, record: Record) {
self.storage.push_back(record);
if self.storage.len() > self.max_length {
let _ = self.storage.pop_front();
}
}
}
impl Logger for Box<MemoryStorageLogger> {
fn log(&mut self, record: Record) {
(**self).log(record)
}
}
#[derive(Debug)]
pub struct ChannelLogger {
sender: mpsc::Sender<Record>,
receiver: Option<mpsc::Receiver<Record>>,
}
impl ChannelLogger {
pub fn new() -> Self {
let (sender, receiver) = mpsc::channel();
Self {
sender,
receiver: Some(receiver),
}
}
#[inline]
pub fn take_receiver(&mut self) -> Option<mpsc::Receiver<Record>> {
self.receiver.take()
}
pub fn take_receiver_unchecked(&mut self) -> mpsc::Receiver<Record> {
self.take_receiver().unwrap()
}
}
impl Default for ChannelLogger {
fn default() -> Self {
Self::new()
}
}
impl Logger for ChannelLogger {
fn log(&mut self, record: Record) {
let _ = self.sender.send(record);
}
}
impl Logger for Box<ChannelLogger> {
fn log(&mut self, record: Record) {
(**self).log(record)
}
}
#[derive(Debug)]
pub struct FileLogger {
file: fs::File,
prefix: Option<Cow<'static, str>>,
}
impl FileLogger {
pub fn new(file: fs::File) -> Self {
Self { file, prefix: None }
}
pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
let file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
Ok(Self::new(file))
}
pub fn with_prefix(mut self, prefix: impl Into<Cow<'static, str>>) -> Self {
self.prefix = Some(prefix.into());
self
}
pub fn set_prefix(&mut self, prefix: impl Into<Cow<'static, str>>) {
self.prefix = Some(prefix.into());
}
pub fn clear_prefix(&mut self) {
self.prefix = None;
}
#[inline]
pub fn prefix(&self) -> Option<&str> {
self.prefix.as_deref()
}
}
impl Logger for FileLogger {
fn log(&mut self, record: Record) {
let line = match self.prefix.as_deref() {
Some(prefix) => format!(
"[{}] {}{} {}\n",
record.time.format("%+"),
prefix,
record.kind,
record.message
),
None => format!(
"[{}] {} {}\n",
record.time.format("%+"),
record.kind,
record.message
),
};
let _ = self.file.write_all(line.as_bytes());
}
}
impl Logger for Box<FileLogger> {
fn log(&mut self, record: Record) {
(**self).log(record)
}
}
#[cfg(test)]
mod tests {
use crate::logger::ChannelLogger;
use crate::logger::ConsoleLogger;
use crate::logger::FileLogger;
use crate::logger::Logger;
use crate::logger::MemoryStorageLogger;
use crate::record::Record;
use crate::record::RecordKind;
use std::cell::RefCell;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Barrier;
use std::sync::Once;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::thread;
thread_local! {
static CAPTURED: RefCell<Vec<(log::Level, String)>> = const { RefCell::new(Vec::new()) };
}
struct CapturingLogger;
impl log::Log for CapturingLogger {
fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
true
}
fn log(&self, record: &log::Record<'_>) {
CAPTURED.with(|captured| {
captured
.borrow_mut()
.push((record.level(), format!("{}", record.args())))
});
}
fn flush(&self) {}
}
static CAPTURING_LOGGER: CapturingLogger = CapturingLogger;
static INIT_CAPTURING_LOGGER: Once = Once::new();
fn install_capturing_logger() {
INIT_CAPTURING_LOGGER.call_once(|| {
let _ = log::set_logger(&CAPTURING_LOGGER);
log::set_max_level(log::LevelFilter::Trace);
});
CAPTURED.with(|captured| captured.borrow_mut().clear());
}
fn captured_lines() -> Vec<String> {
CAPTURED.with(|captured| {
captured
.borrow()
.iter()
.map(|(_, msg)| msg.clone())
.collect()
})
}
fn captured_records() -> Vec<(log::Level, String)> {
CAPTURED.with(|captured| captured.borrow().clone())
}
#[test]
fn test_console_logger_prefix_default_none() {
assert_eq!(ConsoleLogger::new_unchecked("debug").prefix(), None);
assert_eq!(ConsoleLogger::new("info").unwrap().prefix(), None);
}
#[test]
fn test_console_logger_with_prefix() {
let logger = ConsoleLogger::new_unchecked("debug").with_prefix("[conn 5] ");
assert_eq!(logger.prefix(), Some("[conn 5] "));
let id = 42;
let logger = ConsoleLogger::new_unchecked("debug").with_prefix(format!("[conn {id}] "));
assert_eq!(logger.prefix(), Some("[conn 42] "));
}
#[test]
fn test_console_logger_set_and_clear_prefix() {
let mut logger = ConsoleLogger::new_unchecked("debug");
assert_eq!(logger.prefix(), None);
logger.set_prefix(String::from("[server] "));
assert_eq!(logger.prefix(), Some("[server] "));
logger.set_prefix("[client] ");
assert_eq!(logger.prefix(), Some("[client] "));
logger.clear_prefix();
assert_eq!(logger.prefix(), None);
}
#[test]
fn test_console_logger_logs_prefix_before_kind() {
install_capturing_logger();
let mut logger = ConsoleLogger::new_unchecked("debug");
logger.log(Record::new(RecordKind::Write, String::from("ab:cd")));
logger.set_prefix("[conn 5] ");
logger.log(Record::new(RecordKind::Read, String::from("01:02")));
logger.clear_prefix();
logger.log(Record::new(
RecordKind::Shutdown,
String::from("Writer shutdown request."),
));
assert_eq!(
captured_lines(),
vec![
String::from("> ab:cd"),
String::from("[conn 5] < 01:02"),
String::from("- Writer shutdown request."),
]
);
}
#[test]
fn test_console_logger_forces_error_level() {
install_capturing_logger();
let mut logger = ConsoleLogger::new_unchecked("debug");
logger.log(Record::new(RecordKind::Write, String::from("01:02")));
logger.log(Record::new(RecordKind::Error, String::from("boom")));
logger.set_prefix("[conn 5] ");
logger.log(Record::new(RecordKind::Error, String::from("kaboom")));
assert_eq!(
captured_records(),
vec![
(log::Level::Debug, String::from("> 01:02")),
(log::Level::Error, String::from("! boom")),
(log::Level::Error, String::from("[conn 5] ! kaboom")),
]
);
}
#[test]
fn test_console_logger_empty_prefix_matches_no_prefix() {
install_capturing_logger();
let mut logger = ConsoleLogger::new_unchecked("debug");
logger.log(Record::new(RecordKind::Write, String::from("01:02")));
logger.set_prefix("");
logger.log(Record::new(RecordKind::Write, String::from("01:02")));
let lines = captured_lines();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], lines[1]);
assert_eq!(lines[0], "> 01:02");
}
fn temp_log_path(tag: &str) -> PathBuf {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"logged-stream-{}-{}-{}.log",
tag,
std::process::id(),
unique
));
let _ = fs::remove_file(&path);
path
}
fn split_timestamp(line: &str) -> (&str, &str) {
let close = line
.find("] ")
.expect("line should start with a bracketed timestamp");
(&line[1..close], &line[close + 2..])
}
#[test]
fn test_file_logger_prefix_default_none() {
let path = temp_log_path("prefix-default");
let logger = FileLogger::open(&path).unwrap();
assert_eq!(logger.prefix(), None);
drop(logger);
let _ = fs::remove_file(&path);
}
#[test]
fn test_file_logger_set_and_clear_prefix() {
let path = temp_log_path("prefix-set");
let mut logger = FileLogger::open(&path).unwrap();
assert_eq!(logger.prefix(), None);
logger.set_prefix(String::from("[server] "));
assert_eq!(logger.prefix(), Some("[server] "));
logger.set_prefix("[client] ");
assert_eq!(logger.prefix(), Some("[client] "));
logger.clear_prefix();
assert_eq!(logger.prefix(), None);
drop(logger);
let _ = fs::remove_file(&path);
}
#[test]
fn test_file_logger_writes_prefix_after_timestamp() {
let path = temp_log_path("prefix-placement");
let mut logger = FileLogger::open(&path).unwrap();
logger.log(Record::new(RecordKind::Write, String::from("ab:cd")));
logger.set_prefix("[conn 5] ");
logger.log(Record::new(RecordKind::Read, String::from("01:02")));
logger.clear_prefix();
logger.log(Record::new(
RecordKind::Shutdown,
String::from("Writer shutdown request."),
));
drop(logger);
let content = fs::read_to_string(&path).unwrap();
let lines = content.lines().collect::<Vec<&str>>();
assert_eq!(lines.len(), 3);
let expected = ["> ab:cd", "[conn 5] < 01:02", "- Writer shutdown request."];
for (line, expected) in lines.iter().zip(expected) {
assert!(line.starts_with('['), "missing timestamp: {line}");
let (timestamp, rest) = split_timestamp(line);
assert!(
chrono::DateTime::parse_from_rfc3339(timestamp).is_ok(),
"not a timestamp: {timestamp}"
);
assert_eq!(rest, expected);
}
let _ = fs::remove_file(&path);
}
#[test]
fn test_file_logger_concurrent_loggers_do_not_interleave_lines() {
const THREADS: usize = 8;
const RECORDS: usize = 150;
let payload = ["ab"; 120].join(":");
let path = temp_log_path("concurrent");
let barrier = Arc::new(Barrier::new(THREADS));
let mut handles = Vec::new();
for thread_index in 0..THREADS {
let path = path.clone();
let payload = payload.clone();
let barrier = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
let mut logger = FileLogger::open(&path)
.unwrap()
.with_prefix(format!("[conn {thread_index}] "));
barrier.wait();
for _ in 0..RECORDS {
logger.log(Record::new(RecordKind::Write, payload.clone()));
}
}));
}
for handle in handles {
handle.join().unwrap();
}
let content = fs::read_to_string(&path).unwrap();
let lines = content.lines().collect::<Vec<&str>>();
assert_eq!(
lines.len(),
THREADS * RECORDS,
"records were lost or split across lines"
);
for line in lines {
assert!(line.starts_with('['), "spliced line: {line}");
assert_eq!(line.matches("[conn ").count(), 1, "spliced line: {line}");
assert!(line.ends_with(&payload), "truncated line: {line}");
}
let _ = fs::remove_file(&path);
}
fn assert_unpin<T: Unpin>() {}
fn assert_send<T: Send>() {}
fn assert_logger<T: Logger>() {}
#[test]
fn test_unpin() {
assert_unpin::<ConsoleLogger>();
assert_unpin::<ChannelLogger>();
assert_unpin::<MemoryStorageLogger>();
assert_unpin::<FileLogger>();
}
#[test]
fn test_send() {
assert_send::<ConsoleLogger>();
assert_send::<MemoryStorageLogger>();
assert_send::<ChannelLogger>();
assert_send::<FileLogger>();
assert_send::<Box<dyn Logger>>();
assert_send::<Box<ConsoleLogger>>();
assert_send::<Box<MemoryStorageLogger>>();
assert_send::<Box<ChannelLogger>>();
assert_send::<Box<FileLogger>>();
}
#[test]
fn test_box() {
assert_logger::<Box<dyn Logger>>();
assert_logger::<Box<ConsoleLogger>>();
assert_logger::<Box<MemoryStorageLogger>>();
assert_logger::<Box<ChannelLogger>>();
assert_logger::<Box<FileLogger>>();
}
#[test]
fn test_trait_object_safety() {
let mut console: Box<dyn Logger> = Box::new(ConsoleLogger::new_unchecked("debug"));
let mut memory: Box<dyn Logger> = Box::new(MemoryStorageLogger::new(100));
let mut channel: Box<dyn Logger> = Box::new(ChannelLogger::new());
let path = temp_log_path("object-safety");
let mut file: Box<dyn Logger> = Box::new(FileLogger::open(&path).unwrap());
let record = Record::new(RecordKind::Open, String::from("test log record"));
console.log(record.clone());
memory.log(record.clone());
channel.log(record.clone());
file.log(record);
drop(file);
let _ = fs::remove_file(&path);
}
}