tari_log4rs/append/rolling_file/
mod.rs

1//! A rolling file appender.
2//!
3//! Logging directly to a file can be a dangerous proposition for long running
4//! processes. You wouldn't want to start a server up and find out a couple
5//! weeks later that the disk is filled with hundreds of gigabytes of logs! A
6//! rolling file appender alleviates these issues by limiting the amount of log
7//! data that's preserved.
8//!
9//! Like a normal file appender, a rolling file appender is configured with the
10//! location of its log file and the encoder which formats log events written
11//! to it. In addition, it holds a "policy" object which controls when a log
12//! file is rolled over and how the old files are archived.
13//!
14//! For example, you may configure an appender to roll the log over once it
15//! reaches 50 megabytes, and to preserve the last 10 log files.
16//!
17//! Requires the `rolling_file_appender` feature.
18
19use derivative::Derivative;
20use log::Record;
21use parking_lot::Mutex;
22use std::{
23    fs::{self, File, OpenOptions},
24    io::{self, BufWriter, Write},
25    path::{Path, PathBuf},
26};
27
28#[cfg(feature = "config_parsing")]
29use serde_value::Value;
30#[cfg(feature = "config_parsing")]
31use std::collections::BTreeMap;
32
33use crate::{
34    append::Append,
35    encode::{self, pattern::PatternEncoder, Encode},
36};
37
38#[cfg(feature = "config_parsing")]
39use crate::config::{Deserialize, Deserializers};
40#[cfg(feature = "config_parsing")]
41use crate::encode::EncoderConfig;
42
43pub mod policy;
44
45/// Configuration for the rolling file appender.
46#[cfg(feature = "config_parsing")]
47#[derive(Clone, Eq, PartialEq, Hash, Debug, serde::Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct RollingFileAppenderConfig {
50    path: String,
51    append: Option<bool>,
52    encoder: Option<EncoderConfig>,
53    policy: Policy,
54}
55
56#[cfg(feature = "config_parsing")]
57#[derive(Clone, Eq, PartialEq, Hash, Debug)]
58struct Policy {
59    kind: String,
60    config: Value,
61}
62
63#[cfg(feature = "config_parsing")]
64impl<'de> serde::Deserialize<'de> for Policy {
65    fn deserialize<D>(d: D) -> Result<Policy, D::Error>
66    where
67        D: serde::Deserializer<'de>,
68    {
69        let mut map = BTreeMap::<Value, Value>::deserialize(d)?;
70
71        let kind = match map.remove(&Value::String("kind".to_owned())) {
72            Some(kind) => kind.deserialize_into().map_err(|e| e.to_error())?,
73            None => "compound".to_owned(),
74        };
75
76        Ok(Policy {
77            kind,
78            config: Value::Map(map),
79        })
80    }
81}
82
83#[derive(Debug)]
84struct LogWriter {
85    file: BufWriter<File>,
86    len: u64,
87}
88
89impl io::Write for LogWriter {
90    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
91        self.file.write(buf).map(|n| {
92            self.len += n as u64;
93            n
94        })
95    }
96
97    fn flush(&mut self) -> io::Result<()> {
98        self.file.flush()
99    }
100}
101
102impl encode::Write for LogWriter {}
103
104/// Information about the active log file.
105#[derive(Debug)]
106pub struct LogFile<'a> {
107    writer: &'a mut Option<LogWriter>,
108    path: &'a Path,
109    len: u64,
110}
111
112#[allow(clippy::len_without_is_empty)]
113impl<'a> LogFile<'a> {
114    /// Returns the path to the log file.
115    pub fn path(&self) -> &Path {
116        self.path
117    }
118
119    /// Returns an estimate of the log file's current size.
120    ///
121    /// This is calculated by taking the size of the log file when it is opened
122    /// and adding the number of bytes written. It may be inaccurate if any
123    /// writes have failed or if another process has modified the file
124    /// concurrently.
125    #[deprecated(since = "0.9.1", note = "Please use the len_estimate function instead")]
126    pub fn len(&self) -> u64 {
127        self.len
128    }
129
130    /// Returns an estimate of the log file's current size.
131    ///
132    /// This is calculated by taking the size of the log file when it is opened
133    /// and adding the number of bytes written. It may be inaccurate if any
134    /// writes have failed or if another process has modified the file
135    /// concurrently.
136    pub fn len_estimate(&self) -> u64 {
137        self.len
138    }
139
140    /// Triggers the log file to roll over.
141    ///
142    /// A policy must call this method when it wishes to roll the log. The
143    /// appender's handle to the file will be closed, which is necessary to
144    /// move or delete the file on Windows.
145    ///
146    /// If this method is called, the log file must no longer be present on
147    /// disk when the policy returns.
148    pub fn roll(&mut self) {
149        *self.writer = None;
150    }
151}
152
153/// An appender which archives log files in a configurable strategy.
154#[derive(Derivative)]
155#[derivative(Debug)]
156pub struct RollingFileAppender {
157    #[derivative(Debug = "ignore")]
158    writer: Mutex<Option<LogWriter>>,
159    path: PathBuf,
160    append: bool,
161    encoder: Box<dyn Encode>,
162    policy: Box<dyn policy::Policy>,
163}
164
165impl Append for RollingFileAppender {
166    fn append(&self, record: &Record) -> anyhow::Result<()> {
167        // TODO(eas): Perhaps this is better as a concurrent queue?
168        let mut writer = self.writer.lock();
169
170        let len = {
171            let writer = self.get_writer(&mut writer)?;
172            self.encoder.encode(writer, record)?;
173            writer.flush()?;
174            writer.len
175        };
176
177        let mut file = LogFile {
178            writer: &mut writer,
179            path: &self.path,
180            len,
181        };
182
183        // TODO(eas): Idea: make this optionally return a future, and if so, we initialize a queue for
184        // data that comes in while we are processing the file rotation.
185        self.policy.process(&mut file)
186    }
187
188    fn flush(&self) {}
189}
190
191impl RollingFileAppender {
192    /// Creates a new `RollingFileAppenderBuilder`.
193    pub fn builder() -> RollingFileAppenderBuilder {
194        RollingFileAppenderBuilder {
195            append: true,
196            encoder: None,
197        }
198    }
199
200    fn get_writer<'a>(&self, writer: &'a mut Option<LogWriter>) -> io::Result<&'a mut LogWriter> {
201        if writer.is_none() {
202            let file = OpenOptions::new()
203                .write(true)
204                .append(self.append)
205                .truncate(!self.append)
206                .create(true)
207                .open(&self.path)?;
208            let len = if self.append {
209                file.metadata()?.len()
210            } else {
211                0
212            };
213            *writer = Some(LogWriter {
214                file: BufWriter::with_capacity(1024, file),
215                len,
216            });
217        }
218
219        // :( unwrap
220        Ok(writer.as_mut().unwrap())
221    }
222}
223
224/// A builder for the `RollingFileAppender`.
225pub struct RollingFileAppenderBuilder {
226    append: bool,
227    encoder: Option<Box<dyn Encode>>,
228}
229
230impl RollingFileAppenderBuilder {
231    /// Determines if the appender will append to or truncate the log file.
232    ///
233    /// Defaults to `true`.
234    pub fn append(mut self, append: bool) -> RollingFileAppenderBuilder {
235        self.append = append;
236        self
237    }
238
239    /// Sets the encoder used by the appender.
240    ///
241    /// Defaults to a `PatternEncoder` with the default pattern.
242    pub fn encoder(mut self, encoder: Box<dyn Encode>) -> RollingFileAppenderBuilder {
243        self.encoder = Some(encoder);
244        self
245    }
246
247    /// Constructs a `RollingFileAppender`.
248    /// The path argument can contain environment variables of the form $ENV{name_here},
249    /// where 'name_here' will be the name of the environment variable that
250    /// will be resolved. Note that if the variable fails to resolve,
251    /// $ENV{name_here} will NOT be replaced in the path.
252    pub fn build<P>(
253        self,
254        path: P,
255        policy: Box<dyn policy::Policy>,
256    ) -> io::Result<RollingFileAppender>
257    where
258        P: AsRef<Path>,
259    {
260        let path = super::env_util::expand_env_vars(path.as_ref().to_string_lossy());
261        let appender = RollingFileAppender {
262            writer: Mutex::new(None),
263            path: path.as_ref().into(),
264            append: self.append,
265            encoder: self
266                .encoder
267                .unwrap_or_else(|| Box::new(PatternEncoder::default())),
268            policy,
269        };
270
271        if let Some(parent) = appender.path.parent() {
272            fs::create_dir_all(parent)?;
273        }
274
275        // open the log file immediately
276        appender.get_writer(&mut appender.writer.lock())?;
277
278        Ok(appender)
279    }
280}
281
282/// A deserializer for the `RollingFileAppender`.
283///
284/// # Configuration
285///
286/// ```yaml
287/// kind: rolling_file
288///
289/// # The path of the log file. Required.
290/// # The path can contain environment variables of the form $ENV{name_here},
291/// # where 'name_here' will be the name of the environment variable that
292/// # will be resolved. Note that if the variable fails to resolve,
293/// # $ENV{name_here} will NOT be replaced in the path.
294/// path: log/foo.log
295///
296/// # Specifies if the appender should append to or truncate the log file if it
297/// # already exists. Defaults to `true`.
298/// append: true
299///
300/// # The encoder to use to format output. Defaults to `kind: pattern`.
301/// encoder:
302///   kind: pattern
303///
304/// # The policy which handles rotation of the log file. Required.
305/// policy:
306///   # Identifies which policy is to be used. If no kind is specified, it will
307///   # default to "compound".
308///   kind: compound
309///
310///   # The remainder of the configuration is passed along to the policy's
311///   # deserializer, and will vary based on the kind of policy.
312///   trigger:
313///     kind: size
314///     limit: 10 mb
315///
316///   roller:
317///     kind: delete
318/// ```
319#[cfg(feature = "config_parsing")]
320#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
321pub struct RollingFileAppenderDeserializer;
322
323#[cfg(feature = "config_parsing")]
324impl Deserialize for RollingFileAppenderDeserializer {
325    type Trait = dyn Append;
326
327    type Config = RollingFileAppenderConfig;
328
329    fn deserialize(
330        &self,
331        config: RollingFileAppenderConfig,
332        deserializers: &Deserializers,
333    ) -> anyhow::Result<Box<dyn Append>> {
334        let mut builder = RollingFileAppender::builder();
335        if let Some(append) = config.append {
336            builder = builder.append(append);
337        }
338        if let Some(encoder) = config.encoder {
339            let encoder = deserializers.deserialize(&encoder.kind, encoder.config)?;
340            builder = builder.encoder(encoder);
341        }
342
343        let policy = deserializers.deserialize(&config.policy.kind, config.policy.config)?;
344        let appender = builder.build(config.path, policy)?;
345        Ok(Box::new(appender))
346    }
347}
348
349#[cfg(test)]
350mod test {
351    use std::{
352        fs::File,
353        io::{Read, Write},
354    };
355
356    use super::*;
357    use crate::append::rolling_file::policy::Policy;
358
359    #[test]
360    #[cfg(feature = "yaml_format")]
361    fn deserialize() {
362        use crate::config::{Deserializers, RawConfig};
363
364        let dir = tempfile::tempdir().unwrap();
365
366        let config = format!(
367            "
368appenders:
369  foo:
370    kind: rolling_file
371    path: {0}/foo.log
372    policy:
373      trigger:
374        kind: size
375        limit: 1024
376      roller:
377        kind: delete
378  bar:
379    kind: rolling_file
380    path: {0}/foo.log
381    policy:
382      kind: compound
383      trigger:
384        kind: size
385        limit: 5 mb
386      roller:
387        kind: fixed_window
388        pattern: '{0}/foo.log.{{}}'
389        base: 1
390        count: 5
391",
392            dir.path().display()
393        );
394
395        let config = ::serde_yaml::from_str::<RawConfig>(&config).unwrap();
396        let errors = config.appenders_lossy(&Deserializers::new()).1;
397        println!("{:?}", errors);
398        assert!(errors.is_empty());
399    }
400
401    #[derive(Debug)]
402    struct NopPolicy;
403
404    impl Policy for NopPolicy {
405        fn process(&self, _: &mut LogFile) -> anyhow::Result<()> {
406            Ok(())
407        }
408    }
409
410    #[test]
411    fn append() {
412        let dir = tempfile::tempdir().unwrap();
413        let path = dir.path().join("append.log");
414        RollingFileAppender::builder()
415            .append(true)
416            .build(&path, Box::new(NopPolicy))
417            .unwrap();
418        assert!(path.exists());
419        File::create(&path).unwrap().write_all(b"hello").unwrap();
420
421        RollingFileAppender::builder()
422            .append(true)
423            .build(&path, Box::new(NopPolicy))
424            .unwrap();
425        let mut contents = vec![];
426        File::open(&path)
427            .unwrap()
428            .read_to_end(&mut contents)
429            .unwrap();
430        assert_eq!(contents, b"hello");
431    }
432
433    #[test]
434    fn truncate() {
435        let dir = tempfile::tempdir().unwrap();
436        let path = dir.path().join("truncate.log");
437        RollingFileAppender::builder()
438            .append(false)
439            .build(&path, Box::new(NopPolicy))
440            .unwrap();
441        assert!(path.exists());
442        File::create(&path).unwrap().write_all(b"hello").unwrap();
443
444        RollingFileAppender::builder()
445            .append(false)
446            .build(&path, Box::new(NopPolicy))
447            .unwrap();
448        let mut contents = vec![];
449        File::open(&path)
450            .unwrap()
451            .read_to_end(&mut contents)
452            .unwrap();
453        assert_eq!(contents, b"");
454    }
455}