use derivative::Derivative;
use log::Record;
use parking_lot::Mutex;
use std::{
fs::{self, File, OpenOptions},
io::{self, BufWriter, Write},
path::{Path, PathBuf},
};
#[cfg(feature = "config_parsing")]
use crate::config::{Deserialize, Deserializers};
#[cfg(feature = "config_parsing")]
use crate::encode::EncoderConfig;
use crate::{
append::{env_util::expand_env_vars, Append},
encode::{pattern::PatternEncoder, writer::simple::SimpleWriter, Encode},
};
#[cfg(feature = "config_parsing")]
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FileAppenderConfig {
path: String,
encoder: Option<EncoderConfig>,
append: Option<bool>,
}
#[derive(Derivative)]
#[derivative(Debug)]
pub struct FileAppender {
path: PathBuf,
#[derivative(Debug = "ignore")]
file: Mutex<SimpleWriter<BufWriter<File>>>,
encoder: Box<dyn Encode>,
}
impl Append for FileAppender {
fn append(&self, record: &Record) -> anyhow::Result<()> {
let mut file = self.file.lock();
self.encoder.encode(&mut *file, record)?;
file.flush()?;
Ok(())
}
fn flush(&self) {}
}
impl FileAppender {
pub fn builder() -> FileAppenderBuilder {
FileAppenderBuilder {
encoder: None,
append: true,
}
}
}
pub struct FileAppenderBuilder {
encoder: Option<Box<dyn Encode>>,
append: bool,
}
impl FileAppenderBuilder {
pub fn encoder(mut self, encoder: Box<dyn Encode>) -> FileAppenderBuilder {
self.encoder = Some(encoder);
self
}
pub fn append(mut self, append: bool) -> FileAppenderBuilder {
self.append = append;
self
}
pub fn build<P: AsRef<Path>>(self, path: P) -> io::Result<FileAppender> {
let path_cow = path.as_ref().to_string_lossy();
let path: PathBuf = expand_env_vars(path_cow).as_ref().into();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let file = OpenOptions::new()
.write(true)
.append(self.append)
.truncate(!self.append)
.create(true)
.open(&path)?;
Ok(FileAppender {
path,
file: Mutex::new(SimpleWriter(BufWriter::with_capacity(1024, file))),
encoder: self
.encoder
.unwrap_or_else(|| Box::<PatternEncoder>::default()),
})
}
}
#[cfg(feature = "config_parsing")]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct FileAppenderDeserializer;
#[cfg(feature = "config_parsing")]
impl Deserialize for FileAppenderDeserializer {
type Trait = dyn Append;
type Config = FileAppenderConfig;
fn deserialize(
&self,
config: FileAppenderConfig,
deserializers: &Deserializers,
) -> anyhow::Result<Box<Self::Trait>> {
let mut appender = FileAppender::builder();
if let Some(append) = config.append {
appender = appender.append(append);
}
if let Some(encoder) = config.encoder {
appender = appender.encoder(deserializers.deserialize(&encoder.kind, encoder.config)?);
}
Ok(Box::new(appender.build(&config.path)?))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn create_directories() {
let tempdir = tempfile::tempdir().unwrap();
FileAppender::builder()
.build(tempdir.path().join("foo").join("bar").join("baz.log"))
.unwrap();
}
#[test]
fn append_false() {
let tempdir = tempfile::tempdir().unwrap();
FileAppender::builder()
.append(false)
.build(tempdir.path().join("foo.log"))
.unwrap();
}
}