logcontrol_tracing/
lib.rs

1//! A [`LogControl1`] implementation for [`tracing`].
2//!
3//! [`TracingLogControl1`] provides a [`LogControl1`] implementation on top of
4//! tracing, which uses the [reload layer][tracing_subscriber::reload] to
5//! dyanmically switch layers and level filters when the log target or log level
6//! are changed over the log control interfaces.
7//!
8//! It uses a [`LogControl1LayerFactory`] implementation to create the log target
9//! layers each time the log target is changed.  This crates provides a default
10//! [`PrettyLogControl1LayerFactory`] which uses the pretty format of
11//! [`tracing_subscriber`] on stdout for the console target and
12//! [`tracing_journald`] for the Journal target.  You can provide your own
13//! implementation to customize the layer for each target.
14//!
15//! When created [`TracingLogControl1`] additionally returns a layer which needs
16//! to be added to the global tracing subscriber, i.e. a [`tracing_subscriber::Registry`],
17//! for log control to have any effect.
18//!
19//! ```rust
20//! use logcontrol::*;
21//! use logcontrol_tracing::*;
22//! use tracing_subscriber::prelude::*;
23//!
24//! let (control, layer) = TracingLogControl1::new_auto(
25//!     PrettyLogControl1LayerFactory,
26//!     tracing::Level::INFO,
27//! ).unwrap();
28//!
29//! let subscriber = tracing_subscriber::Registry::default().with(layer);
30//! tracing::subscriber::set_global_default(subscriber).unwrap();
31//! // Then register `control` over DBus, e.g. via `logcontrol_zbus::LogControl1`.
32//! ```
33
34#![deny(warnings, clippy::all, clippy::pedantic, missing_docs)]
35#![forbid(unsafe_code)]
36
37use logcontrol::{KnownLogTarget, LogControl1, LogControl1Error, LogLevel};
38use tracing::Subscriber;
39use tracing_subscriber::filter::LevelFilter;
40use tracing_subscriber::layer::Layered;
41use tracing_subscriber::registry::LookupSpan;
42use tracing_subscriber::{fmt, reload, Layer};
43
44pub use logcontrol;
45pub use logcontrol::stderr_connected_to_journal;
46pub use logcontrol::syslog_identifier;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49enum TracingLogTarget {
50    Console,
51    Journal,
52    Null,
53}
54
55impl From<TracingLogTarget> for KnownLogTarget {
56    fn from(value: TracingLogTarget) -> Self {
57        match value {
58            TracingLogTarget::Console => KnownLogTarget::Console,
59            TracingLogTarget::Journal => KnownLogTarget::Journal,
60            TracingLogTarget::Null => KnownLogTarget::Null,
61        }
62    }
63}
64
65fn from_known_log_target(
66    target: KnownLogTarget,
67    connected_to_journal: bool,
68) -> Result<TracingLogTarget, LogControl1Error> {
69    match target {
70        KnownLogTarget::Auto if connected_to_journal => Ok(TracingLogTarget::Journal),
71        KnownLogTarget::Auto | KnownLogTarget::Console => Ok(TracingLogTarget::Console),
72        KnownLogTarget::Journal => Ok(TracingLogTarget::Journal),
73        KnownLogTarget::Null => Ok(TracingLogTarget::Null),
74        other => Err(LogControl1Error::UnsupportedLogTarget(
75            other.as_str().to_string(),
76        )),
77    }
78}
79
80/// Convert [`logcontrol::LogLevel`] to [`tracing::Level`].
81///
82/// Return an error if the systemd log level is not supported, i.e. does not map to a
83/// corresponding [`tracing::Level`].
84///
85/// # Errors
86///
87/// Return [`LogControl1Error::UnsupportedLogLevel`] if `level` does not map to
88/// a [`tracing::Level`].
89pub fn from_log_level(level: LogLevel) -> Result<tracing::Level, LogControl1Error> {
90    match level {
91        LogLevel::Err => Ok(tracing::Level::ERROR),
92        LogLevel::Warning => Ok(tracing::Level::WARN),
93        LogLevel::Notice => Ok(tracing::Level::INFO),
94        LogLevel::Info => Ok(tracing::Level::DEBUG),
95        LogLevel::Debug => Ok(tracing::Level::TRACE),
96        unsupported => Err(LogControl1Error::UnsupportedLogLevel(unsupported)),
97    }
98}
99
100/// Convert [`tracing::Level`] to [`logcontrol::LogLevel`].
101fn to_log_level(level: tracing::Level) -> LogLevel {
102    match level {
103        tracing::Level::ERROR => LogLevel::Err,
104        tracing::Level::WARN => LogLevel::Warning,
105        tracing::Level::INFO => LogLevel::Notice,
106        tracing::Level::DEBUG => LogLevel::Info,
107        tracing::Level::TRACE => LogLevel::Debug,
108    }
109}
110
111/// A factory to create layers for [`TracingLogControl1`].
112pub trait LogControl1LayerFactory {
113    /// The type of the layer to use for [`KnownLogTarget::Journal`].
114    type JournalLayer<S: Subscriber + for<'span> LookupSpan<'span>>: Layer<S>;
115    /// The type of the layer to use for [`KnownLogTarget::Console`].
116    type ConsoleLayer<S: Subscriber + for<'span> LookupSpan<'span>>: Layer<S>;
117
118    /// Create a layer to use when [`KnownLogTarget::Journal`] is selected.
119    ///
120    /// The `syslog_identifier` should be send to the journal as `SYSLOG_IDENTIFIER`, to support `journalctl -t`.
121    /// See [`systemd.journal-fields(7)`](https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html).
122    ///
123    /// # Errors
124    ///
125    /// Return an error if creating the journal layer failed.
126    fn create_journal_layer<S: Subscriber + for<'span> LookupSpan<'span>>(
127        &self,
128        syslog_identifier: String,
129    ) -> Result<Self::JournalLayer<S>, LogControl1Error>;
130
131    /// Create a layer to use when [`KnownLogTarget::Console`] is selected.
132    ///
133    /// # Errors
134    ///
135    /// Return an error if creating the console layer failed.
136    fn create_console_layer<S: Subscriber + for<'span> LookupSpan<'span>>(
137        &self,
138    ) -> Result<Self::ConsoleLayer<S>, LogControl1Error>;
139}
140
141/// A layer factory which uses pretty printing on stdout for the console target.
142///
143/// For [`KnownLogTarget::Console`] this layer factory creates a [`mod@tracing_subscriber::fmt`]
144/// layer which logs to stdout with the built-in pretty format.
145///
146/// For [`KnownLogTarget::Journal`] this layer factory creates a [`tracing_journald`]
147/// layer without field prefixes and no further customization.
148pub struct PrettyLogControl1LayerFactory;
149
150impl LogControl1LayerFactory for PrettyLogControl1LayerFactory {
151    type JournalLayer<S: Subscriber + for<'span> LookupSpan<'span>> = tracing_journald::Layer;
152
153    type ConsoleLayer<S: Subscriber + for<'span> LookupSpan<'span>> =
154        fmt::Layer<S, fmt::format::Pretty, fmt::format::Format<fmt::format::Pretty>>;
155
156    fn create_journal_layer<S: Subscriber + for<'span> LookupSpan<'span>>(
157        &self,
158        syslog_identifier: String,
159    ) -> Result<Self::JournalLayer<S>, LogControl1Error> {
160        Ok(tracing_journald::Layer::new()?
161            .with_field_prefix(None)
162            .with_syslog_identifier(syslog_identifier))
163    }
164
165    fn create_console_layer<S: Subscriber + for<'span> LookupSpan<'span>>(
166        &self,
167    ) -> Result<Self::ConsoleLayer<S>, LogControl1Error> {
168        Ok(tracing_subscriber::fmt::layer().pretty())
169    }
170}
171
172/// The type of the layer that implements the log target.
173pub type LogTargetLayer<F, S> = Layered<
174    Option<<F as LogControl1LayerFactory>::ConsoleLayer<S>>,
175    Option<<F as LogControl1LayerFactory>::JournalLayer<S>>,
176    S,
177>;
178
179/// The final type for the layer that implements the log control interface.
180pub type LogControl1Layer<F, S> =
181    Layered<reload::Layer<LogTargetLayer<F, S>, S>, reload::Layer<LevelFilter, S>, S>;
182
183/// Create a new tracing layer for the given `target`, using the given `factory`.
184///
185/// We don't handle the `Null` target explicitly here; it disables logging
186/// simply because it matches none of the other targets, so we automatically
187/// create an empty layer here.
188///
189/// Return any error returned from the factory methods.
190fn make_target_layer<F: LogControl1LayerFactory, S>(
191    factory: &F,
192    target: TracingLogTarget,
193    syslog_identifier: &str,
194) -> Result<LogTargetLayer<F, S>, LogControl1Error>
195where
196    S: Subscriber + for<'span> LookupSpan<'span>,
197{
198    let stdout = if let TracingLogTarget::Console = target {
199        Some(factory.create_console_layer::<S>()?)
200    } else {
201        None
202    };
203    let journal = if let TracingLogTarget::Journal = target {
204        Some(factory.create_journal_layer::<S>(syslog_identifier.to_string())?)
205    } else {
206        None
207    };
208    Ok(tracing_subscriber::Layer::and_then(journal, stdout))
209}
210
211/// A [`LogControl1`] implementation for [`tracing`].
212///
213/// This implementation creates a tracing layer which combines two reloadable
214/// layers, on for the log target, and another one for the level filter
215/// implementing the desired log level.  It keeps the reload handles internally
216/// and reloads newly created layers whenever the target or the level is changed.
217///
218/// Currently, this implementation only supports the following [`KnownLogTarget`]s:
219///
220/// - [`KnownLogTarget::Console`]
221/// - [`KnownLogTarget::Journal`]
222/// - [`KnownLogTarget::Null`]
223/// - [`KnownLogTarget::Auto`]
224///
225/// Any other target fails with [`LogControl1Error::UnsupportedLogTarget`].
226pub struct TracingLogControl1<F, S>
227where
228    F: LogControl1LayerFactory,
229    S: Subscriber + for<'span> LookupSpan<'span>,
230{
231    /// Whether the current process is connnected to the systemd journal.
232    connected_to_journal: bool,
233    /// The syslog identifier used for logging.
234    syslog_identifier: String,
235    /// The current level active in the level layer.
236    level: tracing::Level,
237    /// The current target active in the target layer.
238    target: TracingLogTarget,
239    /// Factory for layers.
240    layer_factory: F,
241    // /// A handle to reload the level layer in order to change the level.
242    level_handle: reload::Handle<LevelFilter, S>,
243    // /// A handle to reload the target layer in order to change the target.
244    target_handle: reload::Handle<LogTargetLayer<F, S>, S>,
245}
246
247impl<F, S> TracingLogControl1<F, S>
248where
249    F: LogControl1LayerFactory,
250    S: Subscriber + for<'span> LookupSpan<'span>,
251{
252    /// Create a new layer controlled through the log interface.
253    ///
254    /// `factory` creates the [`tracing_subscriber::Layer`] for the selected `target`
255    /// which denotes the initial log target. The `factory` is invoked whenever the
256    /// log target is changed, to create a new layer to use for the selected log
257    /// target.  See [`TracingLogControl1`] for supported `target`s.
258    ///
259    /// `connected_to_journal` indicates whether this process is connected to the systemd
260    /// journal. Set to `true` to make [`KnownLogTarget::Auto`] use [`KnownLogTarget::Journal`],
261    /// otherwise it uses [`KnownLogTarget::Console`].
262    ///
263    /// `level` denotes the default tracing log level to start with.
264    ///
265    /// `syslog_identifier` is passed to [`LogControl1LayerFactory::create_journal_layer`]
266    /// for use as `SYSLOG_IDENTIFIER` journal field.
267    ///
268    /// # Errors
269    ///
270    /// Return an error if `target` is not supported, of if creating a layer fails,
271    /// e.g. when selecting [`KnownLogTarget::Journal`] on a system where journald is
272    /// not running, or inside a container which has no direct access to the journald
273    /// socket.
274    pub fn new(
275        factory: F,
276        connected_to_journal: bool,
277        syslog_identifier: String,
278        target: KnownLogTarget,
279        level: tracing::Level,
280    ) -> Result<(Self, LogControl1Layer<F, S>), LogControl1Error> {
281        let tracing_target = from_known_log_target(target, connected_to_journal)?;
282        let (target_layer, target_handle) = reload::Layer::new(make_target_layer(
283            &factory,
284            tracing_target,
285            &syslog_identifier,
286        )?);
287        let (level_layer, level_handle) = reload::Layer::new(LevelFilter::from_level(level));
288        let control_layer = Layer::and_then(level_layer, target_layer);
289        let control = Self {
290            connected_to_journal,
291            layer_factory: factory,
292            syslog_identifier,
293            level,
294            target: tracing_target,
295            level_handle,
296            target_handle,
297        };
298
299        Ok((control, control_layer))
300    }
301
302    /// Create a new layer controlled through the log interface, with automatic defaults.
303    ///
304    /// Use [`logcontrol::syslog_identifier()`] as the syslog identifier, and
305    /// determine the initial log target automatically according to
306    /// [`logcontrol::stderr_connected_to_journal()`].
307    ///
308    /// `level` denotes the initial level; see [`Self::new`] for `factory`.
309    ///
310    /// # Errors
311    ///
312    /// See [`Self::new`].
313    pub fn new_auto(
314        factory: F,
315        level: tracing::Level,
316    ) -> Result<(Self, LogControl1Layer<F, S>), LogControl1Error> {
317        Self::new(
318            factory,
319            logcontrol::stderr_connected_to_journal(),
320            logcontrol::syslog_identifier(),
321            KnownLogTarget::Auto,
322            level,
323        )
324    }
325}
326
327impl<F, S> LogControl1 for TracingLogControl1<F, S>
328where
329    F: LogControl1LayerFactory,
330    S: Subscriber + for<'span> LookupSpan<'span>,
331{
332    fn level(&self) -> LogLevel {
333        to_log_level(self.level)
334    }
335
336    fn set_level(&mut self, level: LogLevel) -> Result<(), LogControl1Error> {
337        let tracing_level = from_log_level(level)?;
338        self.level_handle
339            .reload(LevelFilter::from_level(tracing_level))
340            .map_err(|error| {
341                LogControl1Error::Failure(format!(
342                    "Failed to reload target layer to switch to log target {level}: {error}"
343                ))
344            })?;
345        self.level = tracing_level;
346        Ok(())
347    }
348
349    fn target(&self) -> &str {
350        KnownLogTarget::from(self.target).as_str()
351    }
352
353    fn set_target<T: AsRef<str>>(&mut self, target: T) -> Result<(), LogControl1Error> {
354        let new_tracing_target = from_known_log_target(
355            KnownLogTarget::try_from(target.as_ref())?,
356            self.connected_to_journal,
357        )?;
358        let new_layer = make_target_layer(
359            &self.layer_factory,
360            new_tracing_target,
361            &self.syslog_identifier,
362        )?;
363        self.target_handle.reload(new_layer).map_err(|error| {
364            LogControl1Error::Failure(format!(
365                "Failed to reload target layer to switch to log target {}: {error}",
366                target.as_ref()
367            ))
368        })?;
369        self.target = new_tracing_target;
370        Ok(())
371    }
372
373    fn syslog_identifier(&self) -> &str {
374        &self.syslog_identifier
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use static_assertions::assert_impl_all;
381    use tracing_subscriber::Registry;
382
383    use crate::{PrettyLogControl1LayerFactory, TracingLogControl1};
384
385    // Ensure that the our default log control layers are Send and Sync, this is required for zbus.
386    assert_impl_all!(TracingLogControl1<PrettyLogControl1LayerFactory, Registry>: Send, Sync);
387}