durylog 0.1.2

An easy to use library to implements logging on stdout, file or both.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
//#![allow(missing_docs)]
#[doc(inline)]
use chrono::Utc;
pub use log::{debug, error, info, trace, warn, LevelFilter};
use log::{Level, Log, Metadata, ParseLevelError, Record, SetLoggerError};
use std::{
    env::{self, VarError},
    str::FromStr, fs::{OpenOptions, File, self}, io::{self, Write, stdout}, path::PathBuf,
};

/// Colors
const COLOR_RED:     &str = "\x1B[38;5;196m";
//const COLOR_GREEN:   &str = "\x1B[38,5,2m";
const COLOR_LIME:    &str = "\x1B[38;5;10m";
const COLOR_YELLOY:  &str = "\x1B[38;5;11m";
//const COLOR_BLUE:    &str = "\x1B[38;5;4m";
//const COLOR_MAGENTA: &str = "\x1B[38;5;13m";
const COLOR_CYAN:    &str = "\x1B[38;5;14m";
const COLOR_DEFAULT: &str = "\x1B[0m";

/// Log levels strings
const STR_ERROR: &str = "ERROR ";
const STR_WARN:  &str = "WARN  ";
const STR_INFO:  &str = "INFO  ";
const STR_DEBUG: &str = "DEBUG ";
const STR_TRACE: &str = "TRACE ";

/// Default settings values
const DEFAULT_TIMESTAMP_FORMAT: &str="%Y/%m/%d %H.%M.%S";
const DEFAULT_SEP: &str = " : ";
//const MB: u64 = 1024 * 1024;

/// Enumaration to handle different kinds of errors.
#[derive(Debug)]
pub enum DLogError {
    #[doc(hidden)]
    Level(ParseLevelError),
    #[doc(hidden)]
    Env(VarError),
    #[doc(hidden)]
    Err(io::Error),
    #[doc(hidden)]
    None,
}
/*
enum DStorageMode {
    BySize,
    ByTime,
}
*/

/// struct to hold all settings to handle logging.
#[derive(Debug)]
pub struct DLog {
    level: LevelFilter,
    target: Option<String>,

    show_color_enabled: bool,
    log_on_stdout: bool,

    // File params
    log_on_file: bool,
    filename: PathBuf,
    file: Option<File>,
    max_file_size: u64,
    max_files_count: u64,

    // Formatting message flags
    timestamp_format: String,
    show_timestamp_enabled: bool,
    show_level_enabled: bool,
    separator: String,
}

impl Default for DLog {
    fn default() -> Self {
        Self::new()
    }
}

impl DLog {
    /// Create an instant of ['DLog'] with default settings:
    /// - Log only on stdout (file disabled).
    /// - Color disabled.
    /// - Show Timestamp.
    /// - Show Level.
    pub fn new() -> Self {
        Self {
            level: LevelFilter::Trace,
            target: None,

            show_color_enabled: false,
            log_on_stdout: true,

            log_on_file:false,
            filename: PathBuf::new(),
            file: None,
            max_file_size: 0, // no limits
            max_files_count: 0, // no limits

            show_timestamp_enabled: true,
            timestamp_format: String::from(DEFAULT_TIMESTAMP_FORMAT),
            show_level_enabled: true,
            separator: String::from(DEFAULT_SEP),
        }
    }
    
// ************** Api for new() initialization **************
    /// Enable logging on file and open it.
    pub fn with_file(mut self, filename: &str) -> Result<Self, DLogError> {
        match self.open_file(filename) {
            Ok(file) => {
                self.file=Some(file);
                self.log_on_file=true;
                self.filename=PathBuf::from(filename);
                Ok(self)
                
            },
            Err(err) => {
                self.file=None;
                self.log_on_file=false;
                self.filename.clear();
                Err(DLogError::Err(err))
            }
        }
    }

    /// Convenient function to enable color in construction.
    pub fn with_color(mut self) -> Self {
        self.enabled_colors(true);
        self
    }

