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}