Skip to main content

tracing_appender/rolling/
builder.rs

1use super::{RollingFileAppender, Rotation};
2use std::{io, path::Path};
3use thiserror::Error;
4
5/// A [builder] for configuring [`RollingFileAppender`]s.
6///
7/// [builder]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html
8#[derive(Debug)]
9pub struct Builder {
10    pub(super) rotation: Rotation,
11    pub(super) prefix: Option<String>,
12    pub(super) suffix: Option<String>,
13    pub(super) latest_symlink: Option<String>,
14    pub(super) max_files: Option<usize>,
15}
16
17/// Errors returned by [`Builder::build`].
18#[derive(Error, Debug)]
19#[error("{context}: {source}")]
20pub struct InitError {
21    context: &'static str,
22    #[source]
23    source: io::Error,
24}
25
26impl InitError {
27    pub(crate) fn ctx(context: &'static str) -> impl FnOnce(io::Error) -> Self {
28        move |source| Self { context, source }
29    }
30}
31
32impl Builder {
33    /// Returns a new `Builder` for configuring a [`RollingFileAppender`], with
34    /// the default parameters.
35    ///
36    /// # Default Values
37    ///
38    /// The default values for the builder are:
39    ///
40    /// | Parameter | Default Value | Notes |
41    /// | :-------- | :------------ | :---- |
42    /// | [`rotation`] | [`Rotation::NEVER`] | By default, log files will never be rotated. |
43    /// | [`filename_prefix`] | `""` | By default, log file names will not have a prefix. |
44    /// | [`filename_suffix`] | `""` | By default, log file names will not have a suffix. |
45    /// | [`max_log_files`] | `None` | By default, there is no limit for maximum log file count. |
46    ///
47    /// [`rotation`]: Self::rotation
48    /// [`filename_prefix`]: Self::filename_prefix
49    /// [`filename_suffix`]: Self::filename_suffix
50    /// [`max_log_files`]: Self::max_log_files
51    #[must_use]
52    pub const fn new() -> Self {
53        Self {
54            rotation: Rotation::NEVER,
55            prefix: None,
56            suffix: None,
57            latest_symlink: None,
58            max_files: None,
59        }
60    }
61
62    /// Sets the [rotation strategy] for log files.
63    ///
64    /// By default, this is [`Rotation::NEVER`].
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// # fn docs() {
70    /// use tracing_appender::rolling::{Rotation, RollingFileAppender};
71    ///
72    /// let appender = RollingFileAppender::builder()
73    ///     .rotation(Rotation::HOURLY) // rotate log files once every hour
74    ///     // ...
75    ///     .build("/var/log")
76    ///     .expect("failed to initialize rolling file appender");
77    ///
78    /// # drop(appender)
79    /// # }
80    /// ```
81    ///
82    /// [rotation strategy]: Rotation
83    #[must_use]
84    pub fn rotation(self, rotation: Rotation) -> Self {
85        Self { rotation, ..self }
86    }
87
88    /// Sets the prefix for log filenames. The prefix is output before the
89    /// timestamp in the file name, and if it is non-empty, it is followed by a
90    /// dot (`.`).
91    ///
92    /// By default, log files do not have a prefix.
93    ///
94    /// # Examples
95    ///
96    /// Setting a prefix:
97    ///
98    /// ```
99    /// use tracing_appender::rolling::RollingFileAppender;
100    ///
101    /// # fn docs() {
102    /// let appender = RollingFileAppender::builder()
103    ///     .filename_prefix("myapp.log") // log files will have names like "myapp.log.2019-01-01"
104    ///     // ...
105    ///     .build("/var/log")
106    ///     .expect("failed to initialize rolling file appender");
107    /// # drop(appender)
108    /// # }
109    /// ```
110    ///
111    /// No prefix:
112    ///
113    /// ```
114    /// use tracing_appender::rolling::RollingFileAppender;
115    ///
116    /// # fn docs() {
117    /// let appender = RollingFileAppender::builder()
118    ///     .filename_prefix("") // log files will have names like "2019-01-01"
119    ///     // ...
120    ///     .build("/var/log")
121    ///     .expect("failed to initialize rolling file appender");
122    /// # drop(appender)
123    /// # }
124    /// ```
125    ///
126    /// [rotation strategy]: Rotation
127    #[must_use]
128    pub fn filename_prefix(self, prefix: impl Into<String>) -> Self {
129        let prefix = prefix.into();
130        // If the configured prefix is the empty string, then don't include a
131        // separator character.
132        let prefix = if prefix.is_empty() {
133            None
134        } else {
135            Some(prefix)
136        };
137        Self { prefix, ..self }
138    }
139
140    /// Sets the suffix for log filenames. The suffix is output after the
141    /// timestamp in the file name, and if it is non-empty, it is preceded by a
142    /// dot (`.`).
143    ///
144    /// By default, log files do not have a suffix.
145    ///
146    /// # Examples
147    ///
148    /// Setting a suffix:
149    ///
150    /// ```
151    /// use tracing_appender::rolling::RollingFileAppender;
152    ///
153    /// # fn docs() {
154    /// let appender = RollingFileAppender::builder()
155    ///     .filename_suffix("myapp.log") // log files will have names like "2019-01-01.myapp.log"
156    ///     // ...
157    ///     .build("/var/log")
158    ///     .expect("failed to initialize rolling file appender");
159    /// # drop(appender)
160    /// # }
161    /// ```
162    ///
163    /// No suffix:
164    ///
165    /// ```
166    /// use tracing_appender::rolling::RollingFileAppender;
167    ///
168    /// # fn docs() {
169    /// let appender = RollingFileAppender::builder()
170    ///     .filename_suffix("") // log files will have names like "2019-01-01"
171    ///     // ...
172    ///     .build("/var/log")
173    ///     .expect("failed to initialize rolling file appender");
174    /// # drop(appender)
175    /// # }
176    /// ```
177    ///
178    /// [rotation strategy]: Rotation
179    #[must_use]
180    pub fn filename_suffix(self, suffix: impl Into<String>) -> Self {
181        let suffix = suffix.into();
182        // If the configured suffix is the empty string, then don't include a
183        // separator character.
184        let suffix = if suffix.is_empty() {
185            None
186        } else {
187            Some(suffix)
188        };
189        Self { suffix, ..self }
190    }
191
192    /// Keeps the last `n` log files on disk.
193    ///
194    /// When constructing a [`RollingFileAppender`] or starting a new log file,
195    /// the appender will delete the oldest matching log files until at most `n`
196    /// files remain. The exact number of retained files can sometimes dip below
197    /// the maximum, so if you need to retain `m` log files, specify a max of
198    /// `m + 1`.
199    ///
200    /// If `0` is supplied, the [`RollingFileAppender`] will not remove any files.
201    ///
202    /// Files are considered candidates for deletion based on the following
203    /// criteria:
204    ///
205    /// * The file must not be a directory or symbolic link.
206    /// * If the appender is configured with a [`filename_prefix`], the file
207    ///   name must start with that prefix.
208    /// * If the appender is configured with a [`filename_suffix`], the file
209    ///   name must end with that suffix.
210    /// * If the appender has neither a filename prefix nor a suffix, then the
211    ///   file name must parse as a valid date based on the appender's date
212    ///   format.
213    ///
214    /// Files matching these criteria may be deleted if the maximum number of
215    /// log files in the directory has been reached.
216    ///
217    /// [`filename_prefix`]: Self::filename_prefix
218    /// [`filename_suffix`]: Self::filename_suffix
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// use tracing_appender::rolling::RollingFileAppender;
224    ///
225    /// # fn docs() {
226    /// let appender = RollingFileAppender::builder()
227    ///     .max_log_files(5) // only the most recent 5 log files will be kept
228    ///     // ...
229    ///     .build("/var/log")
230    ///     .expect("failed to initialize rolling file appender");
231    /// # drop(appender)
232    /// # }
233    /// ```
234    #[must_use]
235    pub fn max_log_files(self, n: usize) -> Self {
236        Self {
237            // Setting `n` to 0 will disable the max files (effectively make it infinite).
238            max_files: Some(n).filter(|&n| n > 0),
239            ..self
240        }
241    }
242
243    /// Create a symbolic link that points to the latest log file.
244    /// The symbolic link will be updated when new log files are created.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// use tracing_appender::rolling::RollingFileAppender;
250    ///
251    /// # fn docs() {
252    /// let appender = RollingFileAppender::builder()
253    ///     .latest_symlink("log.latest")
254    ///     // ...
255    ///     .build("/var/log")
256    ///     .expect("failed to initialize rolling file appender");
257    /// # drop(appender)
258    /// # }
259    /// ```
260    #[must_use]
261    pub fn latest_symlink(self, name: impl Into<String>) -> Self {
262        let name = name.into();
263        let latest_symlink = if name.is_empty() { None } else { Some(name) };
264        Self {
265            latest_symlink,
266            ..self
267        }
268    }
269
270    /// Builds a new [`RollingFileAppender`] with the configured parameters,
271    /// emitting log files to the provided directory.
272    ///
273    /// Unlike [`RollingFileAppender::new`], this returns a `Result` rather than
274    /// panicking when the appender cannot be initialized.
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// use tracing_appender::rolling::{Rotation, RollingFileAppender};
280    ///
281    /// # fn docs() {
282    /// let appender = RollingFileAppender::builder()
283    ///     .rotation(Rotation::DAILY) // rotate log files once per day
284    ///     .filename_prefix("myapp.log") // log files will have names like "myapp.log.2019-01-01"
285    ///     .build("/var/log/myapp") // write log files to the '/var/log/myapp' directory
286    ///     .expect("failed to initialize rolling file appender");
287    /// # drop(appender);
288    /// # }
289    /// ```
290    ///
291    /// This is equivalent to
292    /// ```
293    /// # fn docs() {
294    /// let appender = tracing_appender::rolling::daily("myapp.log", "/var/log/myapp");
295    /// # drop(appender);
296    /// # }
297    /// ```
298    pub fn build(&self, directory: impl AsRef<Path>) -> Result<RollingFileAppender, InitError> {
299        RollingFileAppender::from_builder(self, directory)
300    }
301}
302
303impl Default for Builder {
304    fn default() -> Self {
305        Self::new()
306    }
307}