From f70fcf7c4389ee9eb81f62e8b763e095ed50aea9 Mon Sep 17 00:00:00 2001
From: licunlong <licunlong1@huawei.com>
Date: Tue, 27 Jun 2023 20:15:04 +0800
Subject: [PATCH] feature: use our own file logger

This implements a simple file logger which supports log rorating. we set
the log file mode to 600 forcely
---
 libs/basic/Cargo.toml    |   1 +
 libs/basic/src/logger.rs | 203 ++++++++++++++++++++++++++++++++++++++-
 2 files changed, 202 insertions(+), 2 deletions(-)

diff --git a/libs/basic/Cargo.toml b/libs/basic/Cargo.toml
index 7557d37..6ee7162 100644
--- a/libs/basic/Cargo.toml
+++ b/libs/basic/Cargo.toml
@@ -21,6 +21,7 @@ lazy_static = "1.4.0"
 bitflags = "1.3.2"
 pkg-config = "0.3"
 rand = "0.4.6"
+time = {version = "=0.3.10", features = ["formatting", "macros"] }
 
 [dev-dependencies]
 libtests = { path = "../libtests" }
diff --git a/libs/basic/src/logger.rs b/libs/basic/src/logger.rs
index 56def43..3bf579b 100644
--- a/libs/basic/src/logger.rs
+++ b/libs/basic/src/logger.rs
@@ -11,6 +11,14 @@
 // See the Mulan PSL v2 for more details.
 
 //!
