#![allow(where_clauses_object_safety, clippy::manual_non_exhaustive)]
#![warn(missing_docs)]
use std::{
cmp, collections::HashMap, fmt, hash::BuildHasherDefault, io, io::prelude::*, sync::Arc,
};
use arc_swap::ArcSwap;
use fnv::FnvHasher;
use log::{Level, LevelFilter, Metadata, Record};
pub mod append;
pub mod config;
pub mod encode;
pub mod filter;
#[cfg(feature = "console_writer")]
mod priv_io;
pub use config::{init_config, Config};
#[cfg(feature = "config_parsing")]
pub use config::{init_file, init_raw_config};
use self::{append::Append, filter::Filter};
type FnvHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FnvHasher>>;
#[derive(Debug)]
struct ConfiguredLogger {
level: LevelFilter,
appenders: Vec<usize>,
children: FnvHashMap<String, ConfiguredLogger>,
}
impl ConfiguredLogger {
fn add(&mut self, path: &str, mut appenders: Vec<usize>, additive: bool, level: LevelFilter) {
let (part, rest) = match path.find("::") {
Some(idx) => (&path[..idx], &path[idx + 2..]),
None => (path, ""),
};
if let Some(child) = self.children.get_mut(part) {
child.add(rest, appenders, additive, level);
return;
}
let child = if rest.is_empty() {
if additive {
appenders.extend(self.appenders.iter().cloned());
}
ConfiguredLogger {
level,
appenders,
children: FnvHashMap::default(),
}
} else {
let mut child = ConfiguredLogger {
level: self.level,
appenders: self.appenders.clone(),
children: FnvHashMap::default(),
};
child.add(rest, appenders, additive, level);
child
};
self.children.insert(part.to_owned(), child);
}
fn max_log_level(&self) -> LevelFilter {
let mut max = self.level;
for child in self.children.values() {
max = cmp::max(max, child.max_log_level());
}
max
}
fn find(&self, path: &str) -> &ConfiguredLogger {
let mut node = self;
for part in path.split("::") {
match node.children.get(part) {
Some(child) => node = child,
None => break,
}
}
node
}
fn enabled(&self, level: Level) -> bool {
self.level >= level
}
fn log(&self, record: &log::Record, appenders: &[Appender]) -> Result<(), Vec<anyhow::Error>> {
let mut errors = vec![];
if self.enabled(record.level()) {
for &idx in &self.appenders {
if let Err(err) = appenders[idx].append(record) {
errors.push(err);
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
#[derive(Debug)]
struct Appender {
appender: Box<dyn Append>,
filters: Vec<Box<dyn Filter>>,
}
impl Appender {
fn append(&self, record: &Record) -> anyhow::Result<()> {
for filter in &self.filters {
match filter.filter(record) {
filter::Response::Accept => break,
filter::Response::Neutral => {}
filter::Response::Reject => return Ok(()),
}
}
self.appender.append(record)
}
fn flush(&self) {
self.appender.flush();
}
}
struct SharedLogger {
root: ConfiguredLogger,
appenders: Vec<Appender>,
err_handler: Box<dyn Send + Sync + Fn(&anyhow::Error)>,
}
impl fmt::Debug for SharedLogger {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SharedLogger")
.field("root", &self.root)
.field("appenders", &self.appenders)
.finish()
}
}
impl SharedLogger {
fn new(config: config::Config) -> SharedLogger {
Self::new_with_err_handler(
config,
Box::new(|e: &anyhow::Error| {
let _ = writeln!(io::stderr(), "log4rs: {}", e);
}),
)
}
fn new_with_err_handler(
config: config::Config,
err_handler: Box<dyn Send + Sync + Fn(&anyhow::Error)>,
) -> SharedLogger {
let (appenders, root, mut loggers) = config.unpack();
let root = {
let appender_map = appenders
.iter()
.enumerate()
.map(|(i, appender)| (appender.name(), i))
.collect::<HashMap<_, _>>();
let mut root = ConfiguredLogger {
level: root.level(),
appenders: root
.appenders()
.iter()
.map(|appender| appender_map[&**appender])
.collect(),
children: FnvHashMap::default(),
};
loggers.sort_by_key(|l| l.name().len());
for logger in loggers {
let appenders = logger
.appenders()
.iter()
.map(|appender| appender_map[&**appender])
.collect();
root.add(logger.name(), appenders, logger.additive(), logger.level());
}
root
};
let appenders = appenders
.into_iter()
.map(|appender| {
let (_, appender, filters) = appender.unpack();
Appender { appender, filters }
})
.collect();
SharedLogger {
root,
appenders,
err_handler,
}
}
}
#[derive(Debug)]
pub struct Logger(Arc<ArcSwap<SharedLogger>>);
impl Logger {
pub fn new(config: config::Config) -> Logger {
Logger(Arc::new(ArcSwap::new(Arc::new(SharedLogger::new(config)))))
}
pub fn new_with_err_handler(
config: config::Config,
err_handler: Box<dyn Send + Sync + Fn(&anyhow::Error)>,
) -> Logger {
Logger(Arc::new(ArcSwap::new(Arc::new(
SharedLogger::new_with_err_handler(config, err_handler),
))))
}
pub fn max_log_level(&self) -> LevelFilter {
self.0.load().root.max_log_level()
}
}
impl log::Log for Logger {
fn enabled(&self, metadata: &Metadata) -> bool {
self.0
.load()
.root
.find(metadata.target())
.enabled(metadata.level())
}
fn log(&self, record: &log::Record) {
let shared = self.0.load();
if let Err(errs) = shared
.root
.find(record.target())
.log(record, &shared.appenders)
{
for e in errs {
(shared.err_handler)(&e)
}
}
}
fn flush(&self) {
for appender in &self.0.load().appenders {
appender.flush();
}
}
}
pub(crate) fn handle_error(e: &anyhow::Error) {
let _ = writeln!(io::stderr(), "log4rs: {}", e);
}
#[derive(Clone, Debug)]
pub struct Handle {
shared: Arc<ArcSwap<SharedLogger>>,
}
impl Handle {
pub fn set_config(&self, config: Config) {
let shared = SharedLogger::new(config);
log::set_max_level(shared.root.max_log_level());
self.shared.store(Arc::new(shared));
}
}
#[cfg(test)]
mod test {
use log::{Level, LevelFilter, Log};
use super::*;
#[test]
#[cfg(all(feature = "config_parsing", feature = "json_format"))]
fn init_from_raw_config() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("append.log");
let cfg = serde_json::json!({
"refresh_rate": "60 seconds",
"root" : {
"appenders": ["baz"],
"level": "info",
},
"appenders": {
"baz": {
"kind": "file",
"path": path,
"encoder": {
"pattern": "{m}"
}
}
},
});
let config = serde_json::from_str::<config::RawConfig>(&cfg.to_string()).unwrap();
if let Err(e) = init_raw_config(config) {
panic!("{}", e);
}
assert!(path.exists());
log::info!("init_from_raw_config");
let mut contents = String::new();
std::fs::File::open(&path)
.unwrap()
.read_to_string(&mut contents)
.unwrap();
assert_eq!(contents, "init_from_raw_config");
}
#[test]
fn enabled() {
let root = config::Root::builder().build(LevelFilter::Debug);
let mut config = config::Config::builder();
let logger = config::Logger::builder().build("foo::bar", LevelFilter::Trace);
config = config.logger(logger);
let logger = config::Logger::builder().build("foo::bar::baz", LevelFilter::Off);
config = config.logger(logger);
let logger = config::Logger::builder().build("foo::baz::buz", LevelFilter::Error);
config = config.logger(logger);
let config = config.build(root).unwrap();
let logger = super::Logger::new(config);
assert!(logger.enabled(&Metadata::builder().level(Level::Warn).target("bar").build()));
assert!(!logger.enabled(
&Metadata::builder()
.level(Level::Trace)
.target("bar")
.build()
));
assert!(logger.enabled(
&Metadata::builder()
.level(Level::Debug)
.target("foo")
.build()
));
assert!(logger.enabled(
&Metadata::builder()
.level(Level::Trace)
.target("foo::bar")
.build()
));
assert!(!logger.enabled(
&Metadata::builder()
.level(Level::Error)
.target("foo::bar::baz")
.build()
));
assert!(logger.enabled(
&Metadata::builder()
.level(Level::Debug)
.target("foo::bar::bazbuz")
.build()
));
assert!(!logger.enabled(
&Metadata::builder()
.level(Level::Error)
.target("foo::bar::baz::buz")
.build()
));
assert!(!logger.enabled(
&Metadata::builder()
.level(Level::Warn)
.target("foo::baz::buz")
.build()
));
assert!(!logger.enabled(
&Metadata::builder()
.level(Level::Warn)
.target("foo::baz::buz::bar")
.build()
));
assert!(logger.enabled(
&Metadata::builder()
.level(Level::Error)
.target("foo::baz::buz::bar")
.build()
));
}
}