use log::{Log, Record};
#[cfg(feature = "config_parsing")]
use serde::{de, Deserialize, Deserializer};
#[cfg(feature = "config_parsing")]
use serde_value::Value;
#[cfg(feature = "config_parsing")]
use std::collections::BTreeMap;
use std::fmt;
#[cfg(feature = "config_parsing")]
use crate::config::Deserializable;
#[cfg(feature = "config_parsing")]
use crate::filter::FilterConfig;
#[cfg(feature = "console_appender")]
pub mod console;
#[cfg(feature = "file_appender")]
pub mod file;
#[cfg(feature = "rolling_file_appender")]
pub mod rolling_file;
#[cfg(any(feature = "file_appender", feature = "rolling_file_appender"))]
mod env_util {
use std::borrow::Cow;
const ENV_PREFIX: &str = "$ENV{";
const ENV_PREFIX_LEN: usize = ENV_PREFIX.len();
const ENV_SUFFIX: char = '}';
const ENV_SUFFIX_LEN: usize = 1;
fn is_env_var_start(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn is_env_var_part(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '.'
}
pub fn expand_env_vars<'str, Str>(path: Str) -> Cow<'str, str>
where
Str: Into<Cow<'str, str>>,
{
let mut outpath: Cow<str> = path.into();
let path = outpath.clone();
for (match_start, _) in path.match_indices(ENV_PREFIX) {
let env_name_start = match_start + ENV_PREFIX_LEN;
let (_, tail) = path.split_at(env_name_start);
let mut cs = tail.chars();
if let Some(ch) = cs.next() {
if is_env_var_start(ch) {
let mut env_name = String::new();
env_name.push(ch);
let valid = loop {
match cs.next() {
Some(ch) if is_env_var_part(ch) => env_name.push(ch),
Some(ENV_SUFFIX) => break true,
_ => break false,
}
};
if valid {
if let Ok(env_value) = std::env::var(&env_name) {
let match_end = env_name_start + env_name.len() + ENV_SUFFIX_LEN;
outpath = outpath
.replace(&path[match_start..match_end], &env_value)
.into();
}
}
}
}
}
outpath
}
}
pub trait Append: fmt::Debug + Send + Sync + 'static {
fn append(&self, record: &Record) -> anyhow::Result<()>;
fn flush(&self);
}
#[cfg(feature = "config_parsing")]
impl Deserializable for dyn Append {
fn name() -> &'static str {
"appender"
}
}
impl<T: Log + fmt::Debug + 'static> Append for T {
fn append(&self, record: &Record) -> anyhow::Result<()> {
self.log(record);
Ok(())
}
fn flush(&self) {
Log::flush(self)
}
}
#[cfg(feature = "config_parsing")]
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct AppenderConfig {
pub kind: String,
pub filters: Vec<FilterConfig>,
pub config: Value,
}
#[cfg(feature = "config_parsing")]
impl<'de> Deserialize<'de> for AppenderConfig {
fn deserialize<D>(d: D) -> Result<AppenderConfig, D::Error>
where
D: 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.into_error())?,
None => return Err(de::Error::missing_field("kind")),
};
let filters = match map.remove(&Value::String("filters".to_owned())) {
Some(filters) => filters.deserialize_into().map_err(|e| e.into_error())?,
None => vec![],
};
Ok(AppenderConfig {
kind,
filters,
config: Value::Map(map),
})
}
}
#[cfg(test)]
mod test {
#[cfg(any(feature = "file_appender", feature = "rolling_file_appender"))]
use std::env::{set_var, var};
#[test]
#[cfg(any(feature = "file_appender", feature = "rolling_file_appender"))]
fn expand_env_vars_tests() {
set_var("HELLO_WORLD", "GOOD BYE");
#[cfg(not(target_os = "windows"))]
let test_cases = vec![
("$ENV{HOME}", var("HOME").unwrap()),
("$ENV{HELLO_WORLD}", var("HELLO_WORLD").unwrap()),
("$ENV{HOME}/test", format!("{}/test", var("HOME").unwrap())),
(
"/test/$ENV{HOME}",
format!("/test/{}", var("HOME").unwrap()),
),
(
"/test/$ENV{HOME}/test",
format!("/test/{}/test", var("HOME").unwrap()),
),
(
"/test$ENV{HOME}/test",
format!("/test{}/test", var("HOME").unwrap()),
),
(
"test/$ENV{HOME}/test",
format!("test/{}/test", var("HOME").unwrap()),
),
(
"/$ENV{HOME}/test/$ENV{USER}",
format!("/{}/test/{}", var("HOME").unwrap(), var("USER").unwrap()),
),
(
"$ENV{SHOULD_NOT_EXIST}",
"$ENV{SHOULD_NOT_EXIST}".to_string(),
),
(
"/$ENV{HOME}/test/$ENV{SHOULD_NOT_EXIST}",
format!("/{}/test/$ENV{{SHOULD_NOT_EXIST}}", var("HOME").unwrap()),
),
(
"/unterminated/$ENV{USER",
"/unterminated/$ENV{USER".to_string(),
),
];
#[cfg(target_os = "windows")]
let test_cases = vec![
("$ENV{HOMEPATH}", var("HOMEPATH").unwrap()),
("$ENV{HELLO_WORLD}", var("HELLO_WORLD").unwrap()),
(
"$ENV{HOMEPATH}/test",
format!("{}/test", var("HOMEPATH").unwrap()),
),
(
"/test/$ENV{USERNAME}",
format!("/test/{}", var("USERNAME").unwrap()),
),
(
"/test/$ENV{USERNAME}/test",
format!("/test/{}/test", var("USERNAME").unwrap()),
),
(
"/test$ENV{USERNAME}/test",
format!("/test{}/test", var("USERNAME").unwrap()),
),
(
"test/$ENV{USERNAME}/test",
format!("test/{}/test", var("USERNAME").unwrap()),
),
(
"$ENV{HOMEPATH}/test/$ENV{USERNAME}",
format!(
"{}/test/{}",
var("HOMEPATH").unwrap(),
var("USERNAME").unwrap()
),
),
(
"$ENV{SHOULD_NOT_EXIST}",
"$ENV{SHOULD_NOT_EXIST}".to_string(),
),
(
"$ENV{HOMEPATH}/test/$ENV{SHOULD_NOT_EXIST}",
format!("{}/test/$ENV{{SHOULD_NOT_EXIST}}", var("HOMEPATH").unwrap()),
),
(
"/unterminated/$ENV{USERNAME",
"/unterminated/$ENV{USERNAME".to_string(),
),
];
for (input, expected) in test_cases {
let res = super::env_util::expand_env_vars(input);
assert_eq!(res, expected)
}
}
}