pipe_logger_lib/
lib.rs

1/*!
2# Pipe Logger Lib
3Stores, rotates, compresses process logs.
4
5## Example
6
7```rust
8use pipe_logger_lib::*;
9
10use std::fs;
11use std::path::Path;
12
13let test_folder = {
14  let folder = Path::join(&Path::join(Path::new("tests"), Path::new("out")), "log-example");
15
16  fs::remove_dir_all(&folder);
17
18  fs::create_dir_all(&folder).unwrap();
19
20  folder
21};
22
23let test_log_file = Path::join(&test_folder, Path::new("mylog.txt"));
24
25let mut builder = PipeLoggerBuilder::new(&test_log_file);
26
27builder
28    .set_tee(Some(Tee::Stdout))
29    .set_rotate(Some(RotateMethod::FileSize(30))) // bytes
30    .set_count(Some(10))
31    .set_compress(false);
32
33{
34    let mut logger = builder.build().unwrap();
35
36    logger.write_line("Hello world!").unwrap();
37
38    let rotated_log_file_1 = logger.write_line("This is a convenient logger.").unwrap().unwrap();
39
40    logger.write_line("Other logs...").unwrap();
41    logger.write_line("Other logs...").unwrap();
42
43    let rotated_log_file_2 = logger.write_line("Rotate again!").unwrap().unwrap();
44
45    logger.write_line("Ops!").unwrap();
46}
47
48fs::remove_dir_all(test_folder).unwrap();
49```
50
51Now, the contents of `test_log_file` are,
52
53```text
54Ops!
55```
56
57The contents of `rotated_log_file_1` are,
58
59```text
60Hello world!
61This is a convenient logger.
62```
63
64The contents of `rotated_log_file_2` are,
65
66```text
67Other logs...
68Other logs...
69Rotate again!
70```
71*/
72
73mod rotate_method;
74
75use std::{
76    error::Error,
77    fmt::{Display, Error as FmtError, Formatter},
78    fs::{self, File, OpenOptions},
79    io::{self, Read, Write},
80    path::{Path, PathBuf},
81    thread,
82    time::Duration,
83};
84
85use chrono::{DateTime, Utc};
86use path_absolutize::*;
87use regex::Regex;
88pub use rotate_method::RotateMethod;
89use xz2::write::XzEncoder;
90
91const BUFFER_SIZE: usize = 4096 * 4;
92const FILE_WAIT_MILLI_SECONDS: u64 = 30;
93
94// TODO -----PipeLoggerBuilder START-----
95
96#[derive(Debug)]
97pub enum PipeLoggerBuilderError {
98    /// A valid rotated file size needs bigger than 1.
99    RotateFileSizeTooSmall,
100    /// A valid count of log files needs bigger than 0.
101    CountTooSmall,
102    /// std::io::Error.
103    IOError(io::Error),
104    /// A log file cannot be a directory. Wrap the absolutized log file.
105    FileIsDirectory(PathBuf),
106}
107
108impl Display for PipeLoggerBuilderError {
109    #[inline]
110    fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
111        match self {
112            PipeLoggerBuilderError::RotateFileSizeTooSmall => {
113                f.write_str("A valid rotated file size needs bigger than 1.")
114            },
115            PipeLoggerBuilderError::CountTooSmall => {
116                f.write_str("A valid count of log files needs bigger than 0.")
117            },
118            PipeLoggerBuilderError::IOError(err) => Display::fmt(err, f),
119            PipeLoggerBuilderError::FileIsDirectory(path) => f.write_fmt(format_args!(
120                "A log file cannot be a directory. The path of that file is `{}`.",
121                path.to_string_lossy()
122            )),
123        }
124    }
125}
126
127impl Error for PipeLoggerBuilderError {}
128
129impl From<io::Error> for PipeLoggerBuilderError {
130    #[inline]
131    fn from(err: io::Error) -> Self {
132        PipeLoggerBuilderError::IOError(err)
133    }
134}
135
136impl From<PathBuf> for PipeLoggerBuilderError {
137    #[inline]
138    fn from(err: PathBuf) -> Self {
139        PipeLoggerBuilderError::FileIsDirectory(err)
140    }
141}
142
143#[derive(Debug, Clone)]
144/// Read from standard input and write to standard output.
145pub enum Tee {
146    /// To stdout.
147    Stdout,
148    /// To stderr.
149    Stderr,
150}
151
152#[derive(Debug)]
153/// To build a PipeLogger instance.
154pub struct PipeLoggerBuilder<P: AsRef<Path>> {
155    rotate:   Option<RotateMethod>,
156    count:    Option<usize>,
157    log_path: P,
158    compress: bool,
159    tee:      Option<Tee>,
160}
161
162impl<P: AsRef<Path>> PipeLoggerBuilder<P> {
163    /// Create a new PipeLoggerBuilder.
164    pub fn new(log_path: P) -> PipeLoggerBuilder<P> {
165        PipeLoggerBuilder {
166            rotate: None,
167            count: None,
168            log_path,
169            compress: false,
170            tee: None,
171        }
172    }
173
174    pub fn rotate(&self) -> &Option<RotateMethod> {
175        &self.rotate
176    }
177
178    pub fn count(&self) -> &Option<usize> {
179        &self.count
180    }
181
182    pub fn log_path(&self) -> &P {
183        &self.log_path
184    }
185
186    /// Whether to compress the rotated log files through xz.
187    pub fn compress(&self) -> bool {
188        self.compress
189    }
190
191    pub fn tee(&self) -> &Option<Tee> {
192        &self.tee
193    }
194
195    pub fn set_rotate(&mut self, rotate: Option<RotateMethod>) -> &mut Self {
196        self.rotate = rotate;
197        self
198    }
199
200    pub fn set_count(&mut self, count: Option<usize>) -> &mut Self {
201        self.count = count;
202        self
203    }
204
205    /// Whether to compress the rotated log files through xz.
206    pub fn set_compress(&mut self, compress: bool) -> &mut Self {
207        self.compress = compress;
208        self
209    }
210
211    pub fn set_tee(&mut self, tee: Option<Tee>) -> &mut Self {
212        self.tee = tee;
213        self
214    }
215
216    /// Build a new PipeLogger.
217    pub fn build(self) -> Result<PipeLogger, PipeLoggerBuilderError> {
218        if let Some(rotate) = &self.rotate {
219            match rotate {
220                RotateMethod::FileSize(file_size) => {
221                    if *file_size < 2 {
222                        return Err(PipeLoggerBuilderError::RotateFileSizeTooSmall);
223                    }
224                },
225            }
226
227            if let Some(count) = &self.count {
228                if *count < 1 {
229                    return Err(PipeLoggerBuilderError::CountTooSmall);
230                }
231            }
232        }
233
234        let file_path = self.log_path.as_ref().absolutize()?;
235
236        let file_size;
237
238        let folder_path = match file_path.metadata() {
239            Ok(metadata) => {
240                if metadata.is_dir() {
241                    return Err(PipeLoggerBuilderError::FileIsDirectory(file_path.into_owned()));
242                }
243
244                let p = metadata.permissions();
245
246                if p.readonly() {
247                    return Err(PipeLoggerBuilderError::IOError(io::Error::new(
248                        io::ErrorKind::PermissionDenied,
249                        format!("`{}` is readonly.", file_path.to_str().unwrap()),
250                    )));
251                }
252
253                file_size = metadata.len();
254
255                match file_path.parent() {
256                    Some(parent) => {
257                        if self.rotate.is_some() {
258                            match fs::metadata(parent) {
259                                Ok(m) => {
260                                    let p = m.permissions();
261                                    if p.readonly() {
262                                        return Err(PipeLoggerBuilderError::IOError(
263                                            io::Error::new(
264                                                io::ErrorKind::PermissionDenied,
265                                                format!(
266                                                    "`{}` is readonly.",
267                                                    parent.to_str().unwrap()
268                                                ),
269                                            ),
270                                        ));
271                                    }
272                                },
273                                Err(err) => {
274                                    return Err(PipeLoggerBuilderError::IOError(err));
275                                },
276                            }
277                        }
278                        parent
279                    },
280                    None => unreachable!(),
281                }
282            },
283            Err(_) => {
284                file_size = 0;
285
286                match file_path.parent() {
287                    Some(parent) => match fs::metadata(parent) {
288                        Ok(m) => {
289                            let p = m.permissions();
290                            if p.readonly() {
291                                return Err(PipeLoggerBuilderError::IOError(io::Error::new(
292                                    io::ErrorKind::PermissionDenied,
293                                    format!("`{}` is readonly.", parent.to_str().unwrap()),
294                                )));
295                            }
296                            parent
297                        },
298                        Err(err) => {
299                            return Err(PipeLoggerBuilderError::IOError(err));
300                        },
301                    },
302                    None => {
303                        return Err(PipeLoggerBuilderError::IOError(io::Error::new(
304                            io::ErrorKind::NotFound,
305                            format!("`{}`'s parent does not exist.", file_path.to_str().unwrap()),
306                        )));
307                    },
308                }
309            },
310        }
311        .to_path_buf();
312
313        let file_name =
314            Path::new(file_path.as_ref()).file_name().unwrap().to_str().unwrap().to_string();
315
316        let file_name_point_index = match file_name.rfind('.') {
317            Some(index) => index,
318            None => file_name.len(),
319        };
320
321        let rotated_log_file_names = {
322            let mut rotated_log_file_names = Vec::new();
323
324            let re = Regex::new("^-[1-2][0-9]{3}(-[0-5][0-9]){5}-[0-9]{3}$").unwrap(); // -%Y-%m-%d-%H-%M-%S + $.3f
325
326            let file_name_without_extension = &file_name[..file_name_point_index];
327
328            for entry in folder_path.read_dir().unwrap().filter_map(|entry| entry.ok()) {
329                let rotated_log_file_path = entry.path();
330
331                if !rotated_log_file_path.is_file() {
332                    continue;
333                }
334
335                let rotated_log_file_name =
336                    Path::new(&rotated_log_file_path).file_name().unwrap().to_str().unwrap();
337
338                if !rotated_log_file_name.starts_with(file_name_without_extension) {
339                    continue;
340                }
341
342                let rotated_log_file_name_point_index = match rotated_log_file_name.rfind('.') {
343                    Some(index) => index,
344                    None => rotated_log_file_name.len(),
345                };
346
347                if rotated_log_file_name_point_index < file_name_point_index + 24 {
348                    // -%Y-%m-%d-%H-%M-%S + $.3f
349                    continue;
350                }
351
352                let file_name_without_extension_len = file_name_without_extension.len();
353
354                if !re.is_match(
355                    &rotated_log_file_name
356                        [file_name_without_extension_len..file_name_without_extension_len + 24],
357                ) {
358                    // -%Y-%m-%d-%H-%M-%S + $.3f
359                    continue;
360                }
361
362                let ext = &rotated_log_file_name[rotated_log_file_name_point_index..];
363
364                if ext.eq(&file_name[file_name_point_index..]) {
365                    rotated_log_file_names.push(rotated_log_file_name.to_string());
366                } else if ext.eq(".xz")
367                    && rotated_log_file_name[..rotated_log_file_name_point_index]
368                        .ends_with(&file_name[file_name_point_index..])
369                {
370                    rotated_log_file_names.push(
371                        rotated_log_file_name[..rotated_log_file_name_point_index].to_string(),
372                    );
373                }
374            }
375
376            rotated_log_file_names.sort_unstable();
377
378            rotated_log_file_names
379        };
380
381        let file =
382            OpenOptions::new().create(true).write(true).append(true).open(file_path.as_ref())?;
383
384        Ok(PipeLogger {
385            rotate: self.rotate,
386            count: self.count,
387            file: Some(file),
388            file_name,
389            file_name_point_index,
390            file_path: file_path.into_owned(),
391            file_size,
392            folder_path,
393            rotated_log_file_names,
394            compress: self.compress,
395            tee: self.tee,
396            last_rotated_time: 0,
397        })
398    }
399}
400
401// TODO -----PipeLoggerBuilder END-----
402
403// TODO -----PipeLogger START-----
404
405/// PipeLogger can help you stores, rotates and compresses logs.
406pub struct PipeLogger {
407    rotate:                 Option<RotateMethod>,
408    count:                  Option<usize>,
409    file:                   Option<File>,
410    file_name:              String,
411    file_name_point_index:  usize,
412    file_path:              PathBuf,
413    file_size:              u64,
414    folder_path:            PathBuf,
415    rotated_log_file_names: Vec<String>,
416    compress:               bool,
417    tee:                    Option<Tee>,
418    last_rotated_time:      i64,
419}
420
421impl Write for PipeLogger {
422    /// Write UTF-8 data.
423    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
424        PipeLogger::write(self, String::from_utf8_lossy(buf))?;
425
426        Ok(buf.len())
427    }
428
429    fn flush(&mut self) -> io::Result<()> {
430        match self.file {
431            Some(ref mut file) => file.flush(),
432            None => unreachable!(),
433        }
434    }
435}
436
437impl PipeLogger {
438    /// Create a new PipeLoggerBuilder.
439    pub fn builder<P: AsRef<Path>>(log_path: P) -> PipeLoggerBuilder<P> {
440        PipeLoggerBuilder::new(log_path)
441    }
442
443    /// Write a string. If the log is rotated, this method returns the renamed path.
444    pub fn write<S: AsRef<str>>(&mut self, text: S) -> io::Result<Option<PathBuf>> {
445        let s = text.as_ref();
446
447        let buf = s.as_bytes();
448
449        let len = buf.len();
450
451        if len == 0 {
452            return Ok(None);
453        }
454
455        self.print(s);
456
457        let mut file = self.file.take().unwrap();
458
459        let n = file.write(buf)?;
460
461        self.file_size += n as u64;
462
463        let mut new_file = None;
464
465        if let Some(rotate) = &self.rotate {
466            match rotate {
467                RotateMethod::FileSize(size) => {
468                    if self.file_size >= *size {
469                        let utc: DateTime<Utc> = {
470                            let mut utc: DateTime<Utc> = Utc::now();
471                            let mut millisecond = utc.timestamp_millis();
472                            while self.last_rotated_time == millisecond {
473                                // Especially for Windows, because its time precision is about 15ms.
474                                thread::sleep(Duration::from_millis(FILE_WAIT_MILLI_SECONDS));
475                                utc = Utc::now();
476                                millisecond = utc.timestamp_millis();
477                            }
478                            self.last_rotated_time = millisecond;
479                            utc
480                        };
481
482                        let timestamp = utc.format("%Y-%m-%d-%H-%M-%S").to_string();
483                        let millisecond = utc.format("%.3f").to_string();
484
485                        file.flush()?;
486
487                        file.sync_all()?;
488
489                        drop(file);
490
491                        let rotated_log_file_name = format!(
492                            "{}-{}-{}{}",
493                            &self.file_name[..self.file_name_point_index],
494                            timestamp,
495                            &millisecond[1..],
496                            &self.file_name[self.file_name_point_index..]
497                        );
498
499                        let rotated_log_file =
500                            Path::join(&self.folder_path, Path::new(&rotated_log_file_name));
501
502                        fs::copy(&self.file_path, &rotated_log_file)?;
503
504                        if self.compress {
505                            let rotated_log_file_name_compressed =
506                                format!("{}.xz", rotated_log_file_name);
507                            let rotated_log_file_compressed = Path::join(
508                                &self.folder_path,
509                                Path::new(&rotated_log_file_name_compressed),
510                            );
511                            let rotated_log_file = rotated_log_file.clone();
512
513                            let tee = self.tee.clone();
514
515                            let print_err = move |s| match tee {
516                                Some(tee) => match tee {
517                                    Tee::Stdout => {
518                                        eprintln!("{}", s);
519                                    },
520                                    Tee::Stderr => {
521                                        println!("{}", s);
522                                    },
523                                },
524                                None => {
525                                    eprintln!("{}", s);
526                                },
527                            };
528
529                            thread::spawn(move || {
530                                match File::create(&rotated_log_file_compressed) {
531                                    Ok(file_w) => {
532                                        match File::open(&rotated_log_file) {
533                                            Ok(mut file_r) => {
534                                                let mut compressor = XzEncoder::new(file_w, 9);
535                                                let mut buffer = [0u8; BUFFER_SIZE];
536                                                loop {
537                                                    match file_r.read(&mut buffer) {
538                                                        Ok(c) => {
539                                                            if c == 0 {
540                                                                drop(file_r);
541                                                                if fs::remove_file(
542                                                                    &rotated_log_file,
543                                                                )
544                                                                .is_err()
545                                                                {
546                                                                }
547                                                                break;
548                                                            }
549                                                            match compressor.write(&buffer[..c]) {
550                                                                Ok(cc) => {
551                                                                    if c != cc {
552                                                                        print_err(
553                                                                            "The space is not \
554                                                                             enough."
555                                                                                .to_string(),
556                                                                        );
557                                                                        break;
558                                                                    }
559                                                                },
560                                                                Err(err) => {
561                                                                    print_err(err.to_string());
562                                                                    break;
563                                                                },
564                                                            }
565                                                        },
566                                                        Err(ref err)
567                                                            if err.kind()
568                                                                == io::ErrorKind::NotFound =>
569                                                        {
570                                                            // The rotated log file is deleted because of the count limit
571                                                            drop(compressor);
572                                                            if fs::remove_file(
573                                                                &rotated_log_file_compressed,
574                                                            )
575                                                            .is_err()
576                                                            {
577                                                            }
578                                                            break;
579                                                        },
580                                                        Err(err) => {
581                                                            print_err(err.to_string());
582                                                            break;
583                                                        },
584                                                    }
585                                                }
586                                            },
587                                            Err(ref err)
588                                                if err.kind() == io::ErrorKind::NotFound =>
589                                            {
590                                                // The rotated log file is deleted because of the count limit
591                                                drop(file_w);
592                                                if fs::remove_file(&rotated_log_file_compressed)
593                                                    .is_err()
594                                                {
595                                                }
596                                            },
597                                            Err(err) => {
598                                                print_err(err.to_string());
599                                            },
600                                        }
601                                    },
602                                    Err(err) => {
603                                        print_err(err.to_string());
604                                    },
605                                };
606                            });
607                        }
608
609                        self.rotated_log_file_names.push(rotated_log_file_name);
610
611                        if let Some(count) = self.count {
612                            while self.rotated_log_file_names.len() >= count {
613                                let mut rotated_log_file_name =
614                                    self.rotated_log_file_names.remove(0);
615                                if fs::remove_file(Path::join(
616                                    &self.folder_path,
617                                    Path::new(&rotated_log_file_name),
618                                ))
619                                .is_err()
620                                {}
621
622                                let p_compressed_name = {
623                                    rotated_log_file_name.push_str(".xz");
624
625                                    rotated_log_file_name
626                                };
627
628                                let p_compressed =
629                                    Path::join(&self.folder_path, Path::new(&p_compressed_name));
630                                if fs::remove_file(p_compressed).is_err() {}
631                            }
632                        }
633
634                        file =
635                            OpenOptions::new().write(true).truncate(true).open(&self.file_path)?;
636
637                        self.file_size = 0;
638
639                        new_file = if self.compress {
640                            let mut s = rotated_log_file.into_os_string();
641                            s.push(".xz");
642                            Some(PathBuf::from(s))
643                        } else {
644                            Some(rotated_log_file)
645                        };
646                    }
647                },
648            }
649        }
650
651        if n != len {
652            return Err(io::Error::new(io::ErrorKind::BrokenPipe, "The space is not enough."));
653        }
654
655        self.file = Some(file);
656
657        Ok(new_file)
658    }
659
660    /// Write a string with a new line. If the log is rotated, this method returns the renamed path.
661    pub fn write_line<S: AsRef<str>>(&mut self, text: S) -> io::Result<Option<PathBuf>> {
662        let new_file = self.write(text)?;
663
664        if new_file.is_none() {
665            match self.file {
666                Some(ref mut file) => {
667                    let n = file.write(b"\n")?;
668
669                    if n != 1 {
670                        return Err(io::Error::new(
671                            io::ErrorKind::BrokenPipe,
672                            "The space is not enough.",
673                        ));
674                    }
675
676                    self.file_size += 1u64;
677                },
678                None => unreachable!(),
679            }
680            self.print("\n");
681        }
682
683        Ok(new_file)
684    }
685
686    fn print<S: AsRef<str>>(&self, text: S) {
687        let s = text.as_ref();
688
689        match &self.tee {
690            Some(tee) => match tee {
691                Tee::Stdout => {
692                    print!("{}", s);
693                },
694                Tee::Stderr => {
695                    eprint!("{}", s);
696                },
697            },
698            None => (),
699        }
700    }
701}
702
703// TODO -----PipeLogger END-----