Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

appender: add a builder for constructing RollingFileAppenders #2227

Merged
merged 6 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tracing-appender/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ rust-version = "1.53.0"
crossbeam-channel = "0.5.0"
time = { version = "0.3", default-features = false, features = ["formatting"] }
parking_lot = { optional = true, version = "0.12.0" }
thiserror = "1"

[dependencies.tracing-subscriber]
path = "../tracing-subscriber"
Expand Down
160 changes: 112 additions & 48 deletions tracing-appender/src/rolling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ use std::{
fmt::{self, Debug},
fs::{self, File, OpenOptions},
io::{self, Write},
path::Path,
path::{Path, PathBuf},
sync::atomic::{AtomicUsize, Ordering},
};
use time::{format_description, Duration, OffsetDateTime, Time};

mod builder;
pub use builder::{Builder, InitError};

/// A file appender with the ability to rotate log files at a fixed schedule.
///
/// `RollingFileAppender` implements the [`std:io::Write` trait][write] and will
Expand Down Expand Up @@ -98,8 +101,8 @@ pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);

#[derive(Debug)]
struct Inner {
log_directory: String,
log_filename_prefix: String,
log_directory: PathBuf,
log_filename_prefix: Option<String>,
rotation: Rotation,
next_date: AtomicUsize,
}
Expand All @@ -122,8 +125,10 @@ impl RollingFileAppender {
/// - [`Rotation::daily()`][daily],
/// - [`Rotation::never()`][never()]
///
/// Additional parameters can be configured using [`RollingFileAppender::builder`].
///
/// # Examples
///
/// ```rust
/// # fn docs() {
/// use tracing_appender::rolling::{RollingFileAppender, Rotation};
Expand All @@ -133,16 +138,63 @@ impl RollingFileAppender {
pub fn new(
rotation: Rotation,
directory: impl AsRef<Path>,
file_name_prefix: impl AsRef<Path>,
filename_prefix: impl AsRef<Path>,
) -> RollingFileAppender {
let filename_prefix = filename_prefix
.as_ref()
.to_str()
.expect("filename prefix must be a valid UTF-8 string");
Self::builder()
.rotation(rotation)
.filename_prefix(filename_prefix)
.build(directory)
.expect("initializing rolling file appender failed")
}

/// Returns a new [`Builder`] for configuring a `RollingFileAppender`.
///
/// The builder interface can be used to set additional configuration
/// parameters when constructing a new appender.
///
/// Unlike [`RollingFileAppender::new`], the [`Builder::build`] method
/// returns a `Result` rather than panicking when the appender cannot be
/// initialized. Therefore, the builder interface can also be used when
/// appender initialization errors should be handled gracefully.
///
/// # Examples
///
/// ```rust
/// # fn docs() {
/// use tracing_appender::rolling::{RollingFileAppender, Rotation};
///
/// let file_appender = RollingFileAppender::builder()
/// .rotation(Rotation::HOURLY) // rotate log files once every hour
/// .filename_prefix("myapp") // log file names will be prefixed with `myapp.`
/// .build("/var/log") // try to build an appender that stores log files in `/var/log`
/// .expect("initializing rolling file appender failed");
/// # drop(file_appender);
/// # }
/// ```
#[must_use]
pub fn builder() -> Builder {
Builder::new()
}

fn from_builder(builder: &Builder, directory: impl AsRef<Path>) -> Result<Self, InitError> {
let Builder {
ref rotation,
ref prefix,
} = builder;
let filename_prefix = prefix.clone();
let directory = directory.as_ref().to_path_buf();
let now = OffsetDateTime::now_utc();
let (state, writer) = Inner::new(now, rotation, directory, file_name_prefix);
Self {
let (state, writer) = Inner::new(now, rotation.clone(), directory, filename_prefix)?;
Ok(Self {
state,
writer,
#[cfg(test)]
now: Box::new(OffsetDateTime::now_utc),
}
})
}

#[inline]
Expand Down Expand Up @@ -428,35 +480,42 @@ impl Rotation {
}
}

pub(crate) fn join_date(&self, filename: &str, date: &OffsetDateTime) -> String {
match *self {
pub(crate) fn join_date(&self, filename: Option<&str>, date: &OffsetDateTime) -> String {
let date = match *self {
Rotation::MINUTELY => {
let format = format_description::parse("[year]-[month]-[day]-[hour]-[minute]")
.expect("Unable to create a formatter; this is a bug in tracing-appender");

let date = date
.format(&format)
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
format!("{}.{}", filename, date)
date.format(&format)
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
}
Rotation::HOURLY => {
let format = format_description::parse("[year]-[month]-[day]-[hour]")
.expect("Unable to create a formatter; this is a bug in tracing-appender");

let date = date
.format(&format)
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
format!("{}.{}", filename, date)
date.format(&format)
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
}
Rotation::DAILY => {
let format = format_description::parse("[year]-[month]-[day]")
.expect("Unable to create a formatter; this is a bug in tracing-appender");
let date = date
.format(&format)
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
format!("{}.{}", filename, date)
date.format(&format)
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
}
Rotation::NEVER => {
// If there's a name prefix, use that.
if let Some(filename) = filename {
return filename.to_owned();
}

// Otherwise, just use the date.
let format = format_description::parse("[year]-[month]-[day]")
.expect("Unable to create a formatter; this is a bug in tracing-appender");
date.format(&format)
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
}
Rotation::NEVER => filename.to_string(),
};
match filename {
Some(filename) => format!("{}.{}", filename, date),
None => date,
}
}
}
Expand All @@ -480,32 +539,30 @@ impl Inner {
now: OffsetDateTime,
rotation: Rotation,
directory: impl AsRef<Path>,
file_name_prefix: impl AsRef<Path>,
) -> (Self, RwLock<File>) {
let log_directory = directory.as_ref().to_str().unwrap();
let log_filename_prefix = file_name_prefix.as_ref().to_str().unwrap();

let filename = rotation.join_date(log_filename_prefix, &now);
log_filename_prefix: Option<String>,
) -> Result<(Self, RwLock<File>), builder::InitError> {
let log_directory = directory.as_ref().to_path_buf();
let filename = rotation.join_date(log_filename_prefix.as_deref(), &now);
let next_date = rotation.next_date(&now);
let writer = RwLock::new(
create_writer(log_directory, &filename).expect("failed to create appender"),
);
let writer = RwLock::new(create_writer(log_directory.as_ref(), &filename)?);

let inner = Inner {
log_directory: log_directory.to_string(),
log_filename_prefix: log_filename_prefix.to_string(),
log_directory,
log_filename_prefix,
next_date: AtomicUsize::new(
next_date
.map(|date| date.unix_timestamp() as usize)
.unwrap_or(0),
),
rotation,
};
(inner, writer)
Ok((inner, writer))
}

fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
let filename = self.rotation.join_date(&self.log_filename_prefix, &now);
let filename = self
.rotation
.join_date(self.log_filename_prefix.as_deref(), &now);

match create_writer(&self.log_directory, &filename) {
Ok(new_file) => {
Expand Down Expand Up @@ -552,20 +609,22 @@ impl Inner {
}
}

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

let new_file = open_options.open(path.as_path());
if new_file.is_err() {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
return open_options.open(path);
fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
return open_options
.open(path)
.map_err(InitError::ctx("failed to create initial log file"));
}
}

new_file
new_file.map_err(InitError::ctx("failed to create initial log file"))
}

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

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

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

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

// never
let path = Rotation::NEVER.join_date("app.log", &now);
let path = Rotation::NEVER.join_date(Some("app.log"), &now);
assert_eq!("app.log", path);
}

Expand All @@ -702,8 +761,13 @@ mod test {

let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
let directory = tempfile::tempdir().expect("failed to create tempdir");
let (state, writer) =
Inner::new(now, Rotation::HOURLY, directory.path(), "test_make_writer");
let (state, writer) = Inner::new(
now,
Rotation::HOURLY,
directory.path(),
Some("test_make_writer".to_string()),
)
.unwrap();

let clock = Arc::new(Mutex::new(now));
let now = {
Expand Down
Loading