use crate::config::ConfigPath;
use clap::{Arg, ArgAction, ArgMatches, Command};
use log::{error, LevelFilter, Log};
use serde::Deserialize;
use std::convert::TryFrom;
use std::path::Path;
use std::str::FromStr;
use std::{fmt, io};
#[derive(Deserialize)]
pub struct LogConfig {
#[serde(default)]
pub log_target: LogTarget,
#[serde(default)]
pub log_file: ConfigPath,
#[cfg(unix)]
#[serde(default)]
pub log_facility: LogFacility,
#[serde(default)]
pub log_level: LogFilter,
}
impl LogConfig {
pub fn config_args(app: Command) -> Command {
app.next_help_heading("Options related to logging")
.arg(
Arg::new("quiet")
.short('q')
.long("quiet")
.action(ArgAction::Count)
.conflicts_with("verbose")
.help(" Log less information, twice for no information"),
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.action(ArgAction::Count)
.help(" Log more information, twice or thrice for even more"),
)
.arg(
Arg::new("logfile")
.long("logfile")
.value_name("PATH")
.help(" Log to this file"),
)
.arg(
Arg::new("syslog")
.long("syslog")
.action(ArgAction::SetTrue)
.help(" Log to syslog"),
)
.arg(
Arg::new("syslog-facility")
.long("syslog-facility")
.default_value("daemon")
.value_name("FACILITY")
.help(" Facility to use for syslog logging"),
)
}
pub fn update_with_arg_matches(
&mut self,
matches: &ArgMatches,
cur_dir: &Path,
) -> Result<(), Terminate> {
for _ in 0..matches.get_count("verbose") {
self.log_level.increase()
}
for _ in 0..matches.get_count("quiet") {
self.log_level.decrease()
}
self.apply_log_matches(matches, cur_dir)?;
Ok(())
}
#[cfg(unix)]
fn apply_log_matches(
&mut self,
matches: &ArgMatches,
cur_dir: &Path,
) -> Result<(), Terminate> {
if matches.get_flag("syslog") {
self.log_target = LogTarget::Syslog;
if let Some(value) =
Self::from_str_value_of(matches, "syslog-facility")?
{
self.log_facility = value
}
} else if let Some(file) = matches.get_one::<String>("logfile") {
if file == "-" {
self.log_target = LogTarget::Stderr
} else {
self.log_target = LogTarget::File;
self.log_file = cur_dir.join(file).into();
}
}
Ok(())
}
#[cfg(not(unix))]
#[allow(clippy::unnecessary_wraps)]
fn apply_log_matches(
&mut self,
matches: &ArgMatches,
cur_dir: &Path,
) -> Result<(), Terminate> {
if let Some(file) = matches.value_of("logfile") {
if file == "-" {
self.log_target = LogTarget::Stderr
} else {
self.log_target = LogTarget::File;
self.log_file = cur_dir.join(file).into();
}
}
Ok(())
}
#[allow(dead_code)] fn from_str_value_of<T>(
matches: &ArgMatches,
key: &str,
) -> Result<Option<T>, Terminate>
where
T: FromStr,
T::Err: fmt::Display,
{
match matches.get_one::<String>(key) {
Some(value) => match T::from_str(value) {
Ok(value) => Ok(Some(value)),
Err(err) => {
error!("Invalid value for {}: {}.", key, err);
Err(Terminate::error())
}
},
None => Ok(None),
}
}
pub fn init_logging() -> Result<(), Terminate> {
log::set_max_level(log::LevelFilter::Warn);
if let Err(err) = log_reroute::init() {
eprintln!("Failed to initialize logger: {}.", err);
Err(ExitError)?;
};
let dispatch = fern::Dispatch::new()
.level(log::LevelFilter::Error)
.chain(io::stderr())
.into_log()
.1;
log_reroute::reroute_boxed(dispatch);
Ok(())
}
#[allow(unused_variables)] pub fn switch_logging(&self, daemon: bool) -> Result<(), Terminate> {
let logger = match self.log_target {
#[cfg(unix)]
LogTarget::Default => {
if daemon {
self.syslog_logger()?
} else {
self.stderr_logger(false)
}
}
#[cfg(not(unix))]
LogTarget::Default => self.stderr_logger(daemon),
#[cfg(unix)]
LogTarget::Syslog => self.syslog_logger()?,
LogTarget::Stderr => self.stderr_logger(daemon),
LogTarget::File => self.file_logger()?,
};
log_reroute::reroute_boxed(logger);
log::set_max_level(self.log_level.0);
Ok(())
}
#[cfg(unix)]
fn syslog_logger(&self) -> Result<Box<dyn Log>, Terminate> {
let mut formatter = syslog::Formatter3164 {
facility: self.log_facility.0,
..Default::default()
};
if formatter.hostname.is_none() {
formatter.hostname = Some("rotonda".into());
}
let formatter = formatter;
let logger = syslog::unix(formatter.clone())
.or_else(|_| {
error!("Syslog not available via UNIX socket, falling back to tcp://127.0.0.1:601");
syslog::tcp(formatter.clone(), ("127.0.0.1", 601))
})
.or_else(|_| {
error!("Syslog not available via TCP socket, falling back to udp://127.0.0.1:514");
error!("Warning: Logs may be lost if no syslog daemon is listening at udp://127.0.0.1:514 !");
syslog::udp(formatter, ("127.0.0.1", 0), ("127.0.0.1", 514))
});
match logger {
Ok(logger) => Ok(Box::new(syslog::BasicLogger::new(logger))),
Err(err) => {
error!("Cannot connect to syslog: {}", err);
Err(Terminate::error())
}
}
}
fn stderr_logger(&self, daemon: bool) -> Box<dyn Log> {
self.fern_logger(daemon).chain(io::stderr()).into_log().1
}
fn file_logger(&self) -> Result<Box<dyn Log>, Terminate> {
let file = match fern::log_file(&self.log_file) {
Ok(file) => file,
Err(err) => {
error!(
"Failed to open log file '{}': {}",
self.log_file.display(),
err
);
return Err(Terminate::error());
}
};
Ok(self.fern_logger(true).chain(file).into_log().1)
}
fn fern_logger(&self, timestamp_and_level: bool) -> fern::Dispatch {
let mqtt_log_level = match std::env::var("ROTONDA_MQTT_LOG") {
Ok(_) => self.log_level.0.min(LevelFilter::Trace),
Err(_) => self.log_level.0.min(LevelFilter::Warn),
};
let rotonda_store_log_level = match std::env::var("ROTONDA_STORE_LOG")
{
Ok(_) => self.log_level.0.min(LevelFilter::Trace),
Err(_) => self.log_level.0.min(LevelFilter::Warn),
};
let roto_log_level = match std::env::var("ROTONDA_ROTO_LOG") {
Ok(_) => self.log_level.0.min(LevelFilter::Trace),
Err(_) => self.log_level.0.min(LevelFilter::Warn),
};
let debug_enabled = self.log_level.0 >= LevelFilter::Debug;
let mut res = fern::Dispatch::new();
if timestamp_and_level {
res = res.format(move |out, message, record| {
let module_path = record.module_path().unwrap_or("");
let show_module =
debug_enabled || !module_path.starts_with("rotonda");
out.finish(format_args!(
"[{}] {:5} {}{}{}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
if show_module { module_path } else { "" },
if show_module { ": " } else { "" },
message
))
});
} else {
res = res.format(move |out, message, record| {
let module_path = record.module_path().unwrap_or("");
let show_module =
debug_enabled || !module_path.starts_with("rotonda");
out.finish(format_args!(
"{}{}{}",
if show_module { module_path } else { "" },
if show_module { ": " } else { "" },
message
))
});
}
res = res
.level(self.log_level.0)
.level_for("rustls", LevelFilter::Error)
.level_for("rumqttd", LevelFilter::Warn)
.level_for("tracing::span", LevelFilter::Off)
.level_for("cranelift_codegen", LevelFilter::Warn)
.level_for("cranelift_jit", LevelFilter::Warn);
res = res
.level_for("rotonda_store", rotonda_store_log_level)
.level_for("rumqttc", mqtt_log_level)
.level_for("roto", roto_log_level);
if debug_enabled {
res = res
.level_for("tokio_reactor", LevelFilter::Info)
.level_for("hyper", LevelFilter::Info)
.level_for("reqwest", LevelFilter::Info)
.level_for("h2", LevelFilter::Info)
.level_for("mio", LevelFilter::Info);
res = res
.level_for("rumqttd", self.log_level.0)
.level_for("tracing::span", self.log_level.0);
}
res
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub enum LogTarget {
#[default]
#[serde(rename = "default")]
Default,
#[cfg(unix)]
#[serde(rename = "syslog")]
Syslog,
#[serde(rename = "stderr")]
Stderr,
#[serde(rename = "file")]
File,
}
#[cfg(unix)]
#[derive(Deserialize)]
#[serde(try_from = "String")]
pub struct LogFacility(syslog::Facility);
#[cfg(unix)]
impl Default for LogFacility {
fn default() -> Self {
LogFacility(syslog::Facility::LOG_DAEMON)
}
}
#[cfg(unix)]
impl TryFrom<String> for LogFacility {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
syslog::Facility::from_str(&value)
.map(LogFacility)
.map_err(|_| format!("unknown syslog facility {}", &value))
}
}
#[cfg(unix)]
impl FromStr for LogFacility {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
syslog::Facility::from_str(s)
.map(LogFacility)
.map_err(|_| "unknown facility")
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(try_from = "String")]
pub struct LogFilter(log::LevelFilter);
impl LogFilter {
pub fn increase(&mut self) {
use log::LevelFilter::*;
self.0 = match self.0 {
Off => Error,
Error => Warn,
Warn => Info,
Info => Debug,
Debug => Trace,
Trace => Trace,
}
}
pub fn decrease(&mut self) {
use log::LevelFilter::*;
self.0 = match self.0 {
Off => Off,
Error => Off,
Warn => Error,
Info => Warn,
Debug => Info,
Trace => Debug,
}
}
}
impl Default for LogFilter {
fn default() -> Self {
LogFilter(log::LevelFilter::Info)
}
}
impl TryFrom<String> for LogFilter {
type Error = log::ParseLevelError;
fn try_from(value: String) -> Result<Self, Self::Error> {
log::LevelFilter::from_str(&value).map(LogFilter)
}
}
#[derive(Clone, Copy, Debug)]
pub enum TerminateReason {
Failed(i32),
Normal,
}
#[derive(Clone, Copy, Debug)]
pub struct Terminate(TerminateReason);
impl Terminate {
pub fn normal() -> Self {
Self(TerminateReason::Normal)
}
pub fn error() -> Self {
Self(TerminateReason::Failed(1))
}
pub fn other(exit_code: i32) -> Self {
assert_ne!(exit_code, 0);
Self(TerminateReason::Failed(exit_code))
}
pub fn reason(&self) -> TerminateReason {
self.0
}
pub fn exit_code(&self) -> i32 {
match self.reason() {
TerminateReason::Failed(n) => n,
TerminateReason::Normal => 0,
}
}
}
#[derive(Debug)]
pub struct ExitError;
impl From<Terminate> for ExitError {
fn from(terminate: Terminate) -> ExitError {
match terminate.reason() {
TerminateReason::Failed(_) => ExitError,
TerminateReason::Normal => unreachable!(),
}
}
}
impl From<ExitError> for Terminate {
fn from(_: ExitError) -> Self {
Terminate::error()
}
}