    /// Filter log target.
    pub fn widh_target_filter<S: AsRef<str>>(mut self, target: S) -> Self {
        let target = target.as_ref().replace('-', "_");
        self.target = Some(target);
        self
    }


    /// Filter log level.
    pub fn widh_level(&mut self, level: LevelFilter) -> &mut Self {
        self.level = level;
        self
    }

    /// Filter log level from ['name'] environment variable.
    pub fn with_level_from_env<S: AsRef<str>>(self, name: S) -> Result<Self, DLogError> {
        match env::var(name.as_ref()) {
            Ok(s) => self.widh_level_from_str(&s),
            Err(err) => Err(DLogError::Env(err)),
        }
    }

    /// Filter log level from `str`.
    pub fn widh_level_from_str<S: AsRef<str>>(mut self, s: S) -> Result<Self, DLogError> {
        match LevelFilter::from_str(s.as_ref()) {
            Ok(level) => {
                self.level = level;
                Ok(self)
            }
            Err(err) => Err(DLogError::Level(err)),
        }
    }

    /// Use custom datetime stamp format.
    pub fn widh_timestamp_format(mut self, format: &str) -> Self {
        self.timestamp_format=String::from(format);
        self
    }

    /// Use custom separator for tags. Default separator is ':'.
    /// 
    /// E.g.:
    /// 
    /// 2022/12/28 17.38.42 : ERROR  : Error message 
    pub fn widh_custom_separator(mut self, new_sep: &str) -> Self{
        self.separator=new_sep.to_string();
        self
    }

    /// Disable logging on stdout.
    pub fn without_console(mut self) -> Self {
        self.log_on_stdout=false;
        self
    }

    /// Initialize for use with std::log crate.
    /// 
    /// Must call before using std::log macro: error!() warn!() debug!() trace!()
    /// 
    /// Any use of ['debug!()'] will do nothing without  calling this function.
    pub fn init_logger(self) -> Result<(),SetLoggerError> {
        log::set_boxed_logger(Box::new(self)).map(|()| log::set_max_level(LevelFilter::Trace))
    }

// **********************************************************

// ******************* api for direct use *******************

    /// Enable/disable print in console(stdout).
    pub fn enable_console(&mut self, enabled: bool) {
        self.log_on_stdout=enabled;
    }
    
    /// Enable/disable write in file.
    /// Works only if ['widh_file()'] function has previously called to set the filename.
    pub fn enable_file(&mut self, enabled: bool) {
        self.log_on_file=enabled;
    }

    /// Enable/disable colors in console.
    pub fn enabled_colors(&mut self, enabled: bool) {
        self.show_color_enabled=enabled;
    }

    /// Enable/disable showing timestamp in log string.
    pub fn enable_timestamp_print(&mut self, enabled: bool) {
        self.show_timestamp_enabled=enabled;
    }

    /// Enable/disable showing level in log string.
    pub fn enable_level_print(&mut self, enabled: bool) {
        self.show_level_enabled=enabled;
    }

    /// Log the ['msg'] string on ['Level::Error'].
    /// -Print on console if ['log_on_stdout'] is enabled.
    /// -Print in file if ['log_on_file'] is enabled and file is initialized with ['with_file()'].
    pub fn e(&self, msg: &str) {
        self.write(Level::Error, msg);
    }

    /// Log the ['msg'] string on ['Level::Warn'].
    /// -Print on console if ['log_on_stdout'] is enabled.
    /// -Print in file if ['log_on_file'] is enabled and file is initialized with ['with_file()'].
    pub fn w(&self, msg: &str) {
        self.write(Level::Warn, msg);
    }

    /// Log the ['msg'] string on ['Level::Info'].
    /// -Print on console if ['log_on_stdout'] is enabled.
    /// -Print in file if ['log_on_file'] is enabled and file is initialized with ['with_file()'].
    pub fn i(&self, msg: &str) {
        self.write(Level::Info, msg);
    }

    /// Log the ['msg'] string on ['Level::Debug'].
    /// -Print on console if ['log_on_stdout'] is enabled.
    /// -Print in file if ['log_on_file'] is enabled and file is initialized with ['with_file()'].
    pub fn d(&self, msg: &str) {
        self.write(Level::Debug, msg);
    }

