tari_log4rs/config/
file.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4    thread,
5    time::{Duration, SystemTime},
6};
7
8use thiserror::Error;
9
10use super::{init_config, Config, Deserializers, Handle, RawConfig};
11use crate::handle_error;
12
13/// Initializes the global logger as a log4rs logger configured via a file.
14///
15/// Configuration is read from a file located at the provided path on the
16/// filesystem and components are created from the provided `Deserializers`.
17///
18/// Any nonfatal errors encountered when processing the configuration are
19/// reported to stderr.
20///
21/// Requires the `file` feature (enabled by default).
22pub fn init_file<P>(path: P, deserializers: Deserializers) -> anyhow::Result<()>
23where
24    P: AsRef<Path>,
25{
26    let path = path.as_ref().to_path_buf();
27    let format = Format::from_path(&path)?;
28    let source = read_config(&path)?;
29    // An Err here could come because mtime isn't available, so don't bail
30    let modified = fs::metadata(&path).and_then(|m| m.modified()).ok();
31    let config = format.parse(&source)?;
32
33    let refresh_rate = config.refresh_rate();
34    let config = deserialize(&config, &deserializers);
35
36    match init_config(config) {
37        Ok(handle) => {
38            if let Some(refresh_rate) = refresh_rate {
39                ConfigReloader::start(
40                    path,
41                    format,
42                    refresh_rate,
43                    source,
44                    modified,
45                    deserializers,
46                    handle,
47                );
48            }
49            Ok(())
50        }
51        Err(e) => Err(e.into()),
52    }
53}
54
55/// Loads a log4rs logger configuration from a file.
56///
57/// Unlike `init_file`, this function does not initialize the logger; it only
58/// loads the `Config` and returns it.
59pub fn load_config_file<P>(path: P, deserializers: Deserializers) -> anyhow::Result<Config>
60where
61    P: AsRef<Path>,
62{
63    let path = path.as_ref();
64    let format = Format::from_path(path)?;
65    let source = read_config(path)?;
66    let config = format.parse(&source)?;
67
68    Ok(deserialize(&config, &deserializers))
69}
70
71/// The various types of formatting errors that can be generated.
72#[derive(Debug, Error)]
73pub enum FormatError {
74    /// The YAML feature flag was missing.
75    #[error("the `yaml_format` feature is required for YAML support")]
76    YamlFeatureFlagRequired,
77
78    /// The JSON feature flag was missing.
79    #[error("the `json_format` feature is required for JSON support")]
80    JsonFeatureFlagRequired,
81
82    /// The TOML feature flag was missing.
83    #[error("the `toml_format` feature is required for TOML support")]
84    TomlFeatureFlagRequired,
85
86    /// An unsupported format was specified.
87    #[error("unsupported file format `{0}`")]
88    UnsupportedFormat(String),
89
90    /// Log4rs could not determine the file format.
91    #[error("unable to determine the file format")]
92    UnknownFormat,
93}
94
95#[derive(Debug)]
96enum Format {
97    #[cfg(feature = "yaml_format")]
98    Yaml,
99    #[cfg(feature = "json_format")]
100    Json,
101    #[cfg(feature = "toml_format")]
102    Toml,
103}
104
105impl Format {
106    fn from_path(path: &Path) -> anyhow::Result<Format> {
107        match path.extension().and_then(|s| s.to_str()) {
108            #[cfg(feature = "yaml_format")]
109            Some("yaml") | Some("yml") => Ok(Format::Yaml),
110            #[cfg(not(feature = "yaml_format"))]
111            Some("yaml") | Some("yml") => Err(FormatError::YamlFeatureFlagRequired.into()),
112
113            #[cfg(feature = "json_format")]
114            Some("json") => Ok(Format::Json),
115            #[cfg(not(feature = "json_format"))]
116            Some("json") => Err(FormatError::JsonFeatureFlagRequired.into()),
117
118            #[cfg(feature = "toml_format")]
119            Some("toml") => Ok(Format::Toml),
120            #[cfg(not(feature = "toml_format"))]
121            Some("toml") => Err(FormatError::TomlFeatureFlagRequired.into()),
122
123            Some(f) => Err(FormatError::UnsupportedFormat(f.to_string()).into()),
124            None => Err(FormatError::UnknownFormat.into()),
125        }
126    }
127
128    #[allow(unused_variables)]
129    fn parse(&self, source: &str) -> anyhow::Result<RawConfig> {
130        match *self {
131            #[cfg(feature = "yaml_format")]
132            Format::Yaml => ::serde_yaml::from_str(source).map_err(Into::into),
133            #[cfg(feature = "json_format")]
134            Format::Json => ::serde_json::from_str(source).map_err(Into::into),
135            #[cfg(feature = "toml_format")]
136            Format::Toml => ::toml::from_str(source).map_err(Into::into),
137        }
138    }
139}
140
141fn read_config(path: &Path) -> anyhow::Result<String> {
142    let s = fs::read_to_string(path)?;
143    Ok(s)
144}
145
146fn deserialize(config: &RawConfig, deserializers: &Deserializers) -> Config {
147    let (appenders, mut errors) = config.appenders_lossy(deserializers);
148    errors.handle();
149
150    let (config, mut errors) = Config::builder()
151        .appenders(appenders)
152        .loggers(config.loggers())
153        .build_lossy(config.root());
154
155    errors.handle();
156
157    config
158}
159
160struct ConfigReloader {
161    path: PathBuf,
162    format: Format,
163    source: String,
164    modified: Option<SystemTime>,
165    deserializers: Deserializers,
166    handle: Handle,
167}
168
169impl ConfigReloader {
170    fn start(
171        path: PathBuf,
172        format: Format,
173        rate: Duration,
174        source: String,
175        modified: Option<SystemTime>,
176        deserializers: Deserializers,
177        handle: Handle,
178    ) {
179        let mut reloader = ConfigReloader {
180            path,
181            format,
182            source,
183            modified,
184            deserializers,
185            handle,
186        };
187
188        thread::Builder::new()
189            .name("log4rs refresh".to_owned())
190            .spawn(move || reloader.run(rate))
191            .unwrap();
192    }
193
194    fn run(&mut self, mut rate: Duration) {
195        loop {
196            thread::sleep(rate);
197
198            match self.run_once(rate) {
199                Ok(Some(r)) => rate = r,
200                Ok(None) => break,
201                Err(e) => handle_error(&e),
202            }
203        }
204    }
205
206    fn run_once(&mut self, rate: Duration) -> anyhow::Result<Option<Duration>> {
207        if let Some(last_modified) = self.modified {
208            let modified = fs::metadata(&self.path).and_then(|m| m.modified())?;
209            if last_modified == modified {
210                return Ok(Some(rate));
211            }
212
213            self.modified = Some(modified);
214        }
215
216        let source = read_config(&self.path)?;
217
218        if source == self.source {
219            return Ok(Some(rate));
220        }
221
222        self.source = source;
223
224        let config = self.format.parse(&self.source)?;
225        let rate = config.refresh_rate();
226        let config = deserialize(&config, &self.deserializers);
227
228        self.handle.set_config(config);
229
230        Ok(rate)
231    }
232}