Skip to main content

rustic_rs/config/
logging.rs

1use std::{path::PathBuf, sync::OnceLock};
2
3use anyhow::Result;
4use clap::{Parser, ValueHint};
5use conflate::Merge;
6use log::LevelFilter;
7use log4rs::{
8    Handle,
9    append::{
10        console::{ConsoleAppender, Target},
11        file::FileAppender,
12    },
13    config::{Appender, Config, Logger, Root},
14    encode::pattern::PatternEncoder,
15    filter::threshold::ThresholdFilter,
16};
17use serde::{Deserialize, Serialize};
18use serde_with::{DisplayFromStr, serde_as};
19
20use crate::config::progress_options::multi_progress;
21
22/// Logging Config
23#[serde_as]
24#[derive(Default, Debug, Parser, Clone, Deserialize, Serialize, Merge)]
25#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
26pub struct LoggingOptions {
27    /// Use this log level [default: info]
28    #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL",
29        value_parser(["off", "error", "warn", "info", "debug", "trace"]))]
30    #[serde_as(as = "Option<DisplayFromStr>")]
31    #[merge(strategy=conflate::option::overwrite_none)]
32    pub log_level: Option<String>,
33
34    /// Use this log level for the log file [default: info]
35    #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL_LOGFILE",
36        value_parser(["off", "error", "warn", "info", "debug", "trace"]))]
37    #[merge(strategy=conflate::option::overwrite_none)]
38    pub log_level_logfile: Option<String>,
39
40    /// Use this log level in dry-run mode [default: info]
41    #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL_DRYRUN",
42        value_parser(["off", "error", "warn", "info", "debug", "trace"]))]
43    #[merge(strategy=conflate::option::overwrite_none)]
44    pub log_level_dryrun: Option<String>,
45
46    /// Use this log level for dependencies [default: warn]
47    #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL_DEPENDENCIES",
48        value_parser(["off", "error", "warn", "info", "debug", "trace"]))]
49    #[merge(strategy=conflate::option::overwrite_none)]
50    pub log_level_dependencies: Option<String>,
51
52    /// Write log messages to the given file (using log-level-logfile)
53    #[clap(long, global = true, env = "RUSTIC_LOG_FILE", value_name = "LOGFILE", value_hint = ValueHint::FilePath)]
54    #[merge(strategy=conflate::option::overwrite_none)]
55    pub log_file: Option<PathBuf>,
56}
57
58impl LoggingOptions {
59    pub fn config(&self, dry_run: bool) -> Result<Config> {
60        let log_level = if dry_run {
61            &self.log_level_dryrun
62        } else {
63            &self.log_level
64        };
65
66        let level_filter = log_level
67            .as_ref()
68            .map_or(LevelFilter::Info, |l| l.parse().unwrap());
69        let level_filter_logfile = self
70            .log_level_logfile
71            .as_ref()
72            .map_or(LevelFilter::Info, |l| l.parse().unwrap());
73        let level_filter_dependencies = self
74            .log_level_dependencies
75            .as_ref()
76            .map_or(LevelFilter::Warn, |l| l.parse().unwrap());
77
78        let stdout = ConsoleAppender::builder()
79            .target(Target::Stderr)
80            .encoder(Box::new(PatternEncoder::new("{h([{l}])} {m}{n}")))
81            .build();
82        let stdout = PbPauseAppender(stdout);
83
84        let mut root_builder = Root::builder().appender("stdout");
85        let mut config_builder = Config::builder().appender(
86            Appender::builder()
87                .filter(Box::new(ThresholdFilter::new(level_filter)))
88                .build("stdout", Box::new(stdout)),
89        );
90
91        if let Some(file) = &self.log_file {
92            let file_appender = FileAppender::builder()
93                .encoder(Box::new(PatternEncoder::new("{d} [{l}] - {m}{n}")))
94                .build(file)?;
95            root_builder = root_builder.appender("logfile");
96            config_builder = config_builder.appender(
97                Appender::builder()
98                    .filter(Box::new(ThresholdFilter::new(level_filter_logfile)))
99                    .build("logfile", Box::new(file_appender)),
100            );
101        }
102
103        let root = root_builder.build(level_filter_dependencies);
104        let config = config_builder
105            .logger(Logger::builder().build("rustic_rs", LevelFilter::Trace))
106            .logger(Logger::builder().build("rustic_core", LevelFilter::Trace))
107            .logger(Logger::builder().build("rustic_backend", LevelFilter::Trace))
108            .build(root)?;
109        Ok(config)
110    }
111
112    pub fn start_logger(&self, dry_run: bool) -> Result<()> {
113        static HANDLE: OnceLock<Handle> = OnceLock::new();
114
115        let config = self.config(dry_run)?;
116        if let Some(handle) = HANDLE.get() {
117            handle.set_config(config);
118        } else {
119            let handle = log4rs::init_config(config)?;
120            _ = HANDLE.set(handle);
121        }
122        Ok(())
123    }
124}
125
126/// A wrapper around [`ConsoleAppender`] that suspends the progress bar when writing logs.
127#[derive(Debug)]
128struct PbPauseAppender(ConsoleAppender);
129
130impl log4rs::append::Append for PbPauseAppender {
131    fn append(&self, record: &log::Record<'_>) -> Result<()> {
132        multi_progress().suspend(|| self.0.append(record))
133    }
134
135    fn flush(&self) {
136        // as of log4rs 1.4.0, <ConsoleAppender as Append>::flush does nothing,
137        // so we do not need to pause the progress bar here. In the future,
138        // if log4rs changes this behavior, we might need to add a suspend here.
139        // But that's not necessary right now, so we just call flush directly
140        // to avoid unnecessary suspends.
141        self.0.flush();
142    }
143}