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}