1use 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 c.is_alphanumeric() || c == '_'
37 }
38
39 fn is_env_var_part(c: char) -> bool {
40 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 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 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 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 outpath = outpath
75 .replace(&path[match_start..match_end], &env_value)
76 .into();
77 }
78 }
79 }
80 }
81 }
82 outpath
83 }
84}
85
86pub trait Append: fmt::Debug + Send + Sync + 'static {
91 fn append(&self, record: &Record) -> anyhow::Result<()>;
93
94 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#[cfg(feature = "config_parsing")]
118#[derive(Clone, Eq, PartialEq, Hash, Debug)]
119pub struct AppenderConfig {
120 pub kind: String,
122 pub filters: Vec<FilterConfig>,
124 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}