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 serde_value::Value;
#[cfg(feature = "config_parsing")]
use std::collections::BTreeMap;
use crate::{
append::Append,
encode::{self, pattern::PatternEncoder, Encode},
};
#[cfg(feature = "config_parsing")]
use crate::config::{Deserialize, Deserializers};
#[cfg(feature = "config_parsing")]
use crate::encode::EncoderConfig;
pub mod policy;
#[cfg(feature = "config_parsing")]
#[derive(Clone, Eq, PartialEq, Hash, Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RollingFileAppenderConfig {
path: String,
append: Option<bool>,
encoder: Option<EncoderConfig>,
policy: Policy,
}
#[cfg(feature = "config_parsing")]
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
struct Policy {
kind: String,
config: Value,
}
#[cfg(feature = "config_parsing")]
impl<'de> serde::Deserialize<'de> for Policy {
fn deserialize<D>(d: D) -> Result<Policy, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut map = BTreeMap::<Value, Value>::deserialize(d)?;
let kind = match map.remove(&Value::String("kind".to_owned())) {
Some(kind) => kind.deserialize_into().map_err(|e| e.to_error())?,
None => "compound".to_owned(),
};
Ok(Policy {
kind,
config: Value::Map(map),
})
}
}
#[derive(Debug)]
struct LogWriter {
file: BufWriter<File>,
len: u64,
}
impl io::Write for LogWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file.write(buf).map(|n| {
self.len += n as u64;
n
})
}
fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
}
impl encode::Write for LogWriter {}
#[derive(Debug)]
pub struct LogFile<'a> {
writer: &'a mut Option<LogWriter>,
path: &'a Path,
len: u64,
}
#[allow(clippy::len_without_is_empty)]
impl<'a> LogFile<'a> {
pub fn path(&self) -> &Path {
self.path
}
#[deprecated(since = "0.9.1", note = "Please use the len_estimate function instead")]
pub fn len(&self) -> u64 {
self.len
}
pub fn len_estimate(&self) -> u64 {
self.len
}
pub fn roll(&mut self) {
*self.writer = None;
}
}
#[derive(Derivative)]
#[derivative(Debug)]
pub struct RollingFileAppender {
#[derivative(Debug = "ignore")]
writer: Mutex<Option<LogWriter>>,
path: PathBuf,
append: bool,
encoder: Box<dyn Encode>,
policy: Box<dyn policy::Policy>,
}
impl Append for RollingFileAppender {
fn append(&self, record: &Record) -> anyhow::Result<()> {
let mut writer = self.writer.lock();
let len = {
let writer = self.get_writer(&mut writer)?;
self.encoder.encode(writer, record)?;
writer.flush()?;
writer.len
};
let mut file = LogFile {
writer: &mut writer,
path: &self.path,
len,
};
self.policy.process(&mut file)
}
fn flush(&self) {}
}
impl RollingFileAppender {
pub fn builder() -> RollingFileAppenderBuilder {
RollingFileAppenderBuilder {
append: true,
encoder: None,
}
}
fn get_writer<'a>(&self, writer: &'a mut Option<LogWriter>) -> io::Result<&'a mut LogWriter> {
if writer.is_none() {
let file = OpenOptions::new()
.write(true)
.append(self.append)
.truncate(!self.append)
.create(true)
.open(&self.path)?;
let len = if self.append {
file.metadata()?.len()
} else {
0
};
*writer = Some(LogWriter {
file: BufWriter::with_capacity(1024, file),
len,
});
}
Ok(writer.as_mut().unwrap())
}
}
pub struct RollingFileAppenderBuilder {
append: bool,
encoder: Option<Box<dyn Encode>>,
}
impl RollingFileAppenderBuilder {
pub fn append(mut self, append: bool) -> RollingFileAppenderBuilder {
self.append = append;
self
}
pub fn encoder(mut self, encoder: Box<dyn Encode>) -> RollingFileAppenderBuilder {
self.encoder = Some(encoder);
self
}
pub fn build<P>(
self,
path: P,
policy: Box<dyn policy::Policy>,
) -> io::Result<RollingFileAppender>
where
P: AsRef<Path>,
{
let path = super::env_util::expand_env_vars(path.as_ref().to_path_buf());
let appender = RollingFileAppender {
writer: Mutex::new(None),
path,
append: self.append,
encoder: self
.encoder
.unwrap_or_else(|| Box::new(PatternEncoder::default())),
policy,
};
if let Some(parent) = appender.path.parent() {
fs::create_dir_all(parent)?;
}
appender.get_writer(&mut appender.writer.lock())?;
Ok(appender)
}
}
#[cfg(feature = "config_parsing")]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct RollingFileAppenderDeserializer;
#[cfg(feature = "config_parsing")]
impl Deserialize for RollingFileAppenderDeserializer {
type Trait = dyn Append;
type Config = RollingFileAppenderConfig;
fn deserialize(
&self,
config: RollingFileAppenderConfig,
deserializers: &Deserializers,
) -> anyhow::Result<Box<dyn Append>> {
let mut builder = RollingFileAppender::builder();
if let Some(append) = config.append {
builder = builder.append(append);
}
if let Some(encoder) = config.encoder {
let encoder = deserializers.deserialize(&encoder.kind, encoder.config)?;
builder = builder.encoder(encoder);
}
let policy = deserializers.deserialize(&config.policy.kind, config.policy.config)?;
let appender = builder.build(config.path, policy)?;
Ok(Box::new(appender))
}
}
#[cfg(test)]
mod test {
use std::{
fs::File,
io::{Read, Write},
};
use super::*;
use crate::append::rolling_file::policy::Policy;
#[test]
#[cfg(feature = "yaml_format")]
fn deserialize() {
use crate::config::{Deserializers, RawConfig};
let dir = tempfile::tempdir().unwrap();
let config = format!(
"
appenders:
foo:
kind: rolling_file
path: {0}/foo.log
policy:
trigger:
kind: size
limit: 1024
roller:
kind: delete
bar:
kind: rolling_file
path: {0}/foo.log
policy:
kind: compound
trigger:
kind: size
limit: 5 mb
roller:
kind: fixed_window
pattern: '{0}/foo.log.{{}}'
base: 1
count: 5
",
dir.path().display()
);
let config = ::serde_yaml::from_str::<RawConfig>(&config).unwrap();
let errors = config.appenders_lossy(&Deserializers::new()).1;
println!("{:?}", errors);
assert!(errors.is_empty());
}
#[derive(Debug)]
struct NopPolicy;
impl Policy for NopPolicy {
fn process(&self, _: &mut LogFile) -> anyhow::Result<()> {
Ok(())
}
}
#[test]
fn append() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("append.log");
RollingFileAppender::builder()
.append(true)
.build(&path, Box::new(NopPolicy))
.unwrap();
assert!(path.exists());
File::create(&path).unwrap().write_all(b"hello").unwrap();
RollingFileAppender::builder()
.append(true)
.build(&path, Box::new(NopPolicy))
.unwrap();
let mut contents = vec![];
File::open(&path)
.unwrap()
.read_to_end(&mut contents)
.unwrap();
assert_eq!(contents, b"hello");
}
#[test]
fn truncate() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("truncate.log");
RollingFileAppender::builder()
.append(false)
.build(&path, Box::new(NopPolicy))
.unwrap();
assert!(path.exists());
File::create(&path).unwrap().write_all(b"hello").unwrap();
RollingFileAppender::builder()
.append(false)
.build(&path, Box::new(NopPolicy))
.unwrap();
let mut contents = vec![];
File::open(&path)
.unwrap()
.read_to_end(&mut contents)
.unwrap();
assert_eq!(contents, b"");
}
}