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}