ezlog/
config.rs

1use std::{
2    cmp,
3    collections::hash_map::DefaultHasher,
4    fmt,
5    fs::{
6        self,
7        File,
8        OpenOptions,
9    },
10    hash::{
11        Hash,
12        Hasher,
13    },
14    path::{
15        Path,
16        PathBuf,
17    },
18};
19
20use memmap2::{
21    MmapMut,
22    MmapOptions,
23};
24use time::{
25    format_description,
26    Date,
27    Duration,
28    OffsetDateTime,
29};
30
31use crate::events::Event;
32#[allow(unused_imports)]
33use crate::EZLogger;
34use crate::{
35    errors::LogError,
36    events::event,
37    logger::Header,
38    CipherKind,
39    CompressKind,
40    CompressLevel,
41    Version,
42    DEFAULT_LOG_FILE_SUFFIX,
43    DEFAULT_LOG_NAME,
44    DEFAULT_MAX_LOG_SIZE,
45    LOG_LEVEL_NAMES,
46    MIN_LOG_SIZE,
47};
48
49pub const DATE_FORMAT: &str = "[year]_[month]_[day]";
50
51/// A config to set up [EZLogger]
52#[derive(Debug, Clone)]
53pub struct EZLogConfig {
54    /// max log level
55    ///
56    /// if record level is greater than this, it will be ignored
57    pub level: Level,
58    /// EZLog version
59    ///
60    /// logger version, default is [Version::V2]
61    pub version: Version,
62    /// Log file dir path
63    ///
64    /// all log files will be saved in this dir
65    pub dir_path: String,
66    /// Log name to identify the [EZLogger]
67    ///
68    /// log file name will be `log_name` + `file_suffix`
69    pub name: String,
70    /// Log file suffix
71    ///
72    /// file suffix, default is [DEFAULT_LOG_FILE_SUFFIX]
73    pub file_suffix: String,
74    /// Log file expired after duration
75    ///
76    /// the duration after which the log file will be trimmed
77    pub trim_duration: Duration,
78    /// The maxium size of log file
79    ///
80    /// if log file size is greater than this, logger will rotate the log file
81    pub max_size: u64,
82    /// Log content compress kind.
83    ///
84    // compress kind, default is [CompressKind::NONE]
85    pub compress: CompressKind,
86    /// Log content compress level.
87    ///
88    /// compress level, default is [CompressLevel::Default]
89    pub compress_level: CompressLevel,
90    /// Log content cipher kind.
91    ///
92    /// cipher kind, default is [CipherKind::NONE]
93    pub cipher: CipherKind,
94    /// Log content cipher key.
95    ///
96    /// cipher key, default is `None`
97    pub cipher_key: Option<Vec<u8>>,
98    /// Log content cipher nonce.
99    ///
100    /// cipher nonce, default is `None`
101    pub cipher_nonce: Option<Vec<u8>>,
102    /// rotate duration
103    ///
104    /// the duration after which the log file will be rotated
105    pub rotate_duration: Duration,
106
107    /// Extra info to be added to log header
108    ///
109    /// Plaintext infomation write in log file header
110    pub extra: Option<String>,
111}
112
113impl EZLogConfig {
114    pub(crate) fn file_name(&self) -> crate::Result<String> {
115        let str = format!("{}.{}", self.name, self.file_suffix);
116        Ok(str)
117    }
118
119    pub(crate) fn file_name_with_date(
120        &self,
121        time: OffsetDateTime,
122        count: i32,
123    ) -> crate::Result<String> {
124        let format = time::format_description::parse(DATE_FORMAT).map_err(|e| {
125            crate::errors::LogError::Parse(format!(
126                "Unable to create a formatter; this is a bug in EZLogConfig#file_name_with_date: {}",
127                e
128            ))
129        })?;
130        let date = time.format(&format).map_err(|_| {
131            crate::errors::LogError::Parse(
132                "Unable to format date; this is a bug in EZLogConfig#file_name_with_date"
133                    .to_string(),
134            )
135        })?;
136        let new_name = format!("{}_{}.{}.{}", self.name, date, count, self.file_suffix);
137        Ok(new_name)
138    }
139
140    pub fn is_valid(&self) -> bool {
141        !self.dir_path.is_empty() && !self.name.is_empty() && !self.file_suffix.is_empty()
142    }
143
144    pub fn create_mmap_file(&self) -> crate::Result<(PathBuf, MmapMut)> {
145        let (file, path) = self.create_log_file()?;
146        let mmap = unsafe { MmapOptions::new().map_mut(&file)? };
147        Ok((path, mmap))
148    }
149
150    pub(crate) fn create_log_file(&self) -> crate::Result<(File, PathBuf)> {
151        let file_name = self.file_name()?;
152        let max_size = cmp::max(self.max_size, MIN_LOG_SIZE);
153        let path = Path::new(&self.dir_path).join(file_name);
154
155        if let Some(p) = &path.parent() {
156            if !p.exists() {
157                fs::create_dir_all(p)?;
158            }
159        }
160        let file = OpenOptions::new()
161            .read(true)
162            .write(true)
163            .create(true)
164            .open(&path)?;
165        let mut len = file.metadata()?.len();
166        len = if len != max_size && len != 0 {
167            len
168        } else {
169            max_size
170        };
171        file.set_len(len)?;
172        Ok((file, path))
173    }
174
175    pub(crate) fn is_file_out_of_date(&self, file_name: &str) -> crate::Result<bool> {
176        if file_name == format!("{}.{}", &self.name, &self.file_suffix) {
177            // ignore logging file
178            return Ok(false);
179        }
180        let log_date = self.read_file_name_as_date(file_name)?;
181        let now = OffsetDateTime::now_utc();
182        Ok(self.is_out_of_date(log_date, now))
183    }
184
185    pub(crate) fn read_file_name_as_date(&self, file_name: &str) -> crate::Result<OffsetDateTime> {
186        const SAMPLE: &str = "2022_02_22";
187        if file_name == format!("{}.{}", &self.name, &self.file_suffix) {
188            return Err(LogError::Illegal("The file is logging file".to_string()));
189        }
190        if !file_name.starts_with(format!("{}_", &self.name).as_str()) {
191            return Err(LogError::Illegal(format!(
192                "file name is not start with name {}",
193                file_name
194            )));
195        }
196        if file_name.len() < self.name.len() + 1 + SAMPLE.len() {
197            return Err(LogError::Illegal(format!(
198                "file name length is not right {}",
199                file_name
200            )));
201        }
202        let date_str = &file_name[self.name.len() + 1..self.name.len() + 1 + SAMPLE.len()];
203        let log_date = parse_date_from_str(
204            date_str,
205            "this is a bug in EZLogConfig#read_file_name_as_date:",
206        )?;
207        Ok(log_date.midnight().assume_utc())
208    }
209
210    fn is_out_of_date(&self, target: OffsetDateTime, now: OffsetDateTime) -> bool {
211        target + self.trim_duration < now
212    }
213
214    pub(crate) fn is_file_same_date(&self, file_name: &str, date: OffsetDateTime) -> bool {
215        if file_name == format!("{}.{}", &self.name, &self.file_suffix) {
216            // ignore logging file
217            return false;
218        }
219
220        self.read_file_name_as_date(file_name)
221            .map(|log_date| log_date.date() == date.date())
222            .unwrap_or(false)
223    }
224
225    pub(crate) fn writable_size(&self) -> u64 {
226        self.max_size - Header::length_compat(&self.version) as u64
227    }
228
229    pub fn query_log_files_for_date(&self, date: OffsetDateTime) -> Vec<PathBuf> {
230        let mut logs = Vec::new();
231        match fs::read_dir(&self.dir_path) {
232            Ok(dir) => {
233                for file in dir {
234                    match file {
235                        Ok(file) => {
236                            if let Some(name) = file.file_name().to_str() {
237                                if self.is_file_same_date(name, date) {
238                                    logs.push(file.path());
239                                }
240                            };
241                        }
242                        Err(e) => {
243                            event!(Event::RequestLogError, "get dir entry in dir", &e.into());
244                        }
245                    }
246                }
247            }
248            Err(e) => event!(Event::RequestLogError, "read dir", &e.into()),
249        }
250        logs
251    }
252
253    pub(crate) fn rotate_time(&self, time: OffsetDateTime) -> OffsetDateTime {
254        time + self.rotate_duration
255    }
256
257    pub(crate) fn cipher_hash(&self) -> u32 {
258        let mut hasher = DefaultHasher::new();
259        self.cipher.hash(&mut hasher);
260        self.cipher_key.hash(&mut hasher);
261        hasher.finish() as u32
262    }
263
264    pub fn check_valid(&self) -> crate::Result<()> {
265        if self.dir_path.is_empty() {
266            return Err(LogError::Illegal("dir_path is empty".to_string()));
267        }
268        if self.name.is_empty() {
269            return Err(LogError::Illegal("name is empty".to_string()));
270        }
271        Ok(())
272    }
273}
274
275impl Default for EZLogConfig {
276    fn default() -> Self {
277        EZLogConfigBuilder::new().build()
278    }
279}
280
281impl Hash for EZLogConfig {
282    fn hash<H: Hasher>(&self, state: &mut H) {
283        self.version.hash(state);
284        self.dir_path.hash(state);
285        self.name.hash(state);
286        self.compress.hash(state);
287        self.cipher.hash(state);
288        self.cipher_key.hash(state);
289        self.cipher_nonce.hash(state);
290        self.extra.hash(state)
291    }
292}
293
294/// The builder of [EZLogConfig]
295#[derive(Debug, Clone)]
296pub struct EZLogConfigBuilder {
297    config: EZLogConfig,
298}
299
300impl EZLogConfigBuilder {
301    pub fn new() -> Self {
302        EZLogConfigBuilder {
303            config: EZLogConfig {
304                level: Level::Trace,
305                version: Version::V2,
306                dir_path: "".to_string(),
307                name: DEFAULT_LOG_NAME.to_string(),
308                file_suffix: DEFAULT_LOG_FILE_SUFFIX.to_string(),
309                trim_duration: Duration::days(7),
310                max_size: DEFAULT_MAX_LOG_SIZE,
311                compress: CompressKind::NONE,
312                compress_level: CompressLevel::Default,
313                cipher: CipherKind::NONE,
314                cipher_key: None,
315                cipher_nonce: None,
316                rotate_duration: Duration::days(1),
317                extra: None,
318            },
319        }
320    }
321
322    #[inline]
323    pub fn version(mut self, version: Version) -> Self {
324        self.config.version = version;
325        self
326    }
327
328    #[inline]
329    pub fn level(mut self, level: Level) -> Self {
330        self.config.level = level;
331        self
332    }
333
334    #[inline]
335    pub fn dir_path(mut self, dir_path: String) -> Self {
336        self.config.dir_path = dir_path;
337        self
338    }
339
340    #[inline]
341    pub fn name(mut self, name: String) -> Self {
342        self.config.name = name;
343        self
344    }
345
346    #[inline]
347    pub fn file_suffix(mut self, file_suffix: String) -> Self {
348        self.config.file_suffix = file_suffix;
349        self
350    }
351
352    #[inline]
353    pub fn trim_duration(mut self, duration: Duration) -> Self {
354        self.config.trim_duration = duration;
355        self
356    }
357
358    #[inline]
359    pub fn max_size(mut self, max_size: u64) -> Self {
360        self.config.max_size = max_size;
361        self
362    }
363
364    #[inline]
365    pub fn compress(mut self, compress: CompressKind) -> Self {
366        self.config.compress = compress;
367        self
368    }
369
370    #[inline]
371    pub fn compress_level(mut self, compress_level: CompressLevel) -> Self {
372        self.config.compress_level = compress_level;
373        self
374    }
375
376    #[inline]
377    pub fn cipher(mut self, cipher: CipherKind) -> Self {
378        self.config.cipher = cipher;
379        self
380    }
381
382    #[inline]
383    pub fn cipher_key(mut self, cipher_key: Vec<u8>) -> Self {
384        self.config.cipher_key = Some(cipher_key);
385        self
386    }
387
388    #[inline]
389    pub fn cipher_nonce(mut self, cipher_nonce: Vec<u8>) -> Self {
390        self.config.cipher_nonce = Some(cipher_nonce);
391        self
392    }
393
394    #[inline]
395    pub fn from_header(mut self, header: &Header) -> Self {
396        self.config.version = header.version;
397        self.config.compress = header.compress;
398        self.config.cipher = header.cipher;
399        self
400    }
401
402    #[inline]
403    pub fn rotate_duration(mut self, duration: Duration) -> Self {
404        self.config.rotate_duration = duration;
405        self
406    }
407
408    #[inline]
409    pub fn extra(mut self, extra: String) -> Self {
410        self.config.extra = Some(extra);
411        self
412    }
413
414    #[inline]
415    pub fn build(self) -> EZLogConfig {
416        self.config
417    }
418}
419
420impl Default for EZLogConfigBuilder {
421    fn default() -> Self {
422        Self::new()
423    }
424}
425
426pub(crate) fn parse_date_from_str(date_str: &str, case: &str) -> crate::Result<Date> {
427    let format = format_description::parse(DATE_FORMAT)
428        .map_err(|_e| crate::errors::LogError::Parse(format!("{} {} {}", case, date_str, _e)))?;
429    let date = Date::parse(date_str, &format)
430        .map_err(|_e| crate::errors::LogError::Parse(format!("{} {} {}", case, date_str, _e)))?;
431    Ok(date)
432}
433
434/// Log level, used to filter log records
435#[repr(usize)]
436#[derive(Copy, Eq, Debug)]
437pub enum Level {
438    /// The "error" level.
439    ///
440    /// Designates very serious errors.
441    // This way these line up with the discriminants for LevelFilter below
442    // This works because Rust treats field-less enums the same way as C does:
443    // https://doc.rust-lang.org/reference/items/enumerations.html#custom-discriminant-values-for-field-less-enumerations
444    Error = 1,
445    /// The "warn" level.
446    ///
447    /// Designates hazardous situations.
448    Warn,
449    /// The "info" level.
450    ///
451    /// Designates useful information.
452    Info,
453    /// The "debug" level.
454    ///
455    /// Designates lower priority information.
456    Debug,
457    /// The "trace" level.
458    ///
459    /// Designates very low priority, often extremely verbose, information.
460    Trace,
461}
462
463impl Level {
464    pub fn from_usize(u: usize) -> Option<Level> {
465        match u {
466            1 => Some(Level::Error),
467            2 => Some(Level::Warn),
468            3 => Some(Level::Info),
469            4 => Some(Level::Debug),
470            5 => Some(Level::Trace),
471            _ => None,
472        }
473    }
474
475    /// Returns the most verbose logging level.
476    #[inline]
477    pub fn max() -> Level {
478        Level::Trace
479    }
480
481    /// Returns the string representation of the `Level`.
482    ///
483    /// This returns the same string as the `fmt::Display` implementation.
484    pub fn as_str(&self) -> &'static str {
485        LOG_LEVEL_NAMES[*self as usize]
486    }
487
488    /// Iterate through all supported logging levels.
489    ///
490    /// The order of iteration is from more severe to less severe log messages.
491    ///
492    /// # Examples
493    ///
494    /// ```
495    /// use log::Level;
496    ///
497    /// let mut levels = Level::iter();
498    ///
499    /// assert_eq!(Some(Level::Error), levels.next());
500    /// assert_eq!(Some(Level::Trace), levels.last());
501    /// ```
502    #[cfg(feature = "log")]
503    pub fn iter() -> impl Iterator<Item = Self> {
504        (1..6).map(|i| Self::from_usize(i).unwrap_or(Level::Error))
505    }
506}
507
508impl Clone for Level {
509    #[inline]
510    fn clone(&self) -> Level {
511        *self
512    }
513}
514
515impl PartialEq for Level {
516    #[inline]
517    fn eq(&self, other: &Level) -> bool {
518        *self as usize == *other as usize
519    }
520}
521
522impl PartialOrd for Level {
523    #[inline]
524    fn partial_cmp(&self, other: &Level) -> Option<cmp::Ordering> {
525        Some(self.cmp(other))
526    }
527
528    #[inline]
529    fn lt(&self, other: &Level) -> bool {
530        (*self as usize) < *other as usize
531    }
532
533    #[inline]
534    fn le(&self, other: &Level) -> bool {
535        *self as usize <= *other as usize
536    }
537
538    #[inline]
539    fn gt(&self, other: &Level) -> bool {
540        *self as usize > *other as usize
541    }
542
543    #[inline]
544    fn ge(&self, other: &Level) -> bool {
545        *self as usize >= *other as usize
546    }
547}
548
549impl Ord for Level {
550    #[inline]
551    fn cmp(&self, other: &Level) -> cmp::Ordering {
552        (*self as usize).cmp(&(*other as usize))
553    }
554}
555
556#[cfg(feature = "log")]
557impl From<log::Level> for Level {
558    fn from(log_level: log::Level) -> Self {
559        match log_level {
560            log::Level::Error => Level::Error,
561            log::Level::Warn => Level::Warn,
562            log::Level::Info => Level::Info,
563            log::Level::Debug => Level::Debug,
564            log::Level::Trace => Level::Trace,
565        }
566    }
567}
568
569impl fmt::Display for Level {
570    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
571        fmt.pad(self.as_str())
572    }
573}
574
575#[cfg(test)]
576mod tests {
577
578    use std::fs::{
579        self,
580        OpenOptions,
581    };
582
583    use time::{
584        macros::datetime,
585        Duration,
586        OffsetDateTime,
587    };
588
589    use crate::{
590        appender::EZAppender,
591        CipherKind,
592        CompressKind,
593        EZLogConfigBuilder,
594    };
595
596    #[test]
597    fn test_config_cipher_hash() {
598        let config_builder = EZLogConfigBuilder::default();
599
600        let default1 = config_builder.clone().build();
601        let default2 = config_builder.clone().build();
602        assert_eq!(default1.cipher_hash(), default2.cipher_hash());
603
604        let cipher1 = config_builder
605            .clone()
606            .cipher(CipherKind::AES128GCMSIV)
607            .cipher_key(vec![])
608            .build();
609        let cipher2 = config_builder
610            .clone()
611            .cipher(CipherKind::AES128GCMSIV)
612            .cipher_key(vec![])
613            .build();
614        assert_eq!(cipher1.cipher_hash(), cipher2.cipher_hash());
615
616        let cipher3 = config_builder
617            .clone()
618            .cipher(CipherKind::AES256GCMSIV)
619            .cipher_key(vec![])
620            .build();
621        assert_ne!(cipher1.cipher_hash(), cipher3.cipher_hash());
622
623        let cipher4 = config_builder
624            .clone()
625            .cipher(CipherKind::AES128GCMSIV)
626            .cipher_key(vec![1, 2, 3])
627            .build();
628        assert_ne!(cipher1.cipher_hash(), cipher4.cipher_hash());
629    }
630
631    #[test]
632    fn test_is_out_of_date() {
633        let config = EZLogConfigBuilder::default()
634            .trim_duration(Duration::days(1))
635            .build();
636
637        assert!(!config.is_out_of_date(OffsetDateTime::now_utc(), OffsetDateTime::now_utc()));
638        assert!(config.is_out_of_date(
639            datetime!(2022-06-13 0:00 UTC),
640            datetime!(2022-06-14 0:01 UTC)
641        ));
642        assert!(!config.is_out_of_date(
643            datetime!(2022-06-13 0:00 UTC),
644            datetime!(2022-06-14 0:00 UTC)
645        ))
646    }
647
648    #[test]
649    fn test_read_file_name_as_date() {
650        let config = EZLogConfigBuilder::default()
651            .name("test".to_string())
652            .build();
653
654        assert!(config.read_file_name_as_date("test2019_06_13.log").is_err());
655        assert!(config.read_file_name_as_date("test_201_06_13.log").is_err());
656        assert!(config
657            .read_file_name_as_date("test_2019_06_1X.log")
658            .is_err());
659        assert!(config.read_file_name_as_date("test_2019_06_13.log").is_ok());
660        assert!(config
661            .read_file_name_as_date("test_2019_06_13.1.log")
662            .is_ok());
663        assert!(config
664            .read_file_name_as_date("test_2019_06_13.123.mmap")
665            .is_ok());
666    }
667
668    #[test]
669    fn test_query_log_files() {
670        let temp = dirs::cache_dir().unwrap().join("ezlog_test_config");
671        if temp.exists() {
672            fs::remove_dir_all(&temp).unwrap();
673        }
674
675        let key = b"an example very very secret key.";
676        let nonce = b"unique nonce";
677        let config = EZLogConfigBuilder::new()
678            .dir_path(temp.clone().into_os_string().into_string().unwrap())
679            .name(String::from("all_feature"))
680            .file_suffix(String::from("mmap"))
681            .compress(CompressKind::ZLIB)
682            .cipher(CipherKind::AES128GCMSIV)
683            .cipher_key(key.to_vec())
684            .cipher_nonce(nonce.to_vec())
685            .max_size(1024)
686            .build();
687
688        let mut appender = EZAppender::create_inner(&config).unwrap();
689        let f = OpenOptions::new()
690            .write(true)
691            .create(true)
692            .open(appender.file_path())
693            .unwrap();
694        appender.write(&[0u8; 512]).unwrap();
695        drop(appender);
696
697        f.set_len((crate::Header::max_length() + 1) as u64).unwrap();
698
699        let mut appender = EZAppender::new(std::rc::Rc::new(config.clone())).unwrap();
700        appender.check_config_rolling(&config).unwrap();
701        drop(appender);
702
703        let files = config.query_log_files_for_date(OffsetDateTime::now_utc());
704
705        assert_eq!(files.len(), 1);
706        if temp.exists() {
707            fs::remove_dir_all(&temp).unwrap();
708        }
709    }
710}