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
20/// Logging Config
21#[serde_as]
22#[derive(Default, Debug, Parser, Clone, Deserialize, Serialize, Merge)]
23#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
24pub struct LoggingOptions {
25    /// Use this log level [default: info]
26    #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL",
27        value_parser(["off", "error", "warn", "info", "debug", "trace"]))]
28    #[serde_as(as = "Option<DisplayFromStr>")]
29    #[merge(strategy=conflate::option::overwrite_none)]
30    pub log_level: Option<String>,
31
32    /// Use this log level for the log file [default: info]
33    #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL_LOGFILE",
34        value_parser(["off", "error", "warn", "info", "debug", "trace"]))]
35    #[merge(strategy=conflate::option::overwrite_none)]
36    pub log_level_logfile: Option<String>,
37
38    /// Use this log level in dry-run mode [default: info]
39    #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL_DRYRUN",
40        value_parser(["off", "error", "warn", "info", "debug", "trace"]))]
41    #[merge(strategy=conflate::option::overwrite_none)]
42    pub log_level_dryrun: Option<String>,
43
44    /// Use this log level for dependencies [default: warn]
45    #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL_DEPENDENCIES",
46        value_parser(["off", "error", "warn", "info", "debug", "trace"]))]
47    #[merge(strategy=conflate::option::overwrite_none)]
48    pub log_level_dependencies: Option<String>,
49
50    /// Write log messages to the given file (using log-level-logfile)
51    #[clap(long, global = true, env = "RUSTIC_LOG_FILE", value_name = "LOGFILE", value_hint = ValueHint::FilePath)]
52    #[merge(strategy=conflate::option::overwrite_none)]
53    pub log_file: Option<PathBuf>,
54}
55
56impl LoggingOptions {
57    pub fn config(&self, dry_run: bool) -> Result<Config> {
58        let log_level = if dry_run {
59            &self.log_level_dryrun
60        } else {
61            &self.log_level
62        };
63
64        let level_filter = log_level
65            .as_ref()
66            .map_or(LevelFilter::Info, |l| l.parse().unwrap());
67        let level_filter_logfile = self
68            .log_level_logfile
69            .as_ref()
70            .map_or(LevelFilter::Info, |l| l.parse().unwrap());
71        let level_filter_dependencies = self
72            .log_level_dependencies
73            .as_ref()
74            .map_or(LevelFilter::Warn, |l| l.parse().unwrap());
75
76        let stdout = ConsoleAppender::builder()
77            .target(Target::Stderr)
78            .encoder(Box::new(PatternEncoder::new("{h([{l}])} {m}{n}")))
79            .build();
80
81        let mut root_builder = Root::builder().appender("stdout");
82        let mut config_builder = Config::builder().appender(
83            Appender::builder()
84                .filter(Box::new(ThresholdFilter::new(level_filter)))
85                .build("stdout", Box::new(stdout)),
86        );
87
88        if let Some(file) = &self.log_file {
89            let file_appender = FileAppender::builder()
90                .encoder(Box::new(PatternEncoder::new("{d} [{l}] - {m}{n}")))
91                .build(file)?;
92            root_builder = root_builder.appender("logfile");
93            config_builder = config_builder.appender(
94                Appender::builder()
95                    .filter(Box::new(ThresholdFilter::new(level_filter_logfile)))
96                    .build("logfile", Box::new(file_appender)),
97            );
98        }
99
100        let root = root_builder.build(level_filter_dependencies);
101        let config = config_builder
102            .logger(Logger::builder().build("rustic_rs", LevelFilter::Trace))
103            .logger(Logger::builder().build("rustic_core", LevelFilter::Trace))
104            .logger(Logger::builder().build("rustic_backend", LevelFilter::Trace))
105            .build(root)?;
106        Ok(config)
107    }
108
109    pub fn start_logger(&self, dry_run: bool) -> Result<()> {
110        static HANDLE: OnceLock<Handle> = OnceLock::new();
111
112        let config = self.config(dry_run)?;
113        if let Some(handle) = HANDLE.get() {
114            handle.set_config(config);
115        } else {
116            let handle = log4rs::init_config(config)?;
117            _ = HANDLE.set(handle);
118        }
119        Ok(())
120    }
121}