tari_log4rs/append/
file.rs

1//! The file appender.
2//!
3//! Requires the `file_appender` feature.
4
5use derivative::Derivative;
6use log::Record;
7use parking_lot::Mutex;
8use std::{
9    fs::{self, File, OpenOptions},
10    io::{self, BufWriter, Write},
11    path::{Path, PathBuf},
12};
13
14#[cfg(feature = "config_parsing")]
15use crate::config::{Deserialize, Deserializers};
16#[cfg(feature = "config_parsing")]
17use crate::encode::EncoderConfig;
18
19use crate::{
20    append::{env_util::expand_env_vars, Append},
21    encode::{pattern::PatternEncoder, writer::simple::SimpleWriter, Encode},
22};
23
24/// The file appender's configuration.
25#[cfg(feature = "config_parsing")]
26#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, serde::Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct FileAppenderConfig {
29    path: String,
30    encoder: Option<EncoderConfig>,
31    append: Option<bool>,
32}
33
34/// An appender which logs to a file.
35#[derive(Derivative)]
36#[derivative(Debug)]
37pub struct FileAppender {
38    path: PathBuf,
39    #[derivative(Debug = "ignore")]
40    file: Mutex<SimpleWriter<BufWriter<File>>>,
41    encoder: Box<dyn Encode>,
42}
43
44impl Append for FileAppender {
45    fn append(&self, record: &Record) -> anyhow::Result<()> {
46        let mut file = self.file.lock();
47        self.encoder.encode(&mut *file, record)?;
48        file.flush()?;
49        Ok(())
50    }
51
52    fn flush(&self) {}
53}
54
55impl FileAppender {
56    /// Creates a new `FileAppender` builder.
57    pub fn builder() -> FileAppenderBuilder {
58        FileAppenderBuilder {
59            encoder: None,
60            append: true,
61        }
62    }
63}
64
65/// A builder for `FileAppender`s.
66pub struct FileAppenderBuilder {
67    encoder: Option<Box<dyn Encode>>,
68    append: bool,
69}
70
71impl FileAppenderBuilder {
72    /// Sets the output encoder for the `FileAppender`.
73    pub fn encoder(mut self, encoder: Box<dyn Encode>) -> FileAppenderBuilder {
74        self.encoder = Some(encoder);
75        self
76    }
77
78    /// Determines if the appender will append to or truncate the output file.
79    ///
80    /// Defaults to `true`.
81    pub fn append(mut self, append: bool) -> FileAppenderBuilder {
82        self.append = append;
83        self
84    }
85
86    /// Consumes the `FileAppenderBuilder`, producing a `FileAppender`.
87    /// The path argument can contain environment variables of the form $ENV{name_here},
88    /// where 'name_here' will be the name of the environment variable that
89    /// will be resolved. Note that if the variable fails to resolve,
90    /// $ENV{name_here} will NOT be replaced in the path.
91    pub fn build<P: AsRef<Path>>(self, path: P) -> io::Result<FileAppender> {
92        let path_cow = path.as_ref().to_string_lossy();
93        let path: PathBuf = expand_env_vars(path_cow).as_ref().into();
94        if let Some(parent) = path.parent() {
95            fs::create_dir_all(parent)?;
96        }
97        let file = OpenOptions::new()
98            .write(true)
99            .append(self.append)
100            .truncate(!self.append)
101            .create(true)
102            .open(&path)?;
103
104        Ok(FileAppender {
105            path,
106            file: Mutex::new(SimpleWriter(BufWriter::with_capacity(1024, file))),
107            encoder: self
108                .encoder
109                .unwrap_or_else(|| Box::new(PatternEncoder::default())),
110        })
111    }
112}
113
114/// A deserializer for the `FileAppender`.
115///
116/// # Configuration
117///
118/// ```yaml
119/// kind: file
120///
121/// # The path of the log file. Required.
122/// # The path can contain environment variables of the form $ENV{name_here},
123/// # where 'name_here' will be the name of the environment variable that
124/// # will be resolved. Note that if the variable fails to resolve,
125/// # $ENV{name_here} will NOT be replaced in the path.
126/// path: log/foo.log
127///
128/// # Specifies if the appender should append to or truncate the log file if it
129/// # already exists. Defaults to `true`.
130/// append: true
131///
132/// # The encoder to use to format output. Defaults to `kind: pattern`.
133/// encoder:
134///   kind: pattern
135/// ```
136#[cfg(feature = "config_parsing")]
137#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
138pub struct FileAppenderDeserializer;
139
140#[cfg(feature = "config_parsing")]
141impl Deserialize for FileAppenderDeserializer {
142    type Trait = dyn Append;
143
144    type Config = FileAppenderConfig;
145
146    fn deserialize(
147        &self,
148        config: FileAppenderConfig,
149        deserializers: &Deserializers,
150    ) -> anyhow::Result<Box<Self::Trait>> {
151        let mut appender = FileAppender::builder();
152        if let Some(append) = config.append {
153            appender = appender.append(append);
154        }
155        if let Some(encoder) = config.encoder {
156            appender = appender.encoder(deserializers.deserialize(&encoder.kind, encoder.config)?);
157        }
158        Ok(Box::new(appender.build(&config.path)?))
159    }
160}
161
162#[cfg(test)]
163mod test {
164    use super::*;
165
166    #[test]
167    fn create_directories() {
168        let tempdir = tempfile::tempdir().unwrap();
169
170        FileAppender::builder()
171            .build(tempdir.path().join("foo").join("bar").join("baz.log"))
172            .unwrap();
173    }
174
175    #[test]
176    fn append_false() {
177        let tempdir = tempfile::tempdir().unwrap();
178        FileAppender::builder()
179            .append(false)
180            .build(tempdir.path().join("foo.log"))
181            .unwrap();
182    }
183}