/rust/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-appender-0.2.3/src/rolling.rs
Line | Count | Source |
1 | | //! A rolling file appender. |
2 | | //! |
3 | | //! Creates a new log file at a fixed frequency as defined by [`Rotation`][self::Rotation]. |
4 | | //! Logs will be written to this file for the duration of the period and will automatically roll over |
5 | | //! to the newly created log file once the time period has elapsed. |
6 | | //! |
7 | | //! The log file is created at the specified directory and file name prefix which *may* be appended with |
8 | | //! the date and time. |
9 | | //! |
10 | | //! The following helpers are available for creating a rolling file appender. |
11 | | //! |
12 | | //! - [`Rotation::minutely()`][minutely]: A new log file in the format of `some_directory/log_file_name_prefix.yyyy-MM-dd-HH-mm` |
13 | | //! will be created minutely (once per minute) |
14 | | //! - [`Rotation::hourly()`][hourly]: A new log file in the format of `some_directory/log_file_name_prefix.yyyy-MM-dd-HH` |
15 | | //! will be created hourly |
16 | | //! - [`Rotation::daily()`][daily]: A new log file in the format of `some_directory/log_file_name_prefix.yyyy-MM-dd` |
17 | | //! will be created daily |
18 | | //! - [`Rotation::never()`][never()]: This will result in log file located at `some_directory/log_file_name` |
19 | | //! |
20 | | //! |
21 | | //! # Examples |
22 | | //! |
23 | | //! ```rust |
24 | | //! # fn docs() { |
25 | | //! use tracing_appender::rolling::{RollingFileAppender, Rotation}; |
26 | | //! let file_appender = RollingFileAppender::new(Rotation::HOURLY, "/some/directory", "prefix.log"); |
27 | | //! # } |
28 | | //! ``` |
29 | | use crate::sync::{RwLock, RwLockReadGuard}; |
30 | | use std::{ |
31 | | fmt::{self, Debug}, |
32 | | fs::{self, File, OpenOptions}, |
33 | | io::{self, Write}, |
34 | | path::{Path, PathBuf}, |
35 | | sync::atomic::{AtomicUsize, Ordering}, |
36 | | }; |
37 | | use time::{format_description, Date, Duration, OffsetDateTime, Time}; |
38 | | |
39 | | mod builder; |
40 | | pub use builder::{Builder, InitError}; |
41 | | |
42 | | /// A file appender with the ability to rotate log files at a fixed schedule. |
43 | | /// |
44 | | /// `RollingFileAppender` implements the [`std:io::Write` trait][write] and will |
45 | | /// block on write operations. It may be used with [`NonBlocking`] to perform |
46 | | /// writes without blocking the current thread. |
47 | | /// |
48 | | /// Additionally, `RollingFileAppender` also implements the [`MakeWriter`] |
49 | | /// trait from `tracing-subscriber`, so it may also be used |
50 | | /// directly, without [`NonBlocking`]. |
51 | | /// |
52 | | /// [write]: std::io::Write |
53 | | /// [`NonBlocking`]: super::non_blocking::NonBlocking |
54 | | /// |
55 | | /// # Examples |
56 | | /// |
57 | | /// Rolling a log file once every hour: |
58 | | /// |
59 | | /// ```rust |
60 | | /// # fn docs() { |
61 | | /// let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix"); |
62 | | /// # } |
63 | | /// ``` |
64 | | /// |
65 | | /// Combining a `RollingFileAppender` with another [`MakeWriter`] implementation: |
66 | | /// |
67 | | /// ```rust |
68 | | /// # fn docs() { |
69 | | /// use tracing_subscriber::fmt::writer::MakeWriterExt; |
70 | | /// |
71 | | /// // Log all events to a rolling log file. |
72 | | /// let logfile = tracing_appender::rolling::hourly("/logs", "myapp-logs"); |
73 | | |
74 | | /// // Log `INFO` and above to stdout. |
75 | | /// let stdout = std::io::stdout.with_max_level(tracing::Level::INFO); |
76 | | /// |
77 | | /// tracing_subscriber::fmt() |
78 | | /// // Combine the stdout and log file `MakeWriter`s into one |
79 | | /// // `MakeWriter` that writes to both |
80 | | /// .with_writer(stdout.and(logfile)) |
81 | | /// .init(); |
82 | | /// # } |
83 | | /// ``` |
84 | | /// |
85 | | /// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter |
86 | | pub struct RollingFileAppender { |
87 | | state: Inner, |
88 | | writer: RwLock<File>, |
89 | | #[cfg(test)] |
90 | | now: Box<dyn Fn() -> OffsetDateTime + Send + Sync>, |
91 | | } |
92 | | |
93 | | /// A [writer] that writes to a rolling log file. |
94 | | /// |
95 | | /// This is returned by the [`MakeWriter`] implementation for [`RollingFileAppender`]. |
96 | | /// |
97 | | /// [writer]: std::io::Write |
98 | | /// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter |
99 | | #[derive(Debug)] |
100 | | pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>); |
101 | | |
102 | | #[derive(Debug)] |
103 | | struct Inner { |
104 | | log_directory: PathBuf, |
105 | | log_filename_prefix: Option<String>, |
106 | | log_filename_suffix: Option<String>, |
107 | | date_format: Vec<format_description::FormatItem<'static>>, |
108 | | rotation: Rotation, |
109 | | next_date: AtomicUsize, |
110 | | max_files: Option<usize>, |
111 | | } |
112 | | |
113 | | // === impl RollingFileAppender === |
114 | | |
115 | | impl RollingFileAppender { |
116 | | /// Creates a new `RollingFileAppender`. |
117 | | /// |
118 | | /// A `RollingFileAppender` will have a fixed rotation whose frequency is |
119 | | /// defined by [`Rotation`][self::Rotation]. The `directory` and |
120 | | /// `file_name_prefix` arguments determine the location and file name's _prefix_ |
121 | | /// of the log file. `RollingFileAppender` will automatically append the current date |
122 | | /// and hour (UTC format) to the file name. |
123 | | /// |
124 | | /// Alternatively, a `RollingFileAppender` can be constructed using one of the following helpers: |
125 | | /// |
126 | | /// - [`Rotation::minutely()`][minutely], |
127 | | /// - [`Rotation::hourly()`][hourly], |
128 | | /// - [`Rotation::daily()`][daily], |
129 | | /// - [`Rotation::never()`][never()] |
130 | | /// |
131 | | /// Additional parameters can be configured using [`RollingFileAppender::builder`]. |
132 | | /// |
133 | | /// # Examples |
134 | | /// |
135 | | /// ```rust |
136 | | /// # fn docs() { |
137 | | /// use tracing_appender::rolling::{RollingFileAppender, Rotation}; |
138 | | /// let file_appender = RollingFileAppender::new(Rotation::HOURLY, "/some/directory", "prefix.log"); |
139 | | /// # } |
140 | | /// ``` |
141 | 0 | pub fn new( |
142 | 0 | rotation: Rotation, |
143 | 0 | directory: impl AsRef<Path>, |
144 | 0 | filename_prefix: impl AsRef<Path>, |
145 | 0 | ) -> RollingFileAppender { |
146 | 0 | let filename_prefix = filename_prefix |
147 | 0 | .as_ref() |
148 | 0 | .to_str() |
149 | 0 | .expect("filename prefix must be a valid UTF-8 string"); |
150 | 0 | Self::builder() |
151 | 0 | .rotation(rotation) |
152 | 0 | .filename_prefix(filename_prefix) |
153 | 0 | .build(directory) |
154 | 0 | .expect("initializing rolling file appender failed") |
155 | 0 | } |
156 | | |
157 | | /// Returns a new [`Builder`] for configuring a `RollingFileAppender`. |
158 | | /// |
159 | | /// The builder interface can be used to set additional configuration |
160 | | /// parameters when constructing a new appender. |
161 | | /// |
162 | | /// Unlike [`RollingFileAppender::new`], the [`Builder::build`] method |
163 | | /// returns a `Result` rather than panicking when the appender cannot be |
164 | | /// initialized. Therefore, the builder interface can also be used when |
165 | | /// appender initialization errors should be handled gracefully. |
166 | | /// |
167 | | /// # Examples |
168 | | /// |
169 | | /// ```rust |
170 | | /// # fn docs() { |
171 | | /// use tracing_appender::rolling::{RollingFileAppender, Rotation}; |
172 | | /// |
173 | | /// let file_appender = RollingFileAppender::builder() |
174 | | /// .rotation(Rotation::HOURLY) // rotate log files once every hour |
175 | | /// .filename_prefix("myapp") // log file names will be prefixed with `myapp.` |
176 | | /// .filename_suffix("log") // log file names will be suffixed with `.log` |
177 | | /// .build("/var/log") // try to build an appender that stores log files in `/var/log` |
178 | | /// .expect("initializing rolling file appender failed"); |
179 | | /// # drop(file_appender); |
180 | | /// # } |
181 | | /// ``` |
182 | | #[must_use] |
183 | 0 | pub fn builder() -> Builder { |
184 | 0 | Builder::new() |
185 | 0 | } |
186 | | |
187 | 0 | fn from_builder(builder: &Builder, directory: impl AsRef<Path>) -> Result<Self, InitError> { |
188 | | let Builder { |
189 | 0 | ref rotation, |
190 | 0 | ref prefix, |
191 | 0 | ref suffix, |
192 | 0 | ref max_files, |
193 | 0 | } = builder; |
194 | 0 | let directory = directory.as_ref().to_path_buf(); |
195 | 0 | let now = OffsetDateTime::now_utc(); |
196 | 0 | let (state, writer) = Inner::new( |
197 | 0 | now, |
198 | 0 | rotation.clone(), |
199 | 0 | directory, |
200 | 0 | prefix.clone(), |
201 | 0 | suffix.clone(), |
202 | 0 | *max_files, |
203 | 0 | )?; |
204 | 0 | Ok(Self { |
205 | 0 | state, |
206 | 0 | writer, |
207 | 0 | #[cfg(test)] |
208 | 0 | now: Box::new(OffsetDateTime::now_utc), |
209 | 0 | }) |
210 | 0 | } |
211 | | |
212 | | #[inline] |
213 | 0 | fn now(&self) -> OffsetDateTime { |
214 | | #[cfg(test)] |
215 | | return (self.now)(); |
216 | | |
217 | | #[cfg(not(test))] |
218 | 0 | OffsetDateTime::now_utc() |
219 | 0 | } |
220 | | } |
221 | | |
222 | | impl io::Write for RollingFileAppender { |
223 | 0 | fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
224 | 0 | let now = self.now(); |
225 | 0 | let writer = self.writer.get_mut(); |
226 | 0 | if let Some(current_time) = self.state.should_rollover(now) { |
227 | 0 | let _did_cas = self.state.advance_date(now, current_time); |
228 | 0 | debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp..."); |
229 | 0 | self.state.refresh_writer(now, writer); |
230 | 0 | } |
231 | 0 | writer.write(buf) |
232 | 0 | } |
233 | | |
234 | 0 | fn flush(&mut self) -> io::Result<()> { |
235 | 0 | self.writer.get_mut().flush() |
236 | 0 | } |
237 | | } |
238 | | |
239 | | impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender { |
240 | | type Writer = RollingWriter<'a>; |
241 | 0 | fn make_writer(&'a self) -> Self::Writer { |
242 | 0 | let now = self.now(); |
243 | | |
244 | | // Should we try to roll over the log file? |
245 | 0 | if let Some(current_time) = self.state.should_rollover(now) { |
246 | | // Did we get the right to lock the file? If not, another thread |
247 | | // did it and we can just make a writer. |
248 | 0 | if self.state.advance_date(now, current_time) { |
249 | 0 | self.state.refresh_writer(now, &mut self.writer.write()); |
250 | 0 | } |
251 | 0 | } |
252 | 0 | RollingWriter(self.writer.read()) |
253 | 0 | } |
254 | | } |
255 | | |
256 | | impl fmt::Debug for RollingFileAppender { |
257 | | // This manual impl is required because of the `now` field (only present |
258 | | // with `cfg(test)`), which is not `Debug`... |
259 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
260 | 0 | f.debug_struct("RollingFileAppender") |
261 | 0 | .field("state", &self.state) |
262 | 0 | .field("writer", &self.writer) |
263 | 0 | .finish() |
264 | 0 | } |
265 | | } |
266 | | |
267 | | /// Creates a minutely-rotating file appender. This will rotate the log file once per minute. |
268 | | /// |
269 | | /// The appender returned by `rolling::minutely` can be used with `non_blocking` to create |
270 | | /// a non-blocking, minutely file appender. |
271 | | /// |
272 | | /// The directory of the log file is specified with the `directory` argument. |
273 | | /// `file_name_prefix` specifies the _prefix_ of the log file. `RollingFileAppender` |
274 | | /// adds the current date, hour, and minute to the log file in UTC. |
275 | | /// |
276 | | /// # Examples |
277 | | /// |
278 | | /// ``` rust |
279 | | /// # #[clippy::allow(needless_doctest_main)] |
280 | | /// fn main () { |
281 | | /// # fn doc() { |
282 | | /// let appender = tracing_appender::rolling::minutely("/some/path", "rolling.log"); |
283 | | /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); |
284 | | /// |
285 | | /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender); |
286 | | /// |
287 | | /// tracing::subscriber::with_default(subscriber.finish(), || { |
288 | | /// tracing::event!(tracing::Level::INFO, "Hello"); |
289 | | /// }); |
290 | | /// # } |
291 | | /// } |
292 | | /// ``` |
293 | | /// |
294 | | /// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH-mm`. |
295 | 0 | pub fn minutely( |
296 | 0 | directory: impl AsRef<Path>, |
297 | 0 | file_name_prefix: impl AsRef<Path>, |
298 | 0 | ) -> RollingFileAppender { |
299 | 0 | RollingFileAppender::new(Rotation::MINUTELY, directory, file_name_prefix) |
300 | 0 | } |
301 | | |
302 | | /// Creates an hourly-rotating file appender. |
303 | | /// |
304 | | /// The appender returned by `rolling::hourly` can be used with `non_blocking` to create |
305 | | /// a non-blocking, hourly file appender. |
306 | | /// |
307 | | /// The directory of the log file is specified with the `directory` argument. |
308 | | /// `file_name_prefix` specifies the _prefix_ of the log file. `RollingFileAppender` |
309 | | /// adds the current date and hour to the log file in UTC. |
310 | | /// |
311 | | /// # Examples |
312 | | /// |
313 | | /// ``` rust |
314 | | /// # #[clippy::allow(needless_doctest_main)] |
315 | | /// fn main () { |
316 | | /// # fn doc() { |
317 | | /// let appender = tracing_appender::rolling::hourly("/some/path", "rolling.log"); |
318 | | /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); |
319 | | /// |
320 | | /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender); |
321 | | /// |
322 | | /// tracing::subscriber::with_default(subscriber.finish(), || { |
323 | | /// tracing::event!(tracing::Level::INFO, "Hello"); |
324 | | /// }); |
325 | | /// # } |
326 | | /// } |
327 | | /// ``` |
328 | | /// |
329 | | /// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH`. |
330 | 0 | pub fn hourly( |
331 | 0 | directory: impl AsRef<Path>, |
332 | 0 | file_name_prefix: impl AsRef<Path>, |
333 | 0 | ) -> RollingFileAppender { |
334 | 0 | RollingFileAppender::new(Rotation::HOURLY, directory, file_name_prefix) |
335 | 0 | } |
336 | | |
337 | | /// Creates a daily-rotating file appender. |
338 | | /// |
339 | | /// The appender returned by `rolling::daily` can be used with `non_blocking` to create |
340 | | /// a non-blocking, daily file appender. |
341 | | /// |
342 | | /// A `RollingFileAppender` has a fixed rotation whose frequency is |
343 | | /// defined by [`Rotation`][self::Rotation]. The `directory` and |
344 | | /// `file_name_prefix` arguments determine the location and file name's _prefix_ |
345 | | /// of the log file. `RollingFileAppender` automatically appends the current date in UTC. |
346 | | /// |
347 | | /// # Examples |
348 | | /// |
349 | | /// ``` rust |
350 | | /// # #[clippy::allow(needless_doctest_main)] |
351 | | /// fn main () { |
352 | | /// # fn doc() { |
353 | | /// let appender = tracing_appender::rolling::daily("/some/path", "rolling.log"); |
354 | | /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); |
355 | | /// |
356 | | /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender); |
357 | | /// |
358 | | /// tracing::subscriber::with_default(subscriber.finish(), || { |
359 | | /// tracing::event!(tracing::Level::INFO, "Hello"); |
360 | | /// }); |
361 | | /// # } |
362 | | /// } |
363 | | /// ``` |
364 | | /// |
365 | | /// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH`. |
366 | 0 | pub fn daily( |
367 | 0 | directory: impl AsRef<Path>, |
368 | 0 | file_name_prefix: impl AsRef<Path>, |
369 | 0 | ) -> RollingFileAppender { |
370 | 0 | RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix) |
371 | 0 | } |
372 | | |
373 | | /// Creates a non-rolling file appender. |
374 | | /// |
375 | | /// The appender returned by `rolling::never` can be used with `non_blocking` to create |
376 | | /// a non-blocking, non-rotating appender. |
377 | | /// |
378 | | /// The location of the log file will be specified the `directory` passed in. |
379 | | /// `file_name` specifies the complete name of the log file (no date or time is appended). |
380 | | /// |
381 | | /// # Examples |
382 | | /// |
383 | | /// ``` rust |
384 | | /// # #[clippy::allow(needless_doctest_main)] |
385 | | /// fn main () { |
386 | | /// # fn doc() { |
387 | | /// let appender = tracing_appender::rolling::never("/some/path", "non-rolling.log"); |
388 | | /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); |
389 | | /// |
390 | | /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender); |
391 | | /// |
392 | | /// tracing::subscriber::with_default(subscriber.finish(), || { |
393 | | /// tracing::event!(tracing::Level::INFO, "Hello"); |
394 | | /// }); |
395 | | /// # } |
396 | | /// } |
397 | | /// ``` |
398 | | /// |
399 | | /// This will result in a log file located at `/some/path/non-rolling.log`. |
400 | 0 | pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> RollingFileAppender { |
401 | 0 | RollingFileAppender::new(Rotation::NEVER, directory, file_name) |
402 | 0 | } |
403 | | |
404 | | /// Defines a fixed period for rolling of a log file. |
405 | | /// |
406 | | /// To use a `Rotation`, pick one of the following options: |
407 | | /// |
408 | | /// ### Minutely Rotation |
409 | | /// ```rust |
410 | | /// # fn docs() { |
411 | | /// use tracing_appender::rolling::Rotation; |
412 | | /// let rotation = tracing_appender::rolling::Rotation::MINUTELY; |
413 | | /// # } |
414 | | /// ``` |
415 | | /// |
416 | | /// ### Hourly Rotation |
417 | | /// ```rust |
418 | | /// # fn docs() { |
419 | | /// use tracing_appender::rolling::Rotation; |
420 | | /// let rotation = tracing_appender::rolling::Rotation::HOURLY; |
421 | | /// # } |
422 | | /// ``` |
423 | | /// |
424 | | /// ### Daily Rotation |
425 | | /// ```rust |
426 | | /// # fn docs() { |
427 | | /// use tracing_appender::rolling::Rotation; |
428 | | /// let rotation = tracing_appender::rolling::Rotation::DAILY; |
429 | | /// # } |
430 | | /// ``` |
431 | | /// |
432 | | /// ### No Rotation |
433 | | /// ```rust |
434 | | /// # fn docs() { |
435 | | /// use tracing_appender::rolling::Rotation; |
436 | | /// let rotation = tracing_appender::rolling::Rotation::NEVER; |
437 | | /// # } |
438 | | /// ``` |
439 | | #[derive(Clone, Eq, PartialEq, Debug)] |
440 | | pub struct Rotation(RotationKind); |
441 | | |
442 | | #[derive(Clone, Eq, PartialEq, Debug)] |
443 | | enum RotationKind { |
444 | | Minutely, |
445 | | Hourly, |
446 | | Daily, |
447 | | Never, |
448 | | } |
449 | | |
450 | | impl Rotation { |
451 | | /// Provides an minutely rotation |
452 | | pub const MINUTELY: Self = Self(RotationKind::Minutely); |
453 | | /// Provides an hourly rotation |
454 | | pub const HOURLY: Self = Self(RotationKind::Hourly); |
455 | | /// Provides a daily rotation |
456 | | pub const DAILY: Self = Self(RotationKind::Daily); |
457 | | /// Provides a rotation that never rotates. |
458 | | pub const NEVER: Self = Self(RotationKind::Never); |
459 | | |
460 | 0 | pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> { |
461 | 0 | let unrounded_next_date = match *self { |
462 | 0 | Rotation::MINUTELY => *current_date + Duration::minutes(1), |
463 | 0 | Rotation::HOURLY => *current_date + Duration::hours(1), |
464 | 0 | Rotation::DAILY => *current_date + Duration::days(1), |
465 | 0 | Rotation::NEVER => return None, |
466 | | }; |
467 | 0 | Some(self.round_date(&unrounded_next_date)) |
468 | 0 | } |
469 | | |
470 | | // note that this method will panic if passed a `Rotation::NEVER`. |
471 | 0 | pub(crate) fn round_date(&self, date: &OffsetDateTime) -> OffsetDateTime { |
472 | 0 | match *self { |
473 | | Rotation::MINUTELY => { |
474 | 0 | let time = Time::from_hms(date.hour(), date.minute(), 0) |
475 | 0 | .expect("Invalid time; this is a bug in tracing-appender"); |
476 | 0 | date.replace_time(time) |
477 | | } |
478 | | Rotation::HOURLY => { |
479 | 0 | let time = Time::from_hms(date.hour(), 0, 0) |
480 | 0 | .expect("Invalid time; this is a bug in tracing-appender"); |
481 | 0 | date.replace_time(time) |
482 | | } |
483 | | Rotation::DAILY => { |
484 | 0 | let time = Time::from_hms(0, 0, 0) |
485 | 0 | .expect("Invalid time; this is a bug in tracing-appender"); |
486 | 0 | date.replace_time(time) |
487 | | } |
488 | | // Rotation::NEVER is impossible to round. |
489 | | Rotation::NEVER => { |
490 | 0 | unreachable!("Rotation::NEVER is impossible to round.") |
491 | | } |
492 | | } |
493 | 0 | } |
494 | | |
495 | 0 | fn date_format(&self) -> Vec<format_description::FormatItem<'static>> { |
496 | 0 | match *self { |
497 | 0 | Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"), |
498 | 0 | Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"), |
499 | 0 | Rotation::DAILY => format_description::parse("[year]-[month]-[day]"), |
500 | 0 | Rotation::NEVER => format_description::parse("[year]-[month]-[day]"), |
501 | | } |
502 | 0 | .expect("Unable to create a formatter; this is a bug in tracing-appender") |
503 | 0 | } |
504 | | } |
505 | | |
506 | | // === impl RollingWriter === |
507 | | |
508 | | impl io::Write for RollingWriter<'_> { |
509 | 0 | fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
510 | 0 | (&*self.0).write(buf) |
511 | 0 | } |
512 | | |
513 | 0 | fn flush(&mut self) -> io::Result<()> { |
514 | 0 | (&*self.0).flush() |
515 | 0 | } |
516 | | } |
517 | | |
518 | | // === impl Inner === |
519 | | |
520 | | impl Inner { |
521 | 0 | fn new( |
522 | 0 | now: OffsetDateTime, |
523 | 0 | rotation: Rotation, |
524 | 0 | directory: impl AsRef<Path>, |
525 | 0 | log_filename_prefix: Option<String>, |
526 | 0 | log_filename_suffix: Option<String>, |
527 | 0 | max_files: Option<usize>, |
528 | 0 | ) -> Result<(Self, RwLock<File>), builder::InitError> { |
529 | 0 | let log_directory = directory.as_ref().to_path_buf(); |
530 | 0 | let date_format = rotation.date_format(); |
531 | 0 | let next_date = rotation.next_date(&now); |
532 | | |
533 | 0 | let inner = Inner { |
534 | 0 | log_directory, |
535 | 0 | log_filename_prefix, |
536 | 0 | log_filename_suffix, |
537 | 0 | date_format, |
538 | 0 | next_date: AtomicUsize::new( |
539 | 0 | next_date |
540 | 0 | .map(|date| date.unix_timestamp() as usize) |
541 | 0 | .unwrap_or(0), |
542 | | ), |
543 | 0 | rotation, |
544 | 0 | max_files, |
545 | | }; |
546 | 0 | let filename = inner.join_date(&now); |
547 | 0 | let writer = RwLock::new(create_writer(inner.log_directory.as_ref(), &filename)?); |
548 | 0 | Ok((inner, writer)) |
549 | 0 | } |
550 | | |
551 | 0 | pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String { |
552 | 0 | let date = date |
553 | 0 | .format(&self.date_format) |
554 | 0 | .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); |
555 | | |
556 | | match ( |
557 | 0 | &self.rotation, |
558 | 0 | &self.log_filename_prefix, |
559 | 0 | &self.log_filename_suffix, |
560 | | ) { |
561 | 0 | (&Rotation::NEVER, Some(filename), None) => filename.to_string(), |
562 | 0 | (&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix), |
563 | 0 | (&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(), |
564 | 0 | (_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix), |
565 | 0 | (_, Some(filename), None) => format!("{}.{}", filename, date), |
566 | 0 | (_, None, Some(suffix)) => format!("{}.{}", date, suffix), |
567 | 0 | (_, None, None) => date, |
568 | | } |
569 | 0 | } |
570 | | |
571 | 0 | fn prune_old_logs(&self, max_files: usize) { |
572 | 0 | let files = fs::read_dir(&self.log_directory).map(|dir| { |
573 | 0 | dir.filter_map(|entry| { |
574 | 0 | let entry = entry.ok()?; |
575 | 0 | let metadata = entry.metadata().ok()?; |
576 | | |
577 | | // the appender only creates files, not directories or symlinks, |
578 | | // so we should never delete a dir or symlink. |
579 | 0 | if !metadata.is_file() { |
580 | 0 | return None; |
581 | 0 | } |
582 | | |
583 | 0 | let filename = entry.file_name(); |
584 | | // if the filename is not a UTF-8 string, skip it. |
585 | 0 | let filename = filename.to_str()?; |
586 | 0 | if let Some(prefix) = &self.log_filename_prefix { |
587 | 0 | if !filename.starts_with(prefix) { |
588 | 0 | return None; |
589 | 0 | } |
590 | 0 | } |
591 | | |
592 | 0 | if let Some(suffix) = &self.log_filename_suffix { |
593 | 0 | if !filename.ends_with(suffix) { |
594 | 0 | return None; |
595 | 0 | } |
596 | 0 | } |
597 | | |
598 | 0 | if self.log_filename_prefix.is_none() |
599 | 0 | && self.log_filename_suffix.is_none() |
600 | 0 | && Date::parse(filename, &self.date_format).is_err() |
601 | | { |
602 | 0 | return None; |
603 | 0 | } |
604 | | |
605 | 0 | let created = metadata.created().ok()?; |
606 | 0 | Some((entry, created)) |
607 | 0 | }) |
608 | 0 | .collect::<Vec<_>>() |
609 | 0 | }); |
610 | | |
611 | 0 | let mut files = match files { |
612 | 0 | Ok(files) => files, |
613 | 0 | Err(error) => { |
614 | 0 | eprintln!("Error reading the log directory/files: {}", error); |
615 | 0 | return; |
616 | | } |
617 | | }; |
618 | 0 | if files.len() < max_files { |
619 | 0 | return; |
620 | 0 | } |
621 | | |
622 | | // sort the files by their creation timestamps. |
623 | 0 | files.sort_by_key(|(_, created_at)| *created_at); |
624 | | |
625 | | // delete files, so that (n-1) files remain, because we will create another log file |
626 | 0 | for (file, _) in files.iter().take(files.len() - (max_files - 1)) { |
627 | 0 | if let Err(error) = fs::remove_file(file.path()) { |
628 | 0 | eprintln!( |
629 | 0 | "Failed to remove old log file {}: {}", |
630 | 0 | file.path().display(), |
631 | 0 | error |
632 | 0 | ); |
633 | 0 | } |
634 | | } |
635 | 0 | } |
636 | | |
637 | 0 | fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) { |
638 | 0 | let filename = self.join_date(&now); |
639 | | |
640 | 0 | if let Some(max_files) = self.max_files { |
641 | 0 | self.prune_old_logs(max_files); |
642 | 0 | } |
643 | | |
644 | 0 | match create_writer(&self.log_directory, &filename) { |
645 | 0 | Ok(new_file) => { |
646 | 0 | if let Err(err) = file.flush() { |
647 | 0 | eprintln!("Couldn't flush previous writer: {}", err); |
648 | 0 | } |
649 | 0 | *file = new_file; |
650 | | } |
651 | 0 | Err(err) => eprintln!("Couldn't create writer for logs: {}", err), |
652 | | } |
653 | 0 | } |
654 | | |
655 | | /// Checks whether or not it's time to roll over the log file. |
656 | | /// |
657 | | /// Rather than returning a `bool`, this returns the current value of |
658 | | /// `next_date` so that we can perform a `compare_exchange` operation with |
659 | | /// that value when setting the next rollover time. |
660 | | /// |
661 | | /// If this method returns `Some`, we should roll to a new log file. |
662 | | /// Otherwise, if this returns we should not rotate the log file. |
663 | 0 | fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> { |
664 | 0 | let next_date = self.next_date.load(Ordering::Acquire); |
665 | | // if the next date is 0, this appender *never* rotates log files. |
666 | 0 | if next_date == 0 { |
667 | 0 | return None; |
668 | 0 | } |
669 | | |
670 | 0 | if date.unix_timestamp() as usize >= next_date { |
671 | 0 | return Some(next_date); |
672 | 0 | } |
673 | | |
674 | 0 | None |
675 | 0 | } |
676 | | |
677 | 0 | fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool { |
678 | 0 | let next_date = self |
679 | 0 | .rotation |
680 | 0 | .next_date(&now) |
681 | 0 | .map(|date| date.unix_timestamp() as usize) |
682 | 0 | .unwrap_or(0); |
683 | 0 | self.next_date |
684 | 0 | .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire) |
685 | 0 | .is_ok() |
686 | 0 | } |
687 | | } |
688 | | |
689 | 0 | fn create_writer(directory: &Path, filename: &str) -> Result<File, InitError> { |
690 | 0 | let path = directory.join(filename); |
691 | 0 | let mut open_options = OpenOptions::new(); |
692 | 0 | open_options.append(true).create(true); |
693 | | |
694 | 0 | let new_file = open_options.open(path.as_path()); |
695 | 0 | if new_file.is_err() { |
696 | 0 | if let Some(parent) = path.parent() { |
697 | 0 | fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?; |
698 | 0 | return open_options |
699 | 0 | .open(path) |
700 | 0 | .map_err(InitError::ctx("failed to create initial log file")); |
701 | 0 | } |
702 | 0 | } |
703 | | |
704 | 0 | new_file.map_err(InitError::ctx("failed to create initial log file")) |
705 | 0 | } |
706 | | |
707 | | #[cfg(test)] |
708 | | mod test { |
709 | | use super::*; |
710 | | use std::fs; |
711 | | use std::io::Write; |
712 | | |
713 | | fn find_str_in_log(dir_path: &Path, expected_value: &str) -> bool { |
714 | | let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory"); |
715 | | |
716 | | for entry in dir_contents { |
717 | | let path = entry.expect("Expected dir entry").path(); |
718 | | let file = fs::read_to_string(&path).expect("Failed to read file"); |
719 | | println!("path={}\nfile={:?}", path.display(), file); |
720 | | |
721 | | if file.as_str() == expected_value { |
722 | | return true; |
723 | | } |
724 | | } |
725 | | |
726 | | false |
727 | | } |
728 | | |
729 | | fn write_to_log(appender: &mut RollingFileAppender, msg: &str) { |
730 | | appender |
731 | | .write_all(msg.as_bytes()) |
732 | | .expect("Failed to write to appender"); |
733 | | appender.flush().expect("Failed to flush!"); |
734 | | } |
735 | | |
736 | | fn test_appender(rotation: Rotation, file_prefix: &str) { |
737 | | let directory = tempfile::tempdir().expect("failed to create tempdir"); |
738 | | let mut appender = RollingFileAppender::new(rotation, directory.path(), file_prefix); |
739 | | |
740 | | let expected_value = "Hello"; |
741 | | write_to_log(&mut appender, expected_value); |
742 | | assert!(find_str_in_log(directory.path(), expected_value)); |
743 | | |
744 | | directory |
745 | | .close() |
746 | | .expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.") |
747 | | } |
748 | | |
749 | | #[test] |
750 | | fn write_minutely_log() { |
751 | | test_appender(Rotation::HOURLY, "minutely.log"); |
752 | | } |
753 | | |
754 | | #[test] |
755 | | fn write_hourly_log() { |
756 | | test_appender(Rotation::HOURLY, "hourly.log"); |
757 | | } |
758 | | |
759 | | #[test] |
760 | | fn write_daily_log() { |
761 | | test_appender(Rotation::DAILY, "daily.log"); |
762 | | } |
763 | | |
764 | | #[test] |
765 | | fn write_never_log() { |
766 | | test_appender(Rotation::NEVER, "never.log"); |
767 | | } |
768 | | |
769 | | #[test] |
770 | | fn test_rotations() { |
771 | | // per-minute basis |
772 | | let now = OffsetDateTime::now_utc(); |
773 | | let next = Rotation::MINUTELY.next_date(&now).unwrap(); |
774 | | assert_eq!((now + Duration::MINUTE).minute(), next.minute()); |
775 | | |
776 | | // per-hour basis |
777 | | let now = OffsetDateTime::now_utc(); |
778 | | let next = Rotation::HOURLY.next_date(&now).unwrap(); |
779 | | assert_eq!((now + Duration::HOUR).hour(), next.hour()); |
780 | | |
781 | | // daily-basis |
782 | | let now = OffsetDateTime::now_utc(); |
783 | | let next = Rotation::DAILY.next_date(&now).unwrap(); |
784 | | assert_eq!((now + Duration::DAY).day(), next.day()); |
785 | | |
786 | | // never |
787 | | let now = OffsetDateTime::now_utc(); |
788 | | let next = Rotation::NEVER.next_date(&now); |
789 | | assert!(next.is_none()); |
790 | | } |
791 | | |
792 | | #[test] |
793 | | #[should_panic( |
794 | | expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round." |
795 | | )] |
796 | | fn test_never_date_rounding() { |
797 | | let now = OffsetDateTime::now_utc(); |
798 | | let _ = Rotation::NEVER.round_date(&now); |
799 | | } |
800 | | |
801 | | #[test] |
802 | | fn test_path_concatenation() { |
803 | | let format = format_description::parse( |
804 | | "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \ |
805 | | sign:mandatory]:[offset_minute]:[offset_second]", |
806 | | ) |
807 | | .unwrap(); |
808 | | let directory = tempfile::tempdir().expect("failed to create tempdir"); |
809 | | |
810 | | let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap(); |
811 | | |
812 | | struct TestCase { |
813 | | expected: &'static str, |
814 | | rotation: Rotation, |
815 | | prefix: Option<&'static str>, |
816 | | suffix: Option<&'static str>, |
817 | | } |
818 | | |
819 | | let test = |TestCase { |
820 | | expected, |
821 | | rotation, |
822 | | prefix, |
823 | | suffix, |
824 | | }| { |
825 | | let (inner, _) = Inner::new( |
826 | | now, |
827 | | rotation.clone(), |
828 | | directory.path(), |
829 | | prefix.map(ToString::to_string), |
830 | | suffix.map(ToString::to_string), |
831 | | None, |
832 | | ) |
833 | | .unwrap(); |
834 | | let path = inner.join_date(&now); |
835 | | assert_eq!( |
836 | | expected, path, |
837 | | "rotation = {:?}, prefix = {:?}, suffix = {:?}", |
838 | | rotation, prefix, suffix |
839 | | ); |
840 | | }; |
841 | | |
842 | | let test_cases = vec![ |
843 | | // prefix only |
844 | | TestCase { |
845 | | expected: "app.log.2020-02-01-10-01", |
846 | | rotation: Rotation::MINUTELY, |
847 | | prefix: Some("app.log"), |
848 | | suffix: None, |
849 | | }, |
850 | | TestCase { |
851 | | expected: "app.log.2020-02-01-10", |
852 | | rotation: Rotation::HOURLY, |
853 | | prefix: Some("app.log"), |
854 | | suffix: None, |
855 | | }, |
856 | | TestCase { |
857 | | expected: "app.log.2020-02-01", |
858 | | rotation: Rotation::DAILY, |
859 | | prefix: Some("app.log"), |
860 | | suffix: None, |
861 | | }, |
862 | | TestCase { |
863 | | expected: "app.log", |
864 | | rotation: Rotation::NEVER, |
865 | | prefix: Some("app.log"), |
866 | | suffix: None, |
867 | | }, |
868 | | // prefix and suffix |
869 | | TestCase { |
870 | | expected: "app.2020-02-01-10-01.log", |
871 | | rotation: Rotation::MINUTELY, |
872 | | prefix: Some("app"), |
873 | | suffix: Some("log"), |
874 | | }, |
875 | | TestCase { |
876 | | expected: "app.2020-02-01-10.log", |
877 | | rotation: Rotation::HOURLY, |
878 | | prefix: Some("app"), |
879 | | suffix: Some("log"), |
880 | | }, |
881 | | TestCase { |
882 | | expected: "app.2020-02-01.log", |
883 | | rotation: Rotation::DAILY, |
884 | | prefix: Some("app"), |
885 | | suffix: Some("log"), |
886 | | }, |
887 | | TestCase { |
888 | | expected: "app.log", |
889 | | rotation: Rotation::NEVER, |
890 | | prefix: Some("app"), |
891 | | suffix: Some("log"), |
892 | | }, |
893 | | // suffix only |
894 | | TestCase { |
895 | | expected: "2020-02-01-10-01.log", |
896 | | rotation: Rotation::MINUTELY, |
897 | | prefix: None, |
898 | | suffix: Some("log"), |
899 | | }, |
900 | | TestCase { |
901 | | expected: "2020-02-01-10.log", |
902 | | rotation: Rotation::HOURLY, |
903 | | prefix: None, |
904 | | suffix: Some("log"), |
905 | | }, |
906 | | TestCase { |
907 | | expected: "2020-02-01.log", |
908 | | rotation: Rotation::DAILY, |
909 | | prefix: None, |
910 | | suffix: Some("log"), |
911 | | }, |
912 | | TestCase { |
913 | | expected: "log", |
914 | | rotation: Rotation::NEVER, |
915 | | prefix: None, |
916 | | suffix: Some("log"), |
917 | | }, |
918 | | ]; |
919 | | for test_case in test_cases { |
920 | | test(test_case) |
921 | | } |
922 | | } |
923 | | |
924 | | #[test] |
925 | | fn test_make_writer() { |
926 | | use std::sync::{Arc, Mutex}; |
927 | | use tracing_subscriber::prelude::*; |
928 | | |
929 | | let format = format_description::parse( |
930 | | "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \ |
931 | | sign:mandatory]:[offset_minute]:[offset_second]", |
932 | | ) |
933 | | .unwrap(); |
934 | | |
935 | | let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap(); |
936 | | let directory = tempfile::tempdir().expect("failed to create tempdir"); |
937 | | let (state, writer) = Inner::new( |
938 | | now, |
939 | | Rotation::HOURLY, |
940 | | directory.path(), |
941 | | Some("test_make_writer".to_string()), |
942 | | None, |
943 | | None, |
944 | | ) |
945 | | .unwrap(); |
946 | | |
947 | | let clock = Arc::new(Mutex::new(now)); |
948 | | let now = { |
949 | | let clock = clock.clone(); |
950 | | Box::new(move || *clock.lock().unwrap()) |
951 | | }; |
952 | | let appender = RollingFileAppender { state, writer, now }; |
953 | | let default = tracing_subscriber::fmt() |
954 | | .without_time() |
955 | | .with_level(false) |
956 | | .with_target(false) |
957 | | .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE) |
958 | | .with_writer(appender) |
959 | | .finish() |
960 | | .set_default(); |
961 | | |
962 | | tracing::info!("file 1"); |
963 | | |
964 | | // advance time by one second |
965 | | (*clock.lock().unwrap()) += Duration::seconds(1); |
966 | | |
967 | | tracing::info!("file 1"); |
968 | | |
969 | | // advance time by one hour |
970 | | (*clock.lock().unwrap()) += Duration::hours(1); |
971 | | |
972 | | tracing::info!("file 2"); |
973 | | |
974 | | // advance time by one second |
975 | | (*clock.lock().unwrap()) += Duration::seconds(1); |
976 | | |
977 | | tracing::info!("file 2"); |
978 | | |
979 | | drop(default); |
980 | | |
981 | | let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory"); |
982 | | println!("dir={:?}", dir_contents); |
983 | | for entry in dir_contents { |
984 | | println!("entry={:?}", entry); |
985 | | let path = entry.expect("Expected dir entry").path(); |
986 | | let file = fs::read_to_string(&path).expect("Failed to read file"); |
987 | | println!("path={}\nfile={:?}", path.display(), file); |
988 | | |
989 | | match path |
990 | | .extension() |
991 | | .expect("found a file without a date!") |
992 | | .to_str() |
993 | | .expect("extension should be UTF8") |
994 | | { |
995 | | "2020-02-01-10" => { |
996 | | assert_eq!("file 1\nfile 1\n", file); |
997 | | } |
998 | | "2020-02-01-11" => { |
999 | | assert_eq!("file 2\nfile 2\n", file); |
1000 | | } |
1001 | | x => panic!("unexpected date {}", x), |
1002 | | } |
1003 | | } |
1004 | | } |
1005 | | |
1006 | | #[test] |
1007 | | fn test_max_log_files() { |
1008 | | use std::sync::{Arc, Mutex}; |
1009 | | use tracing_subscriber::prelude::*; |
1010 | | |
1011 | | let format = format_description::parse( |
1012 | | "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \ |
1013 | | sign:mandatory]:[offset_minute]:[offset_second]", |
1014 | | ) |
1015 | | .unwrap(); |
1016 | | |
1017 | | let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap(); |
1018 | | let directory = tempfile::tempdir().expect("failed to create tempdir"); |
1019 | | let (state, writer) = Inner::new( |
1020 | | now, |
1021 | | Rotation::HOURLY, |
1022 | | directory.path(), |
1023 | | Some("test_max_log_files".to_string()), |
1024 | | None, |
1025 | | Some(2), |
1026 | | ) |
1027 | | .unwrap(); |
1028 | | |
1029 | | let clock = Arc::new(Mutex::new(now)); |
1030 | | let now = { |
1031 | | let clock = clock.clone(); |
1032 | | Box::new(move || *clock.lock().unwrap()) |
1033 | | }; |
1034 | | let appender = RollingFileAppender { state, writer, now }; |
1035 | | let default = tracing_subscriber::fmt() |
1036 | | .without_time() |
1037 | | .with_level(false) |
1038 | | .with_target(false) |
1039 | | .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE) |
1040 | | .with_writer(appender) |
1041 | | .finish() |
1042 | | .set_default(); |
1043 | | |
1044 | | tracing::info!("file 1"); |
1045 | | |
1046 | | // advance time by one second |
1047 | | (*clock.lock().unwrap()) += Duration::seconds(1); |
1048 | | |
1049 | | tracing::info!("file 1"); |
1050 | | |
1051 | | // advance time by one hour |
1052 | | (*clock.lock().unwrap()) += Duration::hours(1); |
1053 | | |
1054 | | // depending on the filesystem, the creation timestamp's resolution may |
1055 | | // be as coarse as one second, so we need to wait a bit here to ensure |
1056 | | // that the next file actually is newer than the old one. |
1057 | | std::thread::sleep(std::time::Duration::from_secs(1)); |
1058 | | |
1059 | | tracing::info!("file 2"); |
1060 | | |
1061 | | // advance time by one second |
1062 | | (*clock.lock().unwrap()) += Duration::seconds(1); |
1063 | | |
1064 | | tracing::info!("file 2"); |
1065 | | |
1066 | | // advance time by one hour |
1067 | | (*clock.lock().unwrap()) += Duration::hours(1); |
1068 | | |
1069 | | // again, sleep to ensure that the creation timestamps actually differ. |
1070 | | std::thread::sleep(std::time::Duration::from_secs(1)); |
1071 | | |
1072 | | tracing::info!("file 3"); |
1073 | | |
1074 | | // advance time by one second |
1075 | | (*clock.lock().unwrap()) += Duration::seconds(1); |
1076 | | |
1077 | | tracing::info!("file 3"); |
1078 | | |
1079 | | drop(default); |
1080 | | |
1081 | | let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory"); |
1082 | | println!("dir={:?}", dir_contents); |
1083 | | |
1084 | | for entry in dir_contents { |
1085 | | println!("entry={:?}", entry); |
1086 | | let path = entry.expect("Expected dir entry").path(); |
1087 | | let file = fs::read_to_string(&path).expect("Failed to read file"); |
1088 | | println!("path={}\nfile={:?}", path.display(), file); |
1089 | | |
1090 | | match path |
1091 | | .extension() |
1092 | | .expect("found a file without a date!") |
1093 | | .to_str() |
1094 | | .expect("extension should be UTF8") |
1095 | | { |
1096 | | "2020-02-01-10" => { |
1097 | | panic!("this file should have been pruned already!"); |
1098 | | } |
1099 | | "2020-02-01-11" => { |
1100 | | assert_eq!("file 2\nfile 2\n", file); |
1101 | | } |
1102 | | "2020-02-01-12" => { |
1103 | | assert_eq!("file 3\nfile 3\n", file); |
1104 | | } |
1105 | | x => panic!("unexpected date {}", x), |
1106 | | } |
1107 | | } |
1108 | | } |
1109 | | } |