1. 什么是设计模式
it行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对⼀些经典的常见的场景, 给定了⼀些对应的解决⽅案, 这个就是设计模式。 在it行业中,设计模式(design patterns) 是一套被广泛认可的、用于解决软件设计中常见问题的最佳实践。它们提供了一种标准化的方法来处理特定的设计问题,并且可以帮助开发人员编写更清晰、更具可维护性的代码。
2. 日志认识
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要工具。 日志格式以下几个指标是必须得有的:
时间戳日志等级日志内容
以下几个指标是可选的:
文件名行号进程,线程相关id信息等 日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。这里我们采用设计模式-策略模式来进行日志的设计,我们想要的日志格式如下:代码语言:JavaScript代码运行次数:0运行复制
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
3. 日志实现首先我们需要设置日志等级:代码语言:javascript代码运行次数:0运行复制
// ⽇志等级 enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; // ⽇志转换成为字符串 std::string LogLevelToString(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"; } }
设置日志时间代码语言:javascript代码运行次数:0运行复制
// 根据时间戳,获取可读性较强的时间信息 std::string GetCurrTime() { time_t tm = time(nullptr); struct tm curr; localtime_r(&tm, &curr); char timebuffer[64]; snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d ", curr.tm_year + 1900,//这是因为这里的年份比实际年份少1900 curr.tm_mon+1,//这是因为月份是在0~11 curr.tm_mday, curr.tm_hour, curr.tm_min, curr.tm_sec); return timebuffer; }
日志策略模式:
有了准备工作后,我们在开始设计日志类之前还需要确定日志的策略模式——也就是日志是往控制台上输出还是文件中输出。
代码语言:javascript代码运行次数:0运行复制
// 策略模式,策略接⼝ class LogStrategy { public: virtual ~LogStrategy() = default; // 策略的析构函数 virtual void SyncLog(const std::string &message) = 0; // 不同模式核⼼是刷新⽅式的不同 };
控制台日志策略:代码语言:javascript代码运行次数:0运行复制
// 控制台⽇志策略,就是⽇志只向显⽰器打印,⽅便我们debug class ConsoleLogStrategy : public LogStrategy { public: void SyncLog(const std::string &message) override { LockGuard LockGuard(_mutex); std::cerr 文件日志策略:代码语言:javascript<i class="icon-code"></i>代码运行次数:<!-- -->0<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewbox="0 0 16 16" fill="none"><path d="M6.66666 10.9999L10.6667 7.99992L6.66666 4.99992V10.9999ZM7.99999 1.33325C4.31999 1.33325 1.33333 4.31992 1.33333 7.99992C1.33333 11.6799 4.31999 14.6666 7.99999 14.6666C11.68 14.6666 14.6667 11.6799 14.6667 7.99992C14.6667 4.31992 11.68 1.33325 7.99999 1.33325ZM7.99999 13.3333C5.05999 13.3333 2.66666 10.9399 2.66666 7.99992C2.66666 5.05992 5.05999 2.66659 7.99999 2.66659C10.94 2.66659 13.3333 5.05992 13.3333 7.99992C13.3333 10.9399 10.94 13.3333 7.99999 13.3333Z" fill="currentcolor"></path></svg>运行<svg width="16" height="16" viewbox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 15.5V3.5H14.5V15.5H4.5ZM12.5 5.5H6.5V13.5H12.5V5.5ZM9.5 2.5H3.5V12.5H1.5V0.5H11.5V2.5H9.5Z" fill="currentcolor"></path></svg>复制<pre class="prism-token token line-numbers javascript"> class FileLogStrategy : public LogStrategy { public: // 构造函数,建⽴出来指定的⽬录结构和⽂件结构 FileLogStrategy(const std::string logpath = defaultpath, std::string logfilename = defaultname) : _logpath(logpath), _logfilename(logfilename) { LockGuard lockguard(_mutex); if (std::filesystem::exists(_logpath)) return; try { std::filesystem::create_directories(_logpath); } catch (const std::filesystem::filesystem_error &e) { std::cerr 具体日志类:<p>我们先确定日志策略模式,默认是控制台输出;然后定义一个内部类用来确定日志输出的信息:</p>代码语言:javascript<i class="icon-code"></i>代码运行次数:<!-- -->0<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewbox="0 0 16 16" fill="none"><path d="M6.66666 10.9999L10.6667 7.99992L6.66666 4.99992V10.9999ZM7.99999 1.33325C4.31999 1.33325 1.33333 4.31992 1.33333 7.99992C1.33333 11.6799 4.31999 14.6666 7.99999 14.6666C11.68 14.6666 14.6667 11.6799 14.6667 7.99992C14.6667 4.31992 11.68 1.33325 7.99999 1.33325ZM7.99999 13.3333C5.05999 13.3333 2.66666 10.9399 2.66666 7.99992C2.66666 5.05992 5.05999 2.66659 7.99999 2.66659C10.94 2.66659 13.3333 5.05992 13.3333 7.99992C13.3333 10.9399 10.94 13.3333 7.99999 13.3333Z" fill="currentcolor"></path></svg>运行<svg width="16" height="16" viewbox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 15.5V3.5H14.5V15.5H4.5ZM12.5 5.5H6.5V13.5H12.5V5.5ZM9.5 2.5H3.5V12.5H1.5V0.5H11.5V2.5H9.5Z" fill="currentcolor"></path></svg>复制<pre class="prism-token token line-numbers javascript"> // 具体的⽇志类 class Logger { public: Logger() { // 默认使⽤显⽰器策略,如果⽤⼾⼆次指明了策略,会释放在申请,测试的时候注意析构次数 UseConsoleStrategy(); } ~Logger() { } void UseConsoleStrategy() { _strategy = std::make_unique<consolelogstrategy>(); } void UseFileStrategy() { _strategy = std::make_unique<filelogstrategy>(); } class LogMessage { public: LogMessage(LogLevel type, std::string filename, int line, Logger &logger) : _curr_time(GetCurrTime()), _pid(getpid()), _filename(filename), _line(line), _logger(logger) { // stringstream不允许拷⻉,所以这⾥就当做格式化功能使⽤ std::stringstream ssbuffer; ssbuffer LogMessage &operatorSyncLog(_loginfo); } } private: LogLevel _type; // ⽇志等级 std::string _curr_time; // ⽇志时间 pid_t _pid; // 写⼊⽇志的进程ID std::string _filename; // 对应的⽂件名 int _line; // 对应的⽂件⾏号 Logger &_logger; // 引⽤外部logger类, ⽅便使⽤策略进⾏刷新 std::string _loginfo; // ⼀条合并完成的,完整的⽇志信息 }; LogMessage operator()(LogLevel type, std::string filename, int line) { return LogMessage(type, filename, line, *this); } private: std::unique_ptr<logstrategy> _strategy; };</logstrategy></filelogstrategy></consolelogstrategy>
最后将上述内容放在一个命名空间LogModule内部,并定义一个日志类对象:代码语言:javascript代码运行次数:0运行复制
#include#include #include #include #include #include #include // C++17, 需要⾼版本编译器和-std=c++17#include #include "Mutex.hpp"namespace LogModule{ using namespace MutexModule; // 默认路径和⽇志名称 const std::string defaultpath = "./log/"; const std::string defaultname = "log.txt"; // ⽇志等级 enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; // ⽇志转换成为字符串 std::string LogLevelToString(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 GetCurrTime() { time_t tm = time(nullptr); struct tm curr; localtime_r(&tm, &curr); char timebuffer[64]; snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d ", curr.tm_year + 1900, curr.tm_mon+1, curr.tm_mday, curr.tm_hour, curr.tm_min, curr.tm_sec); return timebuffer; } // 策略模式,策略接⼝ class LogStrategy { public: virtual ~LogStrategy() = default; // 策略的构造函数 virtual void SyncLog(const std::string &message) = 0; // 不同模式核⼼是刷新⽅式的不同 }; // 控制台⽇志策略,就是⽇志只向显⽰器打印,⽅便我们debug class ConsoleLogStrategy : public LogStrategy { public: void SyncLog(const std::string &message) override { LockGuard LockGuard(_mutex); std::cerr (); } void UseFileStrategy() { _strategy = std::make_unique (); } class LogMessage { public: LogMessage(LogLevel type, std::string filename, int line, Logger &logger) : _curr_time(GetCurrTime()), _pid(getpid()), _filename(filename), _line(line), _logger(logger) { // stringstream不允许拷⻉,所以这⾥就当做格式化功能使⽤ std::stringstream ssbuffer; ssbuffer LogMessage &operatorSyncLog(_loginfo); } } private: LogLevel _type; // ⽇志等级 std::string _curr_time; // ⽇志时间 pid_t _pid; // 写⼊⽇志的进程ID std::string _filename; // 对应的⽂件名 int _line; // 对应的⽂件⾏号 Logger &_logger; // 引⽤外部logger类, ⽅便使⽤策略进⾏刷新 std::string _loginfo; // ⼀条合并完成的,完整的⽇志信息 }; LogMessage operator()(LogLevel type, std::string filename, int line) { return LogMessage(type, filename, line, *this); } private: std::unique_ptr _strategy; }; //定义日志类对象 Logger logger; // 使⽤宏,可以进⾏代码插⼊,⽅便随时获取⽂件名和⾏号 #define LOG(type) logger(type, __FILE__, __LINE__) // 提供选择使⽤何种⽇志策略的⽅法 #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy() #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy()}
测试代码:代码语言:javascript代码运行次数:0运行复制
#include <iostream>#include "Log.hpp"using namespace LogModule;void fun(){ int a = 10; LOG(LogLevel::FATAL) <p>结果如下:</p> <figure class=""><img src="https://img.php.cn/upload/article/001/503/042/174487142432849.jpg" alt="【Linux】日志设计模式与实现"></figure>4. 结语<p> 日志可以帮助我们快速准确的了解程序运行的状况,出现的错误以及相关内容;同时日志的设计模式如解耦也值得我们学习。</p></iostream>