use crate::common::{
error::Result,
log::{Log, LogRecord, Loglevel, LoglevelFilter},
util::friendly_enum_parse,
};
use failure::Fail;
use serde::{Deserialize, Serialize};
use std::{fs::File, io::Write, path::PathBuf};
#[derive(Debug, Fail, PartialEq)]
pub enum TeeFileError {
#[fail(display = "{}", 0)]
ParseError(String),
}
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
pub struct TeeFileConfiguration {
pub filter: LoglevelFilter,
pub file: PathBuf,
}
impl TeeFileConfiguration {
pub fn new(
filter: impl Into<LoglevelFilter>,
file: impl Into<PathBuf>,
) -> TeeFileConfiguration {
TeeFileConfiguration {
filter: filter.into(),
file: file.into(),
}
}
}
#[derive(Debug)]
pub struct TeeFile {
pub configuration: TeeFileConfiguration,
buffer: Option<File>,
}
impl TeeFile {
pub fn new(configuration: TeeFileConfiguration) -> Result<TeeFile> {
let buffer = Some(File::create(&configuration.file)?);
Ok(TeeFile {
buffer,
configuration,
})
}
}
impl Log for TeeFile {
fn name(&self) -> &str {
self.configuration.file.to_str().unwrap()
}
fn enabled(&self, level: Loglevel) -> bool {
LoglevelFilter::from(level) <= self.configuration.filter
}
fn log(&self, record: &LogRecord) {
if let Some(mut buffer) = self.buffer.as_ref() {
writeln!(buffer, "{}", record).expect("Failed to write to file");
}
}
}
impl ::std::str::FromStr for TeeFileConfiguration {
type Err = failure::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut splitter = s.splitn(2, ':');
let log_filter = splitter.next().unwrap();
let filter: LoglevelFilter = friendly_enum_parse(log_filter)?;
let file: PathBuf = splitter
.next()
.ok_or_else(|| {
TeeFileError::ParseError("expected a colon in tee file description".to_string())
})?
.into();
Ok(TeeFileConfiguration { filter, file })
}
}
impl ::std::fmt::Display for TeeFileConfiguration {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
write!(f, "{}:{}", self.filter, self.file.to_str().unwrap())
}
}
#[cfg(test)]
mod test {
use super::super::{Log, LogRecord, Loglevel, LoglevelFilter};
use super::*;
use std::str::FromStr;
#[test]
fn from_str() {
assert_eq!(
TeeFileConfiguration::from_str("info:/tmp/hello:/there").unwrap(),
TeeFileConfiguration::new(LoglevelFilter::Info, "/tmp/hello:/there"),
);
let tfc = TeeFileConfiguration::from_str("hello");
assert!(tfc.is_err());
assert_eq!(tfc.unwrap_err().to_string(), "Invalid argument: hello is not a valid loglevel filter, valid values are off, fatal, error, warn, note, info, debug, or trace");
let tfc = TeeFileConfiguration::from_str("info");
assert!(tfc.is_err());
assert_eq!(
tfc.unwrap_err().to_string(),
"expected a colon in tee file description"
);
}
#[test]
fn to_str() {
assert_eq!(
TeeFileConfiguration::new(LoglevelFilter::Info, "/tmp/hello:/there").to_string(),
"Info:/tmp/hello:/there",
);
}
#[test]
fn debug() {
let tf = TeeFileConfiguration::new(LoglevelFilter::Info, "hello:/there");
assert_eq!(
format!("{:?}", tf),
"TeeFileConfiguration { filter: Info, file: \"hello:/there\" }"
);
let tfc = TeeFileConfiguration::new(LoglevelFilter::Trace, "/dev/zero");
let tf = TeeFile::new(tfc);
assert!(
format!("{:?}", tf.unwrap()).starts_with("TeeFile { configuration: TeeFileConfiguration { filter: Trace, file: \"/dev/zero\" }, buffer: Some(File {")
);
}
#[test]
fn with_clone() {
let tf = TeeFileConfiguration::new(LoglevelFilter::Info, "/tmp/log.info");
assert_eq!(
format!("{:?}", tf),
"TeeFileConfiguration { filter: Info, file: \"/tmp/log.info\" }"
);
let _ = tf.clone();
}
#[test]
fn pub_fields() {
let tf = TeeFileConfiguration::new(LoglevelFilter::Info, "/tmp/log.info");
assert_eq!(tf.filter, LoglevelFilter::Info);
assert_eq!(tf.file, PathBuf::from("/tmp/log.info"));
}
#[test]
fn log() {
let tfc = TeeFileConfiguration::new(LoglevelFilter::Info, "/tmp/log.info");
let tf = TeeFile::new(tfc);
assert!(tf.is_ok());
let tf = tf.unwrap();
assert_eq!(tf.name(), "/tmp/log.info");
assert!(tf.enabled(Loglevel::Info));
assert!(!tf.enabled(Loglevel::Trace));
let record = LogRecord::new(
"logger",
"message",
Loglevel::Trace,
"path",
"file",
1234u32,
1u32,
1u64,
);
tf.log(&record);
}
}