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#[derive(Debug, Clone)]
53pub struct EZLogConfig {
54 pub level: Level,
58 pub version: Version,
62 pub dir_path: String,
66 pub name: String,
70 pub file_suffix: String,
74 pub trim_duration: Duration,
78 pub max_size: u64,
82 pub compress: CompressKind,
86 pub compress_level: CompressLevel,
90 pub cipher: CipherKind,
94 pub cipher_key: Option<Vec<u8>>,
98 pub cipher_nonce: Option<Vec<u8>>,
102 pub rotate_duration: Duration,
106
107 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 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 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#[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#[repr(usize)]
436#[derive(Copy, Eq, Debug)]
437pub enum Level {
438 Error = 1,
445 Warn,
449 Info,
453 Debug,
457 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 #[inline]
477 pub fn max() -> Level {
478 Level::Trace
479 }
480
481 pub fn as_str(&self) -> &'static str {
485 LOG_LEVEL_NAMES[*self as usize]
486 }
487
488 #[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}