    /// Log the ['msg'] string on ['Level::Trace'].
    /// -Print on console if ['log_on_stdout'] is enabled.
    /// -Print in file if ['log_on_file'] is enabled and file is initialized with ['with_file()'].
    pub fn t(&self, msg: &str) {
        self.write(Level::Trace, msg);
    }

// ******************* api for internal use *******************
    /// Log the ['msg'] string on ['level'] level.
    /// -Print on console if ['log_on_stdout'] is enabled.
    /// -Print in file if ['log_on_file'] is enabled and file is initialized with ['with_file()'].
    fn write(&self, level: Level, msg: &str) {
        // Now string
        let timestamp_str = Utc::now().format(&self.timestamp_format).to_string() + &self.separator;
        // Level string
        let level_str=self.level_to_str(level).to_string() + &self.separator;

        if self.log_on_stdout {
            // Print on stdout (use color if set)
            write!(stdout(),"{}\n",
                if self.show_color_enabled {self.level_to_color(level).to_string()} else {String::new()} +
                if self.show_timestamp_enabled {&timestamp_str} else {""} +
                if self.show_level_enabled {&level_str} else {""} +
                msg +
                if self.show_color_enabled {&COLOR_DEFAULT} else {""}
            ).ok();
        }

        if self.log_on_file {
            // Write in file
            self.write_file(
                &(
                    if self.show_timestamp_enabled {timestamp_str} else {String::new()} +
                    if self.show_level_enabled {&level_str} else {""} +
                    msg
                )
            ).ok();
        }
    }

    /// ['return'] a string associated to ['level'].
    fn level_to_str(&self, level: Level) -> &'static str {
        match level {
            Level::Error    => STR_ERROR,
            Level::Warn     => STR_WARN,
            Level::Info     => STR_INFO,
            Level::Debug    => STR_DEBUG,
            Level::Trace    => STR_TRACE,
        }
    }

    /// ['return'] a color pattern associated to ['level'].
    fn level_to_color(&self, level: Level) -> &'static str {
        match level {
            Level::Error    => COLOR_RED,
            Level::Warn     => COLOR_YELLOY,
            Level::Info     => COLOR_DEFAULT,
            Level::Debug    => COLOR_CYAN,
            Level::Trace    => COLOR_LIME,
        }
    }

    /// ['return'] info about durylog crate setting.
    /// ### Example
    /// ```rust
    /// let durylog=DLog::new();
    /// println!("{}", durylog.get_status());
    /// ```
    /// Will output:
    /// ```toml
    /// ---- durylog create current settings ----
    /// Show Colors       =  false
    /// Show Level        =  true
    /// Show Timestamp    =  true
    /// Timestamp Format  =  %Y/%m/%d %H.%M.%S
    /// Tags separator    =  ' : '
    /// Level             =  TRACE
    /// Log on stdout     =  true
    /// Log on file       =  false
    /// Max file size     =  no limit
    /// Max files count   =  no limit
    /// --------------------------------------
    /// ```
    pub fn get_status(&self) -> String {
        let max_file_size=self.max_file_size.to_string();
        let max_files_count=self.max_files_count.to_string();

        let mut filename_str=String::new();
        if self.log_on_file {
            let binding = self.filename.canonicalize().ok().unwrap_or_default();
            filename_str.push_str("Current filename  =  ");
            filename_str.push_str(binding.to_str().unwrap_or_default());
            filename_str.push('\n');
        } 

        let status_info=String::new() +
            "----------- durylog current settings -----------" + "\n" +
            "Show Colors       =  " + &self.show_color_enabled.to_string() + "\n" +
            "Show Level        =  " + &self.show_level_enabled.to_string() + "\n" +
            "Show Timestamp    =  " + &self.show_timestamp_enabled.to_string() + "\n" +
            "Timestamp Format  =  " + &self.timestamp_format.to_string() + "\n" +
            "Tags separator    =  '" + &self.separator + "'\n" +
            "Level             =  " + &self.level.to_string() + "\n" +
            "Log on stdout     =  " + &self.log_on_stdout.to_string() + "\n" +
            "Log on file       =  " + &self.log_on_file.to_string() + "\n" +
            if self.log_on_file {&filename_str} else {""} +
            "Max file size     =  " + if self.max_file_size > 0 {&max_file_size} else {"no limit"} + "\n" +
            "Max files count   =  " + if self.max_files_count > 0 {&max_files_count} else {"no limit"} + "\n" +
            "---------------------------------------------";

        status_info
    }
