目錄
日志與策略模式
Log.hpp
?class LogStrategy基類
class ConsoleLogStrategy派生類
?classFileLogStrategy派生類
日志等級
獲得時間戳
localtime_r函數詳解
函數原型
struct tm結構的指針
Logger類(重點)
class LogMessage 日志信息類
std::stringstream
用法
重載?流輸出運算符和析構函數
日志與策略模式
什么是設計模式?
IT行業這么火,涌入的人很多.俗話說林子大了啥鳥都有,大佬和菜雞們兩極分化的越來越嚴重.為了讓 菜雞們不太拖大佬的后腿,于是大佬們針對?些經典的常見的場景,給定了?些對應的解決方案,這個就是設計模式
日志認識
計算機中的日志是記錄系統和軟件運行中發生事件的文件,主要作用是監控運行狀態、記錄異常信 息,幫助快速定位問題并支持程序員進行問題修復。它是系統維護、故障排查和安全管理的重要工?具。
日志格式以下幾個指標是必須得有的
- 時間戳
- 日志等級
- 日志內容
- 文件名(可選)
- 行號(可選)
- 進程/線程id(可選)
日志有現成的解決方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我們依舊采用自定義日志的方式。 這里我們采用設計模式-策略模式來進行日志的設計
我們想要的日志格式如下:
[ 可讀性很好的時間 ] [ 日志等級 ] [ 進程 pid] [ 打印對應日志的文件名 ][ 行號 ] - 消息內容,支持可 變參數
Log.hpp
首先創建Log.hpp文件,命名一個LogModule的空間域,我們將來的代碼就在這里面實現
#ifndef __LOG_HPP__
#define __LOG_HPP__#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp" //互斥鎖(自己實現的)namespace LogMudoule
{}#endif
?class LogStrategy基類
策略模式就是利用多態的特性,我們先定義出一個LogStrategy的基類,后面通過我們傳的不同派生類來實現不同的模式,比如向屏幕打印的ConsoleLogStrategy派生類和向文件寫入的FileLogStrategy派生類
using namespace MutexModule;const std::string gsep = "\r\n";// 策略模式,C++多態特性// 2. 刷新策略 a: 顯示器打印 b:向指定的文件寫入// 刷新策略基類class LogStrategy{public:virtual ~LogStrategy(){}virtual void SyncLog(const std::string &message)=0; //控制臺虛函數};
gsep是換行符,因為要使用鎖來保護臨界資源,這里Sync是同步的意思,基類的析構函數要記得加上virture。SyncLog是純虛函數(強制派生類重寫虛函數,因為不重寫實例化不出對象。)
class ConsoleLogStrategy派生類
下面的ConsoleLogStrategy派生類實現的也十分簡單
// 顯示器打印日志的策略 子類class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::cout << message << gsep;//向屏幕打印消息}~ConsoleLogStrategy(){}private:Mutex _mutex;};
LockGuard 是我們封裝的互斥鎖的類,通過lockguard對象的創建來保護臨界資源,析構后釋放鎖
不懂互斥鎖的話可以去看看博客,我們的重點是完成日志,后續不再講解鎖
override是檢查派生類虛函數是否重寫了基類的某個虛函數。如果對于多態的知識不怎么了解的話,我十分推薦您去看這篇多態博客,點擊多態就能進去閱讀
?classFileLogStrategy派生類
下面的FileLogStrategy派生類稍微復雜一點但是并不難理解
// 文件打印日志的策略 : 子類const std::string defaultpath = "./log";const std::string defaultfile = "my.log";class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path),_file(file){LockGuard lockguard(_mutex);if (std::filesystem::exists(_path)){return;}try{std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);const std::string &filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"std::ofstream out(filename, std::ios::app);if (!out.is_open()){return;}out << message << gsep;out.close();}~FileLogStrategy(){}private:const std::string _path;const std::string _file;Mutex _mutex;};
理解一個類首先要去看它的私有成員,日志要向文件進行打印,首先要知道它的路徑,其次就是它的文件名。所以能理解_path和_file兩個成員變量
C++17 引入了一個重要的新特性:?文件系統庫? (std::filesystem
),它提供了處理文件系統和路徑的標準方法。這個庫基于 Boost.Filesystem,并經過了標準化處理。在構造函數中完成_path和_file的初始化,然后利用std::filesystem::exists來檢查路徑是否存在,存在就返回,不存在就創建一個路徑(在當前路徑下的Log目錄下)
SyncLog函數中filename就是我們要創建的文件名,它由_path和_file組合,
中間的三目操作符意思是如果 _path 的最后一個字符是 '/',那么整個三目運算符的結果就是空字符串 "",否則就是字符串 "/"。然后,這個結果被用于與 _path 和 _file 進行字符串拼接。
ofstream
是 C++ 標準庫中的一個類,全稱為 ?Output File Stream?(輸出文件流)。它用于將數據從程序寫入到文件中,是文件操作的重要組成部分。
- std::ofstream: 是C++標準庫中用于文件輸出的流類,定義在頭文件 <fstream>中。
- out: 是定義的輸出文件流對象的名稱。
- filename: 是要打開的文件名(可以是字符串、字符數組等)。
- std::ios::app: 是打開文件的模式標志,表示以追加模式(append)打開。
- out.close():表示關閉文件
好了,到這里我們日志就實現了大概1/2,我們這里可以去檢驗一下我們寫錯沒有
#include <iostream>
#include "Log.hpp"using namespace LogMudoule;
int main()
{// std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleLogStrategy>();std::unique_ptr<LogStrategy> strategy = std::make_unique<FileLogStrategy>();strategy->SyncLog("hello");return 0;
}
strategy是智能指針由派生類?classFileLogStrategy完成賦值(多態)表示我們選擇文件寫入的模式
下面的圖片也表明了我們的代碼無誤
日志等級
利用枚舉實現日志等級
// 1. 形成日志等級
enum class LogLevel
{DEBUG,INFO,WARNING,ERROR,FATAL
};
我們使用的枚舉實際上是0、1、2、3等數字,最終我們的日志是一串字符串所以我們還要類型轉換一下,也是非常簡單的。
std::string LeveltoStr(LogLevel level)
{switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "UNKNOWN";}
}
獲得時間戳
std::string GetTimeStamp()
{time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm);char timebuffer[128];snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year + 1900,curr_tm.tm_mon + 1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuffer;
}
localtime_r
函數詳解
localtime_r
是一個用于將時間戳轉換為本地時間的線程安全函數。它是 localtime
函數的可重入(reentrant)版本,在多線程編程中特別重要。
函數原型
struct tm *localtime_r(const time_t *timep, struct tm *result);
參數說明
- timep: 指向 time_t類型時間的指針,表示從 1970-01-01 00:00:00 UTC 開始的秒數
- result: 指向 struct tm結構的指針,用于存儲轉換后的時間信息
返回值
- 成功時返回指向 result的指針
- 失敗時返回 NUL
struct tm結構的指針
struct tm {int tm_sec; // 秒 [0, 59]int tm_min; // 分 [0, 59]int tm_hour; // 時 [0, 23]int tm_mday; // 日 [1, 31]int tm_mon; // 月 [0, 11] (0 = 一月)int tm_year; // 年 (從1900開始)int tm_wday; // 星期 [0, 6] (0 = 周日)int tm_yday; // 年中的日 [0, 365]int tm_isdst; // 夏令時標志 (正數: 是, 0: 否, 負數: 未知)
};
snprintf格式化輸入函數,我們期望的年是4位,月是兩位(不足補0——%02d)天、時分秒也是如此
實現效果如下
Logger類(重點)
我們在Logger類中完成整個日志的實現它的結構如下
class Logger
{
public:Logger(){SelectConsoleLogStrategy();}void SelectFileLogStrategy(){_fflush_strategy = std::make_unique<FileLogStrategy>();}void SelectConsoleLogStrategy(){_fflush_strategy = std::make_unique<ConsoleLogStrategy>();}class LogMessage{public:private:std::string _curr_time;LogLevel _level;pid_t _pid;std::string _src_name;int _line_number;std::string _loginfo; // 合并之后,一條完整的信息Logger &_logger;};LogMessage operator()(LogLevel level, std::string name, int line){return LogMessage(level, name, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _fflush_strategy;
};
為什么要在Logger類里實現一個內部類LogMessage最后再談
這里Logger的構造函數是選擇一種模式,我們這里設置的是向屏幕打印
Logger的私有成員變量是指向基類LogStrategy的智能指針(多態行為)在SelectConsoleLogStrategy()函數中完成賦值就是選擇對應模式
我們在Logger類中還完成了一個仿函數-故意沒寫引用&,這個仿函數的作用是體現在接下來的LogMessage類里重載<<中
LogMessage operator()實際使用方式
// 傳統方式可能這樣寫:
logger.log(DEBUG, "main.cpp", 42) << "This is a debug message";// 使用 operator() 可以這樣寫:
logger(DEBUG, "main.cpp", 42) << "This is a debug message";
class LogMessage 日志信息類
class LogMessage
{
public:LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger){// 日志的左邊部分,合并起來std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << LeveltoStr(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();}template <typename T>LogMessage &operator<<(const T &info){// 日志的右半部分,可變的std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}~LogMessage(){if (_logger._fflush_strategy){_logger._fflush_strategy->SyncLog(_loginfo);}}private:std::string _curr_time;LogLevel _level;pid_t _pid;std::string _src_name;int _line_number;std::string _loginfo; // 合并之后,一條完整的信息Logger &_logger;
};
老樣子先看私有成員前五個對應開頭提到的我們希望日志中包含的信息即
- 時間戳
- 日志等級
- 日志內容
- 文件名(可選)
- 行號(可選)
- 進程/線程id(可選)
我們需要把這五個信息合并成一條信息,這就是_loginfo的作用
我們希望logger(DEBUG, "main.cpp", 42) << "This is a debug message";時通過_logger來實現在屏幕上的打印,具體后面詳說,所以在LogMessage中我們還定義Logger &_logger成員
LogMessage的構造函數就是輸出一條完整信息,這里我們利用std::stringstream
std::stringstream
std::stringstream
是 C++ 標準庫中的一個類,它提供了內存中的字符串流處理功能,結合了字符串的靈活性和流的操作接口。它是 <sstream>
頭文件的一部分。
用法
std::stringstream ss;// 向流中插入數據
ss << "Hello, " << 42 << " " << 3.14 << " " << std::boolalpha << true;// 獲取完整的字符串
std::string result = ss.str();
std::cout << result; // 輸出: Hello, 42 3.14 true
在我們LogMessage中它的作用有兩個
//1_loginfo = ss.str(); // 獲取格式化后的字符串//2ss << info; // 將任意類型轉換為字符串_loginfo += ss.str(); // 追加到日志信息
重載?流輸出運算符和析構函數
template <typename T>
LogMessage &operator<<(const T &info)
{// 日志的右半部分,可變的std::stringstream ss;ss << info;_loginfo += ss.str();return *this;
}
~LogMessage()
{if (_logger._fflush_strategy){_logger._fflush_strategy->SyncLog(_loginfo);}
日志的右半部分是可變的,也許是整數,也許是字符串還可能是其他類型,這里我們要設置一個模板
通過stringstream類的ss對象來實現類型轉換并且追加到字符串_loginfo中,返回類型是LogMessage是因為我們期望<<能實現連續使用即下面這樣
logger(LogLevel::DEBUG, "main.cc", 10) << "hello world" << 3.143;
我們在上面實現的仿函數沒有使用引用,當我們向上面代碼這樣使用時,logger返回的是一份臨時對象(類型是LogMessage包含_loginfo的信息)臨時對象在下一行代碼就會被析構,析構時就會調用SyncLog函數打印對應的信息在屏幕上
最后收尾工作
// 全局日志對象
Logger logger;// 使用宏,簡化用戶操作,獲取文件名和行號
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Select_Console_Log_Strategy() logger.SelectConsoleLogStrategy()
#define Select_File_Log_Strategy() logger.SelectFileLogStrategy()
定義全局對象這樣我們就能直接選擇模式了
Select_Console_Log_Strategy();
LOG(LogLevel::DEBUG) << "hello world" << 3.141;
LOG(LogLevel::DEBUG) << "hello world" << 3.142;Select_File_Log_Strategy();
LOG(LogLevel::DEBUG) << "hello world" << 3.143;
LOG(LogLevel::DEBUG) << "hello world" << 3.144;
這就是日志代碼的講解,下面是完整代碼
#ifndef __LOG_HPP__
#define __LOG_HPP__#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"namespace LogMudoule
{using namespace MutexModule;const std::string gsep = "\r\n";// 策略模式,C++多態特性// 2. 刷新策略 a: 顯示器打印 b:向指定的文件寫入// 刷新策略基類class LogStrategy{public:virtual ~LogStrategy(){}virtual void SyncLog(const std::string &message)=0; };// 顯示器打印日志的策略 子類class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::cout << message << gsep;}~ConsoleLogStrategy(){}private:Mutex _mutex;};// 文件打印日志的策略 : 子類const std::string defaultpath = "./log";const std::string defaultfile = "my.log";class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path),_file(file){LockGuard lockguard(_mutex);if (std::filesystem::exists(_path)){return;}try{std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);const std::string &filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"std::ofstream out(filename, std::ios::app);if (!out.is_open()){return;}out << message << gsep;out.close();}~FileLogStrategy(){}private:const std::string _path;const std::string _file;Mutex _mutex;};// 1. 形成日志等級enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};std::string LeveltoStr(LogLevel level){switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "UNKNOWN";}}std::string GetTimeStamp(){time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm);char timebuffer[128];snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year + 1900,curr_tm.tm_mon + 1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuffer;}class Logger{public:Logger(){SelectFileLogStrategy();}void SelectFileLogStrategy(){_fflush_strategy = std::make_unique<FileLogStrategy>();}void SelectConsoleLogStrategy(){_fflush_strategy = std::make_unique<ConsoleLogStrategy>();}class LogMessage{public:LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger){// 日志的左邊部分,合并起來std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << LeveltoStr(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();}template <typename T>LogMessage& operator <<(const T &info){// 日志的右半部分,可變的std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}~LogMessage(){if(_logger._fflush_strategy){_logger._fflush_strategy->SyncLog(_loginfo);}}private:std::string _curr_time;LogLevel _level;pid_t _pid;std::string _src_name;int _line_number;std::string _loginfo; // 合并之后,一條完整的信息Logger &_logger;};LogMessage operator()(LogLevel level, std::string name, int line){return LogMessage(level, name, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _fflush_strategy;};// 全局日志對象Logger logger;// 使用宏,簡化用戶操作,獲取文件名和行號#define LOG(level) logger(level, __FILE__, __LINE__)#define Select_Console_Log_Strategy() logger.SelectConsoleLogStrategy()#define Select_File_Log_Strategy() logger.SelectFileLogStrategy()
}
#endif