1use chrono::prelude::Local;
6use derive_more::Debug;
7use log::Record;
8use parking_lot::Mutex;
9use std::{
10 fs::{self, File, OpenOptions},
11 io::{self, BufWriter, Write},
12 path::{Path, PathBuf},
13};
14
15const TIME_PREFIX: &str = "$TIME{";
16const TIME_PREFIX_LEN: usize = TIME_PREFIX.len();
17const TIME_SUFFIX: char = '}';
18const TIME_SUFFIX_LEN: usize = 1;
19const MAX_REPLACEMENTS: usize = 5;
20
21#[cfg(feature = "config_parsing")]
22use crate::config::{Deserialize, Deserializers};
23#[cfg(feature = "config_parsing")]
24use crate::encode::EncoderConfig;
25
26use crate::{
27 append::{env_util::expand_env_vars, Append},
28 encode::{pattern::PatternEncoder, writer::simple::SimpleWriter, Encode},
29};
30
31#[cfg(feature = "config_parsing")]
33#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, serde::Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct FileAppenderConfig {
36 path: String,
37 encoder: Option<EncoderConfig>,
38 append: Option<bool>,
39}
40
41#[derive(Debug)]
43pub struct FileAppender {
44 #[allow(dead_code)] path: PathBuf,
46 #[debug(skip)]
47 file: Mutex<SimpleWriter<BufWriter<File>>>,
48 encoder: Box<dyn Encode>,
49}
50
51impl Append for FileAppender {
52 fn append(&self, record: &Record) -> anyhow::Result<()> {
53 let mut file = self.file.lock();
54 self.encoder.encode(&mut *file, record)?;
55 file.flush()?;
56 Ok(())
57 }
58
59 fn flush(&self) {}
60}
61
62impl FileAppender {
63 pub fn builder() -> FileAppenderBuilder {
65 FileAppenderBuilder {
66 encoder: None,
67 append: true,
68 }
69 }
70}
71
72pub struct FileAppenderBuilder {
74 encoder: Option<Box<dyn Encode>>,
75 append: bool,
76}
77
78impl FileAppenderBuilder {
79 pub fn encoder(mut self, encoder: Box<dyn Encode>) -> FileAppenderBuilder {
81 self.encoder = Some(encoder);
82 self
83 }
84
85 pub fn append(mut self, append: bool) -> FileAppenderBuilder {
89 self.append = append;
90 self
91 }
92
93 pub fn build<P: AsRef<Path>>(self, path: P) -> io::Result<FileAppender> {
105 let path_cow = path.as_ref().to_string_lossy();
106 let expanded_env_path: PathBuf = expand_env_vars(path_cow).as_ref().into();
108 let final_path = self.date_time_format(expanded_env_path);
110
111 if let Some(parent) = final_path.parent() {
112 fs::create_dir_all(parent)?;
113 }
114 let file = OpenOptions::new()
115 .write(true)
116 .append(self.append)
117 .truncate(!self.append)
118 .create(true)
119 .open(&final_path)?;
120
121 Ok(FileAppender {
122 path: final_path,
123 file: Mutex::new(SimpleWriter(BufWriter::with_capacity(1024, file))),
124 encoder: self
125 .encoder
126 .unwrap_or_else(|| Box::<PatternEncoder>::default()),
127 })
128 }
129
130 fn date_time_format(&self, path: PathBuf) -> PathBuf {
131 let mut replacements = 0;
132 let mut date_time_path = path.to_str().unwrap().to_string();
133 while let Some(start) = date_time_path.find(TIME_PREFIX) {
135 if replacements >= MAX_REPLACEMENTS {
136 break;
137 }
138 if let Some(end) = date_time_path[start..].find(TIME_SUFFIX) {
139 let end = start + end;
140 let date_format = &date_time_path[start + TIME_PREFIX_LEN..end];
142
143 let now = Local::now();
145
146 let formatted_date = now.format(date_format).to_string();
148
149 date_time_path.replace_range(start..end + TIME_SUFFIX_LEN, &formatted_date);
151 replacements += 1;
152 } else {
153 break;
155 }
156 }
157 PathBuf::from(date_time_path)
158 }
159}
160
161#[cfg(feature = "config_parsing")]
184#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
185pub struct FileAppenderDeserializer;
186
187#[cfg(feature = "config_parsing")]
188impl Deserialize for FileAppenderDeserializer {
189 type Trait = dyn Append;
190
191 type Config = FileAppenderConfig;
192
193 fn deserialize(
194 &self,
195 config: FileAppenderConfig,
196 deserializers: &Deserializers,
197 ) -> anyhow::Result<Box<Self::Trait>> {
198 let mut appender = FileAppender::builder();
199 if let Some(append) = config.append {
200 appender = appender.append(append);
201 }
202 if let Some(encoder) = config.encoder {
203 appender = appender.encoder(deserializers.deserialize(&encoder.kind, encoder.config)?);
204 }
205 Ok(Box::new(appender.build(&config.path)?))
206 }
207}
208
209#[cfg(test)]
210mod test {
211 use super::*;
212
213 #[test]
214 fn create_directories() {
215 let tempdir = tempfile::tempdir().unwrap();
216
217 FileAppender::builder()
218 .build(tempdir.path().join("foo").join("bar").join("baz.log"))
219 .unwrap();
220 }
221
222 #[test]
223 fn append_false() {
224 let tempdir = tempfile::tempdir().unwrap();
225 FileAppender::builder()
226 .append(false)
227 .build(tempdir.path().join("foo.log"))
228 .unwrap();
229 }
230
231 #[test]
232 fn test_date_time_format_with_valid_format() {
233 let current_time = Local::now().format("%Y-%m-%d").to_string();
234 let tempdir = tempfile::tempdir().unwrap();
235 let builder = FileAppender::builder()
236 .build(
237 tempdir
238 .path()
239 .join("foo")
240 .join("bar")
241 .join("logs/log-$TIME{%Y-%m-%d}.log"),
242 )
243 .unwrap();
244 let expected_path = tempdir
245 .path()
246 .join(format!("foo/bar/logs/log-{}.log", current_time));
247 assert_eq!(builder.path, expected_path);
248 }
249
250 #[test]
251 fn test_date_time_format_with_invalid_format() {
252 let tempdir = tempfile::tempdir().unwrap();
253 let builder = FileAppender::builder()
254 .build(
255 tempdir
256 .path()
257 .join("foo")
258 .join("bar")
259 .join("logs/log-$TIME{INVALID}.log"),
260 )
261 .unwrap();
262 let expected_path = tempdir.path().join("foo/bar/logs/log-INVALID.log");
263 assert_eq!(builder.path, expected_path);
264 }
265
266 #[test]
267 fn test_date_time_format_with_no_closing_brace() {
268 let tempdir = tempfile::tempdir().unwrap();
269 let current_time = Local::now().format("%Y-%m-%d").to_string();
270 let builder = FileAppender::builder()
271 .build(
272 tempdir
273 .path()
274 .join("foo")
275 .join("bar")
276 .join("logs/log-$TIME{%Y-%m-%d}-$TIME{no_closing_brace.log"),
277 )
278 .unwrap();
279 let expected_path = tempdir.path().join(format!(
280 "foo/bar/logs/log-{}-$TIME{{no_closing_brace.log",
281 current_time
282 ));
283 assert_eq!(builder.path, expected_path);
284 }
285
286 #[test]
287 fn test_date_time_format_with_max_replacements() {
288 let tempdir = tempfile::tempdir().unwrap();
289 let current_time = Local::now().format("%Y-%m-%d").to_string();
290 let builder = FileAppender::builder()
291 .build(
292 tempdir
293 .path()
294 .join("foo")
295 .join("bar")
296 .join("logs/log-$TIME{%Y-%m-%d}-$TIME{%Y-%m-%d}-$TIME{%Y-%m-%d}-$TIME{%Y-%m-%d}-$TIME{%Y-%m-%d}.log"),
297 )
298 .unwrap();
299 let expected_path = tempdir.path().join(format!(
300 "foo/bar/logs/log-{}-{}-{}-{}-{}.log",
301 current_time, current_time, current_time, current_time, current_time
302 ));
303 assert_eq!(builder.path, expected_path);
304 }
305
306 #[test]
307 fn test_date_time_format_over_max_replacements() {
308 let tempdir = tempfile::tempdir().unwrap();
309 let current_time = Local::now().format("%Y-%m-%d").to_string();
310
311 let path_str = format!(
313 "foo/bar/logs/log-{}-{}-{}-{}-{}-{}-{}-{}-{}-{}.log",
314 "$TIME{%Y-%m-%d}",
315 "$TIME{%Y-%m-%d}",
316 "$TIME{%Y-%m-%d}",
317 "$TIME{%Y-%m-%d}",
318 "$TIME{%Y-%m-%d}",
319 "$TIME{%Y-%m-%d}",
320 "$TIME{%Y-%m-%d}",
321 "$TIME{%Y-%m-%d}",
322 "$TIME{%Y-%m-%d}",
323 "$TIME{%Y-%m-%d}"
324 );
325 let builder = FileAppender::builder()
326 .build(tempdir.path().join(path_str.clone()))
327 .unwrap();
328
329 let mut expected_path = format!("foo/bar/logs/log");
331 for i in 0..10 {
332 if i < MAX_REPLACEMENTS {
333 expected_path.push_str(&format!("-{}", current_time));
334 } else {
335 expected_path.push_str("-$TIME{%Y-%m-%d}");
336 }
337 }
338 expected_path.push_str(".log");
339
340 let expected_path = tempdir.path().join(expected_path);
341 assert_eq!(builder.path, expected_path);
342 }
343
344 #[test]
345 fn test_date_time_format_without_placeholder() {
346 let tempdir = tempfile::tempdir().unwrap();
347 let builder = FileAppender::builder()
348 .build(tempdir.path().join("foo").join("bar").join("bar.log"))
349 .unwrap();
350 let expected_path = tempdir.path().join("foo/bar/bar.log");
351 assert_eq!(builder.path, expected_path);
352 }
353
354 #[test]
355 fn test_date_time_format_with_multiple_placeholders() {
356 let current_time = Local::now().format("%Y-%m-%d").to_string();
357 let tempdir = tempfile::tempdir().unwrap();
358 let builder = FileAppender::builder()
359 .build(
360 tempdir
361 .path()
362 .join("foo")
363 .join("bar")
364 .join("logs-$TIME{%Y-%m-%d}/log-$TIME{%Y-%m-%d}.log"),
365 )
366 .unwrap();
367 let expected_path = tempdir.path().join(format!(
368 "foo/bar/logs-{}/log-{}.log",
369 current_time, current_time
370 ));
371 assert_eq!(builder.path, expected_path);
372 }
373}