#![deny(warnings, clippy::all, clippy::pedantic, missing_docs)]
#![forbid(unsafe_code)]
use logcontrol::{KnownLogTarget, LogControl1, LogControl1Error, LogLevel};
use tracing::Subscriber;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::layer::Layered;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::{fmt, reload, Layer};
pub use logcontrol;
pub use logcontrol::stderr_connected_to_journal;
pub use logcontrol::syslog_identifier;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TracingLogTarget {
Console,
Journal,
Null,
}
impl From<TracingLogTarget> for KnownLogTarget {
fn from(value: TracingLogTarget) -> Self {
match value {
TracingLogTarget::Console => KnownLogTarget::Console,
TracingLogTarget::Journal => KnownLogTarget::Journal,
TracingLogTarget::Null => KnownLogTarget::Null,
}
}
}
fn from_known_log_target(
target: KnownLogTarget,
connected_to_journal: bool,
) -> Result<TracingLogTarget, LogControl1Error> {
match target {
KnownLogTarget::Auto if connected_to_journal => Ok(TracingLogTarget::Journal),
KnownLogTarget::Auto | KnownLogTarget::Console => Ok(TracingLogTarget::Console),
KnownLogTarget::Journal => Ok(TracingLogTarget::Journal),
KnownLogTarget::Null => Ok(TracingLogTarget::Null),
other => Err(LogControl1Error::UnsupportedLogTarget(
other.as_str().to_string(),
)),
}
}
pub fn from_log_level(level: LogLevel) -> Result<tracing::Level, LogControl1Error> {
match level {
LogLevel::Err => Ok(tracing::Level::ERROR),
LogLevel::Warning => Ok(tracing::Level::WARN),
LogLevel::Notice => Ok(tracing::Level::INFO),
LogLevel::Info => Ok(tracing::Level::DEBUG),
LogLevel::Debug => Ok(tracing::Level::TRACE),
unsupported => Err(LogControl1Error::UnsupportedLogLevel(unsupported)),
}
}
fn to_log_level(level: tracing::Level) -> LogLevel {
match level {
tracing::Level::ERROR => LogLevel::Err,
tracing::Level::WARN => LogLevel::Warning,
tracing::Level::INFO => LogLevel::Notice,
tracing::Level::DEBUG => LogLevel::Info,
tracing::Level::TRACE => LogLevel::Debug,
}
}
pub trait LogControl1LayerFactory {
type JournalLayer<S: Subscriber + for<'span> LookupSpan<'span>>: Layer<S>;
type ConsoleLayer<S: Subscriber + for<'span> LookupSpan<'span>>: Layer<S>;
fn create_journal_layer<S: Subscriber + for<'span> LookupSpan<'span>>(
&self,
syslog_identifier: String,
) -> Result<Self::JournalLayer<S>, LogControl1Error>;
fn create_console_layer<S: Subscriber + for<'span> LookupSpan<'span>>(
&self,
) -> Result<Self::ConsoleLayer<S>, LogControl1Error>;
}
pub struct PrettyLogControl1LayerFactory;
impl LogControl1LayerFactory for PrettyLogControl1LayerFactory {
type JournalLayer<S: Subscriber + for<'span> LookupSpan<'span>> = tracing_journald::Layer;
type ConsoleLayer<S: Subscriber + for<'span> LookupSpan<'span>> =
fmt::Layer<S, fmt::format::Pretty, fmt::format::Format<fmt::format::Pretty>>;
fn create_journal_layer<S: Subscriber + for<'span> LookupSpan<'span>>(
&self,
syslog_identifier: String,
) -> Result<Self::JournalLayer<S>, LogControl1Error> {
Ok(tracing_journald::Layer::new()?
.with_field_prefix(None)
.with_syslog_identifier(syslog_identifier))
}
fn create_console_layer<S: Subscriber + for<'span> LookupSpan<'span>>(
&self,
) -> Result<Self::ConsoleLayer<S>, LogControl1Error> {
Ok(tracing_subscriber::fmt::layer().pretty())
}
}
pub type LogTargetLayer<F, S> = Layered<
Option<<F as LogControl1LayerFactory>::ConsoleLayer<S>>,
Option<<F as LogControl1LayerFactory>::JournalLayer<S>>,
S,
>;
pub type LogControl1Layer<F, S> =
Layered<reload::Layer<LogTargetLayer<F, S>, S>, reload::Layer<LevelFilter, S>, S>;
fn make_target_layer<F: LogControl1LayerFactory, S>(
factory: &F,
target: TracingLogTarget,
syslog_identifier: &str,
) -> Result<LogTargetLayer<F, S>, LogControl1Error>
where
S: Subscriber + for<'span> LookupSpan<'span>,
{
let stdout = if let TracingLogTarget::Console = target {
Some(factory.create_console_layer::<S>()?)
} else {
None
};
let journal = if let TracingLogTarget::Journal = target {
Some(factory.create_journal_layer::<S>(syslog_identifier.to_string())?)
} else {
None
};
Ok(tracing_subscriber::Layer::and_then(journal, stdout))
}
pub struct TracingLogControl1<F, S>
where
F: LogControl1LayerFactory,
S: Subscriber + for<'span> LookupSpan<'span>,
{
connected_to_journal: bool,
syslog_identifier: String,
level: tracing::Level,
target: TracingLogTarget,
layer_factory: F,
level_handle: reload::Handle<LevelFilter, S>,
target_handle: reload::Handle<LogTargetLayer<F, S>, S>,
}
impl<F, S> TracingLogControl1<F, S>
where
F: LogControl1LayerFactory,
S: Subscriber + for<'span> LookupSpan<'span>,
{
pub fn new(
factory: F,
connected_to_journal: bool,
syslog_identifier: String,
target: KnownLogTarget,
level: tracing::Level,
) -> Result<(Self, LogControl1Layer<F, S>), LogControl1Error> {
let tracing_target = from_known_log_target(target, connected_to_journal)?;
let (target_layer, target_handle) = reload::Layer::new(make_target_layer(
&factory,
tracing_target,
&syslog_identifier,
)?);
let (level_layer, level_handle) = reload::Layer::new(LevelFilter::from_level(level));
let control_layer = Layer::and_then(level_layer, target_layer);
let control = Self {
connected_to_journal,
layer_factory: factory,
syslog_identifier,
level,
target: tracing_target,
level_handle,
target_handle,
};
Ok((control, control_layer))
}
pub fn new_auto(
factory: F,
level: tracing::Level,
) -> Result<(Self, LogControl1Layer<F, S>), LogControl1Error> {
Self::new(
factory,
logcontrol::stderr_connected_to_journal(),
logcontrol::syslog_identifier(),
KnownLogTarget::Auto,
level,
)
}
}
impl<F, S> LogControl1 for TracingLogControl1<F, S>
where
F: LogControl1LayerFactory,
S: Subscriber + for<'span> LookupSpan<'span>,
{
fn level(&self) -> LogLevel {
to_log_level(self.level)
}
fn set_level(&mut self, level: LogLevel) -> Result<(), LogControl1Error> {
let tracing_level = from_log_level(level)?;
self.level_handle
.reload(LevelFilter::from_level(tracing_level))
.map_err(|error| {
LogControl1Error::Failure(format!(
"Failed to reload target layer to switch to log target {level}: {error}"
))
})?;
self.level = tracing_level;
Ok(())
}
fn target(&self) -> &str {
KnownLogTarget::from(self.target).as_str()
}
fn set_target<T: AsRef<str>>(&mut self, target: T) -> Result<(), LogControl1Error> {
let new_tracing_target = from_known_log_target(
KnownLogTarget::try_from(target.as_ref())?,
self.connected_to_journal,
)?;
let new_layer = make_target_layer(
&self.layer_factory,
new_tracing_target,
&self.syslog_identifier,
)?;
self.target_handle.reload(new_layer).map_err(|error| {
LogControl1Error::Failure(format!(
"Failed to reload target layer to switch to log target {}: {error}",
target.as_ref()
))
})?;
self.target = new_tracing_target;
Ok(())
}
fn syslog_identifier(&self) -> &str {
&self.syslog_identifier
}
}
#[cfg(test)]
mod tests {
use static_assertions::assert_impl_all;
use tracing_subscriber::Registry;
use crate::{PrettyLogControl1LayerFactory, TracingLogControl1};
assert_impl_all!(TracingLogControl1<PrettyLogControl1LayerFactory, Registry>: Send, Sync);
}