// *******************************************************************

// *************************** File handle ***************************
    /// Open ['filename'] for with options enabled: read, write, create, append.
    fn open_file(&self, filename: &str) -> io::Result<File> {
        let f = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .append(true)
            .open(filename)?;
    
        Ok(f)
    }

    /// Write string ['msg'] into file.
    /// N.B. If ['file'] is not opened, nothing happens.
    fn write_file(&self, msg: &str) -> Result<usize,DLogError> {
        if let Some(file) = &self.file {
            let mut f=file;
            let s=format!("{}\n",msg);
            match f.write(s.as_bytes()) {
                Ok(b_written) => {
                    self.check_storage()?;
                    return Ok(b_written);
                },
                Err(err) => {
                    return Err(DLogError::Err(err));
                },
            }
        }
        Ok(0)
    }

    /// Make a files rotation/delete due to the settings.
    fn check_storage(&self) -> Result<(),DLogError> {
        if let Some(file) = &self.file {
            // Check for current file size
            let f=file;
            match f.metadata() {
                Ok(metadata) => {
                    if self.max_file_size > 0 && metadata.len() > self.max_file_size {
                        // exceed max size
                        self.write(Level::Trace, &format!("Current log size {} exceed {}, need to rotate",metadata.len(),self.max_file_size));
                        self.rotate_files()?;
                    };
                    return Ok(());
                }
                Err(err) => return Err(DLogError::Err(err)),
            }
        }
        Ok(())
    }

    fn rotate_files(&self) -> Result<(),DLogError>{
        // Get list of log files
        let mut files_list=self.get_files()?;
        files_list.sort();

        // Check for max files count
        if self.max_files_count > 0 && files_list.len() >= self.max_files_count as usize {
            // Delete all files that exceeds max files count
            self.write(Level::Trace, "Files that needs to be deleted:");
            files_list.into_iter().nth(self.max_files_count.saturating_sub(1) as usize).map(|path| {
                self.write(Level::Trace, path.to_str().unwrap());
                //fs::remove_file(path)
            });
        }

        // TODO: rename current file
        // TODO: create and open new one

        Ok(())
    }

    /// ['return'] a vector containing a list of all files in the ['self.filename'] directory path and with ['self.filename'] extension.
    fn get_files(&self) -> Result<Vec<PathBuf>, DLogError> {
        //let filename=Path::new(&self.filename);

        match fs::read_dir(&self.filename.parent().unwrap()) {
            Ok(r) => {
                return Ok(
                    r.into_iter()
                    .filter(|r| r.is_ok()) // Get rid of Err variants for Result<DirEntry>
                    .map(|r| {
                        r.unwrap().path().canonicalize().unwrap()
                    }) // This is safe, since we only have the Ok variants
                    .filter(|r| r.is_file()) // only files
                    .filter(|r|r.extension().unwrap().eq_ignore_ascii_case(&self.filename.extension().unwrap())) // only files with .log extension
                    .collect())
            },
            Err(err) => return Err(DLogError::Err(err)),
        }
    }
// ************************* end File handle *************************
}

impl Log for DLog {
    fn enabled(&self, metadata: &Metadata) -> bool {
        if let Some(level) = self.level.to_level() {
            if level >= metadata.level() {
                return match &self.target {
                    Some(t) => metadata.target().starts_with(t),
                    None => true,
                };
            }
        }
        false
    }

    fn log(&self, record: &Record) {
        if self.enabled(record.metadata()) {
            self.write(record.level(), record.args().to_string().as_str());
        }
    }

    fn flush(&self) {}
}