Skip to content

Commit 46c1fe9

Browse files
authored
appender: add Builder::filename_suffix parameter (#2225)
## Motivation The `RollingFileAppender` currently only supports a filename suffix. A lot of editors have support for log files using the `.log` extension. It would be nice to be able to configure what gets added after the date. ## Solution - Add a `Builder::filename_suffix` method, taking a string. - If the string is non-empty, this gets appended to the filename after the date. - This isn't an `AsRef<Path>` because it's not supposed to be a `Path` - Update the date appending logic to handle cases when the suffix or prefix is empty - Split each part with a `.` so the final output would be `prefix.date.suffix` - Make sure to remove unnecessary `.` when a prefix or suffix is empty - Add tests related to those changes ## Notes It would probably be nicer to simply have a completely configurable file name format, but I went with the easiest approach that achieved what I needed. Closes #1477
1 parent 44d9ee3 commit 46c1fe9

File tree

2 files changed

+138
-46
lines changed

2 files changed

+138
-46
lines changed

tracing-appender/src/rolling.rs

+84-46
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
103103
struct Inner {
104104
log_directory: PathBuf,
105105
log_filename_prefix: Option<String>,
106+
log_filename_suffix: Option<String>,
106107
rotation: Rotation,
107108
next_date: AtomicUsize,
108109
}
@@ -170,6 +171,7 @@ impl RollingFileAppender {
170171
/// let file_appender = RollingFileAppender::builder()
171172
/// .rotation(Rotation::HOURLY) // rotate log files once every hour
172173
/// .filename_prefix("myapp") // log file names will be prefixed with `myapp.`
174+
/// .filename_suffix("log") // log file names will be suffixed with `.log`
173175
/// .build("/var/log") // try to build an appender that stores log files in `/var/log`
174176
/// .expect("initializing rolling file appender failed");
175177
/// # drop(file_appender);
@@ -184,11 +186,17 @@ impl RollingFileAppender {
184186
let Builder {
185187
ref rotation,
186188
ref prefix,
189+
ref suffix,
187190
} = builder;
188-
let filename_prefix = prefix.clone();
189191
let directory = directory.as_ref().to_path_buf();
190192
let now = OffsetDateTime::now_utc();
191-
let (state, writer) = Inner::new(now, rotation.clone(), directory, filename_prefix)?;
193+
let (state, writer) = Inner::new(
194+
now,
195+
rotation.clone(),
196+
directory,
197+
prefix.clone(),
198+
suffix.clone(),
199+
)?;
192200
Ok(Self {
193201
state,
194202
writer,
@@ -480,42 +488,31 @@ impl Rotation {
480488
}
481489
}
482490

483-
pub(crate) fn join_date(&self, filename: Option<&str>, date: &OffsetDateTime) -> String {
484-
let date = match *self {
485-
Rotation::MINUTELY => {
486-
let format = format_description::parse("[year]-[month]-[day]-[hour]-[minute]")
487-
.expect("Unable to create a formatter; this is a bug in tracing-appender");
488-
date.format(&format)
489-
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
490-
}
491-
Rotation::HOURLY => {
492-
let format = format_description::parse("[year]-[month]-[day]-[hour]")
493-
.expect("Unable to create a formatter; this is a bug in tracing-appender");
494-
date.format(&format)
495-
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
496-
}
497-
Rotation::DAILY => {
498-
let format = format_description::parse("[year]-[month]-[day]")
499-
.expect("Unable to create a formatter; this is a bug in tracing-appender");
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")
514-
}
515-
};
516-
match filename {
517-
Some(filename) => format!("{}.{}", filename, date),
518-
None => date,
491+
pub(crate) fn join_date(
492+
&self,
493+
filename: Option<&str>,
494+
date: &OffsetDateTime,
495+
suffix: Option<&str>,
496+
) -> String {
497+
let format = match *self {
498+
Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"),
499+
Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
500+
Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
501+
Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
502+
}
503+
.expect("Unable to create a formatter; this is a bug in tracing-appender");
504+
let date = date
505+
.format(&format)
506+
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
507+
508+
match (self, filename, suffix) {
509+
(&Rotation::NEVER, Some(filename), None) => filename.to_string(),
510+
(&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix),
511+
(&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(),
512+
(_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix),
513+
(_, Some(filename), None) => format!("{}.{}", filename, date),
514+
(_, None, Some(suffix)) => format!("{}.{}", date, suffix),
515+
(_, None, None) => date,
519516
}
520517
}
521518
}
@@ -540,15 +537,21 @@ impl Inner {
540537
rotation: Rotation,
541538
directory: impl AsRef<Path>,
542539
log_filename_prefix: Option<String>,
540+
log_filename_suffix: Option<String>,
543541
) -> Result<(Self, RwLock<File>), builder::InitError> {
544542
let log_directory = directory.as_ref().to_path_buf();
545-
let filename = rotation.join_date(log_filename_prefix.as_deref(), &now);
543+
let filename = rotation.join_date(
544+
log_filename_prefix.as_deref(),
545+
&now,
546+
log_filename_suffix.as_deref(),
547+
);
546548
let next_date = rotation.next_date(&now);
547549
let writer = RwLock::new(create_writer(log_directory.as_ref(), &filename)?);
548550

549551
let inner = Inner {
550552
log_directory,
551553
log_filename_prefix,
554+
log_filename_suffix,
552555
next_date: AtomicUsize::new(
553556
next_date
554557
.map(|date| date.unix_timestamp() as usize)
@@ -560,9 +563,11 @@ impl Inner {
560563
}
561564

562565
fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
563-
let filename = self
564-
.rotation
565-
.join_date(self.log_filename_prefix.as_deref(), &now);
566+
let filename = self.rotation.join_date(
567+
self.log_filename_prefix.as_deref(),
568+
&now,
569+
self.log_filename_suffix.as_deref(),
570+
);
566571

567572
match create_writer(&self.log_directory, &filename) {
568573
Ok(new_file) => {
@@ -732,19 +737,51 @@ mod test {
732737
let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
733738

734739
// per-minute
735-
let path = Rotation::MINUTELY.join_date(Some("app.log"), &now);
740+
let path = Rotation::MINUTELY.join_date(Some("app.log"), &now, None);
736741
assert_eq!("app.log.2020-02-01-10-01", path);
737742

738743
// per-hour
739-
let path = Rotation::HOURLY.join_date(Some("app.log"), &now);
744+
let path = Rotation::HOURLY.join_date(Some("app.log"), &now, None);
740745
assert_eq!("app.log.2020-02-01-10", path);
741746

742747
// per-day
743-
let path = Rotation::DAILY.join_date(Some("app.log"), &now);
748+
let path = Rotation::DAILY.join_date(Some("app.log"), &now, None);
744749
assert_eq!("app.log.2020-02-01", path);
745750

746751
// never
747-
let path = Rotation::NEVER.join_date(Some("app.log"), &now);
752+
let path = Rotation::NEVER.join_date(Some("app.log"), &now, None);
753+
assert_eq!("app.log", path);
754+
755+
// per-minute with suffix
756+
let path = Rotation::MINUTELY.join_date(Some("app"), &now, Some("log"));
757+
assert_eq!("app.2020-02-01-10-01.log", path);
758+
759+
// per-hour with suffix
760+
let path = Rotation::HOURLY.join_date(Some("app"), &now, Some("log"));
761+
assert_eq!("app.2020-02-01-10.log", path);
762+
763+
// per-day with suffix
764+
let path = Rotation::DAILY.join_date(Some("app"), &now, Some("log"));
765+
assert_eq!("app.2020-02-01.log", path);
766+
767+
// never with suffix
768+
let path = Rotation::NEVER.join_date(Some("app"), &now, Some("log"));
769+
assert_eq!("app.log", path);
770+
771+
// per-minute without prefix
772+
let path = Rotation::MINUTELY.join_date(None, &now, Some("app.log"));
773+
assert_eq!("2020-02-01-10-01.app.log", path);
774+
775+
// per-hour without prefix
776+
let path = Rotation::HOURLY.join_date(None, &now, Some("app.log"));
777+
assert_eq!("2020-02-01-10.app.log", path);
778+
779+
// per-day without prefix
780+
let path = Rotation::DAILY.join_date(None, &now, Some("app.log"));
781+
assert_eq!("2020-02-01.app.log", path);
782+
783+
// never without prefix
784+
let path = Rotation::NEVER.join_date(None, &now, Some("app.log"));
748785
assert_eq!("app.log", path);
749786
}
750787

@@ -766,6 +803,7 @@ mod test {
766803
Rotation::HOURLY,
767804
directory.path(),
768805
Some("test_make_writer".to_string()),
806+
None,
769807
)
770808
.unwrap();
771809

tracing-appender/src/rolling/builder.rs

+54
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use thiserror::Error;
99
pub struct Builder {
1010
pub(super) rotation: Rotation,
1111
pub(super) prefix: Option<String>,
12+
pub(super) suffix: Option<String>,
1213
}
1314

1415
/// Errors returned by [`Builder::build`].
@@ -46,6 +47,7 @@ impl Builder {
4647
Self {
4748
rotation: Rotation::NEVER,
4849
prefix: None,
50+
suffix: None,
4951
}
5052
}
5153

@@ -127,6 +129,58 @@ impl Builder {
127129
Self { prefix, ..self }
128130
}
129131

132+
/// Sets the suffix for log filenames. The suffix is output after the
133+
/// timestamp in the file name, and if it is non-empty, it is preceded by a
134+
/// dot (`.`).
135+
///
136+
/// By default, log files do not have a suffix.
137+
///
138+
/// # Examples
139+
///
140+
/// Setting a suffix:
141+
///
142+
/// ```
143+
/// use tracing_appender::rolling::RollingFileAppender;
144+
///
145+
/// # fn docs() {
146+
/// let appender = RollingFileAppender::builder()
147+
/// .filename_suffix("myapp.log") // log files will have names like "2019-01-01.myapp.log"
148+
/// // ...
149+
/// .build("/var/log")
150+
/// .expect("failed to initialize rolling file appender");
151+
/// # drop(appender)
152+
/// # }
153+
/// ```
154+
///
155+
/// No suffix:
156+
///
157+
/// ```
158+
/// use tracing_appender::rolling::RollingFileAppender;
159+
///
160+
/// # fn docs() {
161+
/// let appender = RollingFileAppender::builder()
162+
/// .filename_suffix("") // log files will have names like "2019-01-01"
163+
/// // ...
164+
/// .build("/var/log")
165+
/// .expect("failed to initialize rolling file appender");
166+
/// # drop(appender)
167+
/// # }
168+
/// ```
169+
///
170+
/// [rotation strategy]: Rotation
171+
#[must_use]
172+
pub fn filename_suffix(self, suffix: impl Into<String>) -> Self {
173+
let suffix = suffix.into();
174+
// If the configured suffix is the empty string, then don't include a
175+
// separator character.
176+
let suffix = if suffix.is_empty() {
177+
None
178+
} else {
179+
Some(suffix)
180+
};
181+
Self { suffix, ..self }
182+
}
183+
130184
/// Builds a new [`RollingFileAppender`] with the configured parameters,
131185
/// emitting log files to the provided directory.
132186
///

0 commit comments

Comments
 (0)