logcontrol_log/
lib.rs

1//! A [`LogControl1`] implementation for [`log`].
2//!
3//! [`LogController`] provides a [`LogControl1`] implementation on top of [`log`]
4//! which uses the [`log_reload`] crate to dynamically switch loggers and levels
5//! depending on the target and level selected over the log control interface.
6//!
7//! It uses a [`LogFactory`] implementation to create the actual [`Log`] instances
8//! each time the log target is changed.  This crates provides _no_ default
9//! implementation of this trait; users have to provide an implementation on
10//! their own.  This avoids a dependency on any specific log implementation for
11//! the `console` target.
12//!
13//! For the `journal` target this crate uses the [`systemd_journal_logger`] crate.
14//!
15//! See [`LogController::install_auto`] for the recommended entry point to this crate.
16
17#![deny(warnings, clippy::all, clippy::pedantic, missing_docs)]
18#![forbid(unsafe_code)]
19
20use log::Log;
21use log_reload::LevelFilter;
22use log_reload::ReloadHandle;
23use log_reload::ReloadLog;
24use logcontrol::KnownLogTarget;
25use logcontrol::LogControl1;
26use logcontrol::LogControl1Error;
27use logcontrol::LogLevel;
28
29pub use logcontrol;
30pub use logcontrol::stderr_connected_to_journal;
31pub use logcontrol::syslog_identifier;
32use systemd_journal_logger::JournalLog;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35enum SupportedLogTarget {
36    Console,
37    Journal,
38}
39
40impl From<SupportedLogTarget> for KnownLogTarget {
41    fn from(value: SupportedLogTarget) -> Self {
42        match value {
43            SupportedLogTarget::Console => KnownLogTarget::Console,
44            SupportedLogTarget::Journal => KnownLogTarget::Journal,
45        }
46    }
47}
48
49fn from_known_log_target(
50    target: KnownLogTarget,
51    connected_to_journal: bool,
52) -> Result<SupportedLogTarget, LogControl1Error> {
53    match target {
54        KnownLogTarget::Auto if connected_to_journal => Ok(SupportedLogTarget::Journal),
55        KnownLogTarget::Auto | KnownLogTarget::Console => Ok(SupportedLogTarget::Console),
56        KnownLogTarget::Journal => Ok(SupportedLogTarget::Journal),
57        other => Err(LogControl1Error::UnsupportedLogTarget(
58            other.as_str().to_string(),
59        )),
60    }
61}
62
63/// Convert [`logcontrol::LogLevel`] to [`log::Level`].
64///
65/// Return an error if the systemd log level is not supported, i.e. does not map to a
66/// corresponding [`log::Level`].
67///
68/// # Errors
69///
70/// Return [`LogControl1Error::UnsupportedLogLevel`] if the log `level` from the
71/// logcontrol interface does not map to a [`log::Level`].
72pub fn from_log_level(level: LogLevel) -> Result<log::Level, LogControl1Error> {
73    match level {
74        LogLevel::Err => Ok(log::Level::Error),
75        LogLevel::Warning => Ok(log::Level::Warn),
76        LogLevel::Notice => Ok(log::Level::Info),
77        LogLevel::Info => Ok(log::Level::Debug),
78        LogLevel::Debug => Ok(log::Level::Trace),
79        unsupported => Err(LogControl1Error::UnsupportedLogLevel(unsupported)),
80    }
81}
82
83/// Convert [`log::Level`] to [`logcontrol::LogLevel`].
84fn to_log_level(level: log::Level) -> LogLevel {
85    match level {
86        log::Level::Error => LogLevel::Err,
87        log::Level::Warn => LogLevel::Warning,
88        log::Level::Info => LogLevel::Notice,
89        log::Level::Debug => LogLevel::Info,
90        log::Level::Trace => LogLevel::Debug,
91    }
92}
93
94fn create_logger<F: LogFactory>(
95    target: SupportedLogTarget,
96    factory: &F,
97    syslog_identifier: &str,
98) -> Result<Box<dyn Log>, LogControl1Error> {
99    match target {
100        SupportedLogTarget::Console => factory.create_console_log(),
101        SupportedLogTarget::Journal => factory.create_journal_log(syslog_identifier.to_string()),
102    }
103}
104
105/// A factory for log implementations.
106pub trait LogFactory {
107    /// Create a logger for the console log target.
108    ///
109    /// # Errors
110    ///
111    /// Return an error if creating the logger failed.
112    fn create_console_log(&self) -> Result<Box<dyn Log>, LogControl1Error>;
113
114    /// Create a logger for journal log target.
115    ///
116    /// The implementation should use `syslog_identifier` for the corresponding journal field.
117    ///
118    /// The default implementation creates a [`systemd_journal_logger::JournalLog`].
119    ///
120    /// # Errors
121    ///
122    /// Return [`LogControl1Error::InputOutputError`] if journald is unavailable.
123    fn create_journal_log(
124        &self,
125        syslog_identifier: String,
126    ) -> Result<Box<dyn Log>, LogControl1Error> {
127        Ok(Box::new(
128            JournalLog::empty()?.with_syslog_identifier(syslog_identifier),
129        ))
130    }
131}
132
133/// The type of a controlled [`log::Log`].
134pub type ControlledLog = ReloadLog<LevelFilter<Box<dyn Log>>>;
135
136/// A [`LogControl1`] implementation for [`log`].
137///
138/// This implementation creates a [`log::Log`] implementation whose level and
139/// underlying logger can be dynamically reconfigured through the [`LogControl1`]
140/// interface.  It uses a [`ReloadLog`] together with a [`LevelFilter`] under
141/// the hood.
142///
143/// Currently, this implementation only supports for following [`KnownLogTarget`]s:
144///
145/// - [`KnownLogTarget::Console`]
146/// - [`KnownLogTarget::Journal`]
147/// - [`KnownLogTarget::Auto`]
148///
149/// Any other target fails with [`LogControl1Error::UnsupportedLogTarget`].
150pub struct LogController<F: LogFactory> {
151    /// The reload handler.
152    handle: ReloadHandle<LevelFilter<Box<dyn Log>>>,
153    /// The factory to create loggers with when switching targets.
154    factory: F,
155    /// Whether the current process is connnected to the systemd journal.
156    connected_to_journal: bool,
157    /// The syslog identifier used for logging.
158    syslog_identifier: String,
159    /// The current level active in the level layer.
160    level: LogLevel,
161    /// The current target active in the target layer.
162    target: SupportedLogTarget,
163}
164
165impl<F: LogFactory> LogController<F> {
166    /// Create a new logger which can be controlled through the log control interface.
167    ///
168    /// `factory` creates the inner [`log::Log`] instances for the selected `target` which
169    /// denotes the initial log target.  The `factory` is invoked whenever the log target
170    /// is changed, to create a new logger for the corresponding target.  See
171    /// [`LogController`] for supported log targets.
172    ///
173    /// `connected_to_journal` indicates whether this process is connected to the systemd
174    /// journal. Set to `true` to make [`KnownLogTarget::Auto`] use [`KnownLogTarget::Journal`],
175    /// otherwise it uses [`KnownLogTarget::Console`].
176    ///
177    /// `level` denotes the default tracing log level to start with.
178    ///
179    /// `syslog_identifier` is passed to [`LogFactory::create_journal_log`]
180    /// for use as `SYSLOG_IDENTIFIER` journal field.
181    ///
182    /// Returns an error if `target` is not supported, of if creating a layer fails,
183    ///
184    /// # Errors
185    ///
186    /// Return a [`LogControl1Error::UnsupportedLogTarget`] if `target` is
187    /// not supported, and [`LogControl1Error::InputOutputError`] if creating
188    /// the logger for `target` failed, e.g. when selecting [`KnownLogTarget::Journal`]
189    /// on a system where journald is not running, or inside a container which
190    /// has no direct access to the journald socket.
191    pub fn new(
192        factory: F,
193        connected_to_journal: bool,
194        syslog_identifier: String,
195        target: KnownLogTarget,
196        level: log::Level,
197    ) -> Result<(Self, ControlledLog), LogControl1Error> {
198        let log_target = from_known_log_target(target, connected_to_journal)?;
199        let inner_logger = create_logger(log_target, &factory, &syslog_identifier)?;
200        let log = ReloadLog::new(LevelFilter::new(level, inner_logger));
201        let control = Self {
202            handle: log.handle(),
203            factory,
204            connected_to_journal,
205            syslog_identifier,
206            level: to_log_level(level),
207            target: log_target,
208        };
209        Ok((control, log))
210    }
211
212    /// Create a new logger which can be controlled through the log control interface, using automatic defaults.
213    ///
214    /// Use [`logcontrol::syslog_identifier()`] as the syslog identifier, and
215    /// determine the initial log target automatically according to
216    /// [`logcontrol::stderr_connected_to_journal()`].
217    ///
218    /// `level` denotes the initial level; for `factory` and returned errors,
219    ///  see [`Self::new`].
220    ///
221    /// # Errors
222    ///
223    /// Return [`LogControl1Error::InputOutputError`] if journald is not
224    /// available, but should have been.  This will only happen on a broken
225    /// system.
226    pub fn new_auto(
227        factory: F,
228        level: log::Level,
229    ) -> Result<(Self, ControlledLog), LogControl1Error> {
230        Self::new(
231            factory,
232            logcontrol::stderr_connected_to_journal(),
233            logcontrol::syslog_identifier(),
234            KnownLogTarget::Auto,
235            level,
236        )
237    }
238
239    /**
240     * Create and install a controlled logger, with automatic defaults.
241     *
242     * See [`Self::new_auto`] for arguments.
243     *
244     * # Errors
245     *
246     * See [`Self::new_auto`] for errors. Additionally, this function fails with
247     * [`LogControl1Error::Failure`] if [`log::set_boxed_logger`] fails.
248     */
249    pub fn install_auto(factory: F, level: log::Level) -> Result<Self, LogControl1Error> {
250        let (control, logger) = Self::new_auto(factory, level)?;
251        log::set_boxed_logger(Box::new(logger))
252            .map_err(|error| LogControl1Error::Failure(format!("{error}")))?;
253        Ok(control)
254    }
255}
256
257impl<F: LogFactory> LogControl1 for LogController<F> {
258    fn level(&self) -> logcontrol::LogLevel {
259        self.level
260    }
261
262    fn set_level(
263        &mut self,
264        level: logcontrol::LogLevel,
265    ) -> Result<(), logcontrol::LogControl1Error> {
266        let log_level = from_log_level(level)?;
267        self.handle
268            .modify(|l| l.set_level(log_level))
269            .map_err(|error| {
270                LogControl1Error::Failure(format!("Failed to change level to {level}: {error}"))
271            })?;
272        self.level = level;
273        Ok(())
274    }
275
276    fn target(&self) -> &str {
277        KnownLogTarget::from(self.target).as_str()
278    }
279
280    fn set_target<S: AsRef<str>>(&mut self, target: S) -> Result<(), logcontrol::LogControl1Error> {
281        let log_target = from_known_log_target(
282            KnownLogTarget::try_from(target.as_ref())?,
283            self.connected_to_journal,
284        )?;
285        let new_logger = create_logger(log_target, &self.factory, &self.syslog_identifier)?;
286        self.handle
287            .modify(|l| l.set_inner(new_logger))
288            .map_err(|error| {
289                LogControl1Error::Failure(format!(
290                    "Failed to change log target to {}: {error}",
291                    target.as_ref()
292                ))
293            })?;
294        self.target = log_target;
295        Ok(())
296    }
297
298    fn syslog_identifier(&self) -> &str {
299        &self.syslog_identifier
300    }
301}