buffered_logger/
lib.rs

1//! # buffered_logger
2//!
3//! This is a file logger implemetation for crate [log](https://docs.rs/log/latest/log/). It provides a buffer to save
4//! the log contents temporarily indeed of writing file every time. When the size of buffer exceeds the max buffer
5//! size, it writes the buffer to current log file. When the size of the current log file exceeds the max file size,
6//! the log file is rotated. It uses crate [flate2](https://docs.rs/flate2/latest/flate2/) to compress rotatd file.
7//! It uses crate [crossbeam-channel](https://docs.rs/crossbeam-channel/0.5.1/crossbeam_channel/) for multi-threading.
8//!
9//! # Usage
10//! ```rust
11//! use buffered_logger::Logger;
12//!
13//! // Initialize the logger and start the service.
14//! let logger = Logger::init(log::Level::Trace, "logs/m.log".to_string(), 10, 1024, 1024 * 5, true).unwrap();
15//! logger.start();
16//!
17//! // Now you can start logging.
18//! log::info!("this is an info message");
19//! log::debug!("this is a debug message");
20//!
21//! // Logger is clonable. This is useful for passing it to a different thread.
22//! let logger_clone = logger.clone();
23//!
24//! // You can manually write the buffer to current log file. eg. run it every second.
25//! logger_clone.flush();
26//!
27//! // You can manually rotate the log file. eg. run it every day at 00:00.
28//! logger_clone.rotate();
29//! ```
30
31use std::{
32    fs::{create_dir_all, read_dir, remove_file, rename, File, OpenOptions},
33    io::{stdout, Write},
34    path::{Path, PathBuf},
35    process::exit,
36};
37
38use crossbeam_channel::{unbounded, Receiver, Sender};
39use flate2::{write::GzEncoder, Compression};
40use log::{Level, Metadata, Record, SetLoggerError};
41use regex::Regex;
42
43#[cfg(windows)]
44const LINE_ENDING: &'static str = "\r\n";
45#[cfg(not(windows))]
46const LINE_ENDING: &'static str = "\n";
47
48enum Message {
49    Flush,
50    Rotate,
51    Msg(String),
52}
53
54#[derive(Clone)]
55/// The logger struct
56pub struct Logger {
57    log_path: String,
58    retain: usize,
59    buffer_size: usize,
60    rotate_size: usize,
61    stdout: bool,
62    sender: Sender<Message>,
63    receiver: Receiver<Message>,
64}
65
66struct Log {
67    level: Level,
68    sender: Sender<Message>,
69}
70
71static mut TIME_DIFF: i64 = 0;
72
73impl Logger {
74    /// Initialize a logger
75    ///
76    /// # Arguments
77    /// * `level` - log level. eg. `Level::Info`.
78    /// * `log_path` - relative or absolute path.
79    /// * `retain` - max number of rotated logs.
80    /// * `buffer_size` - When the size of log buffer becomes higher than this value it will write it to log file.
81    /// * `rotate_size` - When the size of current log file becomes higher than this value it will rotate it.
82    /// * `stdout` - also log to standard output.
83    ///
84    /// # Example
85    /// ```rust
86    /// use buffered_logger::Logger;
87    ///
88    /// let logger = Logger::init(log::Level::Trace, "logs/m.log".to_string(), 10, 1024, 1024 * 5, true).unwrap();
89    /// ```
90    pub fn init(
91        level: Level,
92        log_path: String,
93        retain: usize,
94        buffer_size: usize,
95        rotate_size: usize,
96        stdout: bool,
97    ) -> Result<Logger, SetLoggerError> {
98        let (sender, receiver) = unbounded();
99        let lf = match level {
100            Level::Trace => log::LevelFilter::Trace,
101            Level::Debug => log::LevelFilter::Debug,
102            Level::Info => log::LevelFilter::Info,
103            Level::Warn => log::LevelFilter::Warn,
104            Level::Error => log::LevelFilter::Error,
105        };
106        log::set_boxed_logger(Box::new(Log {
107            level,
108            sender: sender.clone(),
109        }))
110        .map(|()| log::set_max_level(lf))?;
111        Ok(Logger {
112            log_path,
113            retain,
114            buffer_size,
115            rotate_size,
116            stdout,
117            sender,
118            receiver,
119        })
120    }
121
122    /// start the logger service.
123    pub fn start(&self) {
124        let this = self.clone();
125        std::thread::spawn(move || {
126            let mut curr_len: usize = 0;
127            let mut bytes_buf = vec![0u8; this.buffer_size];
128            let log_path = Path::new(this.log_path.as_str());
129            let log_dir = log_path.parent().unwrap();
130            let file_stem = log_path.file_stem().unwrap().to_str().unwrap();
131            let file_ext = log_path.extension().unwrap().to_str().unwrap();
132
133            match create_dir_all(&log_dir) {
134                Err(err) => {
135                    eprintln!(
136                        "buffered_logger: Failed to created dir {} - {}",
137                        log_dir.to_str().unwrap(),
138                        err
139                    );
140                    exit(-1);
141                }
142                _ => (),
143            }
144
145            let mut file = OpenOptions::new()
146                .create(true)
147                .append(true)
148                .open(log_path)
149                .unwrap();
150            let mut file_size = file.metadata().unwrap().len() as usize;
151
152            let re = format!(
153                "{}\\.\\d{{6}}\\.\\d{{6}}\\.\\d{{3}}\\.{}\\.gz$",
154                &file_stem, &file_ext
155            );
156            let mut rotated_items: Vec<PathBuf> = read_dir(&log_dir)
157                .unwrap()
158                .filter_map(|res| {
159                    let path = res.unwrap().path();
160                    let re = Regex::new(&re).unwrap();
161                    if re.is_match(path.to_str().unwrap()) {
162                        return Some(path);
163                    }
164                    None
165                })
166                .collect();
167            rotated_items.sort();
168
169            let retain = this.retain;
170            let rotate =
171                |rotated_items: &mut Vec<PathBuf>, file: &mut File, file_size: &mut usize| {
172                    *file_size = 0;
173
174                    let now = chrono::Local::now().naive_local();
175                    let rotated_log_base_name =
176                        format!("{}.{}", file_stem, now.format("%y%m%d.%H%M%S%.3f"),);
177                    let rotated_log_file_name = format!("{}.{}", rotated_log_base_name, file_ext);
178                    let rotated_log_path = log_dir.join(&rotated_log_file_name);
179                    rename(log_path, &rotated_log_path).unwrap();
180                    *file = OpenOptions::new()
181                        .create(true)
182                        .append(true)
183                        .open(log_path)
184                        .unwrap();
185                    let zip_name = format!("{}.gz", rotated_log_file_name);
186                    let zip_path = log_dir.join(zip_name);
187                    rotated_items.push(zip_path.clone());
188                    while rotated_items.len() > retain {
189                        match remove_file(&rotated_items[0]) {
190                            _ => (),
191                        }
192                        rotated_items.remove(0);
193                    }
194                    std::thread::spawn(move || {
195                        let zip_file = std::fs::File::create(zip_path).unwrap();
196                        let mut rotated_file = OpenOptions::new()
197                            .read(true)
198                            .open(&rotated_log_path)
199                            .unwrap();
200                        let mut zip = GzEncoder::new(&zip_file, Compression::default());
201                        std::io::copy(&mut rotated_file, &mut zip).unwrap();
202                        zip.flush().unwrap();
203                        zip.finish().unwrap();
204                        remove_file(rotated_log_path).unwrap();
205                    });
206                };
207
208            loop {
209                let data = this.receiver.recv().unwrap();
210                match data {
211                    Message::Flush => {
212                        let s = &bytes_buf[0..curr_len];
213                        file.write_all(s).unwrap();
214                        if this.stdout {
215                            stdout().write_all(s).unwrap();
216                        }
217                        curr_len = 0;
218                    }
219                    Message::Rotate => {
220                        rotate(&mut rotated_items, &mut file, &mut file_size);
221                    }
222                    Message::Msg(data) => {
223                        let len = data.len();
224                        let next_len = curr_len + len;
225                        let next_file_size = file_size + len;
226                        if next_file_size > this.rotate_size {
227                            rotate(&mut rotated_items, &mut file, &mut file_size);
228                        } else {
229                            file_size = next_file_size;
230                        }
231                        if next_len > this.buffer_size {
232                            let s = &bytes_buf[0..curr_len];
233                            file.write_all(s).unwrap();
234                            if this.stdout {
235                                stdout().write_all(s).unwrap();
236                            }
237                            bytes_buf[0..len].copy_from_slice(data.as_bytes());
238                            curr_len = len;
239                        } else {
240                            bytes_buf[curr_len..next_len].copy_from_slice(data.as_bytes());
241                            curr_len = next_len;
242                        }
243                    }
244                }
245            }
246        });
247    }
248
249    /// manually write the buffer to the current log file.
250    pub fn flush(&self) {
251        self.sender.send(Message::Flush).unwrap();
252    }
253
254    /// manually rotate the current log file.
255    pub fn rotate(&self) {
256        self.sender.send(Message::Rotate).unwrap();
257    }
258
259    /// set time_diff in secs.
260    pub fn set_time_diff(time_diff: i64) {
261        unsafe {
262            TIME_DIFF = time_diff;
263        }
264    }
265}
266
267impl log::Log for Log {
268    fn enabled(&self, metadata: &Metadata) -> bool {
269        metadata.level() <= self.level
270    }
271
272    fn log(&self, record: &Record) {
273        if self.enabled(record.metadata()) {
274            let now = chrono::Local::now().naive_local();
275            let nsecs = now.timestamp_subsec_nanos();
276            let secs = now.timestamp();
277            let secs = unsafe { secs + TIME_DIFF };
278            let now = chrono::NaiveDateTime::from_timestamp(secs, nsecs);
279            self.sender
280                .send(Message::Msg(format!(
281                    "[{} {}] {}{}",
282                    now.format("%F %H:%M:%S%.3f"),
283                    record.level(),
284                    record.args(),
285                    LINE_ENDING
286                )))
287                .unwrap();
288        }
289    }
290
291    fn flush(&self) {
292        self.sender.send(Message::Flush).unwrap();
293    }
294}