#![allow(clippy::unnecessary_debug_formatting)]
use crate::Error;
use crate::internal::ToDateTime;
use crate::log::logger::LogEvent;
use crate::log::prefix_file_set::{PrefixFile, PrefixFileSet};
use crate::log::{Level, tag};
use std::fs::{File, OpenOptions};
use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, SyncSender, sync_channel};
use std::time::{Duration, SystemTime};
pub struct LogFile {
pub file: File,
pub len: u64,
pub path: PathBuf,
pub created: SystemTime,
}
impl LogFile {
pub fn create(path_prefix: &Path) -> Result<Self, String> {
for n in 0..u64::MAX {
let dt = SystemTime::now().to_datetime();
let mut path_str = path_prefix.as_os_str().to_os_string();
path_str.push(format!(
".{:04}{:02}{:02}T{:02}{:02}{:02}Z-{n}",
dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec
));
let path: PathBuf = PathBuf::from(path_str);
match OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => {
return Ok(Self {
file,
len: 0,
path,
created: SystemTime::now(),
});
}
Err(e) if e.kind() == ErrorKind::AlreadyExists => {}
Err(e) => {
return Err(format!("error creating file {path:?}: {e:?}"));
}
}
}
unreachable!();
}
pub fn write_all(&mut self, buffer: &[u8]) -> Result<(), String> {
self.file
.write_all(buffer)
.map_err(|e| format!("error writing file {:?}: {e:?}", self.path))?;
self.len += buffer.len() as u64;
Ok(())
}
#[must_use]
pub fn age(&self, now: SystemTime) -> Duration {
now.duration_since(self.created)
.unwrap_or(Duration::from_secs(0))
}
}
#[allow(clippy::module_name_repetitions)]
pub struct LogFileWriter {
pub max_keep_age: Option<Duration>,
pub max_keep_bytes: u64,
pub max_write_age: Duration,
pub max_write_bytes: u64,
pub path_prefix: PathBuf,
}
impl LogFileWriter {
#[must_use]
pub fn new_builder(path_prefix: impl Into<PathBuf>, max_keep_bytes: u64) -> LogFileWriter {
LogFileWriter {
max_keep_age: None,
max_keep_bytes,
max_write_age: Duration::from_secs(24 * 3600),
max_write_bytes: 10 * 1024 * 1024,
path_prefix: path_prefix.into(),
}
}
#[must_use]
pub fn with_max_keep_age(mut self, duration: Duration) -> Self {
assert!(
duration >= Duration::from_secs(60),
"duration is less than 1 minute: {duration:?}"
);
self.max_keep_age = Some(duration);
self
}
#[must_use]
pub fn with_max_write_age(mut self, duration: Duration) -> Self {
assert!(
duration >= Duration::from_secs(1),
"duration is less than 1 second: {duration:?}"
);
self.max_write_age = duration;
self
}
#[must_use]
pub fn with_max_write_bytes(mut self, len: u64) -> Self {
assert!(len >= (64 * 1024), "len is less than 64 KiB: {len}");
self.max_write_bytes = len;
self
}
#[allow(clippy::missing_panics_doc)]
pub fn start_writer_thread(self) -> Result<SyncSender<LogEvent>, Error> {
let dir = self
.path_prefix
.parent()
.ok_or_else(|| format!("path has no parent: {:?}", self.path_prefix))?;
let mut path_prefix = if dir.is_absolute() {
dir.to_path_buf()
} else {
std::env::current_dir()
.map_err(|e| format!("cannot find current directory to resolve path {dir:?}: {e}"))?
.join(dir)
}
.canonicalize()
.map_err(|e| {
format!(
"error getting canonical path of {:?}: {e}",
self.path_prefix
)
})?;
let file_name = self.path_prefix.file_name().ok_or_else(|| {
format!(
"path_prefix does not contain a filename part: {:?}",
path_prefix.to_string_lossy()
)
})?;
path_prefix.push(Path::new(file_name));
let mut file_set = PrefixFileSet::new(&path_prefix)?;
file_set.delete_oldest_while_over_max_len(self.max_keep_bytes)?;
let mut file = LogFile::create(&path_prefix)?;
let mut buffer: Vec<u8> = Vec::new();
LogEvent::new(Level::Info, tag("msg", "Starting log writer")).write_jsonl(&mut buffer)?;
file.write_all(&buffer)?;
buffer.clear();
let (sender, receiver): (SyncSender<LogEvent>, Receiver<LogEvent>) = sync_channel(100);
std::thread::spawn(move || {
for event in receiver {
event.write_jsonl(&mut buffer).unwrap();
let now = SystemTime::now();
if file.len + (buffer.len() as u64) > self.max_write_bytes
|| file.age(now) > self.max_write_age
{
file_set.push(PrefixFile {
path: file.path.clone(),
mtime: now,
len: file.len,
});
file = LogFile::create(&path_prefix).unwrap();
}
if let Some(duration) = self.max_keep_age {
file_set.delete_older_than(now, duration).unwrap();
}
file_set
.delete_oldest_while_over_max_len(
self.max_keep_bytes - file.len - (buffer.len() as u64),
)
.unwrap();
file.write_all(&buffer).unwrap();
buffer.clear();
}
file.file.sync_all().unwrap();
});
Ok(sender)
}
}