+use std::{
+    fs::{File, OpenOptions},
+    io::Write,
+    os::unix::prelude::OpenOptionsExt,
+    path::{Path, PathBuf},
+    sync::Mutex,
+};
+
 use log::{LevelFilter, Log};
 use log4rs::{
     append::{
@@ -27,6 +35,7 @@ use log4rs::{
     encode::pattern::PatternEncoder,
 };
 use nix::libc;
+use time::UtcOffset;
 
 /// sysmaster log parttern:
 ///
@@ -83,6 +92,174 @@ impl log::Log for SysLogger {
     fn flush(&self) {}
 }
 
+struct FileLogger {
+    level: log::Level,
+    file_path: PathBuf,
+    #[allow(dead_code)]
+    file_mode: u32,
+    file_number: u32,
+    max_size: u32,
+    file: Mutex<File>,
+}
+
+impl log::Log for FileLogger {
+    fn enabled(&self, metadata: &log::Metadata) -> bool {
+        metadata.level() <= self.level
+    }
+
+    fn log(&self, record: &log::Record) {
+        if !self.enabled(record.metadata()) {
+            return;
+        }
+        let current_size: u32;
+        {
+            let mut file = self.file.lock().unwrap();
+            self.write(
+                &mut file,
+                record.module_path().unwrap(),
+                record.args().to_string(),
+            );
+            current_size = file.metadata().unwrap().len() as u32;
+            /* file is automatically unlocked. */
+        }
+        if current_size > self.max_size {
+            self.rotate();
+            let file = self.file.lock().unwrap();
+            let _ = file.set_len(0);
+        }
+    }
+
+    fn flush(&self) {
+        let mut file = self.file.lock().unwrap();
+        let _ = file.flush();
+    }
+}
+
+impl FileLogger {
+    fn file_open(file_path: &PathBuf, file_mode: u32) -> File {
+        OpenOptions::new()
+            .write(true)
+            .create(true)
+            .append(true)
+            .mode(file_mode)
+            .open(file_path)
+            .unwrap()
+    }
+
+    fn mv_file_in_dir(src: &str, dst: Option<&str>, dir: &Path) {
+        let src = dir.join(src);
+        if dst.is_none() {
+            let _ = std::fs::remove_file(src);
+            return;
+        }
+        let dst = dir.join(dst.unwrap());
+        let _ = std::fs::rename(src, dst);
+    }
+
+    fn cp_file_in_dir(src: &str, dst: &str, dir: &Path) {
+        let src = dir.join(src);
+        let dst = dir.join(dst);
+        let _ = std::fs::copy(src, dst);
+    }
+
+    fn new(
+        level: log::Level,
+        file_path: PathBuf,
+        file_mode: u32,
+        max_size: u32,
+        file_number: u32,
+    ) -> Self {
+        let file = Self::file_open(&file_path, file_mode);
+        Self {
+            level,
+            file_path,
+            file_mode,
+            file_number,
+            max_size: max_size * 1024,
+            file: Mutex::new(file),
+        }
+    }
+
+    fn write(&self, file: &mut File, module: &str, msg: String) {
+        let now = time::OffsetDateTime::now_utc().to_offset(UtcOffset::UTC);
+        let format =
+            time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
+        let now = now.format(&format).unwrap();
+
+        /* 1. Write time */
+        if let Err(e) = file.write(format!("{now} ").as_bytes()) {
+            println!("Failed to log time message: {e}");
+            return;
+        }
+
+        /* 2. Write module */
+        if let Err(e) = file.write((module.to_string() + " ").as_bytes()) {
+            println!("Failed to log module message: {e}");
+            return;
+        }
+
+        /* 3. Write message */
+        if let Err(e) = file.write((msg + "\n").as_bytes()) {
+            println!("Failed to log message: {e}");
+            return;
+        }
+    }
+
+    fn rotate(&self) {
+        let dir = self.file_path.parent().unwrap();
+        let file_name = self
+            .file_path
+            .file_name()
+            .unwrap()
+            .to_string_lossy()
+            .to_string();
+        let file_name_dot = String::from(&file_name) + ".";
+        let mut num_list: Vec<usize> = Vec::new();
+
+        for de in dir.read_dir().unwrap() {
+            let de = match de {
+                Err(_) => continue,
+                Ok(v) => v,
+            };
+            if !de.file_type().unwrap().is_file() {
+                continue;
+            }
+            let de_file_name = de.file_name().to_string_lossy().to_string();
+            let rotated_num = de_file_name.trim_start_matches(&file_name_dot);
+            let rotated_num = match rotated_num.parse::<usize>() {
+                Err(_) => {
+                    continue;
+                }
+                Ok(v) => v,
+            };
+            num_list.push(rotated_num);
+        }
+
+        num_list.sort();
+
+        /* 1. delete surplus rotated file */
+        /* We only keep (file_number - 1) rotated files, because we will generate a new one later. */
+        let file_number = self.file_number as usize;
+        for i in file_number - 1..num_list.len() {
+            let src = String::from(&file_name_dot) + &num_list[i].to_string();
+            Self::mv_file_in_dir(&src, None, dir);
+        }
+
+        let end = std::cmp::min(num_list.len(), file_number);
+        /* 2. {sysmaster.log.1, sysmaster.log.2, ...} => {sysmaster.log.2, sysmaster.log.3, ...} */
+        for i in (0..end).rev() {
+            let src = String::from(&file_name_dot) + &num_list[i].to_string();
+            let dst = String::from(&file_name_dot) + &(num_list[i] + 1).to_string();
+            Self::mv_file_in_dir(&src, Some(&dst), dir);
+        }
+
+        /* 3. **copy** sysmaster.log => sysmaster.log.1 */
+        let src = String::from(&file_name);
+        let dst = String::from(&file_name_dot) + "1";
+        Self::cp_file_in_dir(&src, &dst, dir);
+    }
+}
+
 fn append_log(
     app_name: &str,
     level: LevelFilter,
@@ -96,6 +273,17 @@ fn append_log(
         log::set_max_level(level);
         return;
     }
+    if target == "file" {
+        log::set_max_level(level);
+        let _ = log::set_boxed_logger(Box::new(FileLogger::new(
+            log::Level::Debug,
+            PathBuf::from(file_path),
+            0o600,
+            file_size,
+            file_number,
+        )));
+        return;
+    }
     let config = build_log_config(app_name, level, target, file_path, file_size, file_number);
     let logger = log4rs::Logger::new(config);
     log::set_max_level(level);
@@ -173,6 +361,17 @@ pub fn init_log(
         log::set_max_level(level);
         return;
     }
+    if target == "file" {
+        let _ = log::set_boxed_logger(Box::new(FileLogger::new(
+            log::Level::Debug,
+            PathBuf::from(&file_path),
+            0o600,
+            file_size,
+            file_number,
+        )));
+        log::set_max_level(level);
+        return;
+    }
     let config = build_log_config(app_name, level, target, file_path, file_size, file_number);
     let r = log4rs::init_config(config);
     if let Err(e) = r {
@@ -190,7 +389,7 @@ fn build_log_config(
 ) -> Config {
     let mut target = target;
     /* If the file is configured to None, use console forcely. */
-    if (file_path.is_empty() || file_size == 0 || file_number == 0) && target == "file" {
+    if (file_path.is_empty() || file_size == 0 || file_number == 0) && target == "rolling_file" {
         println!(
             "LogTarget is configured to `file`, but configuration is invalid, changing the \
              LogTarget to `console`, file: {file_path}, file_size: {file_size}, file_number: \
@@ -206,7 +405,7 @@ fn build_log_config(
                 .target(Target::Stdout)
                 .build(),
         ),
-        "file" => {
+        "rolling_file" => {
             let pattern = file_path.to_string() + ".{}";
             let policy = Box::new(CompoundPolicy::new(
                 Box::new(SizeTrigger::new(file_size as u64 * 1024)),
-- 
2.33.0

