概述
所有的商業軟件或線上系統都具有日志功能,因為日志信息提供了系統啟動以來的重要的操作或狀態遷移記錄,是追蹤各種異常錯誤的第一手資料。絕大部分系統的日志模塊會自動保留歷史日志文件,即:日志文件大小達到約定上限時,自動轉儲到一個新的歷史文件,當前文件清空並繼續記錄新的日志信息,例如:假設當前日志文件名為test.log, 當它的大小到達上限(例如10MB)時,就把其文件內容轉儲到新文件test.log.1, 然后test.log清空並繼續記錄新信息。根據配置不同,我們可以保留1到N份歷史日志文件。當歷史日志文件數目達到上限時,我們可以采用round-robin策略(或其他策略)依據文件生成時間依次覆蓋老的文件。本文嘗試用C++實現上述日志文件功能,其功能歸納總結如下:
1) 日志文件提供接口讓用戶配置日志文件名、日志大小上限、歷史日志文件數目上限;
2) 日志文件提供Append()接口,讓用戶向文件追加日志消息;
3) 日志文件在執行Append()接口過程中,自動檢測當前日志文件大小:如果追加當前消息后,文件大小超過約定上限,則記錄當前消息前,將已有消息轉儲到歷史文件並保證歷史日志文件數不超過約定上限;否者,直接記錄當前消息;
4) 日志文件提供接口讓用戶配置是否對歷史日志文件進行壓縮;
實現
LogFile類實現了上述功能,先看一下其接口定義:
1 #ifndef _LOGFILE_H 2 #define _LOGFILE_H 3 4 #include <fstream> 5 #include <iostream> 6 7 8 class LogFile { 9 public: 10 LogFile(const LogFile &) = delete; 11 LogFile& operator=(const LogFile &) = delete; 12 LogFile(const std::string&, double, unsigned int, bool); 13 14 ~LogFile(); 15 void Append(std::string &&msg); 16 17 private: 18 void Rotate(); 19 double GetFileSize(); 20 std::string NextHistoryFile(); 21 22 private: 23 std::ofstream ofs_; 24 std::string file_name_; 25 double cur_size_; 26 double max_size_; 27 unsigned int max_file_num_;
28 bool compress_; 28 }; 29 30 #endif //_LOGFILE_H
LogFile成員變量說明
ofs_: c++ std::ofstream類型對象,通過其操作符"<<"把消息寫入日志文件
file_name_: 日志文件名;
cur_size_: 實時記錄當前日志文件大小,避免每次執行Append()操作時調用系統函數獲取文件大小;
max_size_: 用戶指定的日志文件上限;
max_file_num_: 用戶指定的最大歷史文件數;
LogFile成員函數說明
LogFile(const std::string&, double, unsigned int): LogFile類的唯一構造函數,可以指定日志文件名,文件大小上限,歷史文件數量上限,其實現也非常簡單:
LogFile::LogFile(const std::string& file_name, double max_size, unsigned int max_file_num,
bool compress) : file_name_(file_name), max_size_(max_size), max_file_num_(max_file_num),
compress_(compress) { assert(max_file_num_ > 0); cur_size_ = GetFileSize(); ofs_.open(file_name_, std::ofstream::out|std::ofstream::app); }
~LogFile(): 析構函數,主要功能是關閉在構造函數中打開的ofstream對象.
LogFile::~LogFile() { if (ofs_.is_open()) { ofs_.close(); } }
void Append(std::string &&msg):LogFile類最重要的接口,讓用戶向日志文件追加新的消息。其實現邏輯為:在寫文件前,先檢查當前日志文件大小加上當前消息的長度之和是否會超過創建LogFile對象時指定的日志文件大小上限:如果超過,就將當前文件內容轉存到歷史文件,並清空當前日志文件(通過Rotate函數),然后,繼續把當前消息寫入日志文件。 有一點值得說明:獲取當前日志文件大小不是通過調用系統函數,而是通過類成員變量cur_size_(構造函數調用一次系統函數為cur_size_賦初值,之后,每次執行Append(), cur_size_都累加消息長度,從而實時追蹤日志文件長度), 這樣避免了每次執行Append()時都調用系統函數檢查文件長度,從而提高了效率。
void LogFile::Append(std::string&& msg) { double msg_size = (double)msg.size(); if (cur_size_+ msg_size >= max_size_) { Rotate(); } ofs_ << std::forward<std::string>(msg) << std::endl; cur_size_ += msg_size; }
void Rotate(): 轉儲函數,當前日志文件內容被轉存到某個歷史日志文件,當前日志文件被清空並被重新打開
// // Compress file "old_file" and save data into file "new_file", the // compression is based on zlib. // void LogFile::compress_file(const char *old_file, const char *new_file) { gzFile gf = gzopen(new_file, "wb"); if (gf == NULL) { std::cout << "gzopen() failed" << std::endl; return; } const int BUF_LEN = 500; char buf[BUF_LEN]; size_t reads = 0; FILE *fd = fopen(old_file, "rb"); if (fd == NULL) { std::cout << "fopen(" << old_file << ") failed" << std::endl; } while((reads = fread(buf, 1, BUF_LEN, fd)) > 0) { if (ferror(fd)) { std::cout << "fread() failed!" << std::endl; break; } gzwrite(gf, buf, reads); } fclose(fd); gzclose(gf); } // // Save existing log messages into history file and empty log file. // // Note: call this function only when log file reaches maximum size. // void LogFile::Rotate() { if (ofs_.is_open()) { ofs_.close(); } std::string history_file = NextHistoryFile(); if (compress_) { compress_file(file_name_.c_str(), history_file.c_str()); } else { std::rename(file_name_.c_str(), history_file.c_str()); } ofs_.open(file_name_, std::ofstream::out|std::ofstream::trunc); cur_size_ = 0; }
std::string NextHistoryFile(): 返回下一個歷史日志文件名以用於轉存日志文件信息
std::string LogFile::NextHistoryFile() { static int next_file_no = 0; int file_num = (next_file_no++) % max_file_num_; return file_name_ + "." + std::to_string(file_num); }
double LogFile::GetFileSize(): 返回當前日志文件大小(bytes),當前實現只針對Linux
double LogFile::GetFileSize() { struct stat statbuf; if (stat(file_name_.c_str(), &statbuf) == 0) { return (double)statbuf.st_size; } else { perror("Faild to get log file size"); return 0; } }
測試
針對上述LogFile的實現,我們可以進行如下測試:
#include "logfile.h" int main(void) { LogFile lf("testlogfile.log", 1024, 8, false); for (auto i = 0; i < 200; i++) { lf.Append(std::to_string(i) + ": this is a very very very very very very very very very very very very very very vvery very very very very ery long log"); } }
測試結果如下:
stephenw@stephenw-devbox1:/local/project/logcpp$ ls -lart testlogfile.log* -rw-rw-r-- 1 stephenw stephenw 992 6月 27 23:02 testlogfile.log.4 -rw-rw-r-- 1 stephenw stephenw 992 6月 27 23:02 testlogfile.log.3 -rw-rw-r-- 1 stephenw stephenw 992 6月 27 23:02 testlogfile.log.2 -rw-rw-r-- 1 stephenw stephenw 992 6月 27 23:02 testlogfile.log.1 -rw-rw-r-- 1 stephenw stephenw 992 6月 27 23:02 testlogfile.log.7 -rw-rw-r-- 1 stephenw stephenw 992 6月 27 23:02 testlogfile.log.6 -rw-rw-r-- 1 stephenw stephenw 992 6月 27 23:02 testlogfile.log.5 -rw-rw-r-- 1 stephenw stephenw 992 6月 27 23:02 testlogfile.log.0 -rw-rw-r-- 1 stephenw stephenw 124 6月 27 23:02 testlogfile.log
stephenw@stephenw-devbox1:/local/project/logcpp$ tail -n5 testlogfile.log.0 testlogfile.log.1 testlogfile.log.2
==> testlogfile.log.0 <==
194: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
195: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
196: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
197: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
198: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
==> testlogfile.log.1 <==
138: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
139: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
140: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
141: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
142: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
==> testlogfile.log.2 <==
146: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
147: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
148: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
149: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
150: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
總結
本文通過一個簡單得c++類實現了日志文件主要功能,但仍然有許多提升空間,例如各種異常的處理(文件無法打開,寫文件失敗等),多線程以及跨平台的支持等,但仍不失為一個好的起點,稍加改造即可應用於生產環境。