dzahui/
logger.rs

1// External dependencies
2extern crate log;
3extern crate chrono;
4extern crate regex;
5
6use regex::Regex;
7use log::{Log, Record, Level, Metadata};
8use std::fs::{File, OpenOptions, read_dir};
9use std::path::PathBuf;
10use std::io::{prelude::*, BufReader};
11
12// Communication with file writer
13use std::sync::mpsc::{sync_channel, SyncSender, Receiver};
14use std::thread;
15
16// Print time adequately
17use chrono::prelude::*;
18
19/// # General Information
20/// 
21/// Struct that writes logs to files. Changes files when maximum number of lines has been reached.
22/// 
23/// # Fields
24/// 
25/// * `log_path` - Direction in which files will be generated
26/// * `line_count` - Number of lines currently written 
27/// * `line_maximum` - Maximum number of lines per file
28/// * `current_log_file_number` - Number of current file in which logs are being written
29/// * `log_file` - Current log file
30/// * `rx` - Used for internal communication between log, warn and error in this structure
31/// 
32pub struct LogWriter {
33    pub log_path: PathBuf,
34    pub line_count: u64,
35    pub line_maximum: u64,
36    pub current_log_file_number: i64,
37    pub log_file: File,
38    pub rx: Receiver<String>
39}
40
41impl LogWriter {
42    /// # General Information
43    /// 
44    /// Creates new instance of LogWriter while taking into account previous log files, last log file and state of last log file
45    /// 
46    /// # Parameters
47    /// 
48    /// * `log_path` - A path-like object where all logs remain
49    /// * `rx` - A receiver for internal communication
50    /// * `line_maximum` - Maximum number of lines for log file
51    /// 
52    pub fn new(log_path: PathBuf, rx: Receiver<String>, line_maximum: u64) -> LogWriter {
53        if !log_path.as_path().exists() {
54            panic!("Log folder does not exist!");
55        }
56
57        let reg = Regex::new(r"(?x)^trace-(?P<number>[0-9]+)\.log$").unwrap();
58        // We look for the current log file number.
59        let dir_iter = match read_dir(&log_path) {
60            Ok(v) => v,
61            Err(_) => panic!("Could not iterate through files in log dir")
62        };
63        let mut current_log_file_number: i64 = -1;
64        for dir in dir_iter {
65            match dir {
66                Ok(v) => {
67                    let filename = String::from(v.file_name().to_string_lossy());
68                    match reg.captures(&filename).and_then(|cap| {
69                        cap.name("number").map(|number| number.as_str().parse::<i64>().unwrap())
70                    }) {
71                        Some(v) => {
72                            if v > current_log_file_number {
73                                current_log_file_number = v;
74                            }
75                        },
76                        None => ()
77                    };
78                },
79                Err(_) => panic!("Could not see file inside log dir")
80            };
81        }
82        // We will check the number of lines of the current log file.
83        let (line_count, log_file_path) = if current_log_file_number == -1 {
84            // No log yet
85            current_log_file_number += 1;
86            let mut file_path = log_path.clone();
87            file_path.push(format!("trace-{}.log", current_log_file_number));
88            (0, file_path.clone())
89        } else {
90            let mut log_file_path = log_path.clone();
91            log_file_path.push(format!("trace-{}.log", current_log_file_number));
92            match File::open(&log_file_path) {
93                Ok(f) => {
94                    let mut curr_line_count = 0;
95                    for _line in BufReader::new(f).lines() {
96                        curr_line_count += 1;
97                    }
98                    // Finally, we check how many lines it has
99                    if curr_line_count < line_maximum {
100                        (curr_line_count, log_file_path.clone())
101                    } else {
102                        // Time to create a new file
103                        current_log_file_number += 1;
104                        let mut file_path = log_path.clone();
105                        file_path.push(format!("trace-{}.log", current_log_file_number));
106                        (0, file_path.clone())
107                    }
108                },
109                Err(e) => {
110                    panic!("Could not count file lines: {}", e);
111                }
112            }
113        };
114        
115        // With the chosen log path, we continue the log
116        let f = if log_file_path.as_path().exists() {
117            match OpenOptions::new()
118                .write(true)
119                .append(true)
120                .open(&log_file_path) {
121                Ok(v) => v,
122                Err(_) => panic!("Imposible sobreescribir la bitácora.")
123            }
124        } else {
125            match File::create(&log_file_path) {
126                Ok(v) => v,
127                Err(e) => panic!("Could not create log file {}! ({})", log_file_path.as_os_str().to_string_lossy(), e)
128            }
129        };
130        LogWriter{
131            log_path,
132            log_file: f,
133            rx: rx,
134            line_count,
135            line_maximum,
136            current_log_file_number
137        }
138    }
139
140    /// # General Information
141    /// 
142    /// Writes to log file and changes internal values like line number and log file if necessary
143    /// 
144    /// # Parameters
145    /// 
146    /// * `&mut self` - A mutable reference to write and change internal state
147    /// 
148    pub fn run(&mut self){
149        for record in &self.rx {
150            match self.log_file.write((record+"\n").as_bytes()) {
151                Ok(_) => (),
152                Err(e) => {
153                    println!("Could not write to log file: {}", e)
154                }
155            }
156            self.line_count = (self.line_count + 1) % self.line_maximum;
157            if self.line_count == 0 {
158                // Time to swap logs
159                match self.log_file.flush() {
160                    Ok(_) => (),
161                    Err(_) => panic!("Could not flush contents")
162                };
163                self.current_log_file_number += 1;
164                let mut log_file_path = self.log_path.clone();
165                log_file_path.push(format!("trace-{}.log", self.current_log_file_number));
166                self.log_file = match File::create(&log_file_path) {
167                    //Ok(v) => BufWriter::with_capacity(2000, v),
168                    Ok(v) => v,
169                    Err(e) => panic!("Could not create log file {}! ({})", log_file_path.as_os_str().to_string_lossy(), e)
170                }
171            }
172        }
173    }
174}
175
176/// # General Information
177/// 
178/// Logger structure for Dzahui
179/// 
180/// # Fields
181/// 
182/// * `print_to_term` - Wether log should be printed to standard exit or not
183/// * `print_to_file` - Wether log should be written to file
184/// * `tx` - Communication between thread from logs and the rest of Dzahui
185/// * `logger_id` - String to print on log
186/// 
187pub struct DzahuiLogger {
188    print_to_term: bool,
189    print_to_file: bool,
190    tx: SyncSender<String>,
191    logger_id: &'static str
192}
193
194impl Log for DzahuiLogger {
195    /// # General Information
196    /// 
197    /// Indicates which level of log will be used
198    /// 
199    /// # Parameters
200    /// 
201    /// * `&self` - to acess method via '.'
202    /// * `metadata` - to check metadata level
203    /// 
204    fn enabled(&self, metadata: &Metadata) -> bool {
205        match metadata.level() {
206            Level::Error => true,
207            Level::Warn => true,
208            Level::Info => true,
209            Level::Debug => true,
210            Level::Trace => true
211        }
212    }
213
214    /// # General Information
215    /// 
216    /// Deals with every log on an individual manner
217    /// 
218    /// # Parameters
219    /// 
220    /// * `&self` - An instance to check some internal state variables
221    /// * `record` - Payload of a log message to record
222    /// 
223    fn log(&self, record: &Record) {
224        // Only process messages we're interested on
225        if self.enabled(record.metadata()) {
226            let level_string = {
227                match record.level() {
228                    Level::Error => format!("\u{001b}[0;31m{}\u{001b}[0m", record.level().to_string()),
229                    Level::Warn => format!("\u{001b}[0;33m{}\u{001b}[0m", record.level().to_string()),
230                    Level::Info => format!("\u{001b}[0;36m{}\u{001b}[0m", record.level().to_string()),
231                    Level::Debug => format!("\u{001b}[0;35m{}\u{001b}[0m", record.level().to_string()),
232                    Level::Trace => format!("{}", record.level().to_string()),
233                }
234            };
235            let registry = if cfg!(feature = "log-module") {format!(
236                "{} {}[{:<5}, {}]: {}",
237                Local::now().format("%Y-%m-%d %H:%M:%S,%3f"),
238                self.logger_id,
239                level_string,
240                record.module_path().unwrap_or_default(),
241                record.args()
242            )} else {format!(
243                "{} {}[{:>16}]: {}",
244                Local::now().format("%Y-%m-%d %H:%M:%S,%3f"),
245                self.logger_id,
246                level_string,
247                record.args()
248            )}; 
249
250            if self.print_to_term {
251                println!("{}",&registry);
252            }
253            if self.print_to_file {
254                match self.tx.send(registry) {
255                    Ok(_) => (),
256                    Err(_) => {
257                        println!("Cannot write anymore to log file (thread crashed)");
258                        panic!("Could not write anymore to the log file");
259                    }
260                };
261            }
262        }
263    }
264
265    /// Empty function
266    fn flush(&self) {}
267}
268
269impl DzahuiLogger {
270    /// # General Information
271    /// 
272    /// Creates a new instance of a logger. Will print to term if file is not provided
273    /// 
274    /// # Parameters
275    /// 
276    /// * `logger_id` - An id for this instance 
277    /// * `print_to_term` - Wether to print to terminal or not 
278    /// * `log_path` - Where to store logs
279    ///  
280    pub fn new(logger_id: &'static str, print_to_term: bool, log_path: Option<PathBuf>) -> DzahuiLogger {
281        if let Some(log_path) = log_path {
282            if !log_path.as_path().exists() {
283                panic!("Could not find log path ({})", log_path.as_os_str().to_string_lossy());
284            }
285            // We generate the communication channel
286            let (sender, receiver) = sync_channel::<String>(0);
287            // This thread will receive all log messages
288            thread::spawn(move || {
289                LogWriter::new(log_path, receiver, 10_000_000).run()
290            });
291            DzahuiLogger{print_to_term, print_to_file: true, tx: sender, logger_id}
292        } else {
293            let (sender, _) = sync_channel::<String>(0);
294            // The sender will anyways never be used
295            DzahuiLogger{print_to_term, print_to_file: false, tx: sender, logger_id}
296        }
297    }
298}
299
300/// # General Information
301/// 
302/// Spawns a boxed logger
303/// Must only be called once.
304/// 
305/// # Parameters
306/// 
307/// * `log_level` - Which level of logging to use 
308/// * `prefix` - Id of logger
309/// 
310pub fn spawn(log_level: log::LevelFilter, prefix: &'static str) -> Result<(), log::SetLoggerError> {
311    log::set_boxed_logger(Box::new(DzahuiLogger::new(prefix, true, None))).map(|()| 
312        log::set_max_level(log_level)
313    )
314}