rustic_rs/config/
logging.rs1use 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#[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 #[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 #[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 #[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 #[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 #[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}