uxum/logging/
mod.rs

1//! Logging configuration via [`tracing`] crate.
2
3pub(crate) mod json;
4pub(crate) mod span;
5
6use std::{collections::BTreeMap, fs, io};
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10use tracing_appender::{
11    non_blocking::{NonBlocking, NonBlockingBuilder, WorkerGuard},
12    rolling::{RollingFileAppender, Rotation},
13};
14use tracing_subscriber::{
15    filter::{LevelFilter, Targets},
16    fmt::{self, writer::BoxMakeWriter},
17    layer::{Layer, Layered, SubscriberExt},
18    registry::Registry,
19};
20
21use crate::logging::json::{ExtensibleJsonFormat, JsonKeyNames};
22
23type LoggingRegistry = Layered<Vec<Box<dyn Layer<Registry> + Send + Sync>>, Registry>;
24
25/// Error type used in logging configuration.
26#[derive(Debug, Error)]
27pub enum LoggingError {
28    /// Log destination I/O error.
29    #[error("Log destination I/O error: {0}")]
30    Io(#[from] io::Error),
31    /// Error while initializing log directory writer.
32    #[error("Error while initializing log directory writer: {0}")]
33    Directory(#[from] tracing_appender::rolling::InitError),
34}
35
36/// Logging configuration.
37#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
38#[non_exhaustive]
39pub struct LoggingConfig {
40    /// List of subscribers defined in configuration.
41    #[serde(default, skip_serializing_if = "Vec::is_empty")]
42    pub subscribers: Vec<LoggingSubscriberConfig>,
43}
44
45impl LoggingConfig {
46    /// Create registry subscriber from configuration.
47    ///
48    /// # Errors
49    ///
50    /// Returns `Err` if any of the subscribers cannot be initialized.
51    pub fn make_registry(&self) -> Result<(LoggingRegistry, Vec<WorkerGuard>), LoggingError> {
52        let num_subs = self.subscribers.len();
53        let (subs, buf_guards) = self.subscribers.iter().try_fold(
54            (Vec::with_capacity(num_subs), Vec::with_capacity(num_subs)),
55            |(mut acc_s, mut acc_g), sub_cfg| {
56                let (sub, guard) = sub_cfg.make_layer()?;
57                acc_s.push(sub);
58                acc_g.push(guard);
59                Ok::<_, LoggingError>((acc_s, acc_g))
60            },
61        )?;
62        Ok((Registry::default().with(subs), buf_guards))
63    }
64}
65
66/// Individual logging subscriber configuration.
67#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
68#[non_exhaustive]
69pub struct LoggingSubscriberConfig {
70    /// Overall format for logging output.
71    #[serde(default, flatten)]
72    pub format: LoggingFormat,
73    /// Minimum severity level to include in output.
74    #[serde(default)]
75    pub level: LoggingLevel,
76    /// Custom minimum severity levels for span targets.
77    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
78    pub targets: BTreeMap<String, LoggingLevel>,
79    /// Use ANSI escape sequences for output colors and formatting.
80    #[serde(default)]
81    pub color: bool,
82    /// Include errors of logging subsystem in output.
83    #[serde(default = "crate::util::default_true")]
84    pub internal_errors: bool,
85    /// Additional span information to include in output.
86    #[serde(default)]
87    pub print: LoggingPrintingConfig,
88    /// Write buffer configuration for a non-blocking writer.
89    #[serde(default)]
90    pub buffer: LoggingBufferConfig,
91    /// Log destination configuration.
92    #[serde(default)]
93    pub output: LoggingDestination,
94}
95
96impl Default for LoggingSubscriberConfig {
97    fn default() -> Self {
98        Self {
99            format: LoggingFormat::default(),
100            level: LoggingLevel::default(),
101            targets: BTreeMap::new(),
102            color: false,
103            internal_errors: true,
104            print: LoggingPrintingConfig::default(),
105            buffer: LoggingBufferConfig::default(),
106            output: LoggingDestination::default(),
107        }
108    }
109}
110
111impl LoggingSubscriberConfig {
112    /// Logging subscriber template for use in development.
113    #[must_use]
114    pub fn default_for_dev() -> Self {
115        Self {
116            format: LoggingFormat::Pretty,
117            level: LoggingLevel::Trace,
118            targets: BTreeMap::new(),
119            color: true,
120            internal_errors: true,
121            print: LoggingPrintingConfig {
122                target: true,
123                file: true,
124                line_number: true,
125                level: true,
126                thread_name: true,
127                thread_id: false,
128            },
129            buffer: LoggingBufferConfig::default(),
130            output: LoggingDestination::default(),
131        }
132    }
133
134    /// Make [`tracing_subscriber::Layer`] from subscriber configuration.
135    pub fn make_layer<T>(
136        &self,
137    ) -> Result<(Box<dyn Layer<T> + Send + Sync>, WorkerGuard), LoggingError>
138    where
139        T: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
140    {
141        let buf_builder = self.buffer.make_builder();
142        let (buf_writer, buf_guard) = self.output.make_writer(buf_builder)?;
143        let layer = fmt::layer()
144            .with_writer(buf_writer)
145            .with_ansi(self.color)
146            .log_internal_errors(self.internal_errors)
147            .with_target(self.print.target)
148            .with_file(self.print.file)
149            .with_line_number(self.print.line_number)
150            .with_level(self.print.level)
151            .with_thread_names(self.print.thread_name)
152            .with_thread_ids(self.print.thread_id);
153        let boxed_layer = match self.format {
154            LoggingFormat::Full => layer.boxed(),
155            LoggingFormat::Compact => layer.compact().boxed(),
156            LoggingFormat::Pretty => layer.pretty().boxed(),
157            LoggingFormat::Json {
158                flatten_metadata,
159                current_span,
160                ref static_fields,
161                ref key_names,
162            } => {
163                let json_fmt = ExtensibleJsonFormat::new()
164                    .with_target(self.print.target)
165                    .with_file(self.print.file)
166                    .with_line_number(self.print.line_number)
167                    .with_level(self.print.level)
168                    .with_thread_names(self.print.thread_name)
169                    .with_thread_ids(self.print.thread_id)
170                    .flatten_event(flatten_metadata)
171                    .with_current_span(current_span)
172                    .with_static_fields(static_fields.clone())
173                    .with_key_names(*key_names.clone());
174                layer.json().event_format(json_fmt).boxed()
175            }
176        };
177        let boxed_layer = if self.targets.is_empty() {
178            boxed_layer
179                .with_filter(LevelFilter::from(self.level))
180                .boxed()
181        } else {
182            boxed_layer
183                .with_filter(
184                    Targets::new()
185                        .with_targets(self.targets.clone())
186                        .with_default(LevelFilter::from(self.level)),
187                )
188                .boxed()
189        };
190        Ok((boxed_layer, buf_guard))
191    }
192}
193
194/// Format for logging output.
195#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
196#[non_exhaustive]
197#[serde(rename_all = "lowercase", tag = "format")]
198pub enum LoggingFormat {
199    /// Format which prints span context before log message.
200    ///
201    /// See [`tracing_subscriber::fmt::format::Full`].
202    #[default]
203    Full,
204    /// More compact format, span names are hidden.
205    ///
206    /// See [`tracing_subscriber::fmt::format::Compact`].
207    Compact,
208    /// Excessively verbose and pretty multiline format.
209    ///
210    /// Might be useful when developing and testing code.
211    /// See [`tracing_subscriber::fmt::format::Pretty`].
212    Pretty,
213    /// Formats logs as newline-delimited JSON objects.
214    ///
215    /// See [`tracing_subscriber::fmt::format::Json`].
216    Json {
217        /// Flatten event metadata fields into object.
218        ///
219        /// See [`tracing_subscriber::fmt::format::Json::flatten_event`].
220        #[serde(default)]
221        flatten_metadata: bool,
222        /// Add current span name to object.
223        ///
224        /// See [`tracing_subscriber::fmt::format::Json::with_current_span`].
225        #[serde(default)]
226        current_span: bool,
227        /// Static fields to add to object.
228        #[serde(default)]
229        static_fields: BTreeMap<String, serde_json::Value>,
230        /// Custom names for JSON keys.
231        #[serde(default)]
232        key_names: Box<JsonKeyNames>,
233    },
234}
235
236/// Minumum event severity for log output.
237#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
238#[serde(rename_all = "UPPERCASE")]
239pub enum LoggingLevel {
240    /// Disables logging altogether.
241    ///
242    /// See [`tracing_subscriber::filter::LevelFilter::OFF`].
243    #[serde(alias = "off", alias = "disabled", alias = "DISABLED")]
244    Off,
245    /// Write "error" level only.
246    ///
247    /// See [`tracing_subscriber::filter::LevelFilter::ERROR`].
248    #[serde(alias = "error", alias = "err", alias = "ERR")]
249    Error,
250    /// Write "warn" and more severe levels.
251    ///
252    /// See [`tracing_subscriber::filter::LevelFilter::WARN`].
253    #[serde(alias = "warn", alias = "warning", alias = "WARNING")]
254    Warn,
255    /// Write "info" and more severe levels.
256    ///
257    /// See [`tracing_subscriber::filter::LevelFilter::INFO`].
258    #[serde(alias = "info")]
259    Info,
260    /// Write "debug" and more severe levels.
261    ///
262    /// See [`tracing_subscriber::filter::LevelFilter::DEBUG`].
263    #[serde(alias = "debug")]
264    #[default]
265    Debug,
266    /// Write everything.
267    ///
268    /// See [`tracing_subscriber::filter::LevelFilter::TRACE`].
269    #[serde(alias = "trace")]
270    Trace,
271}
272
273impl From<LoggingLevel> for LevelFilter {
274    fn from(value: LoggingLevel) -> Self {
275        match value {
276            LoggingLevel::Off => LevelFilter::OFF,
277            LoggingLevel::Error => LevelFilter::ERROR,
278            LoggingLevel::Warn => LevelFilter::WARN,
279            LoggingLevel::Info => LevelFilter::INFO,
280            LoggingLevel::Debug => LevelFilter::DEBUG,
281            LoggingLevel::Trace => LevelFilter::TRACE,
282        }
283    }
284}
285
286/// Additional information to include in output.
287#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
288#[non_exhaustive]
289#[allow(clippy::struct_excessive_bools)]
290pub struct LoggingPrintingConfig {
291    /// Print event target.
292    #[serde(default)]
293    pub target: bool,
294    /// Print source file path.
295    #[serde(default)]
296    pub file: bool,
297    /// Print source line number.
298    #[serde(default)]
299    pub line_number: bool,
300    /// Print severity level.
301    #[serde(default = "crate::util::default_true")]
302    pub level: bool,
303    /// Print thread name.
304    #[serde(default)]
305    pub thread_name: bool,
306    /// Print thread ID.
307    #[serde(default)]
308    pub thread_id: bool,
309}
310
311impl Default for LoggingPrintingConfig {
312    fn default() -> Self {
313        Self {
314            target: false,
315            file: false,
316            line_number: false,
317            level: true,
318            thread_name: false,
319            thread_id: false,
320        }
321    }
322}
323
324/// Configuration for a non-blocking writer.
325#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
326#[non_exhaustive]
327pub struct LoggingBufferConfig {
328    /// Maximum buffered lines to store.
329    ///
330    /// After reaching this number of lines, new events will either be dropped or block, depending
331    /// on [`Self::lossy`] parameter.
332    ///
333    /// See [`tracing_appender::non_blocking::NonBlockingBuilder::buffered_lines_limit`].
334    #[serde(default = "LoggingBufferConfig::default_lines")]
335    pub lines: usize,
336    /// What to do with log lines that cannot be added to the buffer.
337    ///
338    /// Either drop them if `true`, or block execution until there is sufficient space in the buffer
339    /// if `false`.
340    ///
341    /// See [`tracing_appender::non_blocking::NonBlockingBuilder::lossy`].
342    #[serde(default = "crate::util::default_true")]
343    pub lossy: bool,
344    /// Override the thread name of log appender.
345    ///
346    /// See [`tracing_appender::non_blocking::NonBlockingBuilder::thread_name`].
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub thread_name: Option<String>,
349}
350
351impl Default for LoggingBufferConfig {
352    fn default() -> Self {
353        Self {
354            lines: Self::default_lines(),
355            lossy: true,
356            thread_name: None,
357        }
358    }
359}
360
361impl LoggingBufferConfig {
362    /// Default value for [`Self::lines`].
363    #[must_use]
364    #[inline]
365    fn default_lines() -> usize {
366        128_000
367    }
368
369    /// Construct a builder for non-blocking writer.
370    #[must_use]
371    pub fn make_builder(&self) -> NonBlockingBuilder {
372        let mut builder = NonBlockingBuilder::default()
373            .buffered_lines_limit(self.lines)
374            .lossy(self.lossy);
375        if let Some(thr_name) = &self.thread_name {
376            builder = builder.thread_name(thr_name);
377        }
378        builder
379    }
380
381    /// Construct a non-blocking writer.
382    pub fn make_writer<W>(&self, ll_writer: W) -> (NonBlocking, WorkerGuard)
383    where
384        W: io::Write + Send + 'static,
385    {
386        self.make_builder().finish(ll_writer)
387    }
388}
389
390/// Logging output destination configuration.
391#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
392#[non_exhaustive]
393#[serde(rename_all = "lowercase", tag = "type")]
394pub enum LoggingDestination {
395    /// Output to standard output (stdout, fd 1).
396    #[default]
397    #[serde(alias = "stdout", alias = "out")]
398    StdOut,
399    /// Output to standard error (stderr, fd 2).
400    #[serde(alias = "stderr", alias = "err")]
401    StdErr,
402    /// Output to file.
403    File(LoggingFileConfig),
404    /// Output to files in a directory with optional rotation.
405    #[serde(alias = "dir")]
406    Directory(LoggingDirectoryConfig),
407}
408
409impl LoggingDestination {
410    /// Make [`tracing_subscriber::fmt::writer::BoxMakeWriter`] from configuration.
411    pub fn make_writer(
412        &self,
413        buf_builder: NonBlockingBuilder,
414    ) -> Result<(BoxMakeWriter, WorkerGuard), LoggingError> {
415        match self {
416            Self::StdOut => {
417                let (wr, wg) = buf_builder.finish(io::stdout());
418                Ok((BoxMakeWriter::new(wr), wg))
419            }
420            Self::StdErr => {
421                let (wr, wg) = buf_builder.finish(io::stderr());
422                Ok((BoxMakeWriter::new(wr), wg))
423            }
424            Self::File(file_cfg) => {
425                let file = fs::OpenOptions::new()
426                    .append(true)
427                    .create(true)
428                    .open(&file_cfg.path)?;
429                let (wr, wg) = buf_builder.finish(file);
430                Ok((BoxMakeWriter::new(wr), wg))
431            }
432            Self::Directory(dir_cfg) => {
433                let mut builder = RollingFileAppender::builder().rotation(dir_cfg.rotate.into());
434                if let Some(prefix) = &dir_cfg.prefix {
435                    builder = builder.filename_prefix(prefix);
436                }
437                if let Some(suffix) = &dir_cfg.suffix {
438                    builder = builder.filename_suffix(suffix);
439                }
440                if let Some(max_files) = dir_cfg.max_files {
441                    builder = builder.max_log_files(max_files);
442                }
443                let appender = builder.build(&dir_cfg.path)?;
444                let (wr, wg) = buf_builder.finish(appender);
445                Ok((BoxMakeWriter::new(wr), wg))
446            }
447        }
448    }
449}
450
451/// Configuration of file output.
452#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
453#[non_exhaustive]
454pub struct LoggingFileConfig {
455    /// Path to file.
456    pub path: String,
457}
458
459/// Configuration of directory output.
460#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
461#[non_exhaustive]
462pub struct LoggingDirectoryConfig {
463    /// Path to directory.
464    #[serde(default = "LoggingDirectoryConfig::default_path")]
465    pub path: String,
466    /// Log rotation configuration.
467    #[serde(default)]
468    pub rotate: LogRotation,
469    /// Template prefix for file names.
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub prefix: Option<String>,
472    /// Template suffix for file names.
473    #[serde(default = "LoggingDirectoryConfig::default_suffix")]
474    pub suffix: Option<String>,
475    /// Maximum amount of files to keep in directory.
476    #[serde(default)]
477    pub max_files: Option<usize>,
478}
479
480impl Default for LoggingDirectoryConfig {
481    fn default() -> Self {
482        Self {
483            path: Self::default_path(),
484            rotate: LogRotation::default(),
485            prefix: None,
486            suffix: Self::default_suffix(),
487            max_files: None,
488        }
489    }
490}
491
492impl LoggingDirectoryConfig {
493    /// Default value for [`Self::path`].
494    #[must_use]
495    #[inline]
496    fn default_path() -> String {
497        ".".into()
498    }
499
500    /// Default value for [`Self::suffix`].
501    #[must_use]
502    #[inline]
503    #[allow(clippy::unnecessary_wraps)]
504    fn default_suffix() -> Option<String> {
505        Some("log".into())
506    }
507}
508
509/// Log rotation configuration.
510#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
511#[non_exhaustive]
512#[serde(rename_all = "UPPERCASE")]
513pub enum LogRotation {
514    /// Rotate logs once every minute.
515    Minutely,
516    /// Rotate logs once every hour.
517    Hourly,
518    /// Rotate logs once every day.
519    #[default]
520    Daily,
521    /// Never rotate logs.
522    Never,
523}
524
525impl From<LogRotation> for Rotation {
526    fn from(value: LogRotation) -> Self {
527        match value {
528            LogRotation::Minutely => Rotation::MINUTELY,
529            LogRotation::Hourly => Rotation::HOURLY,
530            LogRotation::Daily => Rotation::DAILY,
531            LogRotation::Never => Rotation::NEVER,
532        }
533    }
534}