Skip to main content

easylog/
log_file.rs

1extern crate chrono;
2
3use std::error::Error;
4use std::fs;
5use std::fs::File;
6use std::fs::OpenOptions;
7use std::io::prelude::Write;
8use std::string::ToString;
9
10use crate::log_file_config::LogFileConfig;
11
12///
13/// Represents the needed file attributes
14///
15struct FileAttr {
16    write: bool,
17    append: bool,
18    create: bool,
19    truncate: bool,
20}
21
22impl FileAttr {
23    ///
24    /// Returns a FilleAttr object with default values
25    ///
26    pub fn new() -> FileAttr {
27        FileAttr {
28            write: true,
29            append: true,
30            create: true,
31            truncate: false,
32        }
33    }
34}
35
36///
37/// Enum to represent the different Loglevel
38///
39pub enum LogLevel {
40    DEBUG,
41    INFO,
42    WARNING,
43    ERROR,
44    CRITICAL,
45}
46
47///
48/// Struct that represents the LogFile and its config
49///
50pub struct LogFile {
51    /// Maximum allowed size for the Logfile in Megabyte (2^20 = 1024*1024)
52    max_size: u64,
53
54    /// Holds the configuration for the Logfile
55    config: LogFileConfig,
56
57    /// Complete path for the Logfile
58    complete_path: String,
59
60    /// The number of the Logfile
61    counter: u64,
62
63    /// The file it self
64    file: File,
65}
66
67impl LogFile {
68    ///
69    /// Creates a new Logfile.
70    ///
71    /// # Example 1
72    /// ```rust
73    /// extern crate easylog;
74    ///
75    /// use easylog::log_file::{LogFile, LogLevel};
76    /// use easylog::log_file_config::LogFileConfig;
77    ///
78    /// fn main() {
79    ///     let default = LogFileConfig::new();
80    ///     let logfile = match LogFile::new(default) {
81    ///         Ok(file) => file,
82    ///
83    ///         Err(error) => {
84    ///             panic!("Error: `{}`", error);
85    ///         }
86    ///     };
87    /// }
88    /// ```
89    ///
90    /// You can also modify the config-struct
91    ///
92    /// # Example 2
93    /// ```rust
94    /// extern crate easylog;
95    ///
96    /// use easylog::log_file::{LogFile, LogLevel};
97    /// use easylog::log_file_config::LogFileConfig;
98    ///
99    /// fn main() {
100    ///     let mut custom_config = LogFileConfig::new();
101    ///
102    ///     custom_config.max_size_in_mb = 2;
103    ///     custom_config.path = String::from("./path/to/logfile/");
104    ///     custom_config.name = String::from("my_logfile");
105    ///     custom_config.extension = String::from(".txt");
106    ///     custom_config.num_files_to_keep = 2;
107    ///
108    ///     let logfile = match LogFile::new(custom_config) {
109    ///         Ok(file) => file,
110    ///
111    ///         Err(error) => {
112    ///             panic!("Error: `{}`", error);
113    ///         }
114    ///     };
115    /// }
116    /// ```
117    ///
118    /// # Example 3
119    /// ```rust
120    /// extern crate easylog;
121    ///
122    /// use easylog::log_file::{LogFile, LogLevel};
123    /// use easylog::log_file_config::LogFileConfig;
124    ///
125    /// fn main() {
126    ///     let mut custom_config = LogFileConfig::new();
127    ///
128    ///     custom_config.max_size_in_mb = 2;
129    ///     custom_config.path = String::from("./path/to/logfile/");
130    ///     custom_config.name = String::from("my_logfile");
131    ///     custom_config.extension = String::from(".txt");
132    ///     custom_config.overwrite = false;
133    ///     custom_config.num_files_to_keep = 1337; // has no effect, because overwrite is false
134    ///
135    ///     let logfile = match LogFile::new(custom_config) {
136    ///         Ok(file) => file,
137    ///
138    ///         Err(error) => {
139    ///             panic!("Error: `{}`", error);
140    ///         }
141    ///     };
142    /// }
143    /// ```
144    ///
145    /// # Details
146    /// At first this function checks, if already a logfile (specified
147    /// by the config argument) exist.
148    /// If a logfile exist the size is checked. If the size is okay
149    /// (actual size < max size) the file will be opened. When
150    /// the size is not okay (actual size > max size) a new file will
151    /// be created.
152    ///
153    /// If max_size_in_mb is set to 0 (zero) it will be set to 1. So 1 Megabyte
154    /// (1 * 1024 * 1024) is the smallest size for a Logfile.
155    ///
156    /// If the overwrite param is set to true (default) and num_files_to_keep
157    /// (default value is 5) is reached, the first logfile will be overwritten
158    /// instead of creating a new one. So you will always have only num_files_to_keep
159    /// of logfiles. If overwrite is set to false then you will get as much files as
160    /// your machine allows.
161    ///
162    /// If num_files_to_keep is set to 0 (zero) it will be set to 1. So 1 Logfile
163    /// is the smallest amount of Logfiles.
164    ///
165    pub fn new(mut config: LogFileConfig) -> Result<Self, Box<dyn Error>> {
166        if config.max_size_in_mb == 0 {
167            config.max_size_in_mb = 1;
168        }
169
170        const MEGABYTE: u64 = 1024u64 * 1024u64;
171        let max_size = config.max_size_in_mb * MEGABYTE;
172
173        let mut counter = 0u64;
174        let mut path = assemble_path(&config, counter);
175
176        loop {
177            let file_exist = check_if_file_exist(&path);
178            if !file_exist {
179                break;
180            }
181
182            let size_ok = is_size_ok(&path, max_size);
183            if size_ok {
184                break;
185            } else {
186                counter += 1;
187                path = assemble_path(&config, counter);
188            }
189        }
190
191        if config.num_files_to_keep == 0 {
192            config.num_files_to_keep = 1;
193        }
194
195        if config.overwrite && counter > config.num_files_to_keep {
196            counter -= config.num_files_to_keep;
197        };
198
199        let default_file_attr = FileAttr::new();
200        let logfile = open(max_size, config, counter, default_file_attr)?;
201
202        Ok(logfile)
203    }
204
205    ///
206    /// Write the given message to the logfile.
207    ///
208    /// # Example
209    /// ```rust
210    /// extern crate easylog;
211    ///
212    /// use easylog::log_file::{LogFile, LogLevel};
213    /// use easylog::log_file_config::LogFileConfig;
214    ///
215    /// fn main() {
216    ///     let default = LogFileConfig::new();
217    ///     let mut logfile = match LogFile::new(default) {
218    ///         Ok(file) => file,
219    ///
220    ///         Err(error) => {
221    ///             panic!("Error: `{}`", error);
222    ///         }
223    ///     };
224    ///
225    ///     logfile.write(LogLevel::DEBUG, "Insert your logmessage here...");
226    /// }
227    /// ```
228    ///
229    /// # Example Output
230    /// `2018-06-08 20:23:44.278165  [DEBUG   ]  Insert your logmessage here...`
231    ///
232    /// # Details
233    /// The function will append a newline at the end of the message and insert
234    /// a timestamp and the given loglevel at the beginning of the message.
235    ///
236    /// This function also check if the actual logfilesize is less then the allowed
237    /// maximum size.
238    ///
239    /// If the actual logfilesize is bigger as the allowed maximum, the actual
240    /// file will be closed, the filecounter will be incresaesed by 1 and a new
241    /// file will be opened.
242    ///
243    /// After writing the message to the file flush will be called to ensure
244    /// that all is written to file.
245    ///
246    pub fn write(&mut self, level: LogLevel, msg: &str) {
247        let log_msg = self.build_log_msg(level, msg);
248
249        let log_size = self.get_logsize();
250
251        if log_size > self.max_size {
252            self.rotate();
253        }
254
255        self._write(log_msg);
256    }
257
258    ///
259    /// Returns a clone of the actual object state
260    ///
261    pub fn clone(&mut self) -> Self {
262        LogFile {
263            max_size: self.max_size,
264            config: self.config.clone(),
265            complete_path: self.complete_path.clone(),
266            counter: self.counter,
267            file: self.file.try_clone().unwrap(),
268        }
269    }
270
271    ///
272    /// Write the given message to the logfile
273    ///
274    fn _write(&mut self, msg: String) {
275        match self.file.write_all(&msg.into_bytes()) {
276            Ok(_) => (),
277            Err(error) => panic!("panic while writing to file: `{}`", error),
278        }
279    }
280
281    ///
282    /// Rotates the Logfiles.
283    ///
284    fn rotate(&mut self) {
285        let mut file_attr = FileAttr::new();
286
287        if self.config.overwrite {
288            if self.counter == self.config.num_files_to_keep - 1 {
289                self.counter -= self.config.num_files_to_keep - 1;
290            } else {
291                self.counter += 1;
292            };
293            file_attr.append = false;
294            file_attr.truncate = self.config.truncate;
295        } else {
296            self.counter += 1;
297        };
298
299        self.complete_path = assemble_path(&self.config, self.counter);
300
301        self.file = match open_file(&self.complete_path, file_attr) {
302            Ok(file) => file,
303
304            Err(error) => {
305                let msg = format!(
306                    "Could not open new log-file `{}`!  Reason: `{}`",
307                    self.complete_path, error
308                );
309                let msg = self.build_log_msg(LogLevel::CRITICAL, &msg);
310                self._write(msg);
311
312                panic!("panic while rotating files: `{}`", error);
313            }
314        };
315    }
316
317    ///
318    /// Returns the size in Bytes of the actual Logfile
319    ///
320    fn get_logsize(&self) -> u64 {
321        let meta = self.file.metadata().unwrap();
322
323        meta.len()
324    }
325
326    ///
327    /// Converts a given LogLevel to a pre formatted string that is ready to use.
328    ///
329    fn get_loglevel_str(&self, level: LogLevel) -> String {
330        match level {
331            LogLevel::DEBUG => String::from("  [DEBUG   ]  "),
332            LogLevel::INFO => String::from("  [INFO    ]  "),
333            LogLevel::WARNING => String::from("  [WARNING ]  "),
334            LogLevel::ERROR => String::from("  [ERROR   ]  "),
335            LogLevel::CRITICAL => String::from("  [CRITICAL]  "),
336        }
337    }
338
339    ///
340    /// Put all pieces of the Logmessage together so it is ready to be written.
341    /// timestamp + loglevel + msg + '\n'
342    ///
343    fn build_log_msg(&self, level: LogLevel, msg: &str) -> String {
344        let mut log_msg = String::new();
345
346        let time_as_string = get_actual_timestamp();
347        log_msg.push_str(&time_as_string);
348
349        let level_as_string = self.get_loglevel_str(level);
350        log_msg.push_str(&level_as_string);
351
352        log_msg.push_str(&msg);
353        log_msg.push('\n');
354
355        log_msg
356    }
357}
358
359///
360/// Opens a file and connect it to a LogFile Object.
361///
362fn open(
363    max_size: u64,
364    config: LogFileConfig,
365    counter: u64,
366    file_attr: FileAttr,
367) -> Result<LogFile, Box<dyn Error>> {
368    let path = assemble_path(&config, counter);
369    let f = open_file(&path, file_attr)?;
370
371    Ok(LogFile {
372        max_size,
373        config,
374        complete_path: path,
375        counter,
376        file: f,
377    })
378}
379
380///
381/// Opens the file it self and set the file options.
382///
383fn open_file(path: &str, attr: FileAttr) -> Result<File, Box<dyn Error>> {
384    let f = OpenOptions::new()
385        .write(attr.write)
386        .append(attr.append)
387        .create(attr.create)
388        .truncate(attr.truncate)
389        .open(path)?;
390
391    Ok(f)
392}
393
394///
395/// Returns the actual timestamp in the form of: "2018-06-09 22:51:37.443883"
396///
397/// https://docs.rs/chrono/0.4.0/chrono/format/strftime/index.html#specifiers
398///
399fn get_actual_timestamp() -> String {
400    let timestamp = chrono::Local::now();
401
402    timestamp.format("%F %T%.6f").to_string()
403}
404
405///
406/// Checks if a file (specified by the given path) exist without to open the file.
407/// Returns true if the file exist, false otherwise.
408///
409/// https://stackoverflow.com/questions/32384594/how-to-check-whether-a-path-exists/32384768#32384768
410///
411fn check_if_file_exist(path: &str) -> bool {
412    fs::metadata(path).is_ok()
413}
414
415///
416/// Put the path pieces, given by the LogFileConfig, together and returns it as as String.
417///
418fn assemble_path(config: &LogFileConfig, counter: u64) -> String {
419    let path = format!(
420        "{}{}{}{}",
421        config.path,
422        config.name,
423        counter.to_string(),
424        config.extension
425    );
426
427    path
428}
429
430///
431/// Checks if the size of the file (specified by path) is smaller then max_size.
432/// If the size of the file is smaller then max_size it will return true,
433/// false otherwise.
434///
435fn is_size_ok(path: &str, max_size: u64) -> bool {
436    let mut check = false;
437
438    match fs::metadata(path) {
439        Ok(meta) => {
440            if meta.len() < max_size {
441                check = true;
442            }
443        }
444
445        Err(error) => {
446            panic!("Something went wrong while checking size!\n`{}`\n", error);
447        }
448    };
449
450    check
451}
452
453#[cfg(test)]
454mod test {
455    use super::*;
456
457    #[test]
458    fn assemble_path_test() {
459        let default_conf = LogFileConfig::new();
460        let counter = 0u64;
461        let path = assemble_path(&default_conf, counter);
462
463        assert_eq!(path, "./logfile_0.log");
464    }
465
466    #[test]
467    fn file_exist_test() {
468        assert_eq!(check_if_file_exist("testfile.txt"), true);
469        assert_eq!(check_if_file_exist("none_existing_file.txt"), false);
470    }
471
472    #[test]
473    fn size_is_ok_test() {
474        assert_eq!(is_size_ok("testfile.txt", 30), true);
475        assert_eq!(is_size_ok("testfile.txt", 20), false);
476    }
477
478    #[test]
479    #[should_panic(expected = "Something went wrong while checking size!")]
480    fn size_is_not_ok_file_not_exist_test() {
481        assert_eq!(is_size_ok("non_existing_file.txt", 20), true);
482    }
483
484    #[test]
485    fn get_loglevel_str_test() {
486        let default = LogFileConfig::new();
487        let logfile = match LogFile::new(default) {
488            Ok(file) => file,
489
490            Err(error) => {
491                panic!("Error: `{}`", error);
492            }
493        };
494
495        assert_eq!(logfile.get_loglevel_str(LogLevel::DEBUG), "  [DEBUG   ]  ");
496        assert_eq!(logfile.get_loglevel_str(LogLevel::INFO), "  [INFO    ]  ");
497        assert_eq!(
498            logfile.get_loglevel_str(LogLevel::WARNING),
499            "  [WARNING ]  "
500        );
501        assert_eq!(logfile.get_loglevel_str(LogLevel::ERROR), "  [ERROR   ]  ");
502        assert_eq!(
503            logfile.get_loglevel_str(LogLevel::CRITICAL),
504            "  [CRITICAL]  "
505        );
506    }
507
508    /*
509// commented out, becaus it writes 5 MB to the path. maybe not everyone like
510// that behavior. so this test will be disabled for the moment
511    #[test]
512    fn rotating_test() {
513        const TO_KEEP: u64 = 3;
514        let mut custom_conf = LogFileConfig::new();
515        custom_conf.num_files_to_keep = TO_KEEP;
516
517        let mut logfile = match LogFile::new(custom_conf) {
518            Ok(file) => file,
519
520            Err(error) => {
521                panic!("Error: `{}`", error);
522            }
523        };
524
525        for _ in 0..200_000 {
526            logfile.write(LogLevel::DEBUG, "~(^-^)~");
527        }
528
529        assert!(logfile.counter <= TO_KEEP);
530    }
531*/
532}