use crate::sync::{RwLock, RwLockReadGuard};
use std::{
fmt::{self, Debug},
fs::{self, File, OpenOptions},
io::{self, Write},
path::{Path, PathBuf},
sync::atomic::{AtomicUsize, Ordering},
time::SystemTime,
};
use time::{format_description, Date, Duration, OffsetDateTime, PrimitiveDateTime, Time};
mod builder;
pub use builder::{Builder, InitError};
pub struct RollingFileAppender {
state: Inner,
writer: RwLock<File>,
#[cfg(test)]
now: Box<dyn Fn() -> OffsetDateTime + Send + Sync>,
}
#[derive(Debug)]
pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
#[derive(Debug)]
struct Inner {
log_directory: PathBuf,
log_filename_prefix: Option<String>,
log_filename_suffix: Option<String>,
log_latest_symlink_name: Option<String>,
date_format: Vec<format_description::FormatItem<'static>>,
rotation: Rotation,
next_date: AtomicUsize,
max_files: Option<usize>,
}
impl RollingFileAppender {
pub fn new(
rotation: Rotation,
directory: 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")
}
#[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,
ref suffix,
ref latest_symlink,
ref max_files,
} = builder;
let directory = directory.as_ref().to_path_buf();
let now = OffsetDateTime::now_utc();
let (state, writer) = Inner::new(
now,
rotation.clone(),
directory,
prefix.clone(),
suffix.clone(),
latest_symlink.clone(),
*max_files,
)?;
Ok(Self {
state,
writer,
#[cfg(test)]
now: Box::new(OffsetDateTime::now_utc),
})
}
#[inline]
fn now(&self) -> OffsetDateTime {
#[cfg(test)]
return (self.now)();
#[cfg(not(test))]
OffsetDateTime::now_utc()
}
}
impl io::Write for RollingFileAppender {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let now = self.now();
let writer = self.writer.get_mut();
if let Some(current_time) = self.state.should_rollover(now) {
let _did_cas = self.state.advance_date(now, current_time);
debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp...");
self.state.refresh_writer(now, writer);
}
writer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.get_mut().flush()
}
}
impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender {
type Writer = RollingWriter<'a>;
fn make_writer(&'a self) -> Self::Writer {
let now = self.now();
if let Some(current_time) = self.state.should_rollover(now) {
if self.state.advance_date(now, current_time) {
self.state.refresh_writer(now, &mut self.writer.write());
}
}
RollingWriter(self.writer.read())
}
}
impl fmt::Debug for RollingFileAppender {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RollingFileAppender")
.field("state", &self.state)
.field("writer", &self.writer)
.finish()
}
}
pub fn minutely(
directory: impl AsRef<Path>,
file_name_prefix: impl AsRef<Path>,
) -> RollingFileAppender {
RollingFileAppender::new(Rotation::MINUTELY, directory, file_name_prefix)
}
pub fn hourly(
directory: impl AsRef<Path>,
file_name_prefix: impl AsRef<Path>,
) -> RollingFileAppender {
RollingFileAppender::new(Rotation::HOURLY, directory, file_name_prefix)
}
pub fn daily(
directory: impl AsRef<Path>,
file_name_prefix: impl AsRef<Path>,
) -> RollingFileAppender {
RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix)
}
pub fn weekly(
directory: impl AsRef<Path>,
file_name_prefix: impl AsRef<Path>,
) -> RollingFileAppender {
RollingFileAppender::new(Rotation::WEEKLY, directory, file_name_prefix)
}
pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> RollingFileAppender {
RollingFileAppender::new(Rotation::NEVER, directory, file_name)
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Rotation(RotationKind);
#[derive(Clone, Eq, PartialEq, Debug)]
enum RotationKind {
Minutely,
Hourly,
Daily,
Weekly,
Never,
}
impl Rotation {
pub const MINUTELY: Self = Self(RotationKind::Minutely);
pub const HOURLY: Self = Self(RotationKind::Hourly);
pub const DAILY: Self = Self(RotationKind::Daily);
pub const WEEKLY: Self = Self(RotationKind::Weekly);
pub const NEVER: Self = Self(RotationKind::Never);
pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> {
let unrounded_next_date = match *self {
Rotation::MINUTELY => *current_date + Duration::minutes(1),
Rotation::HOURLY => *current_date + Duration::hours(1),
Rotation::DAILY => *current_date + Duration::days(1),
Rotation::WEEKLY => *current_date + Duration::weeks(1),
Rotation::NEVER => return None,
};
Some(self.round_date(unrounded_next_date))
}
pub(crate) fn round_date(&self, date: OffsetDateTime) -> OffsetDateTime {
match *self {
Rotation::MINUTELY => {
let time = Time::from_hms(date.hour(), date.minute(), 0)
.expect("Invalid time; this is a bug in tracing-appender");
date.replace_time(time)
}
Rotation::HOURLY => {
let time = Time::from_hms(date.hour(), 0, 0)
.expect("Invalid time; this is a bug in tracing-appender");
date.replace_time(time)
}
Rotation::DAILY => {
let time = Time::from_hms(0, 0, 0)
.expect("Invalid time; this is a bug in tracing-appender");
date.replace_time(time)
}
Rotation::WEEKLY => {
let zero_time = Time::from_hms(0, 0, 0)
.expect("Invalid time; this is a bug in tracing-appender");
let days_since_sunday = date.weekday().number_days_from_sunday();
let date = date - Duration::days(days_since_sunday.into());
date.replace_time(zero_time)
}
Rotation::NEVER => {
unreachable!("Rotation::NEVER is impossible to round.")
}
}
}
fn date_format(&self) -> Vec<format_description::FormatItem<'static>> {
match *self {
Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"),
Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
Rotation::WEEKLY => format_description::parse("[year]-[month]-[day]"),
Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
}
.expect("Unable to create a formatter; this is a bug in tracing-appender")
}
}
impl io::Write for RollingWriter<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
(&*self.0).write(buf)
}
fn flush(&mut self) -> io::Result<()> {
(&*self.0).flush()
}
}
impl Inner {
fn new(
now: OffsetDateTime,
rotation: Rotation,
directory: impl AsRef<Path>,
log_filename_prefix: Option<String>,
log_filename_suffix: Option<String>,
log_latest_symlink_name: Option<String>,
max_files: Option<usize>,
) -> Result<(Self, RwLock<File>), builder::InitError> {
let log_directory = directory.as_ref().to_path_buf();
let date_format = rotation.date_format();
let next_date = rotation.next_date(&now);
let inner = Inner {
log_directory,
log_filename_prefix,
log_filename_suffix,
log_latest_symlink_name,
date_format,
next_date: AtomicUsize::new(
next_date
.map(|date| date.unix_timestamp() as usize)
.unwrap_or(0),
),
rotation,
max_files,
};
if let Some(max_files) = max_files {
inner.prune_old_logs(max_files);
}
let filename = inner.join_date(&now);
let writer = RwLock::new(create_writer(
inner.log_directory.as_ref(),
&filename,
inner.log_latest_symlink_name.as_deref(),
)?);
Ok((inner, writer))
}
pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String {
let date = if let Rotation::NEVER = self.rotation {
date.format(&self.date_format)
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
} else {
self.rotation
.round_date(*date)
.format(&self.date_format)
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
};
match (
&self.rotation,
&self.log_filename_prefix,
&self.log_filename_suffix,
) {
(&Rotation::NEVER, Some(filename), None) => filename.to_string(),
(&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix),
(&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(),
(_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix),
(_, Some(filename), None) => format!("{}.{}", filename, date),
(_, None, Some(suffix)) => format!("{}.{}", date, suffix),
(_, None, None) => date,
}
}
fn prune_old_logs(&self, max_files: usize) {
let files = fs::read_dir(&self.log_directory).map(|dir| {
dir.filter_map(|entry| {
let entry = entry.ok()?;
let metadata = entry.metadata().ok()?;
if !metadata.is_file() {
return None;
}
let filename = entry.file_name();
let filename = filename.to_str()?;
if let Some(prefix) = &self.log_filename_prefix {
if !filename.starts_with(prefix) {
return None;
}
}
if let Some(suffix) = &self.log_filename_suffix {
if !filename.ends_with(suffix) {
return None;
}
}
if self.log_filename_prefix.is_none()
&& self.log_filename_suffix.is_none()
&& Date::parse(filename, &self.date_format).is_err()
{
return None;
}
let created = metadata.created().ok().or_else(|| {
parse_date_from_filename(
filename,
&self.date_format,
self.log_filename_prefix.as_deref(),
self.log_filename_suffix.as_deref(),
)
})?;
Some((entry, created))
})
.collect::<Vec<_>>()
});
let mut files = match files {
Ok(files) => files,
Err(error) => {
eprintln!("Error reading the log directory/files: {}", error);
return;
}
};
if files.len() < max_files {
return;
}
files.sort_by_key(|(_, created_at)| *created_at);
for (file, _) in files.iter().take(files.len() - (max_files - 1)) {
if let Err(error) = fs::remove_file(file.path()) {
eprintln!(
"Failed to remove old log file {}: {}",
file.path().display(),
error
);
}
}
}
fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
let filename = self.join_date(&now);
if let Some(max_files) = self.max_files {
self.prune_old_logs(max_files);
}
match create_writer(
&self.log_directory,
&filename,
self.log_latest_symlink_name.as_deref(),
) {
Ok(new_file) => {
if let Err(err) = file.flush() {
eprintln!("Couldn't flush previous writer: {}", err);
}
*file = new_file;
}
Err(err) => eprintln!("Couldn't create writer for logs: {}", err),
}
}
fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
let next_date = self.next_date.load(Ordering::Acquire);
if next_date == 0 {
return None;
}
if date.unix_timestamp() as usize >= next_date {
return Some(next_date);
}
None
}
fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
let next_date = self
.rotation
.next_date(&now)
.map(|date| date.unix_timestamp() as usize)
.unwrap_or(0);
self.next_date
.compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
}
}
fn create_writer(
directory: &Path,
filename: &str,
latest_symlink_name: Option<&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).or_else(|_| {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
}
open_options
.open(&path)
.map_err(InitError::ctx("failed to create log file"))
})?;
if let Some(symlink_name) = latest_symlink_name {
let symlink_path = directory.join(symlink_name);
let _ = symlink::remove_symlink_file(&symlink_path);
symlink::symlink_file(path, symlink_path).map_err(InitError::ctx(
"failed to create symlink to latest log file",
))?;
}
Ok(new_file)
}
fn parse_date_from_filename(
filename: &str,
date_format: &Vec<format_description::FormatItem<'static>>,
prefix: Option<&str>,
suffix: Option<&str>,
) -> Option<SystemTime> {
let mut datetime = filename;
if let Some(prefix) = prefix {
datetime = datetime.strip_prefix(prefix)?;
datetime = datetime.strip_prefix('.')?;
}
if let Some(suffix) = suffix {
datetime = datetime.strip_suffix(suffix)?;
datetime = datetime.strip_suffix('.')?;
}
PrimitiveDateTime::parse(datetime, date_format)
.or_else(|_| Date::parse(datetime, date_format).map(|d| d.with_time(Time::MIDNIGHT)))
.ok()
.map(|dt| dt.assume_utc().into())
}
#[cfg(test)]
mod test {
use super::*;
use std::fs;
use std::io::Write;
fn find_str_in_log(dir_path: &Path, expected_value: &str) -> bool {
let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory");
for entry in dir_contents {
let path = entry.expect("Expected dir entry").path();
let file = fs::read_to_string(&path).expect("Failed to read file");
println!("path={}\nfile={:?}", path.display(), file);
if file.as_str() == expected_value {
return true;
}
}
false
}
fn write_to_log(appender: &mut RollingFileAppender, msg: &str) {
appender
.write_all(msg.as_bytes())
.expect("Failed to write to appender");
appender.flush().expect("Failed to flush!");
}
fn test_appender(rotation: Rotation, file_prefix: &str) {
let directory = tempfile::tempdir().expect("failed to create tempdir");
let mut appender = RollingFileAppender::new(rotation, directory.path(), file_prefix);
let expected_value = "Hello";
write_to_log(&mut appender, expected_value);
assert!(find_str_in_log(directory.path(), expected_value));
directory
.close()
.expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.")
}
#[test]
fn write_minutely_log() {
test_appender(Rotation::MINUTELY, "minutely.log");
}
#[test]
fn write_hourly_log() {
test_appender(Rotation::HOURLY, "hourly.log");
}
#[test]
fn write_daily_log() {
test_appender(Rotation::DAILY, "daily.log");
}
#[test]
fn write_weekly_log() {
test_appender(Rotation::WEEKLY, "weekly.log");
}
#[test]
fn write_never_log() {
test_appender(Rotation::NEVER, "never.log");
}
#[test]
fn test_rotations() {
let now = OffsetDateTime::now_utc();
let next = Rotation::MINUTELY.next_date(&now).unwrap();
assert_eq!((now + Duration::MINUTE).minute(), next.minute());
let now = OffsetDateTime::now_utc();
let next = Rotation::HOURLY.next_date(&now).unwrap();
assert_eq!((now + Duration::HOUR).hour(), next.hour());
let now = OffsetDateTime::now_utc();
let next = Rotation::DAILY.next_date(&now).unwrap();
assert_eq!((now + Duration::DAY).day(), next.day());
let now = OffsetDateTime::now_utc();
let now_rounded = Rotation::WEEKLY.round_date(now);
let next = Rotation::WEEKLY.next_date(&now).unwrap();
assert!(now_rounded < next);
let now = OffsetDateTime::now_utc();
let next = Rotation::NEVER.next_date(&now);
assert!(next.is_none());
}
#[test]
fn test_join_date() {
struct TestCase {
expected: &'static str,
rotation: Rotation,
prefix: Option<&'static str>,
suffix: Option<&'static str>,
now: OffsetDateTime,
}
let format = format_description::parse(
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
sign:mandatory]:[offset_minute]:[offset_second]",
)
.unwrap();
let directory = tempfile::tempdir().expect("failed to create tempdir");
let test_cases = vec![
TestCase {
expected: "my_prefix.2025-02-16.log",
rotation: Rotation::WEEKLY,
prefix: Some("my_prefix"),
suffix: Some("log"),
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
},
TestCase {
expected: "my_prefix.2024-12-29.log",
rotation: Rotation::WEEKLY,
prefix: Some("my_prefix"),
suffix: Some("log"),
now: OffsetDateTime::parse("2025-01-01 10:01:00 +00:00:00", &format).unwrap(),
},
TestCase {
expected: "my_prefix.2025-02-17.log",
rotation: Rotation::DAILY,
prefix: Some("my_prefix"),
suffix: Some("log"),
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
},
TestCase {
expected: "my_prefix.2025-02-17-10.log",
rotation: Rotation::HOURLY,
prefix: Some("my_prefix"),
suffix: Some("log"),
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
},
TestCase {
expected: "my_prefix.2025-02-17-10-01.log",
rotation: Rotation::MINUTELY,
prefix: Some("my_prefix"),
suffix: Some("log"),
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
},
TestCase {
expected: "my_prefix.log",
rotation: Rotation::NEVER,
prefix: Some("my_prefix"),
suffix: Some("log"),
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
},
];
for test_case in test_cases {
let (inner, _) = Inner::new(
test_case.now,
test_case.rotation.clone(),
directory.path(),
test_case.prefix.map(ToString::to_string),
test_case.suffix.map(ToString::to_string),
None,
None,
)
.unwrap();
let path = inner.join_date(&test_case.now);
assert_eq!(path, test_case.expected);
}
}
#[test]
#[should_panic(
expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
)]
fn test_never_date_rounding() {
let now = OffsetDateTime::now_utc();
let _ = Rotation::NEVER.round_date(now);
}
#[test]
fn test_path_concatenation() {
let format = format_description::parse(
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
sign:mandatory]:[offset_minute]:[offset_second]",
)
.unwrap();
let directory = tempfile::tempdir().expect("failed to create tempdir");
let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
struct TestCase {
expected: &'static str,
rotation: Rotation,
prefix: Option<&'static str>,
suffix: Option<&'static str>,
}
let test = |TestCase {
expected,
rotation,
prefix,
suffix,
}| {
let (inner, _) = Inner::new(
now,
rotation.clone(),
directory.path(),
prefix.map(ToString::to_string),
suffix.map(ToString::to_string),
None,
None,
)
.unwrap();
let path = inner.join_date(&now);
assert_eq!(
expected, path,
"rotation = {:?}, prefix = {:?}, suffix = {:?}",
rotation, prefix, suffix
);
};
let test_cases = vec![
TestCase {
expected: "app.log.2020-02-01-10-01",
rotation: Rotation::MINUTELY,
prefix: Some("app.log"),
suffix: None,
},
TestCase {
expected: "app.log.2020-02-01-10",
rotation: Rotation::HOURLY,
prefix: Some("app.log"),
suffix: None,
},
TestCase {
expected: "app.log.2020-02-01",
rotation: Rotation::DAILY,
prefix: Some("app.log"),
suffix: None,
},
TestCase {
expected: "app.log",
rotation: Rotation::NEVER,
prefix: Some("app.log"),
suffix: None,
},
TestCase {
expected: "app.2020-02-01-10-01.log",
rotation: Rotation::MINUTELY,
prefix: Some("app"),
suffix: Some("log"),
},
TestCase {
expected: "app.2020-02-01-10.log",
rotation: Rotation::HOURLY,
prefix: Some("app"),
suffix: Some("log"),
},
TestCase {
expected: "app.2020-02-01.log",
rotation: Rotation::DAILY,
prefix: Some("app"),
suffix: Some("log"),
},
TestCase {
expected: "app.log",
rotation: Rotation::NEVER,
prefix: Some("app"),
suffix: Some("log"),
},
TestCase {
expected: "2020-02-01-10-01.log",
rotation: Rotation::MINUTELY,
prefix: None,
suffix: Some("log"),
},
TestCase {
expected: "2020-02-01-10.log",
rotation: Rotation::HOURLY,
prefix: None,
suffix: Some("log"),
},
TestCase {
expected: "2020-02-01.log",
rotation: Rotation::DAILY,
prefix: None,
suffix: Some("log"),
},
TestCase {
expected: "log",
rotation: Rotation::NEVER,
prefix: None,
suffix: Some("log"),
},
];
for test_case in test_cases {
test(test_case)
}
}
#[test]
fn test_make_writer() {
use std::sync::{Arc, Mutex};
use tracing_subscriber::prelude::*;
let format = format_description::parse(
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
sign:mandatory]:[offset_minute]:[offset_second]",
)
.unwrap();
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(),
Some("test_make_writer".to_string()),
None,
None,
None,
)
.unwrap();
let clock = Arc::new(Mutex::new(now));
let now = {
let clock = clock.clone();
Box::new(move || *clock.lock().unwrap())
};
let appender = RollingFileAppender { state, writer, now };
let default = tracing_subscriber::fmt()
.without_time()
.with_level(false)
.with_target(false)
.with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
.with_writer(appender)
.finish()
.set_default();
tracing::info!("file 1");
(*clock.lock().unwrap()) += Duration::seconds(1);
tracing::info!("file 1");
(*clock.lock().unwrap()) += Duration::hours(1);
tracing::info!("file 2");
(*clock.lock().unwrap()) += Duration::seconds(1);
tracing::info!("file 2");
drop(default);
let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
println!("dir={:?}", dir_contents);
for entry in dir_contents {
println!("entry={:?}", entry);
let path = entry.expect("Expected dir entry").path();
let file = fs::read_to_string(&path).expect("Failed to read file");
println!("path={}\nfile={:?}", path.display(), file);
match path
.extension()
.expect("found a file without a date!")
.to_str()
.expect("extension should be UTF8")
{
"2020-02-01-10" => {
assert_eq!("file 1\nfile 1\n", file);
}
"2020-02-01-11" => {
assert_eq!("file 2\nfile 2\n", file);
}
x => panic!("unexpected date {}", x),
}
}
}
#[test]
fn test_max_log_files() {
use std::sync::{Arc, Mutex};
use tracing_subscriber::prelude::*;
let format = format_description::parse(
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
sign:mandatory]:[offset_minute]:[offset_second]",
)
.unwrap();
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(),
Some("test_max_log_files".to_string()),
None,
None,
Some(2),
)
.unwrap();
let clock = Arc::new(Mutex::new(now));
let now = {
let clock = clock.clone();
Box::new(move || *clock.lock().unwrap())
};
let appender = RollingFileAppender { state, writer, now };
let default = tracing_subscriber::fmt()
.without_time()
.with_level(false)
.with_target(false)
.with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
.with_writer(appender)
.finish()
.set_default();
tracing::info!("file 1");
(*clock.lock().unwrap()) += Duration::seconds(1);
tracing::info!("file 1");
(*clock.lock().unwrap()) += Duration::hours(1);
std::thread::sleep(std::time::Duration::from_secs(1));
tracing::info!("file 2");
(*clock.lock().unwrap()) += Duration::seconds(1);
tracing::info!("file 2");
(*clock.lock().unwrap()) += Duration::hours(1);
std::thread::sleep(std::time::Duration::from_secs(1));
tracing::info!("file 3");
(*clock.lock().unwrap()) += Duration::seconds(1);
tracing::info!("file 3");
drop(default);
let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
println!("dir={:?}", dir_contents);
for entry in dir_contents {
println!("entry={:?}", entry);
let path = entry.expect("Expected dir entry").path();
let file = fs::read_to_string(&path).expect("Failed to read file");
println!("path={}\nfile={:?}", path.display(), file);
match path
.extension()
.expect("found a file without a date!")
.to_str()
.expect("extension should be UTF8")
{
"2020-02-01-10" => {
panic!("this file should have been pruned already!");
}
"2020-02-01-11" => {
assert_eq!("file 2\nfile 2\n", file);
}
"2020-02-01-12" => {
assert_eq!("file 3\nfile 3\n", file);
}
x => panic!("unexpected date {}", x),
}
}
}
#[test]
fn test_parse_date_from_filename_daily() {
let date_format = Rotation::DAILY.date_format();
let filename = "app.2020-02-01.log";
let created = parse_date_from_filename(filename, &date_format, Some("app"), Some("log"));
assert_eq!(
created,
Some(SystemTime::UNIX_EPOCH + Duration::seconds(1580515200))
);
}
#[test]
fn test_parse_date_from_filename_hourly() {
let date_format = Rotation::HOURLY.date_format();
let filename = "app.2020-02-01-10.log";
let created = parse_date_from_filename(filename, &date_format, Some("app"), Some("log"));
assert_eq!(
created,
Some(SystemTime::UNIX_EPOCH + Duration::seconds(1580551200))
);
}
#[test]
fn test_parse_date_from_filename_minutely() {
let date_format = Rotation::MINUTELY.date_format();
let filename = "app.2020-02-01-10-01.log";
let created = parse_date_from_filename(filename, &date_format, Some("app"), Some("log"));
assert_eq!(
created,
Some(SystemTime::UNIX_EPOCH + Duration::seconds(1580551260))
);
}
#[test]
fn test_latest_symlink() {
use std::sync::{Arc, Mutex};
let format = format_description::parse(
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
sign:mandatory]:[offset_minute]:[offset_second]",
)
.unwrap();
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(),
Some("test_latest_symlink".to_string()),
None,
Some("latest.log".to_string()),
None,
)
.unwrap();
let symlink_path = directory.path().join("latest.log");
assert!(symlink_path.is_symlink(), "latest.log should be a symlink");
let target = fs::read_link(&symlink_path).expect("failed to read symlink");
assert!(
target.to_string_lossy().contains("2020-02-01-10"),
"symlink should point to file with date 2020-02-01-10, but points to {:?}",
target
);
let clock = Arc::new(Mutex::new(now));
let now_fn = {
let clock = clock.clone();
Box::new(move || *clock.lock().unwrap())
};
let mut appender = RollingFileAppender {
state,
writer,
now: now_fn,
};
*clock.lock().unwrap() += Duration::hours(1);
appender.write_all(b"test\n").expect("failed to write");
appender.flush().expect("failed to flush");
let target = fs::read_link(&symlink_path).expect("failed to read symlink");
assert!(
target.to_string_lossy().contains("2020-02-01-11"),
"symlink should point to file with date 2020-02-01-11, but points to {:?}",
target
);
let content = fs::read_to_string(&symlink_path).expect("failed to read through symlink");
assert_eq!("test\n", content);
}
}