tari_log4rs/append/
mod.rs

1//! Appenders
2
3use log::{Log, Record};
4#[cfg(feature = "config_parsing")]
5use serde::{de, Deserialize, Deserializer};
6#[cfg(feature = "config_parsing")]
7use serde_value::Value;
8#[cfg(feature = "config_parsing")]
9use std::collections::BTreeMap;
10use std::fmt;
11
12#[cfg(feature = "config_parsing")]
13use crate::config::Deserializable;
14#[cfg(feature = "config_parsing")]
15use crate::filter::FilterConfig;
16
17#[cfg(feature = "console_appender")]
18pub mod console;
19#[cfg(feature = "file_appender")]
20pub mod file;
21#[cfg(feature = "rolling_file_appender")]
22pub mod rolling_file;
23
24#[cfg(any(feature = "file_appender", feature = "rolling_file_appender"))]
25mod env_util {
26    use std::borrow::Cow;
27
28    const ENV_PREFIX: &str = "$ENV{";
29    const ENV_PREFIX_LEN: usize = ENV_PREFIX.len();
30    const ENV_SUFFIX: char = '}';
31    const ENV_SUFFIX_LEN: usize = 1;
32
33    fn is_env_var_start(c: char) -> bool {
34        // Close replacement for old [\w]
35        // Note that \w implied \d and '_' and non-ASCII letters/digits.
36        c.is_alphanumeric() || c == '_'
37    }
38
39    fn is_env_var_part(c: char) -> bool {
40        // Close replacement for old [\w\d_.]
41        c.is_alphanumeric() || c == '_' || c == '.'
42    }
43
44    pub fn expand_env_vars<'str, Str>(path: Str) -> Cow<'str, str>
45    where
46        Str: Into<Cow<'str, str>>,
47    {
48        let mut outpath: Cow<str> = path.into();
49        let path = outpath.clone();
50        for (match_start, _) in path.match_indices(ENV_PREFIX) {
51            let env_name_start = match_start + ENV_PREFIX_LEN;
52            let (_, tail) = path.split_at(env_name_start);
53            let mut cs = tail.chars();
54            // Check first character.
55            if let Some(ch) = cs.next() {
56                if is_env_var_start(ch) {
57                    let mut env_name = String::new();
58                    env_name.push(ch);
59                    // Consume following characters.
60                    let valid = loop {
61                        match cs.next() {
62                            Some(ch) if is_env_var_part(ch) => env_name.push(ch),
63                            Some(ENV_SUFFIX) => break true,
64                            _ => break false,
65                        }
66                    };
67                    // Try replacing properly terminated env var.
68                    if valid {
69                        if let Ok(env_value) = std::env::var(&env_name) {
70                            let match_end = env_name_start + env_name.len() + ENV_SUFFIX_LEN;
71                            // This simply rewrites the entire outpath with all instances
72                            // of this var replaced. Could be done more efficiently by building
73                            // `outpath` as we go when processing `path`. Not critical.
74                            outpath = outpath
75                                .replace(&path[match_start..match_end], &env_value)
76                                .into();
77                        }
78                    }
79                }
80            }
81        }
82        outpath
83    }
84}
85
86/// A trait implemented by log4rs appenders.
87///
88/// Appenders take a log record and processes them, for example, by writing it
89/// to a file or the console.
90pub trait Append: fmt::Debug + Send + Sync + 'static {
91    /// Processes the provided `Record`.
92    fn append(&self, record: &Record) -> anyhow::Result<()>;
93
94    /// Flushes all in-flight records.
95    fn flush(&self);
96}
97
98#[cfg(feature = "config_parsing")]
99impl Deserializable for dyn Append {
100    fn name() -> &'static str {
101        "appender"
102    }
103}
104
105impl<T: Log + fmt::Debug + 'static> Append for T {
106    fn append(&self, record: &Record) -> anyhow::Result<()> {
107        self.log(record);
108        Ok(())
109    }
110
111    fn flush(&self) {
112        Log::flush(self)
113    }
114}
115
116/// Configuration for an appender.
117#[cfg(feature = "config_parsing")]
118#[derive(Clone, Eq, PartialEq, Hash, Debug)]
119pub struct AppenderConfig {
120    /// The appender kind.
121    pub kind: String,
122    /// The filters attached to the appender.
123    pub filters: Vec<FilterConfig>,
124    /// The appender configuration.
125    pub config: Value,
126}
127
128#[cfg(feature = "config_parsing")]
129impl<'de> Deserialize<'de> for AppenderConfig {
130    fn deserialize<D>(d: D) -> Result<AppenderConfig, D::Error>
131    where
132        D: Deserializer<'de>,
133    {
134        let mut map = BTreeMap::<Value, Value>::deserialize(d)?;
135
136        let kind = match map.remove(&Value::String("kind".to_owned())) {
137            Some(kind) => kind.deserialize_into().map_err(|e| e.into_error())?,
138            None => return Err(de::Error::missing_field("kind")),
139        };
140
141        let filters = match map.remove(&Value::String("filters".to_owned())) {
142            Some(filters) => filters.deserialize_into().map_err(|e| e.into_error())?,
143            None => vec![],
144        };
145
146        Ok(AppenderConfig {
147            kind,
148            filters,
149            config: Value::Map(map),
150        })
151    }
152}
153
154#[cfg(test)]
155mod test {
156    #[cfg(any(feature = "file_appender", feature = "rolling_file_appender"))]
157    use std::env::{set_var, var};
158
159    #[test]
160    #[cfg(any(feature = "file_appender", feature = "rolling_file_appender"))]
161    fn expand_env_vars_tests() {
162        set_var("HELLO_WORLD", "GOOD BYE");
163        #[cfg(not(target_os = "windows"))]
164        let test_cases = vec![
165            ("$ENV{HOME}", var("HOME").unwrap()),
166            ("$ENV{HELLO_WORLD}", var("HELLO_WORLD").unwrap()),
167            ("$ENV{HOME}/test", format!("{}/test", var("HOME").unwrap())),
168            (
169                "/test/$ENV{HOME}",
170                format!("/test/{}", var("HOME").unwrap()),
171            ),
172            (
173                "/test/$ENV{HOME}/test",
174                format!("/test/{}/test", var("HOME").unwrap()),
175            ),
176            (
177                "/test$ENV{HOME}/test",
178                format!("/test{}/test", var("HOME").unwrap()),
179            ),
180            (
181                "test/$ENV{HOME}/test",
182                format!("test/{}/test", var("HOME").unwrap()),
183            ),
184            (
185                "/$ENV{HOME}/test/$ENV{USER}",
186                format!("/{}/test/{}", var("HOME").unwrap(), var("USER").unwrap()),
187            ),
188            (
189                "$ENV{SHOULD_NOT_EXIST}",
190                "$ENV{SHOULD_NOT_EXIST}".to_string(),
191            ),
192            (
193                "/$ENV{HOME}/test/$ENV{SHOULD_NOT_EXIST}",
194                format!("/{}/test/$ENV{{SHOULD_NOT_EXIST}}", var("HOME").unwrap()),
195            ),
196            (
197                "/unterminated/$ENV{USER",
198                "/unterminated/$ENV{USER".to_string(),
199            ),
200        ];
201
202        #[cfg(target_os = "windows")]
203        let test_cases = vec![
204            ("$ENV{HOMEPATH}", var("HOMEPATH").unwrap()),
205            ("$ENV{HELLO_WORLD}", var("HELLO_WORLD").unwrap()),
206            (
207                "$ENV{HOMEPATH}/test",
208                format!("{}/test", var("HOMEPATH").unwrap()),
209            ),
210            (
211                "/test/$ENV{USERNAME}",
212                format!("/test/{}", var("USERNAME").unwrap()),
213            ),
214            (
215                "/test/$ENV{USERNAME}/test",
216                format!("/test/{}/test", var("USERNAME").unwrap()),
217            ),
218            (
219                "/test$ENV{USERNAME}/test",
220                format!("/test{}/test", var("USERNAME").unwrap()),
221            ),
222            (
223                "test/$ENV{USERNAME}/test",
224                format!("test/{}/test", var("USERNAME").unwrap()),
225            ),
226            (
227                "$ENV{HOMEPATH}/test/$ENV{USERNAME}",
228                format!(
229                    "{}/test/{}",
230                    var("HOMEPATH").unwrap(),
231                    var("USERNAME").unwrap()
232                ),
233            ),
234            (
235                "$ENV{SHOULD_NOT_EXIST}",
236                "$ENV{SHOULD_NOT_EXIST}".to_string(),
237            ),
238            (
239                "$ENV{HOMEPATH}/test/$ENV{SHOULD_NOT_EXIST}",
240                format!("{}/test/$ENV{{SHOULD_NOT_EXIST}}", var("HOMEPATH").unwrap()),
241            ),
242            (
243                "/unterminated/$ENV{USERNAME",
244                "/unterminated/$ENV{USERNAME".to_string(),
245            ),
246        ];
247
248        for (input, expected) in test_cases {
249            let res = super::env_util::expand_env_vars(input);
250            assert_eq!(res, expected)
251        }
252    }
253}