1#![allow(clippy::module_name_repetitions)]
2mod builder;
3mod config;
4mod infix_filter;
5mod rotation_config;
6mod state;
7mod state_handle;
8mod threads;
9
10pub use self::builder::{ArcFileLogWriter, FileLogWriterBuilder, FileLogWriterHandle};
11pub use self::config::FileLogWriterConfig;
12pub(crate) use infix_filter::InfixFilter;
13
14use self::{rotation_config::RotationConfig, state::State, state_handle::StateHandle};
15use crate::{
16 writers::LogWriter, DeferredNow, EffectiveWriteMode, FileSpec, FlexiLoggerError,
17 FormatFunction, LogfileSelector,
18};
19use log::Record;
20use std::path::PathBuf;
21
22const WINDOWS_LINE_ENDING: &[u8] = b"\r\n";
23const UNIX_LINE_ENDING: &[u8] = b"\n";
24
25#[derive(Debug)]
29pub struct FileLogWriter {
30 state_handle: StateHandle,
34 max_log_level: log::LevelFilter,
35}
36impl FileLogWriter {
37 fn new(
38 state: State,
39 max_log_level: log::LevelFilter,
40 format_function: FormatFunction,
41 ) -> FileLogWriter {
42 let state_handle = match state.config().write_mode.effective_write_mode() {
43 EffectiveWriteMode::Direct
44 | EffectiveWriteMode::BufferAndFlushWith(_)
45 | EffectiveWriteMode::BufferDontFlushWith(_) => {
46 StateHandle::new_sync(state, format_function)
47 }
48
49 #[cfg(feature = "async")]
50 EffectiveWriteMode::AsyncWith {
51 pool_capa,
52 message_capa,
53 flush_interval: _,
54 } => StateHandle::new_async(pool_capa, message_capa, state, format_function),
55 };
56
57 FileLogWriter {
58 state_handle,
59 max_log_level,
60 }
61 }
62
63 #[must_use]
65 pub fn builder(file_spec: FileSpec) -> FileLogWriterBuilder {
66 FileLogWriterBuilder::new(file_spec)
67 }
68
69 #[must_use]
71 #[inline]
72 pub fn format(&self) -> FormatFunction {
73 self.state_handle.format_function()
74 }
75
76 pub(crate) fn plain_write(&self, buffer: &[u8]) -> std::result::Result<usize, std::io::Error> {
77 self.state_handle.plain_write(buffer)
78 }
79
80 pub fn reset(&self, flwb: &FileLogWriterBuilder) -> Result<(), FlexiLoggerError> {
96 self.state_handle.reset(flwb)
97 }
98
99 pub fn config(&self) -> Result<FileLogWriterConfig, FlexiLoggerError> {
105 self.state_handle.config()
106 }
107
108 pub fn reopen_outputfile(&self) -> Result<(), FlexiLoggerError> {
129 self.state_handle.reopen_outputfile()
130 }
131
132 pub fn rotate(&self) -> Result<(), FlexiLoggerError> {
142 self.state_handle.rotate()
143 }
144
145 pub fn existing_log_files(
153 &self,
154 selector: &LogfileSelector,
155 ) -> Result<Vec<PathBuf>, FlexiLoggerError> {
156 self.state_handle.existing_log_files(selector)
157 }
158}
159
160impl LogWriter for FileLogWriter {
161 #[inline]
162 fn write(&self, now: &mut DeferredNow, record: &Record) -> std::io::Result<()> {
163 if record.level() <= self.max_log_level {
164 self.state_handle.write(now, record)
165 } else {
166 Ok(())
167 }
168 }
169
170 #[inline]
171 fn flush(&self) -> std::io::Result<()> {
172 self.state_handle.flush()
173 }
174
175 #[inline]
176 fn max_log_level(&self) -> log::LevelFilter {
177 self.max_log_level
178 }
179
180 fn reopen_output(&self) -> Result<(), FlexiLoggerError> {
181 self.reopen_outputfile()
182 }
183
184 fn rotate(&self) -> Result<(), FlexiLoggerError> {
185 self.state_handle.rotate()
186 }
187
188 fn validate_logs(&self, expected: &[(&'static str, &'static str, &'static str)]) {
189 self.state_handle.validate_logs(expected);
190 }
191
192 fn shutdown(&self) {
193 self.state_handle.shutdown();
194 }
195}
196
197impl Drop for FileLogWriter {
198 fn drop(&mut self) {
199 self.shutdown();
200 }
201}
202
203#[cfg(test)]
204mod test {
205 #[cfg(feature = "async")]
206 use crate::ZERO_DURATION;
207 use crate::{writers::LogWriter, Cleanup, Criterion, DeferredNow, FileSpec, Naming, WriteMode};
208 use chrono::Local;
209 use std::ops::Add;
210 use std::path::{Path, PathBuf};
211 use std::time::Duration;
212
213 const DIRECTORY: &str = r"log_files/rotate";
214 const ONE: &str = "ONE";
215 const TWO: &str = "TWO";
216 const THREE: &str = "THREE";
217 const FOUR: &str = "FOUR";
218 const FIVE: &str = "FIVE";
219 const SIX: &str = "SIX";
220 const SEVEN: &str = "SEVEN";
221 const EIGHT: &str = "EIGHT";
222 const NINE: &str = "NINE";
223
224 const FMT_DASHES_U_DASHES: &str = "%Y-%m-%d_%H-%M-%S";
225
226 #[test]
227 fn test_rotate_no_append_numbers() {
228 let ts =
230 String::from("false-numbers-") + &Local::now().format(FMT_DASHES_U_DASHES).to_string();
231 let naming = Naming::Numbers;
232
233 assert!(not_exists("00000", &ts));
235 assert!(not_exists("00001", &ts));
236 assert!(not_exists("CURRENT", &ts));
237
238 write_loglines(false, naming, &ts, &[ONE]);
240 assert!(not_exists("00000", &ts));
241 assert!(not_exists("00001", &ts));
242 assert!(contains("CURRENT", &ts, ONE));
243
244 write_loglines(false, naming, &ts, &[TWO]);
246 assert!(contains("00000", &ts, ONE));
247 assert!(not_exists("00001", &ts));
248 assert!(contains("CURRENT", &ts, TWO));
249
250 remove("CURRENT", &ts);
252 assert!(not_exists("CURRENT", &ts));
253 write_loglines(false, naming, &ts, &[TWO]);
254 assert!(contains("00000", &ts, ONE));
255 assert!(not_exists("00001", &ts));
256 assert!(contains("CURRENT", &ts, TWO));
257
258 write_loglines(false, naming, &ts, &[THREE]);
260 assert!(contains("00000", &ts, ONE));
261 assert!(contains("00001", &ts, TWO));
262 assert!(contains("CURRENT", &ts, THREE));
263 }
264
265 #[test]
266 fn test_rotate_with_append_numbers() {
267 let ts =
269 String::from("true-numbers-") + &Local::now().format(FMT_DASHES_U_DASHES).to_string();
270 let naming = Naming::Numbers;
271
272 assert!(not_exists("00000", &ts));
274 assert!(not_exists("00001", &ts));
275 assert!(not_exists("CURRENT", &ts));
276
277 write_loglines(true, naming, &ts, &[ONE, TWO, THREE]);
279 assert!(contains("00000", &ts, ONE));
280 assert!(contains("00000", &ts, TWO));
281 assert!(not_exists("00001", &ts));
282 assert!(contains("CURRENT", &ts, THREE));
283
284 write_loglines(true, naming, &ts, &[FOUR, FIVE, SIX]);
286 assert!(contains("00000", &ts, ONE));
287 assert!(contains("00000", &ts, TWO));
288 assert!(contains("00001", &ts, THREE));
289 assert!(contains("00001", &ts, FOUR));
290 assert!(contains("CURRENT", &ts, FIVE));
291 assert!(contains("CURRENT", &ts, SIX));
292
293 remove("CURRENT", &ts);
295 remove("00001", &ts);
296 assert!(not_exists("CURRENT", &ts));
297 write_loglines(true, naming, &ts, &[THREE, FOUR, FIVE, SIX]);
298 assert!(contains("00000", &ts, ONE));
299 assert!(contains("00000", &ts, TWO));
300 assert!(contains("00001", &ts, THREE));
301 assert!(contains("00001", &ts, FOUR));
302 assert!(contains("CURRENT", &ts, FIVE));
303 assert!(contains("CURRENT", &ts, SIX));
304
305 write_loglines(true, naming, &ts, &[SEVEN, EIGHT, NINE]);
307 assert!(contains("00002", &ts, FIVE));
308 assert!(contains("00002", &ts, SIX));
309 assert!(contains("00003", &ts, SEVEN));
310 assert!(contains("00003", &ts, EIGHT));
311 assert!(contains("CURRENT", &ts, NINE));
312 }
313
314 #[test]
315 fn test_rotate_no_append_timestamps() {
316 let ts_discr = String::from("false-timestamps-")
318 + &Local::now().format(FMT_DASHES_U_DASHES).to_string();
319
320 let basename = String::from(DIRECTORY).add("/").add(
321 &Path::new(&std::env::args().next().unwrap())
322 .file_stem().unwrap()
323 .to_string_lossy(),
324 );
325 let naming = Naming::Timestamps;
326
327 println!("{} ensure we start with -/-/-", chrono::Local::now());
328 assert!(list_rotated_files(&basename, &ts_discr).is_empty());
329 assert!(not_exists("CURRENT", &ts_discr));
330
331 println!("{} ensure this produces -/-/ONE", chrono::Local::now());
332 write_loglines(false, naming, &ts_discr, &[ONE]);
333 assert!(list_rotated_files(&basename, &ts_discr).is_empty());
334 assert!(contains("CURRENT", &ts_discr, ONE));
335
336 std::thread::sleep(Duration::from_secs(2));
337 println!("{} ensure this produces ONE/-/TWO", chrono::Local::now());
338 write_loglines(false, naming, &ts_discr, &[TWO]);
339 assert_eq!(list_rotated_files(&basename, &ts_discr).len(), 1);
340 assert!(contains("CURRENT", &ts_discr, TWO));
341
342 std::thread::sleep(Duration::from_secs(2));
343 println!(
344 "{} ensure this produces ONE/TWO/THREE",
345 chrono::Local::now()
346 );
347 write_loglines(false, naming, &ts_discr, &[THREE]);
348 assert_eq!(list_rotated_files(&basename, &ts_discr).len(), 2);
349 assert!(contains("CURRENT", &ts_discr, THREE));
350 }
351
352 #[test]
353 fn test_rotate_with_append_timestamps() {
354 let ts = String::from("true-timestamps-")
356 + &Local::now().format(FMT_DASHES_U_DASHES).to_string();
357
358 let basename = String::from(DIRECTORY).add("/").add(
359 &Path::new(&std::env::args().next().unwrap())
360 .file_stem().unwrap()
361 .to_string_lossy(),
362 );
363 let naming = Naming::Timestamps;
364
365 assert!(list_rotated_files(&basename, &ts).is_empty());
367 assert!(not_exists("CURRENT", &ts));
368
369 write_loglines(true, naming, &ts, &[ONE, TWO, THREE]);
371 assert_eq!(list_rotated_files(&basename, &ts).len(), 1);
372 assert!(contains("CURRENT", &ts, THREE));
373
374 write_loglines(true, naming, &ts, &[FOUR, FIVE, SIX]);
376 assert!(contains("CURRENT", &ts, FIVE));
377 assert!(contains("CURRENT", &ts, SIX));
378 assert_eq!(list_rotated_files(&basename, &ts).len(), 2);
379
380 write_loglines(true, naming, &ts, &[SEVEN, EIGHT, NINE]);
382 assert_eq!(list_rotated_files(&basename, &ts).len(), 4);
383 assert!(contains("CURRENT", &ts, NINE));
384 }
385
386 #[test]
387 fn issue_38() {
388 const NUMBER_OF_FILES: usize = 5;
389 const NUMBER_OF_PSEUDO_PROCESSES: usize = 11;
390 const ISSUE_38: &str = "issue_38";
391 const LOG_FOLDER: &str = "log_files/issue_38";
392
393 for _ in 0..NUMBER_OF_PSEUDO_PROCESSES {
394 let flwb = crate::writers::file_log_writer::FileLogWriter::builder(
395 FileSpec::default()
396 .directory(LOG_FOLDER)
397 .discriminant(ISSUE_38),
398 )
399 .rotate(
400 Criterion::Size(500),
401 Naming::Timestamps,
402 Cleanup::KeepLogFiles(NUMBER_OF_FILES),
403 )
404 .o_append(false);
405
406 #[cfg(feature = "async")]
407 let flwb = flwb.write_mode(WriteMode::AsyncWith {
408 pool_capa: 5,
409 message_capa: 400,
410 flush_interval: ZERO_DURATION,
411 });
412
413 let flw = flwb.try_build().unwrap();
414
415 for i in 0..4 {
417 flw.write(
418 &mut DeferredNow::new(),
419 &log::Record::builder()
420 .args(format_args!("{i}"))
421 .level(log::Level::Error)
422 .target("myApp")
423 .file(Some("server.rs"))
424 .line(Some(144))
425 .module_path(Some("server"))
426 .build(),
427 )
428 .unwrap();
429 }
430 flw.flush().ok();
431 }
432
433 std::thread::sleep(Duration::from_millis(50));
435
436 let fn_pattern = String::with_capacity(180)
437 .add(
438 &String::from(LOG_FOLDER).add("/").add(
439 &Path::new(&std::env::args().next().unwrap())
440 .file_stem().unwrap()
441 .to_string_lossy(),
442 ),
443 )
444 .add("_")
445 .add(ISSUE_38)
446 .add("_r[0-9]*")
447 .add(".log");
448
449 assert_eq!(
450 glob::glob(&fn_pattern)
451 .unwrap()
452 .filter_map(Result::ok)
453 .count(),
454 NUMBER_OF_FILES
455 );
456 }
457
458 #[test]
459 fn test_reset() {
460 #[cfg(not(feature = "async"))]
461 let write_mode = WriteMode::BufferDontFlushWith(4);
462 #[cfg(feature = "async")]
463 let write_mode = WriteMode::AsyncWith {
464 pool_capa: 7,
465 message_capa: 8,
466 flush_interval: ZERO_DURATION,
467 };
468 let flw = super::FileLogWriter::builder(
469 FileSpec::default()
470 .directory(DIRECTORY)
471 .discriminant("test_reset-1"),
472 )
473 .rotate(
474 Criterion::Size(28),
475 Naming::Numbers,
476 Cleanup::KeepLogFiles(20),
477 )
478 .append()
479 .write_mode(write_mode)
480 .try_build()
481 .unwrap();
482
483 flw.write(
484 &mut DeferredNow::new(),
485 &log::Record::builder()
486 .args(format_args!("{}", "test_reset-1"))
487 .level(log::Level::Error)
488 .target("test_reset")
489 .file(Some("server.rs"))
490 .line(Some(144))
491 .module_path(Some("server"))
492 .build(),
493 )
494 .unwrap();
495
496 println!("FileLogWriter {flw:?}");
497
498 flw.reset(
499 &super::FileLogWriter::builder(
500 FileSpec::default()
501 .directory(DIRECTORY)
502 .discriminant("test_reset-2"),
503 )
504 .rotate(
505 Criterion::Size(28),
506 Naming::Numbers,
507 Cleanup::KeepLogFiles(20),
508 )
509 .write_mode(write_mode),
510 )
511 .unwrap();
512 flw.write(
513 &mut DeferredNow::new(),
514 &log::Record::builder()
515 .args(format_args!("{}", "test_reset-2"))
516 .level(log::Level::Error)
517 .target("test_reset")
518 .file(Some("server.rs"))
519 .line(Some(144))
520 .module_path(Some("server"))
521 .build(),
522 )
523 .unwrap();
524 println!("FileLogWriter {flw:?}");
525
526 assert!(flw
527 .reset(
528 &super::FileLogWriter::builder(
529 FileSpec::default()
530 .directory(DIRECTORY)
531 .discriminant("test_reset-3"),
532 )
533 .rotate(
534 Criterion::Size(28),
535 Naming::Numbers,
536 Cleanup::KeepLogFiles(20),
537 )
538 .write_mode(WriteMode::Direct),
539 )
540 .is_err());
541 }
542
543 #[test]
544 fn test_max_log_level() {
545 let spec = FileSpec::default()
546 .directory(DIRECTORY)
547 .discriminant("test_max_log_level-1")
548 .suppress_basename()
549 .suppress_timestamp();
550 let flw = super::FileLogWriter::builder(spec.clone())
551 .max_level(log::LevelFilter::Warn)
552 .write_mode(WriteMode::Direct)
553 .try_build()
554 .unwrap();
555
556 let write_msg = |level, msg| {
557 flw.write(
558 &mut DeferredNow::new(),
559 &log::Record::builder()
560 .args(format_args!("{msg}"))
561 .level(level)
562 .target("test_max_log_level")
563 .file(Some("server.rs"))
564 .line(Some(144))
565 .module_path(Some("server"))
566 .build(),
567 )
568 .unwrap();
569 };
570
571 write_msg(log::Level::Trace, "trace message");
572 write_msg(log::Level::Debug, "debug message");
573 write_msg(log::Level::Info, "info message");
574 write_msg(log::Level::Warn, "warn message");
575 write_msg(log::Level::Error, "error message");
576
577 let log_contents = std::fs::read_to_string(spec.as_pathbuf(None)).unwrap();
578
579 assert!(!log_contents.contains("trace message"));
580 assert!(!log_contents.contains("debug message"));
581 assert!(!log_contents.contains("info message"));
582 assert!(log_contents.contains("warn message"));
583 assert!(log_contents.contains("error message"));
584 }
585
586 fn remove(s: &str, discr: &str) {
587 std::fs::remove_file(get_hackyfilepath(s, discr)).unwrap();
588 }
589
590 fn not_exists(s: &str, discr: &str) -> bool {
591 !get_hackyfilepath(s, discr).exists()
592 }
593
594 fn contains(s: &str, discr: &str, text: &str) -> bool {
595 match std::fs::read_to_string(get_hackyfilepath(s, discr)) {
596 Err(_) => false,
597 Ok(s) => s.contains(text),
598 }
599 }
600
601 fn get_hackyfilepath(infix: &str, discr: &str) -> Box<Path> {
602 let arg0 = std::env::args().next().unwrap();
603 let mut s_filename = Path::new(&arg0)
604 .file_stem()
605 .unwrap()
606 .to_string_lossy()
607 .to_string();
608 s_filename += "_";
609 s_filename += discr;
610 s_filename += "_r";
611 s_filename += infix;
612 s_filename += ".log";
613 let mut path_buf = PathBuf::from(DIRECTORY);
614 path_buf.push(s_filename);
615 path_buf.into_boxed_path()
616 }
617
618 fn write_loglines(append: bool, naming: Naming, discr: &str, texts: &[&'static str]) {
619 let flw = get_file_log_writer(append, naming, discr);
620 for text in texts {
621 flw.write(
622 &mut DeferredNow::new(),
623 &log::Record::builder()
624 .args(format_args!("{text}"))
625 .level(log::Level::Error)
626 .target("myApp")
627 .file(Some("server.rs"))
628 .line(Some(144))
629 .module_path(Some("server"))
630 .build(),
631 )
632 .unwrap();
633 }
634 }
635
636 fn get_file_log_writer(
637 append: bool,
638 naming: Naming,
639 discr: &str,
640 ) -> crate::writers::FileLogWriter {
641 super::FileLogWriter::builder(FileSpec::default().directory(DIRECTORY).discriminant(discr))
642 .rotate(
643 Criterion::Size(if append { 28 } else { 10 }),
644 naming,
645 Cleanup::Never,
646 )
647 .o_append(append)
648 .try_build()
649 .unwrap()
650 }
651
652 fn list_rotated_files(basename: &str, discr: &str) -> Vec<String> {
653 let fn_pattern = String::with_capacity(180)
654 .add(basename)
655 .add("_")
656 .add(discr)
657 .add("_r2[0-9]*") .add(".log");
659
660 glob::glob(&fn_pattern)
661 .unwrap()
662 .map(|r| r.unwrap().into_os_string().to_string_lossy().to_string())
663 .collect()
664 }
665}