#[doc(hidden)]
pub use ref_thread_local as _ref_thread_local;
pub mod callback;
pub mod proxy;
pub mod stdio;
pub mod tee_file;
pub mod thread;
use crate::common::{
channel::Sender,
error,
error::{ErrorKind, ResultExt},
};
use lazy_static::lazy_static;
use named_type::NamedType;
use named_type_derive::*;
use ref_thread_local::ref_thread_local;
use serde::{Deserialize, Serialize};
use std::{cell::RefCell, fmt};
use strum_macros::{Display, EnumIter, EnumString};
pub trait Log {
fn name(&self) -> &str;
fn enabled(&self, level: Loglevel) -> bool;
fn log(&self, record: &LogRecord);
}
thread_local! {
pub static LOGGERS: RefCell<Option<Vec<Box<dyn Log>>>> = RefCell::new(None);
}
lazy_static! {
#[doc(hidden)]
pub static ref PID: u32 = std::process::id();
}
ref_thread_local! {
#[doc(hidden)]
pub static managed TID: u64 = unsafe { std::mem::transmute(std::thread::current().id()) };
}
#[derive(
Copy,
Clone,
Debug,
Eq,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
EnumString,
Display,
EnumIter,
NamedType,
)]
pub enum Loglevel {
Fatal = 1,
Error,
Warn,
Note,
Info,
Debug,
Trace,
}
impl Into<term::color::Color> for Loglevel {
fn into(self) -> term::color::Color {
match self {
Loglevel::Fatal => term::color::BRIGHT_RED,
Loglevel::Error => term::color::RED,
Loglevel::Warn => term::color::YELLOW,
Loglevel::Note => term::color::WHITE,
Loglevel::Info => term::color::BLUE,
Loglevel::Debug => term::color::CYAN,
Loglevel::Trace => term::color::BRIGHT_BLACK,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Eq,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
EnumString,
Display,
EnumIter,
NamedType,
)]
pub enum LoglevelFilter {
#[strum(to_string = "Off", serialize = "off", serialize = "o")]
Off = 0,
#[strum(to_string = "Fatal", serialize = "fatal", serialize = "f")]
Fatal,
#[strum(to_string = "Error", serialize = "error", serialize = "e")]
Error,
#[strum(to_string = "Warn", serialize = "warn", serialize = "w")]
Warn,
#[strum(to_string = "Note", serialize = "note", serialize = "n")]
Note,
#[strum(to_string = "Info", serialize = "info", serialize = "i")]
Info,
#[strum(to_string = "Debug", serialize = "debug", serialize = "d")]
Debug,
#[strum(to_string = "Trace", serialize = "trace", serialize = "t")]
Trace,
}
impl Loglevel {
pub fn try_from(levelfilter: LoglevelFilter) -> Result<Loglevel, ()> {
match levelfilter {
LoglevelFilter::Fatal => Ok(Loglevel::Fatal),
LoglevelFilter::Error => Ok(Loglevel::Error),
LoglevelFilter::Warn => Ok(Loglevel::Warn),
LoglevelFilter::Note => Ok(Loglevel::Note),
LoglevelFilter::Info => Ok(Loglevel::Info),
LoglevelFilter::Debug => Ok(Loglevel::Debug),
LoglevelFilter::Trace => Ok(Loglevel::Trace),
LoglevelFilter::Off => Err(()),
}
}
}
impl From<Loglevel> for LoglevelFilter {
fn from(level: Loglevel) -> LoglevelFilter {
match level {
Loglevel::Fatal => LoglevelFilter::Fatal,
Loglevel::Error => LoglevelFilter::Error,
Loglevel::Warn => LoglevelFilter::Warn,
Loglevel::Note => LoglevelFilter::Note,
Loglevel::Info => LoglevelFilter::Info,
Loglevel::Debug => LoglevelFilter::Debug,
Loglevel::Trace => LoglevelFilter::Trace,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Metadata {
level: Loglevel,
module_path: Option<String>,
file: Option<String>,
line: Option<u32>,
timestamp: std::time::SystemTime,
process: u32,
thread: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LogRecord {
payload: String,
metadata: Metadata,
logger: String,
}
impl LogRecord {
pub fn payload(&self) -> &str {
&self.payload
}
pub fn level(&self) -> Loglevel {
self.metadata.level
}
pub fn module_path(&self) -> Option<&str> {
self.metadata.module_path.as_ref().map(String::as_str)
}
pub fn file(&self) -> Option<&str> {
self.metadata.file.as_ref().map(String::as_str)
}
pub fn line(&self) -> Option<u32> {
self.metadata.line
}
pub fn timestamp(&self) -> std::time::SystemTime {
self.metadata.timestamp
}
pub fn process(&self) -> u32 {
self.metadata.process
}
pub fn thread(&self) -> u64 {
self.metadata.thread
}
pub fn logger(&self) -> &str {
self.logger.as_str()
}
}
impl LogRecord {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::too_many_arguments))]
pub fn new(
logger: impl Into<String>,
payload: impl Into<String>,
level: Loglevel,
module_path: impl Into<String>,
file: impl Into<String>,
line: u32,
process: u32,
thread: u64,
) -> LogRecord {
LogRecord {
payload: payload.into(),
metadata: Metadata {
level,
module_path: Some(module_path.into()),
file: Some(file.into()),
line: Some(line),
timestamp: std::time::SystemTime::now(),
process,
thread,
},
logger: logger.into(),
}
}
}
impl fmt::Display for LogRecord {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
humantime::format_rfc3339_seconds(self.metadata.timestamp)
)?;
write!(
f,
"{:<6}",
format!(
"+{}ms",
self.metadata.timestamp.elapsed().unwrap().as_millis()
)
)?;
write!(f, "{:>5} ", format!("{}", self.metadata.level))?;
write!(
f,
"{:<32} ",
format!(
"{:>5}:{:<2} {} ",
self.metadata.process, self.metadata.thread, self.logger,
)
)?;
write!(f, "{}", self.payload)
}
}
fn update(loggers: Option<Vec<Box<dyn Log>>>) -> error::Result<()> {
LOGGERS.with(|x| {
let mut x = x.try_borrow_mut().context(ErrorKind::LogError(
"Unable to update thread-local loggers".to_string(),
))?;
*x = loggers;
Ok(())
})
}
pub fn init(loggers: Vec<Box<dyn Log>>) -> error::Result<()> {
update(Some(loggers))
}
pub fn deinit() -> error::Result<()> {
update(None)
}
#[macro_export]
macro_rules! log {
(? target: $target:expr, location: ($file:expr, $line:expr), $lvl:expr, $($arg:tt)+) => ({
use $crate::common::log::_ref_thread_local::RefThreadLocal;
$crate::common::log::LOGGERS.try_with(|loggers| {
if let Some(ref loggers) = *loggers.borrow() {
loggers.iter().for_each(|logger| {
if logger.enabled($lvl) {
logger.log(&$crate::common::log::LogRecord::new(
logger.name(),
format!($($arg)+),
$lvl,
$target,
$file,
$line,
*$crate::common::log::PID,
*$crate::common::log::TID.borrow()
));
}
});
true
} else {
false
}
}).unwrap_or(false)
});
(target: $target:expr, location: ($file:expr, $line:expr), $lvl:expr, $($arg:tt)+) => (
{
$crate::log!(?
target: module_path!(),
location: ($file, $line),
$lvl, $($arg)+
);
}
);
(target: $target:expr, $lvl:expr, $($arg:tt)+) => (
$crate::log!(
target: module_path!(),
location: (file!(), line!()),
$lvl, $($arg)+
)
);
($lvl:expr, $($arg:tt)+) => (
$crate::log!(
target: module_path!(),
$lvl, $($arg)+
)
)
}
#[macro_export]
macro_rules! fatal {
(target: $target:expr, $($arg:tt)+) => (
$crate::log!(target: $target, $crate::common::log::Loglevel::Fatal, $($arg)+);
);
($($arg:tt)+) => (
$crate::log!($crate::common::log::Loglevel::Fatal, $($arg)+);
)
}
#[macro_export]
macro_rules! error {
(target: $target:expr, $($arg:tt)+) => (
$crate::log!(target: $target, $crate::common::log::Loglevel::Error, $($arg)+);
);
($($arg:tt)+) => (
$crate::log!($crate::common::log::Loglevel::Error, $($arg)+);
)
}
#[macro_export]
macro_rules! warn {
(target: $target:expr, $($arg:tt)+) => (
$crate::log!(target: $target, $crate::common::log::Loglevel::Warn, $($arg)+);
);
($($arg:tt)+) => (
$crate::log!($crate::common::log::Loglevel::Warn, $($arg)+);
)
}
#[macro_export]
macro_rules! note {
(target: $target:expr, $($arg:tt)+) => (
$crate::log!(target: $target, $crate::common::log::Loglevel::Note, $($arg)+);
);
($($arg:tt)+) => (
$crate::log!($crate::common::log::Loglevel::Note, $($arg)+);
)
}
#[macro_export]
macro_rules! info {
(target: $target:expr, $($arg:tt)+) => (
$crate::log!(target: $target, $crate::common::log::Loglevel::Info, $($arg)+);
);
($($arg:tt)+) => (
$crate::log!($crate::common::log::Loglevel::Info, $($arg)+);
)
}
#[macro_export]
macro_rules! debug {
(target: $target:expr, $($arg:tt)+) => (
$crate::log!(target: $target, $crate::common::log::Loglevel::Debug, $($arg)+);
);
($($arg:tt)+) => (
$crate::log!($crate::common::log::Loglevel::Debug, $($arg)+);
)
}
#[macro_export]
macro_rules! trace {
(target: $target:expr, $($arg:tt)+) => (
$crate::log!(target: $target, $crate::common::log::Loglevel::Trace, $($arg)+);
);
($($arg:tt)+) => (
$crate::log!($crate::common::log::Loglevel::Trace, $($arg)+);
)
}
#[cfg(test)]
mod tests {
use super::{LogRecord, Loglevel, LoglevelFilter};
#[test]
fn level_order() {
assert!(Loglevel::Debug < Loglevel::Trace);
assert!(Loglevel::Info < Loglevel::Debug);
assert!(Loglevel::Note < Loglevel::Info);
assert!(Loglevel::Warn < Loglevel::Note);
assert!(Loglevel::Error < Loglevel::Warn);
assert!(Loglevel::Fatal < Loglevel::Error);
assert!(LoglevelFilter::Off < LoglevelFilter::from(Loglevel::Fatal));
}
#[test]
fn level_colors() {
let color: term::color::Color = Loglevel::Error.into();
assert_eq!(color, term::color::RED);
let color: term::color::Color = Loglevel::Note.into();
assert_eq!(color, term::color::WHITE);
}
#[test]
fn filter_to_level() {
assert!(Loglevel::try_from(LoglevelFilter::Fatal).is_ok());
assert!(Loglevel::try_from(LoglevelFilter::Error).is_ok());
assert!(Loglevel::try_from(LoglevelFilter::Warn).is_ok());
assert!(Loglevel::try_from(LoglevelFilter::Note).is_ok());
assert!(Loglevel::try_from(LoglevelFilter::Info).is_ok());
assert!(Loglevel::try_from(LoglevelFilter::Debug).is_ok());
assert!(Loglevel::try_from(LoglevelFilter::Trace).is_ok());
assert!(Loglevel::try_from(LoglevelFilter::Off).is_err());
}
#[test]
fn log_record_debug_getters() {
let record = LogRecord::new("", "", Loglevel::Debug, "path", "file", 1234u32, 1u32, 1u64);
assert_eq!(record.module_path(), Some("path"));
assert_eq!(record.file(), Some("file"));
assert_eq!(record.line(), Some(1234u32));
}
#[test]
fn display_record() {
let record = LogRecord::new(
"logger",
"message",
Loglevel::Trace,
"path",
"file",
1234u32,
1u32,
1u64,
);
assert_eq!(
&format!("{}", record).as_str()[24..],
" Trace 1:1 logger message"
);
}
}