use crate::calconfig::{LogFormat, LoggingConfig, SinkConfig, SinkType};
use slog::{Drain, KV, Logger, o};
use slog_async::Async;
use slog_term::{FullFormat, PlainDecorator, TermDecorator};
use std::fs::OpenOptions;
type BoxDrain = Box<dyn Drain<Ok = (), Err = slog::Never> + Send + Sync>;
struct SubsystemFilteredDrain {
inner: BoxDrain,
allowed: Vec<String>,
}
impl Drain for SubsystemFilteredDrain {
type Ok = ();
type Err = slog::Never;
fn log(
&self,
record: &slog::Record,
values: &slog::OwnedKVList,
) -> Result<Self::Ok, Self::Err> {
let mut found = String::new();
{
let mut ser = SubsystemExtractor(&mut found);
let _ = values.serialize(record, &mut ser);
let _ = record.kv().serialize(record, &mut ser);
}
if self.allowed.iter().any(|a| a == &found) {
self.inner.log(record, values)
} else {
Ok(())
}
}
}
struct SubsystemExtractor<'a>(&'a mut String);
impl<'a> slog::Serializer for SubsystemExtractor<'a> {
fn emit_str(&mut self, key: slog::Key, val: &str) -> slog::Result {
if key == "subsystem" {
self.0.clear();
self.0.push_str(val);
}
Ok(())
}
fn emit_arguments(&mut self, key: slog::Key, val: &std::fmt::Arguments) -> slog::Result {
if key == "subsystem" {
self.0.clear();
self.0.push_str(&val.to_string());
}
Ok(())
}
}
struct LogfmtDrain<W: std::io::Write + Send + Sync + 'static> {
writer: std::sync::Mutex<W>,
}
impl<W: std::io::Write + Send + Sync + 'static> Drain for LogfmtDrain<W> {
type Ok = ();
type Err = slog::Never;
fn log(
&self,
record: &slog::Record,
values: &slog::OwnedKVList,
) -> Result<Self::Ok, Self::Err> {
let mut buf = format!(
"level={} msg={:?}",
record.level().as_short_str().to_lowercase(),
record.msg().to_string(),
);
let mut ser = LogfmtSerializer(&mut buf);
let _ = values.serialize(record, &mut ser);
let _ = record.kv().serialize(record, &mut ser);
buf.push('\n');
let mut w = self.writer.lock().unwrap();
w.write_all(buf.as_bytes()).ok();
Ok(())
}
}
struct LogfmtSerializer<'a>(&'a mut String);
impl<'a> slog::Serializer for LogfmtSerializer<'a> {
fn emit_arguments(&mut self, key: slog::Key, val: &std::fmt::Arguments) -> slog::Result {
let v = val.to_string();
if v.contains(' ') || v.contains('"') {
self.0.push_str(&format!(" {}={:?}", key, v));
} else {
self.0.push_str(&format!(" {}={}", key, v));
}
Ok(())
}
}
struct FanoutDrain {
drains: Vec<BoxDrain>,
}
impl Drain for FanoutDrain {
type Ok = ();
type Err = slog::Never;
fn log(
&self,
record: &slog::Record,
values: &slog::OwnedKVList,
) -> Result<Self::Ok, Self::Err> {
for d in self.drains.iter() {
let _ = d.log(record, values);
}
Ok(())
}
}
unsafe impl Sync for FanoutDrain {}
impl std::panic::UnwindSafe for FanoutDrain {}
impl std::panic::RefUnwindSafe for FanoutDrain {}
fn with_filter(drain: BoxDrain, allowed: Vec<String>) -> BoxDrain {
if allowed.is_empty() {
drain
} else {
Box::new(SubsystemFilteredDrain {
inner: drain,
allowed,
})
}
}
fn build_sink(sink: &SinkConfig) -> Option<BoxDrain> {
let level: slog::Level = sink.level.into();
macro_rules! make_async_drain {
($drain:expr) => {{
let async_drain = Async::new($drain.fuse()).build().fuse();
let filtered = slog::LevelFilter::new(async_drain, level).fuse();
with_filter(Box::new(filtered), sink.subsystems.clone())
}};
}
match &sink.sink_type {
SinkType::Stdout | SinkType::Stderr => {
let stderr = matches!(sink.sink_type, SinkType::Stderr);
Some(match sink.format {
LogFormat::Pretty => {
let dec = if stderr {
TermDecorator::new().stderr().build()
} else {
TermDecorator::new().stdout().build()
};
make_async_drain!(FullFormat::new(dec).build())
}
LogFormat::Basic => {
let w: Box<dyn std::io::Write + Send + Sync> = if stderr {
Box::new(std::io::stderr())
} else {
Box::new(std::io::stdout())
};
make_async_drain!(FullFormat::new(PlainDecorator::new(w)).build())
}
LogFormat::Logfmt => {
let w: Box<dyn std::io::Write + Send + Sync> = if stderr {
Box::new(std::io::stderr())
} else {
Box::new(std::io::stdout())
};
make_async_drain!(LogfmtDrain {
writer: std::sync::Mutex::new(w)
})
}
LogFormat::Json => {
let w: Box<dyn std::io::Write + Send + Sync> = if stderr {
Box::new(std::io::stderr())
} else {
Box::new(std::io::stdout())
};
make_async_drain!(slog_json::Json::default(w))
}
})
}
SinkType::File { path } => {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| eprintln!("rcal logging: failed to open log file '{}': {}", path, e))
.ok()?;
Some(match sink.format {
LogFormat::Pretty | LogFormat::Basic => {
make_async_drain!(FullFormat::new(PlainDecorator::new(file)).build())
}
LogFormat::Logfmt => {
make_async_drain!(LogfmtDrain {
writer: std::sync::Mutex::new(file)
})
}
LogFormat::Json => make_async_drain!(slog_json::Json::default(file)),
})
}
}
}
pub fn build_logger(config: &LoggingConfig) -> Logger {
let default_level: slog::Level = config.default_level.into();
let mut drains: Vec<BoxDrain> = config.sink.iter().filter_map(build_sink).collect();
if drains.is_empty() {
let dec = TermDecorator::new().stdout().build();
let async_drain = Async::new(FullFormat::new(dec).build().fuse())
.build()
.fuse();
let filtered = slog::LevelFilter::new(async_drain, default_level).fuse();
drains.push(Box::new(filtered));
}
let fanned = FanoutDrain { drains };
let filtered = slog::LevelFilter::new(fanned, default_level).fuse();
Logger::root(filtered, o!())
}
pub fn build_test_logger() -> Logger {
let use_color = std::env::var("NO_COLOR").is_err();
let async_drain = if use_color {
let d = FullFormat::new(TermDecorator::new().stdout().build())
.build()
.fuse();
Async::new(d).build().fuse()
} else {
let d = FullFormat::new(PlainDecorator::new(std::io::stdout()))
.build()
.fuse();
Async::new(d).build().fuse()
};
Logger::root(async_drain, o!("test_context" => "unit_tests"))
}