Skip to content

Commit 67b9474

Browse files
committed
appender: add a builder for constructing RollingFileAppenders (#2227)
## Motivation Several currently open PRs, such as #2225 and #2221, add new configuration parameters to the rolling file appender in `tracing-appender`. The best way to add new optional configuration settings without breaking existing APIs or creating a very large number of new constructors is to add a builder interface. ## Solution Since a number of PRs would all need to add the builder API, introducing potential conflicts, this branch _just_ adds the builder interface without adding any new configuration options. Once this merges, the existing in-flight PRs can be rebased onto this branch to use the builder interface without conflicting with each other. Also, the `Builder::build` method is fallible and returns a `Result`, rather than panicking. This is a relatively common pattern in Rust --- for example, `std::thread::Builder::spawn` returns a `Result` if a new thread cannot be spawned, while `std::thread::spawn` simply panics. This allows users to handle appender initialization errors gracefully without breaking the API of the existing `new` constructor. Fixes #1953 Signed-off-by: Eliza Weisman <[email protected]>
1 parent af36ba5 commit 67b9474

File tree

3 files changed

+280
-48
lines changed

3 files changed

+280
-48
lines changed

tracing-appender/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ rust-version = "1.53.0"
2424
crossbeam-channel = "0.5.0"
2525
time = { version = "0.3", default-features = false, features = ["formatting"] }
2626
parking_lot = { optional = true, version = "0.12.0" }
27+
thiserror = "1"
2728

2829
[dependencies.tracing-subscriber]
2930
path = "../tracing-subscriber"

tracing-appender/src/rolling.rs

+112-48
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@ use std::{
3131
fmt::{self, Debug},
3232
fs::{self, File, OpenOptions},
3333
io::{self, Write},
34-
path::Path,
34+
path::{Path, PathBuf},
3535
sync::atomic::{AtomicUsize, Ordering},
3636
};
3737
use time::{format_description, Duration, OffsetDateTime, Time};
3838

39+
mod builder;
40+
pub use builder::{Builder, InitError};
41+
3942
/// A file appender with the ability to rotate log files at a fixed schedule.
4043
///
4144
/// `RollingFileAppender` implements the [`std:io::Write` trait][write] and will
@@ -98,8 +101,8 @@ pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
98101

99102
#[derive(Debug)]
100103
struct Inner {
101-
log_directory: String,
102-
log_filename_prefix: String,
104+
log_directory: PathBuf,
105+
log_filename_prefix: Option<String>,
103106
rotation: Rotation,
104107
next_date: AtomicUsize,
105108
}
@@ -122,8 +125,10 @@ impl RollingFileAppender {
122125
/// - [`Rotation::daily()`][daily],
123126
/// - [`Rotation::never()`][never()]
124127
///
128+
/// Additional parameters can be configured using [`RollingFileAppender::builder`].
125129
///
126130
/// # Examples
131+
///
127132
/// ```rust
128133
/// # fn docs() {
129134
/// use tracing_appender::rolling::{RollingFileAppender, Rotation};
@@ -133,16 +138,63 @@ impl RollingFileAppender {
133138
pub fn new(
134139
rotation: Rotation,
135140
directory: impl AsRef<Path>,
136-
file_name_prefix: impl AsRef<Path>,
141+
filename_prefix: impl AsRef<Path>,
137142
) -> RollingFileAppender {
143+
let filename_prefix = filename_prefix
144+
.as_ref()
145+
.to_str()
146+
.expect("filename prefix must be a valid UTF-8 string");
147+
Self::builder()
148+
.rotation(rotation)
149+
.filename_prefix(filename_prefix)
150+
.build(directory)
151+
.expect("initializing rolling file appender failed")
152+
}
153+
154+
/// Returns a new [`Builder`] for configuring a `RollingFileAppender`.
155+
///
156+
/// The builder interface can be used to set additional configuration
157+
/// parameters when constructing a new appender.
158+
///
159+
/// Unlike [`RollingFileAppender::new`], the [`Builder::build`] method
160+
/// returns a `Result` rather than panicking when the appender cannot be
161+
/// initialized. Therefore, the builder interface can also be used when
162+
/// appender initialization errors should be handled gracefully.
163+
///
164+
/// # Examples
165+
///
166+
/// ```rust
167+
/// # fn docs() {
168+
/// use tracing_appender::rolling::{RollingFileAppender, Rotation};
169+
///
170+
/// let file_appender = RollingFileAppender::builder()
171+
/// .rotation(Rotation::HOURLY) // rotate log files once every hour
172+
/// .filename_prefix("myapp") // log file names will be prefixed with `myapp.`
173+
/// .build("/var/log") // try to build an appender that stores log files in `/var/log`
174+
/// .expect("initializing rolling file appender failed");
175+
/// # drop(file_appender);
176+
/// # }
177+
/// ```
178+
#[must_use]
179+
pub fn builder() -> Builder {
180+
Builder::new()
181+
}
182+
183+
fn from_builder(builder: &Builder, directory: impl AsRef<Path>) -> Result<Self, InitError> {
184+
let Builder {
185+
ref rotation,
186+
ref prefix,
187+
} = builder;
188+
let filename_prefix = prefix.clone();
189+
let directory = directory.as_ref().to_path_buf();
138190
let now = OffsetDateTime::now_utc();
139-
let (state, writer) = Inner::new(now, rotation, directory, file_name_prefix);
140-
Self {
191+
let (state, writer) = Inner::new(now, rotation.clone(), directory, filename_prefix)?;
192+
Ok(Self {
141193
state,
142194
writer,
143195
#[cfg(test)]
144196
now: Box::new(OffsetDateTime::now_utc),
145-
}
197+
})
146198
}
147199

148200
#[inline]
@@ -428,35 +480,42 @@ impl Rotation {
428480
}
429481
}
430482

431-
pub(crate) fn join_date(&self, filename: &str, date: &OffsetDateTime) -> String {
432-
match *self {
483+
pub(crate) fn join_date(&self, filename: Option<&str>, date: &OffsetDateTime) -> String {
484+
let date = match *self {
433485
Rotation::MINUTELY => {
434486
let format = format_description::parse("[year]-[month]-[day]-[hour]-[minute]")
435487
.expect("Unable to create a formatter; this is a bug in tracing-appender");
436-
437-
let date = date
438-
.format(&format)
439-
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
440-
format!("{}.{}", filename, date)
488+
date.format(&format)
489+
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
441490
}
442491
Rotation::HOURLY => {
443492
let format = format_description::parse("[year]-[month]-[day]-[hour]")
444493
.expect("Unable to create a formatter; this is a bug in tracing-appender");
445-
446-
let date = date
447-
.format(&format)
448-
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
449-
format!("{}.{}", filename, date)
494+
date.format(&format)
495+
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
450496
}
451497
Rotation::DAILY => {
452498
let format = format_description::parse("[year]-[month]-[day]")
453499
.expect("Unable to create a formatter; this is a bug in tracing-appender");
454-
let date = date
455-
.format(&format)
456-
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
457-
format!("{}.{}", filename, date)
500+
date.format(&format)
501+
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
502+
}
503+
Rotation::NEVER => {
504+
// If there's a name prefix, use that.
505+
if let Some(filename) = filename {
506+
return filename.to_owned();
507+
}
508+
509+
// Otherwise, just use the date.
510+
let format = format_description::parse("[year]-[month]-[day]")
511+
.expect("Unable to create a formatter; this is a bug in tracing-appender");
512+
date.format(&format)
513+
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
458514
}
459-
Rotation::NEVER => filename.to_string(),
515+
};
516+
match filename {
517+
Some(filename) => format!("{}.{}", filename, date),
518+
None => date,
460519
}
461520
}
462521
}
@@ -480,32 +539,30 @@ impl Inner {
480539
now: OffsetDateTime,
481540
rotation: Rotation,
482541
directory: impl AsRef<Path>,
483-
file_name_prefix: impl AsRef<Path>,
484-
) -> (Self, RwLock<File>) {
485-
let log_directory = directory.as_ref().to_str().unwrap();
486-
let log_filename_prefix = file_name_prefix.as_ref().to_str().unwrap();
487-
488-
let filename = rotation.join_date(log_filename_prefix, &now);
542+
log_filename_prefix: Option<String>,
543+
) -> Result<(Self, RwLock<File>), builder::InitError> {
544+
let log_directory = directory.as_ref().to_path_buf();
545+
let filename = rotation.join_date(log_filename_prefix.as_deref(), &now);
489546
let next_date = rotation.next_date(&now);
490-
let writer = RwLock::new(
491-
create_writer(log_directory, &filename).expect("failed to create appender"),
492-
);
547+
let writer = RwLock::new(create_writer(log_directory.as_ref(), &filename)?);
493548

494549
let inner = Inner {
495-
log_directory: log_directory.to_string(),
496-
log_filename_prefix: log_filename_prefix.to_string(),
550+
log_directory,
551+
log_filename_prefix,
497552
next_date: AtomicUsize::new(
498553
next_date
499554
.map(|date| date.unix_timestamp() as usize)
500555
.unwrap_or(0),
501556
),
502557
rotation,
503558
};
504-
(inner, writer)
559+
Ok((inner, writer))
505560
}
506561

507562
fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
508-
let filename = self.rotation.join_date(&self.log_filename_prefix, &now);
563+
let filename = self
564+
.rotation
565+
.join_date(self.log_filename_prefix.as_deref(), &now);
509566

510567
match create_writer(&self.log_directory, &filename) {
511568
Ok(new_file) => {
@@ -552,20 +609,22 @@ impl Inner {
552609
}
553610
}
554611

555-
fn create_writer(directory: &str, filename: &str) -> io::Result<File> {
556-
let path = Path::new(directory).join(filename);
612+
fn create_writer(directory: &Path, filename: &str) -> Result<File, InitError> {
613+
let path = directory.join(filename);
557614
let mut open_options = OpenOptions::new();
558615
open_options.append(true).create(true);
559616

560617
let new_file = open_options.open(path.as_path());
561618
if new_file.is_err() {
562619
if let Some(parent) = path.parent() {
563-
fs::create_dir_all(parent)?;
564-
return open_options.open(path);
620+
fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
621+
return open_options
622+
.open(path)
623+
.map_err(InitError::ctx("failed to create initial log file"));
565624
}
566625
}
567626

568-
new_file
627+
new_file.map_err(InitError::ctx("failed to create initial log file"))
569628
}
570629

571630
#[cfg(test)]
@@ -673,19 +732,19 @@ mod test {
673732
let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
674733

675734
// per-minute
676-
let path = Rotation::MINUTELY.join_date("app.log", &now);
735+
let path = Rotation::MINUTELY.join_date(Some("app.log"), &now);
677736
assert_eq!("app.log.2020-02-01-10-01", path);
678737

679738
// per-hour
680-
let path = Rotation::HOURLY.join_date("app.log", &now);
739+
let path = Rotation::HOURLY.join_date(Some("app.log"), &now);
681740
assert_eq!("app.log.2020-02-01-10", path);
682741

683742
// per-day
684-
let path = Rotation::DAILY.join_date("app.log", &now);
743+
let path = Rotation::DAILY.join_date(Some("app.log"), &now);
685744
assert_eq!("app.log.2020-02-01", path);
686745

687746
// never
688-
let path = Rotation::NEVER.join_date("app.log", &now);
747+
let path = Rotation::NEVER.join_date(Some("app.log"), &now);
689748
assert_eq!("app.log", path);
690749
}
691750

@@ -702,8 +761,13 @@ mod test {
702761

703762
let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
704763
let directory = tempfile::tempdir().expect("failed to create tempdir");
705-
let (state, writer) =
706-
Inner::new(now, Rotation::HOURLY, directory.path(), "test_make_writer");
764+
let (state, writer) = Inner::new(
765+
now,
766+
Rotation::HOURLY,
767+
directory.path(),
768+
Some("test_make_writer".to_string()),
769+
)
770+
.unwrap();
707771

708772
let clock = Arc::new(Mutex::new(now));
709773
let now = {

0 commit comments

Comments
 (0)