#![warn(missing_docs)]
#![cfg_attr(not(doctest), doc = include_str!("../README.md"))]
use std::error::Error;
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{self, RecvError, SendError};
use std::sync::Arc;
use std::thread;
use chrono::DateTime;
use chrono::Local;
use once_cell::sync::OnceCell;
const CHANNEL_SIZE: usize = 512;
#[doc(hidden)]
pub mod sender;
use self::sender::Sender;
#[doc(hidden)]
pub mod receiver;
use self::receiver::Receiver;
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub enum Level {
Trace = 0x01,
Debug = 0x02,
Info = 0x04,
Warning = 0x08,
Error = 0x10,
Fatal = 0x20,
}
#[doc(hidden)]
pub struct MsgTuple {
pub timestamp: DateTime<Local>,
pub level: Level,
pub fn_name: String,
pub line: u32,
pub msg: String,
}
#[derive(Debug, Copy, Clone)]
pub enum OutputStream {
Neither = 0x0,
StdOut = 0x1,
File = 0x2,
Both = 0x3,
}
#[doc(hidden)]
pub enum Command {
LogMsg(MsgTuple),
SetOutputLevel(Level),
SetOutputStream(OutputStream),
Flush(mpsc::Sender<()>),
}
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct MtLogger {
enabled: bool,
sender: Sender,
msg_count: Arc<AtomicU64>,
}
#[derive(Debug)]
pub enum MtLoggerError {
LoggerNotInitialized,
SendError(SendError<Command>),
RecvError(RecvError),
}
#[doc(hidden)]
pub static INSTANCE: OnceCell<MtLogger> = OnceCell::new();
impl MtLogger {
pub fn new(
logfile_prefix: &'static str,
output_level: Level,
output_stream: OutputStream,
) -> Self {
let (logger_tx, logger_rx) = mpsc::sync_channel::<Command>(CHANNEL_SIZE);
let msg_count = Arc::new(AtomicU64::new(0));
let mut log_receiver = Receiver::new(
logfile_prefix,
logger_rx,
output_level,
output_stream,
Arc::clone(&msg_count),
);
thread::Builder::new()
.name("log_receiver".to_string())
.spawn(move || log_receiver.main())
.unwrap();
let log_sender = Sender::new(logger_tx);
Self {
enabled: true,
sender: log_sender,
msg_count,
}
}
#[doc(hidden)]
pub fn msg_count(&self) -> u64 {
self.msg_count.load(Ordering::SeqCst)
}
#[doc(hidden)]
pub fn log_msg(
&self,
timestamp: DateTime<Local>,
level: Level,
fn_name: String,
line: u32,
msg: String,
) -> Result<(), SendError<Command>> {
if self.enabled {
let log_tuple = MsgTuple {
timestamp,
level,
fn_name,
line,
msg,
};
self.sender.send_log(Command::LogMsg(log_tuple))
} else {
Ok(())
}
}
#[doc(hidden)]
pub fn log_cmd(&self, cmd: Command) -> Result<(), SendError<Command>> {
if self.enabled {
self.sender.send_cmd(cmd)
} else {
Ok(())
}
}
#[doc(hidden)]
pub fn flush(&self) -> Result<(), MtLoggerError> {
let (flush_ack_tx, flush_ack_rx) = mpsc::channel::<()>();
self.sender.send_cmd(Command::Flush(flush_ack_tx))?;
flush_ack_rx.recv()?;
Ok(())
}
}
#[doc(hidden)]
pub fn mt_now() -> DateTime<Local> {
Local::now()
}
impl fmt::Display for Level {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Trace => write!(f, "TRACE"),
Self::Debug => write!(f, "DEBUG"),
Self::Info => write!(f, "INFO"),
Self::Warning => write!(f, "WARNING"),
Self::Error => write!(f, "ERROR"),
Self::Fatal => write!(f, "FATAL"),
}
}
}
impl Error for MtLoggerError {}
impl fmt::Display for MtLoggerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::LoggerNotInitialized => {
write!(
f,
"Attempted a command before the logger instance was initialized"
)
}
Self::SendError(send_err) => {
write!(
f,
"Encountered SendError '{}' while performing a logger command",
send_err
)
}
Self::RecvError(recv_err) => {
write!(
f,
"Encountered RecvError '{}' while performing a logger command",
recv_err
)
}
}
}
}
impl From<SendError<Command>> for MtLoggerError {
fn from(src: SendError<Command>) -> Self {
Self::SendError(src)
}
}
impl From<RecvError> for MtLoggerError {
fn from(src: RecvError) -> Self {
Self::RecvError(src)
}
}
#[macro_export]
macro_rules! mt_new {
($logfile_prefix:expr, $output_level:expr, $output_stream:expr) => {{
let prefix = match $logfile_prefix {
Some(specified_prefix) => specified_prefix,
None => env!("CARGO_PKG_NAME"),
};
let logger = $crate::MtLogger::new(prefix, $output_level, $output_stream);
$crate::INSTANCE
.set(logger)
.expect("MtLogger INSTANCE already initialized");
}};
}
#[macro_export]
macro_rules! mt_log {
($log_level:expr, $( $fmt_args:expr ),*) => {{
let timestamp = $crate::mt_now();
let fn_name = {
fn f() {}
fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
let name = type_name_of(f);
&name[..name.len() - 3]
};
let msg_content: String = format!($( $fmt_args ),*);
$crate::INSTANCE
.get()
.and_then(|instance| instance.log_msg(
timestamp,
$log_level,
fn_name.to_string(),
line!(),
msg_content)
.ok()
);
}};
}
#[macro_export]
macro_rules! mt_stream {
($output_stream:expr) => {{
$crate::INSTANCE
.get()
.and_then(|instance| {
instance
.log_cmd($crate::Command::SetOutputStream($output_stream))
.ok()
});
}};
}
#[macro_export]
macro_rules! mt_level {
($output_level:expr) => {{
$crate::INSTANCE
.get()
.and_then(|instance| {
instance
.log_cmd($crate::Command::SetOutputLevel($output_level))
.ok()
});
}};
}
#[macro_export]
macro_rules! mt_count {
() => {{
$crate::INSTANCE
.get()
.and_then(|instance| Some(instance.msg_count()))
.unwrap()
}};
}
#[macro_export]
macro_rules! mt_flush {
() => {
$crate::INSTANCE.get().map_or(
Err($crate::MtLoggerError::LoggerNotInitialized),
|instance| instance.flush(),
)
};
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time;
use lazy_static::lazy_static;
use regex::Regex;
use crate::receiver::{FILE_OUT_FILENAME, STDOUT_FILENAME};
use crate::{Level, OutputStream, INSTANCE};
type TestResult = Result<(), Box<dyn Error>>;
lazy_static! {
static ref LOGGER_MUTEX: Mutex<()> = Mutex::new(());
}
#[derive(Debug, PartialEq)]
enum VerfFile {
StdOut,
FileOut,
}
const LOGFILE_PREFIX: Option<&'static str> = Some("TEST");
const STDOUT_HDR_REGEX_STR: &str = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{9}: \x1b\[(\d{3};\d{3}m)\[(\s*(\w*)\s*)\]\x1b\[0m (.*)\(\) line (\d*):";
const STDOUT_COLOR_IDX: usize = 1;
const STDOUT_PADDED_LEVEL_IDX: usize = 2;
const STDOUT_PADLESS_LEVEL_IDX: usize = 3;
const STDOUT_FN_NAME_IDX: usize = 4;
const STDOUT_LINE_NUM_IDX: usize = 5;
const FILE_OUT_HDR_REGEX_STR: &str =
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{9}: \[(\s*(\w*)\s*)\] (.*)\(\) line (\d*):";
const FILE_OUT_PADDED_LEVEL_IDX: usize = 1;
const FILE_OUT_PADLESS_LEVEL_IDX: usize = 2;
const FILE_OUT_FN_NAME_IDX: usize = 3;
const FILE_OUT_LINE_NUM_IDX: usize = 4;
fn reset_verf_files() -> TestResult {
let path_buf = PathBuf::from(STDOUT_FILENAME);
if path_buf.as_path().exists() {
fs::write(STDOUT_FILENAME, "")?;
}
let path_buf = PathBuf::from(FILE_OUT_FILENAME);
if path_buf.as_path().exists() {
fs::write(FILE_OUT_FILENAME, "")?;
}
Ok(())
}
fn format_verf_helper(
verf_type: VerfFile,
verf_string: String,
first_line_num: u32,
) -> TestResult {
const FN_NAME: &str = "mt_logger::tests::format_verification";
const VERF_MATRIX: [[&str; 3]; 6] = [
["TRACE", "030;105m", " TRACE "],
["DEBUG", "030;106m", " DEBUG "],
["INFO", "030;107m", " INFO "],
["WARNING", "030;103m", " WARNING "],
["ERROR", "030;101m", " ERROR "],
["FATAL", "031;040m", " FATAL "],
];
const LEVEL_VERF_IDX: usize = 0;
const COLOR_VERF_IDX: usize = 1;
const PADDED_LEVEL_VERF_IDX: usize = 2;
let padded_level_hdr_capture_idx;
let fn_name_hdr_capture_idx;
let line_num_hdr_capture_idx;
let header_regex;
match verf_type {
VerfFile::StdOut => {
padded_level_hdr_capture_idx = STDOUT_PADDED_LEVEL_IDX;
fn_name_hdr_capture_idx = STDOUT_FN_NAME_IDX;
line_num_hdr_capture_idx = STDOUT_LINE_NUM_IDX;
header_regex = Regex::new(STDOUT_HDR_REGEX_STR)?;
}
VerfFile::FileOut => {
padded_level_hdr_capture_idx = FILE_OUT_PADDED_LEVEL_IDX;
fn_name_hdr_capture_idx = FILE_OUT_FN_NAME_IDX;
line_num_hdr_capture_idx = FILE_OUT_LINE_NUM_IDX;
header_regex = Regex::new(FILE_OUT_HDR_REGEX_STR)?;
}
}
let level_content_capture_idx = 1;
let content_regex = Regex::new(r"^ This is an? (\w*) message.")?;
let mut verf_lines: Vec<&str> = verf_string.split('\n').collect();
let mut verf_line_iter = verf_lines.iter_mut();
let mut i = 0;
while let Some(header_line) = verf_line_iter.next().filter(|v| !v.is_empty()) {
let header_captures = header_regex.captures(header_line).unwrap_or_else(|| {
panic!(
"{:?}: Header line {} '{}' did not match Regex:\n {}",
verf_type,
i,
header_line,
header_regex.as_str()
)
});
if verf_type == VerfFile::StdOut
&& &header_captures[STDOUT_COLOR_IDX] != VERF_MATRIX[i][COLOR_VERF_IDX]
{
panic!(
"{:?}: Wrong color '{}' on line '{}', should be '{}'",
verf_type,
&header_captures[STDOUT_COLOR_IDX],
header_line,
VERF_MATRIX[i][COLOR_VERF_IDX]
);
}
if &header_captures[padded_level_hdr_capture_idx]
!= VERF_MATRIX[i][PADDED_LEVEL_VERF_IDX]
{
panic!(
"{:?}: Wrong padded level '{}' on line '{}', should be '{}'",
verf_type,
&header_captures[padded_level_hdr_capture_idx],
header_line,
VERF_MATRIX[i][PADDED_LEVEL_VERF_IDX]
);
}
if &header_captures[fn_name_hdr_capture_idx] != FN_NAME {
panic!(
"{:?}: Wrong function name '{}' on line '{}', should be '{}'",
verf_type, &header_captures[fn_name_hdr_capture_idx], header_line, FN_NAME
);
}
if header_captures[line_num_hdr_capture_idx].parse::<u32>()?
!= first_line_num + i as u32
{
panic!(
"{:?}: Wrong line number '{}' on line '{}', should be '{}'",
verf_type,
&header_captures[line_num_hdr_capture_idx],
header_line,
first_line_num + i as u32
);
}
let content_line = verf_line_iter
.next()
.unwrap_or_else(|| panic!("Missing content line after header '{}'", header_line));
let content_captures = content_regex.captures(content_line).unwrap_or_else(|| {
panic!(
"{:?}: Content line {} '{}' did not match content Regex:\n {}",
verf_type,
i,
content_line,
content_regex.as_str()
)
});
if &content_captures[level_content_capture_idx] != VERF_MATRIX[i][LEVEL_VERF_IDX] {
panic!(
"{:?}: Wrong level '{}' in content line '{}', should be '{}'",
verf_type,
&content_captures[level_content_capture_idx],
content_line,
VERF_MATRIX[i][LEVEL_VERF_IDX]
)
}
i += 1;
}
Ok(())
}
#[test]
fn format_verification() -> TestResult {
let mutex = LOGGER_MUTEX.lock()?;
reset_verf_files()?;
if INSTANCE.get().is_none() {
mt_new!(LOGFILE_PREFIX, Level::Trace, OutputStream::Both);
} else {
mt_level!(Level::Trace);
mt_stream!(OutputStream::Both);
}
let first_line_num = line!() + 1;
mt_log!(Level::Trace, "This is a TRACE message.");
mt_log!(Level::Debug, "This is a DEBUG message.");
mt_log!(Level::Info, "This is an INFO message.");
mt_log!(Level::Warning, "This is a WARNING message.");
mt_log!(Level::Error, "This is an ERROR message.");
mt_log!(Level::Fatal, "This is a FATAL message.");
println!("Flushing all messages to their output...");
let start_time = time::Instant::now();
mt_flush!()?;
println!("Done flushing after {}ms", start_time.elapsed().as_millis());
let mut verf_file_stdout = fs::OpenOptions::new().read(true).open(STDOUT_FILENAME)?;
let mut verf_string_stdout = String::new();
verf_file_stdout.read_to_string(&mut verf_string_stdout)?;
let mut verf_file_file_out = fs::OpenOptions::new().read(true).open(FILE_OUT_FILENAME)?;
let mut verf_string_file_out = String::new();
verf_file_file_out.read_to_string(&mut verf_string_file_out)?;
std::mem::drop(mutex);
format_verf_helper(VerfFile::StdOut, verf_string_stdout, first_line_num)?;
format_verf_helper(VerfFile::FileOut, verf_string_file_out, first_line_num)?;
Ok(())
}
fn outputstream_verf_helper(verf_type: VerfFile, verf_string: String) -> TestResult {
const VERF_MATRIX: [[[&str; 2]; 4]; 2] = [
[
["TRACE", "BOTH"],
["FATAL", "BOTH"],
["TRACE", "STDOUT"],
["FATAL", "STDOUT"],
],
[
["TRACE", "BOTH"],
["FATAL", "BOTH"],
["TRACE", "FILEOUT"],
["FATAL", "FILEOUT"],
],
];
const STDOUT_TYPE_IDX: usize = 0;
const FILE_OUT_TYPE_IDX: usize = 1;
const LEVEL_VERF_IDX: usize = 0;
const OUTPUT_TYPE_VERF_IDX: usize = 1;
let verf_type_idx;
let padless_level_hdr_capture_idx;
let header_regex;
match verf_type {
VerfFile::StdOut => {
verf_type_idx = STDOUT_TYPE_IDX;
padless_level_hdr_capture_idx = STDOUT_PADLESS_LEVEL_IDX;
header_regex = Regex::new(STDOUT_HDR_REGEX_STR)?;
}
VerfFile::FileOut => {
verf_type_idx = FILE_OUT_TYPE_IDX;
padless_level_hdr_capture_idx = FILE_OUT_PADLESS_LEVEL_IDX;
header_regex = Regex::new(FILE_OUT_HDR_REGEX_STR)?;
}
}
let output_type_capture_idx = 1;
let content_regex = Regex::new(r"^\s*This message appears in (\w*).")?;
let mut verf_lines: Vec<&str> = verf_string.split('\n').collect();
let mut verf_line_iter = verf_lines.iter_mut();
let mut i = 0;
while let Some(header_line) = verf_line_iter.next().filter(|v| !v.is_empty()) {
let header_captures = header_regex.captures(header_line).unwrap_or_else(|| {
panic!(
"{:?}: Header line {} '{}' did not match Regex:\n {}",
verf_type,
i,
header_line,
header_regex.as_str()
)
});
if &header_captures[padless_level_hdr_capture_idx]
!= VERF_MATRIX[verf_type_idx][i][LEVEL_VERF_IDX]
{
panic!(
"{:?}: Wrong level '{}' on line '{}', should be '{}'",
verf_type,
&header_captures[padless_level_hdr_capture_idx],
header_line,
VERF_MATRIX[verf_type_idx][i][LEVEL_VERF_IDX]
);
}
let content_line = verf_line_iter
.next()
.unwrap_or_else(|| panic!("Missing content line after header '{}'", header_line));
let content_captures = content_regex.captures(content_line).unwrap_or_else(|| {
panic!(
"{:?}: Content line {} '{}' did not match content Regex:\n {}",
verf_type,
i,
content_line,
content_regex.as_str()
)
});
if &content_captures[output_type_capture_idx]
!= VERF_MATRIX[verf_type_idx][i][OUTPUT_TYPE_VERF_IDX]
{
panic!(
"{:?}: Wrong output type '{}' on line '{}', should be '{}'",
verf_type,
&content_captures[output_type_capture_idx],
content_line,
VERF_MATRIX[verf_type_idx][i][OUTPUT_TYPE_VERF_IDX]
)
}
i += 1;
}
Ok(())
}
#[test]
fn outputstream_verification() -> TestResult {
let mutex = LOGGER_MUTEX.lock()?;
reset_verf_files()?;
if INSTANCE.get().is_none() {
mt_new!(LOGFILE_PREFIX, Level::Trace, OutputStream::Both);
} else {
mt_level!(Level::Trace);
mt_stream!(OutputStream::Both);
}
mt_log!(Level::Trace, "This message appears in BOTH.");
mt_log!(Level::Fatal, "This message appears in BOTH.");
mt_stream!(OutputStream::StdOut);
mt_log!(Level::Trace, "This message appears in STDOUT.");
mt_log!(Level::Fatal, "This message appears in STDOUT.");
mt_stream!(OutputStream::File);
mt_log!(Level::Trace, "This message appears in FILEOUT.");
mt_log!(Level::Fatal, "This message appears in FILEOUT.");
mt_stream!(OutputStream::Neither);
mt_log!(Level::Trace, "This message appears in NEITHER.");
mt_log!(Level::Fatal, "This message appears in NEITHER.");
println!("Flushing all messages to their output...");
let start_time = time::Instant::now();
mt_flush!()?;
println!("Done flushing after {}ms", start_time.elapsed().as_millis());
let mut verf_file_stdout = fs::OpenOptions::new().read(true).open(STDOUT_FILENAME)?;
let mut verf_string_stdout = String::new();
verf_file_stdout.read_to_string(&mut verf_string_stdout)?;
let mut verf_file_file_out = fs::OpenOptions::new().read(true).open(FILE_OUT_FILENAME)?;
let mut verf_string_file_out = String::new();
verf_file_file_out.read_to_string(&mut verf_string_file_out)?;
std::mem::drop(mutex);
outputstream_verf_helper(VerfFile::StdOut, verf_string_stdout)?;
outputstream_verf_helper(VerfFile::FileOut, verf_string_file_out)?;
Ok(())
}
#[test]
fn flush_test() -> TestResult {
let _mutex = LOGGER_MUTEX.lock()?;
reset_verf_files()?;
if INSTANCE.get().is_none() {
mt_new!(LOGFILE_PREFIX, Level::Info, OutputStream::StdOut);
} else {
mt_level!(Level::Info);
mt_stream!(OutputStream::StdOut);
}
let initial_msg_count = mt_count!();
eprintln!("Initial message count: {}", initial_msg_count);
eprintln!("Sending messages...");
let sent_msg_count = 5;
for i in 0..sent_msg_count {
mt_log!(Level::Info, "Message #{}", i);
}
mt_flush!()?;
eprintln!("Messages processed: {}", mt_count!());
assert_eq!(initial_msg_count + sent_msg_count, mt_count!());
Ok(())
}
}