#![deny(warnings, clippy::all, clippy::pedantic, missing_docs)]
#![forbid(unsafe_code)]
use log::Log;
use log_reload::LevelFilter;
use log_reload::ReloadHandle;
use log_reload::ReloadLog;
use logcontrol::KnownLogTarget;
use logcontrol::LogControl1;
use logcontrol::LogControl1Error;
use logcontrol::LogLevel;
pub use logcontrol;
pub use logcontrol::stderr_connected_to_journal;
pub use logcontrol::syslog_identifier;
use systemd_journal_logger::JournalLog;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SupportedLogTarget {
Console,
Journal,
}
impl From<SupportedLogTarget> for KnownLogTarget {
fn from(value: SupportedLogTarget) -> Self {
match value {
SupportedLogTarget::Console => KnownLogTarget::Console,
SupportedLogTarget::Journal => KnownLogTarget::Journal,
}
}
}
fn from_known_log_target(
target: KnownLogTarget,
connected_to_journal: bool,
) -> Result<SupportedLogTarget, LogControl1Error> {
match target {
KnownLogTarget::Auto if connected_to_journal => Ok(SupportedLogTarget::Journal),
KnownLogTarget::Auto | KnownLogTarget::Console => Ok(SupportedLogTarget::Console),
KnownLogTarget::Journal => Ok(SupportedLogTarget::Journal),
other => Err(LogControl1Error::UnsupportedLogTarget(
other.as_str().to_string(),
)),
}
}
pub fn from_log_level(level: LogLevel) -> Result<log::Level, LogControl1Error> {
match level {
LogLevel::Err => Ok(log::Level::Error),
LogLevel::Warning => Ok(log::Level::Warn),
LogLevel::Notice => Ok(log::Level::Info),
LogLevel::Info => Ok(log::Level::Debug),
LogLevel::Debug => Ok(log::Level::Trace),
unsupported => Err(LogControl1Error::UnsupportedLogLevel(unsupported)),
}
}
fn to_log_level(level: log::Level) -> LogLevel {
match level {
log::Level::Error => LogLevel::Err,
log::Level::Warn => LogLevel::Warning,
log::Level::Info => LogLevel::Notice,
log::Level::Debug => LogLevel::Info,
log::Level::Trace => LogLevel::Debug,
}
}
fn create_logger<F: LogFactory>(
target: SupportedLogTarget,
factory: &F,
syslog_identifier: &str,
) -> Result<Box<dyn Log>, LogControl1Error> {
match target {
SupportedLogTarget::Console => factory.create_console_log(),
SupportedLogTarget::Journal => factory.create_journal_log(syslog_identifier.to_string()),
}
}
pub trait LogFactory {
fn create_console_log(&self) -> Result<Box<dyn Log>, LogControl1Error>;
fn create_journal_log(
&self,
syslog_identifier: String,
) -> Result<Box<dyn Log>, LogControl1Error> {
Ok(Box::new(
JournalLog::empty()?.with_syslog_identifier(syslog_identifier),
))
}
}
pub type ControlledLog = ReloadLog<LevelFilter<Box<dyn Log>>>;
pub struct LogController<F: LogFactory> {
handle: ReloadHandle<LevelFilter<Box<dyn Log>>>,
factory: F,
connected_to_journal: bool,
syslog_identifier: String,
level: LogLevel,
target: SupportedLogTarget,
}
impl<F: LogFactory> LogController<F> {
pub fn new(
factory: F,
connected_to_journal: bool,
syslog_identifier: String,
target: KnownLogTarget,
level: log::Level,
) -> Result<(Self, ControlledLog), LogControl1Error> {
let log_target = from_known_log_target(target, connected_to_journal)?;
let inner_logger = create_logger(log_target, &factory, &syslog_identifier)?;
let log = ReloadLog::new(LevelFilter::new(level, inner_logger));
let control = Self {
handle: log.handle(),
factory,
connected_to_journal,
syslog_identifier,
level: to_log_level(level),
target: log_target,
};
Ok((control, log))
}
pub fn new_auto(
factory: F,
level: log::Level,
) -> Result<(Self, ControlledLog), LogControl1Error> {
Self::new(
factory,
logcontrol::stderr_connected_to_journal(),
logcontrol::syslog_identifier(),
KnownLogTarget::Auto,
level,
)
}
pub fn install_auto(factory: F, level: log::Level) -> Result<Self, LogControl1Error> {
let (control, logger) = Self::new_auto(factory, level)?;
log::set_boxed_logger(Box::new(logger))
.map_err(|error| LogControl1Error::Failure(format!("{error}")))?;
Ok(control)
}
}
impl<F: LogFactory> LogControl1 for LogController<F> {
fn level(&self) -> logcontrol::LogLevel {
self.level
}
fn set_level(
&mut self,
level: logcontrol::LogLevel,
) -> Result<(), logcontrol::LogControl1Error> {
let log_level = from_log_level(level)?;
self.handle
.modify(|l| l.set_level(log_level))
.map_err(|error| {
LogControl1Error::Failure(format!("Failed to change level to {level}: {error}"))
})?;
self.level = level;
Ok(())
}
fn target(&self) -> &str {
KnownLogTarget::from(self.target).as_str()
}
fn set_target<S: AsRef<str>>(&mut self, target: S) -> Result<(), logcontrol::LogControl1Error> {
let log_target = from_known_log_target(
KnownLogTarget::try_from(target.as_ref())?,
self.connected_to_journal,
)?;
let new_logger = create_logger(log_target, &self.factory, &self.syslog_identifier)?;
self.handle
.modify(|l| l.set_inner(new_logger))
.map_err(|error| {
LogControl1Error::Failure(format!(
"Failed to change log target to {}: {error}",
target.as_ref()
))
})?;
self.target = log_target;
Ok(())
}
fn syslog_identifier(&self) -> &str {
&self.syslog_identifier
}
}