#![doc(test(attr(deny(warnings))))]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![allow(
unknown_lints,
renamed_and_removed_lints,
clippy::unknown_clippy_lints,
clippy::needless_doctest_main
)]
use std::cmp;
use std::collections::HashMap;
use std::fmt::Arguments;
use std::io::{self, Write};
use std::iter;
use std::net::TcpStream;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use chrono::format::{DelayedFormat, StrftimeItems};
use chrono::{Local, Utc};
use fern::Dispatch;
use itertools::Itertools;
use log::{debug, trace, LevelFilter, Log, STATIC_MAX_LEVEL};
use serde::de::{Deserializer, Error as DeError};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use spirit::extension::{Extensible, Extension};
use spirit::fragment::driver::Trivial as TrivialDriver;
use spirit::fragment::{Fragment, Installer};
use spirit::AnyError;
#[cfg(feature = "cfg-help")]
use structdoc::StructDoc;
use structopt::StructOpt;
#[cfg(feature = "background")]
pub mod background;
#[cfg(feature = "background")]
pub use background::{Background, FlushGuard, OverflowMode};
const UNKNOWN_THREAD: &str = "<unknown>";
#[cfg_attr(not(doc), allow(missing_docs))]
#[cfg_attr(
doc,
doc = r#"
A fragment for command line options.
By flattening this into the top-level `StructOpt` structure, you get the `-l` and `-L` command
line options. The `-l` (`--log`) sets the global logging level for `stderr`. The `-L` accepts
pairs (eg. `-L spirit=TRACE`) specifying levels for specific logging targets.
If used, the logging will be sent to `stderr`.
"#
)]
#[derive(Clone, Debug, Default, StructOpt)]
pub struct Opts {
#[structopt(short = "l", long = "log", number_of_values(1))]
log: Option<LevelFilter>,
#[structopt(
short = "L",
long = "log-module",
parse(try_from_str = spirit::utils::key_val),
number_of_values(1),
)]
log_modules: Vec<(String, LevelFilter)>,
}
impl Opts {
fn logger_cfg(&self) -> Option<Logger> {
self.log.map(|level| Logger {
level: LevelFilterSerde(level),
destination: LogDestination::StdErr,
per_module: self
.log_modules
.iter()
.map(|(module, lf)| (module.clone(), LevelFilterSerde(*lf)))
.collect(),
clock: Clock::Local,
time_format: cmdline_time_format(),
format: Format::Short,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[cfg_attr(feature = "cfg-help", derive(StructDoc))]
#[serde(tag = "type", rename_all = "kebab-case")] enum LogDestination {
File {
filename: PathBuf,
},
#[cfg(feature = "syslog")]
Syslog {
#[serde(skip_serializing_if = "Option::is_none")]
host: Option<String>,
},
Network {
host: String,
port: u16,
},
#[serde(rename = "stdout")]
StdOut,
#[serde(rename = "stderr")]
StdErr, }
const LEVEL_FILTERS: &[&str] = &["OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
#[derive(Copy, Clone, Debug)]
struct LevelFilterSerde(LevelFilter);
impl Default for LevelFilterSerde {
fn default() -> LevelFilterSerde {
LevelFilterSerde(LevelFilter::Error)
}
}
impl<'de> Deserialize<'de> for LevelFilterSerde {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<LevelFilterSerde, D::Error> {
let s = String::deserialize(d)?;
s.parse()
.map(LevelFilterSerde)
.map_err(|_| D::Error::unknown_variant(&s, LEVEL_FILTERS))
}
}
impl Serialize for LevelFilterSerde {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format!("{:?}", self.0).to_uppercase())
}
}
#[cfg(feature = "cfg-help")]
impl structdoc::StructDoc for LevelFilterSerde {
fn document() -> structdoc::Documentation {
use structdoc::{Documentation, Field, Tagging};
let filters = LEVEL_FILTERS
.iter()
.map(|name| (*name, Field::new(Documentation::leaf_empty(), "")));
Documentation::enum_(filters, Tagging::External)
}
}
#[derive(Clone, Debug)]
#[cfg(feature = "syslog")]
pub struct SyslogError(String);
#[cfg(feature = "syslog")]
impl std::fmt::Display for SyslogError {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(fmt)
}
}
#[cfg(feature = "syslog")]
impl std::error::Error for SyslogError {}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "cfg-help", derive(StructDoc))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum Clock {
Local,
Utc,
}
impl Clock {
fn now(self, format: &str) -> DelayedFormat<StrftimeItems> {
match self {
Clock::Local => Local::now().format(format),
Clock::Utc => Utc::now().format(format),
}
}
}
impl Default for Clock {
fn default() -> Self {
Clock::Local
}
}
fn default_time_format() -> String {
"%+".to_owned()
}
fn cmdline_time_format() -> String {
"%F %T%.3f".to_owned()
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "cfg-help", derive(StructDoc))]
#[serde(rename_all = "kebab-case")]
enum Format {
MessageOnly,
Short,
Extended,
Full,
Machine,
Json,
Logstash,
}
impl Default for Format {
fn default() -> Self {
Format::Short
}
}
#[cfg(not(feature = "background"))]
fn get_thread_name(thread: &thread::Thread) -> &str {
thread.name().unwrap_or(UNKNOWN_THREAD)
}
#[cfg(feature = "background")]
use background::get_thread_name;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "cfg-help", derive(StructDoc))]
#[serde(rename_all = "kebab-case")] struct Logger {
#[serde(flatten)]
destination: LogDestination,
#[serde(default)]
clock: Clock,
#[serde(default = "default_time_format")]
time_format: String,
#[serde(default)]
format: Format,
#[serde(default)]
level: LevelFilterSerde,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
per_module: HashMap<String, LevelFilterSerde>,
}
impl Logger {
fn create(&self) -> Result<Dispatch, AnyError> {
trace!("Creating logger for {:?}", self);
let mut logger = Dispatch::new().level(self.level.0);
logger = self
.per_module
.iter()
.fold(logger, |logger, (module, level)| {
logger.level_for(module.clone(), level.0)
});
let clock = self.clock;
let time_format = self.time_format.clone();
let format = self.format;
#[allow(clippy::unknown_clippy_lints, clippy::match_single_binding)]
match self.destination {
#[cfg(feature = "syslog")]
LogDestination::Syslog { .. } => (),
_ => {
logger = logger.format(move |out, message, record| {
match format {
Format::MessageOnly => out.finish(format_args!("{}", message)),
Format::Short => out.finish(format_args!(
"{} {:5} {:30} {}",
clock.now(&time_format),
record.level(),
record.target(),
message,
)),
Format::Extended => {
out.finish(format_args!(
"{} {:5} {:30} {:30} {}",
clock.now(&time_format),
record.level(),
get_thread_name(&thread::current()),
record.target(),
message,
));
}
Format::Full => {
out.finish(format_args!(
"{} {:5} {:10} {:>25}:{:<5} {:30} {}",
clock.now(&time_format),
record.level(),
get_thread_name(&thread::current()),
record.file().unwrap_or("<unknown>"),
record.line().unwrap_or(0),
record.target(),
message,
));
}
Format::Machine => {
out.finish(format_args!(
"{}\t{}\t{}\t{}\t{}\t{}\t{}",
clock.now(&time_format),
record.level(),
get_thread_name(&thread::current()),
record.file().unwrap_or("<unknown>"),
record.line().unwrap_or(0),
record.target(),
message,
));
}
Format::Json => {
#[derive(Serialize)]
struct Msg<'a> {
timestamp: Arguments<'a>,
level: Arguments<'a>,
thread_name: &'a str,
file: Option<&'a str>,
line: Option<u32>,
target: &'a str,
message: &'a Arguments<'a>,
}
let log = |msg: &Msg| {
let msg = serde_json::to_string(msg)
.expect("Failed to serialize JSON log");
out.finish(format_args!("{}", msg));
};
log(&Msg {
timestamp: format_args!("{}", clock.now(&time_format)),
level: format_args!("{}", record.level()),
thread_name: &get_thread_name(&thread::current()),
file: record.file(),
line: record.line(),
target: record.target(),
message,
});
}
Format::Logstash => {
#[derive(Serialize)]
struct Msg<'a> {
#[serde(rename = "@timestamp")]
timestamp: Arguments<'a>,
#[serde(rename = "@version")]
version: u8,
level: Arguments<'a>,
thread_name: &'a str,
logger_name: &'a str,
message: &'a Arguments<'a>,
}
let log = |msg: &Msg| {
let msg = serde_json::to_string(msg)
.expect("Failed to serialize JSON log");
out.finish(format_args!("{}", msg));
};
log(&Msg {
timestamp: format_args!("{}", clock.now(&time_format)),
version: 1,
level: format_args!("{}", record.level()),
thread_name: &get_thread_name(&thread::current()),
logger_name: record.target(),
message,
});
}
}
});
}
}
match self.destination {
LogDestination::File { ref filename } => Ok(logger.chain(fern::log_file(filename)?)),
#[cfg(feature = "syslog")]
LogDestination::Syslog { ref host } => {
let formatter = syslog::Formatter3164 {
facility: syslog::Facility::LOG_USER,
hostname: host.clone(),
process: env!("CARGO_PKG_NAME").to_owned(),
pid: 0,
};
let sys_logger =
syslog::unix(formatter).map_err(|e| SyslogError(format!("{}", e)))?;
let sys_logger: Box<dyn Log> = Box::new(syslog::BasicLogger::new(sys_logger));
Ok(logger.chain(sys_logger))
}
LogDestination::Network { ref host, port } => {
let conn = TcpStream::connect((host as &str, port))?;
Ok(logger.chain(Box::new(conn) as Box<dyn Write + Send>))
}
LogDestination::StdOut => Ok(logger.chain(io::stdout())),
LogDestination::StdErr => Ok(logger.chain(io::stderr())),
}
}
}
impl Default for Logger {
fn default() -> Self {
Self {
destination: LogDestination::StdErr,
level: LevelFilterSerde(LevelFilter::Warn),
per_module: HashMap::new(),
clock: Clock::Local,
time_format: cmdline_time_format(),
format: Format::Short,
}
}
}
fn create<'a, I>(logging: I) -> Result<Dispatch, AnyError>
where
I: IntoIterator<Item = &'a Logger>,
{
debug!("Creating loggers");
logging
.into_iter()
.map(Logger::create)
.fold_ok(Dispatch::new(), Dispatch::chain)
.map_err(AnyError::from)
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "cfg-help", derive(StructDoc))]
#[serde(transparent)]
pub struct Cfg(Vec<Logger>);
struct Configured;
impl Cfg {
pub fn init_extension<E: Extensible>() -> impl Extension<E> {
|mut e: E| {
if e.singleton::<Configured>() {
init();
let logger = Logger {
destination: LogDestination::StdErr,
level: LevelFilterSerde(LevelFilter::Warn),
per_module: HashMap::new(),
clock: Clock::Local,
time_format: cmdline_time_format(),
format: Format::Short,
};
install(create(iter::once(&logger)).unwrap());
}
e
}
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
static INIT_CALLED: AtomicBool = AtomicBool::new(false);
pub fn init() {
log_panics::init();
let _ = log_reroute::init();
INIT_CALLED.store(true, Ordering::Relaxed);
}
pub fn install_parts(level: LevelFilter, logger: Box<dyn Log>) {
assert!(
INIT_CALLED.load(Ordering::Relaxed),
"spirit_log::init not called yet"
);
let actual_level = cmp::min(level, STATIC_MAX_LEVEL);
log::set_max_level(actual_level);
log_reroute::reroute_boxed(logger);
debug!(
"Installed loggers with global level filter {:?} (compiled with {:?}, runtime config {:?})",
actual_level, STATIC_MAX_LEVEL, level,
);
}
pub fn install(logger: Dispatch) {
let (level, logger) = logger.into_log();
install_parts(level, logger);
}
impl Fragment for Cfg {
type Driver = TrivialDriver;
type Seed = ();
type Resource = Dispatch;
type Installer = LogInstaller;
fn make_seed(&self, _name: &str) -> Result<(), AnyError> {
Ok(())
}
fn make_resource(&self, _: &mut (), _name: &str) -> Result<Dispatch, AnyError> {
create(&self.0)
}
}
#[derive(Clone, Debug)]
pub struct CfgAndOpts {
pub cfg: Cfg,
pub opts: Opts,
}
impl Fragment for CfgAndOpts {
type Driver = TrivialDriver;
type Seed = ();
type Resource = Dispatch;
type Installer = LogInstaller;
const RUN_BEFORE_CONFIG: bool = true;
fn make_seed(&self, _name: &str) -> Result<(), AnyError> {
Ok(())
}
fn make_resource(&self, _: &mut (), _name: &str) -> Result<Dispatch, AnyError> {
let mut cmd = self.opts.logger_cfg();
if self.cfg.0.is_empty() && cmd.is_none() {
cmd = Some(Logger::default());
}
create(
self.cfg
.0
.iter()
.filter(|l| l.destination != LogDestination::StdErr || cmd.is_none())
.chain(cmd.as_ref()),
)
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct LogInstaller;
impl<O, C> Installer<Dispatch, O, C> for LogInstaller {
type UninstallHandle = ();
fn install(&mut self, logger: Dispatch, _: &str) {
install(logger);
}
fn init<B: Extensible<Ok = B>>(&mut self, builder: B, _name: &str) -> Result<B, AnyError> {
#[cfg(feature = "background")]
let builder = builder.with_singleton(FlushGuard);
builder.with(Cfg::init_extension())
}
}
impl<O, C> Installer<(LevelFilter, Box<dyn Log>), O, C> for LogInstaller {
type UninstallHandle = ();
fn install(&mut self, (level, logger): (LevelFilter, Box<dyn Log>), _: &str) {
install_parts(level, logger);
}
fn init<B: Extensible<Ok = B>>(&mut self, builder: B, _name: &str) -> Result<B, AnyError> {
#[cfg(feature = "background")]
let builder = builder.with_singleton(FlushGuard);
builder.with(Cfg::init_extension())
}
}