1上一篇前两篇我们完成了整个日志库的基础支撑模块工具模块util.hpp实现了跨平台的时间获取、文件存在判断、递归目录创建日志等级模块level.hpp采用类嵌套枚举定义了 7 级日志等级提供等级转字符串接口日志消息模块message.hpp封装了包含时间、等级、文件名、行号、线程 ID 等 7 个核心字段的LogMsg结构体现在我们已经有了完整的日志原始数据但是这些零散的结构体字段无法直接输出成可读的日志文本现在我们着手编写日志库里面灵活的日志格式化板块format.hpp支持自定义日志格式将对LogMsg对象换成规整的日志字符串2整体设计思路1为什么不直接用硬编码拼接LogMsg里加一个toString()方法直接把所有字段按固定格式拼接起来。但很快就发现了致命问题完全没有灵活性如果想改日志格式比如调整字段顺序、新增 / 删除字段必须修改LogMsg类的代码违反开闭原则无法满足不同场景需求开发环境可能需要详细的调试信息生产环境只需要核心字段控制台输出和文件输出可能需要不同的格式扩展性差以后想新增自定义字段比如进程 ID、模块名必须修改拼接逻辑2最终方案组合模式占位符解析核心思路是将每个日志字段的格式化逻辑封装成独立的FormatItem子类比如时间格式化、等级格式化、文件名格式化解析用户传入的格式模板字符串如[%d{%Y-%m-%d}][%p] %m%n将其拆分成一系列FormatItem对象的组合格式化日志时按顺序调用每个FormatItem的format方法拼接成最终的日志字符串这种设计的优势非常明显高度灵活通过修改格式字符串即可任意调整日志格式无需修改代码扩展性强新增字段只需要新增一个FormatItem子类原有代码完全不用动逻辑清晰每个类只负责一个字段的格式化符合单一职责原则3核心代码实现1头文件与跨平台宏定义这是整个模块最基础的部分也是最容易踩跨平台坑的地方。我把所有平台相关的代码都集中在这里避免业务逻辑中出现平台判断。#pragma once // 依赖的基础模块头文件 #include level.hpp // 日志等级定义 #include message.hpp // 日志消息结构体定义 // C标准库头文件 #include iostream // 标准输入输出流 #include string // 字符串处理 #include memory // 智能指针shared_ptr #include vector // 动态数组存储格式化子项 #include sstream // 字符串流用于拼接日志 #include ctime // 时间处理 #include cassert // 断言用于调试阶段的错误检查 /************************** 跨平台兼容宏定义 **************************/ #ifdef _WIN32 #define LOCAL_TIME_SAFE(timestamp,tm_ptr) localtime_s(tm_ptr,(timestamp)) #else #define LOCAL_TIME_SAFE(timestamp,tm_ptr) localtime_r((timestamp),tm_ptr) #endif // _WIN32 namespace my_log {一开始我直接用了C标准库的localtime函数结果发现localtime不是线程安全的它使用全局静态变量存储结果多线程环境下会出现数据竞争而且Windows和Linux的线程安全版本接口参数顺序完全相反-Windowslocaltime_s(struct tm*, const time_t*) → 第一个参数是输出第二个是输入-Linuxlocaltime_r(const time_t*, struct tm*) → 第一个参数是输入第二个是输出解决用宏统一两个平台的接口业务代码中直接使用LOCAL_TIME_SAFE即可2格式化子项抽象基类首先定义抽象基类FormatItem所有具体的格式化子项都继承自它/************************** 格式化子项抽象基类 ************************** * 设计模式组合模式Composite Pattern * 作用定义所有格式化子项的统一接口每个子项负责格式化日志的一个特定字段 * 扩展新增字段只需要新增一个继承自FormatItem的子类无需修改原有代码 ***********************************************************************/ class FormatItem{ public: // 智能指针类型别名简化代码书写 using ptr std::shared_ptrFormatItem; /** * brief 纯虚函数格式化日志消息的对应字段到输出流 * param out 输出流可以是std::cout、文件流、字符串流等 * param msg 待格式化的日志消息对象 */ virtual void format(std::ostream out, const LogMsg msg) 0; // 虚析构函数确保子类对象通过基类指针析构时能正确调用子类析构函数 // 如果不写虚析构函数子类对象通过基类指针delete时会发生内存泄漏 virtual ~FormatItem() default; };3具体格式化子项实现每个子类只负责一个字段的格式化严格遵循单一职责原则。大部分子类的实现非常简单只有时间格式化稍微复杂一点。/************************** 具体格式化子项实现 ************************** * 每个子类对应一个日志字段的格式化逻辑实现单一职责原则 * 对应关系 * - MsgFormatItem → %m 日志消息内容 * - LevelFormatItem → %p 日志等级 * - TimeFormatItem → %d 时间支持自定义格式 * - FileNameFormatItem → %f 源代码文件名 * - LineFormatItem → %l 源代码行号 * - ThreadFormatItem → %t 线程ID * - LoggerFormatItem → %c 日志器名称 * - TabFormatItem → %T 制表符 * - NLineFormatItem → %n 换行符 * - OtherFormatItem → 普通字符串非格式化字符 ***********************************************************************/ // 日志消息内容格式化子项 %m class MsgFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { // 直接输出日志消息的有效载荷 out msg._payload; } }; // 日志等级格式化子项 %p class LevelFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { // 调用LogLevel类的静态方法将枚举值转换为字符串 out LogLevel::ToString(msg._level); } }; // 时间格式化子项 %d支持自定义格式默认 %H:%M:%S class TimeFormatItem : public FormatItem { public: /** * brief 构造函数 * param fmt 时间格式字符串遵循strftime函数的格式规范 */ TimeFormatItem(const std::string fmt %H:%M:%S) : _time_fmt(fmt) {} void format(std::ostream out, const LogMsg msg) override { // 定义tm结构体存储分解后的时间信息 struct tm local_tm {}; // 调用跨平台安全的本地时间转换函数 LOCAL_TIME_SAFE(msg._ctime, local_tm); // 定义缓冲区存储格式化后的时间字符串64字节足够存储所有常见时间格式 char buf[64] { 0 }; // 将tm结构体格式化为指定格式的字符串 strftime(buf, sizeof(buf), _time_fmt.c_str(), local_tm); // 输出格式化后的时间字符串 out buf; } private: std::string _time_fmt; // 存储用户自定义的时间格式 }; // 源代码文件名格式化子项 %f class FileNameFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { out msg._filename; } }; // 源代码行号格式化子项 %l class LineFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { out msg._line; } }; // 线程ID格式化子项 %t class ThreadFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { // std::thread::id 支持直接通过流输出非常方便 out msg._tid; } }; // 日志器名称格式化子项 %c class LoggerFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { out msg._logger; } }; // 制表符格式化子项 %T class TabFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg) override { // 不需要使用日志消息对象直接输出制表符 out \t; } }; // 换行符格式化子项 %n class NLineFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg) override { // 不需要使用日志消息对象直接输出换行符 out \n; } }; // 普通字符串格式化子项处理所有非格式化字符 class OtherFormatItem : public FormatItem { public: /** * brief 构造函数 * param str 要输出的普通字符串 */ OtherFormatItem(const std::string str) : _str(str) {} void format(std::ostream out, const LogMsg msg) override { // 直接输出存储的普通字符串 out _str; } private: std::string _str; // 存储普通字符串内容 };4格式化器核心类这是整个模块的核心负责模板解析和日志格式化两大核心功能同时用到了简单工厂模式来创建格式化子项。/************************** 格式化器核心类 ************************** * 作用 * 1. 解析用户传入的格式模板字符串 * 2. 将模板字符串转换为一系列FormatItem对象的组合 * 3. 调用所有FormatItem的format方法拼接成最终的日志字符串 * 设计模式组合模式管理多个FormatItem对象 工厂模式createItem方法 ***********************************************************************/ class Formatter{ public: using ptr std::shared_ptrFormatter; /** * brief 构造函数 * param pattern 日志格式模板字符串默认格式为 * [%d{%Y-%m-%d %H:%M:%S}][%p][%c][%f:%l] %m%n * note 构造函数中会自动调用parsePattern解析模板用assert确保解析成功 */ Formatter(const std::string pattern [%d{%Y-%m-%d %H:%M:%S}][%p][%c][%f:%l] %m%n) : _pattern(pattern) { // 断言模板解析必须成功否则程序终止仅在调试模式生效 assert(parsePattern()); } /** * brief 格式化日志消息返回字符串 * param msg 待格式化的日志消息对象 * return std::string 格式化后的完整日志字符串 */ std::string format(const LogMsg msg) { // 使用字符串流作为中间缓冲区拼接所有格式化子项的输出 // 比直接字符串拼接效率高很多尤其是多个子项拼接的情况 std::stringstream ss; format(ss, msg); // 将字符串流中的内容转换为字符串返回 return ss.str(); } /** * brief 格式化日志消息输出到指定流 * param out 输出流可以是std::cout、文件流、字符串流等 * param msg 待格式化的日志消息对象 * 设计亮点提供流输出接口支持任意输出目标灵活性极强 */ void format(std::ostream out, const LogMsg msg) { // 按顺序遍历所有格式化子项依次调用format方法输出 // 组合模式的核心统一处理单个对象和对象组合 for (auto it : _items) { it-format(out, msg); } } /** * brief 解析格式模板字符串生成格式化子项列表 * return bool 解析成功返回true失败返回false * note 这是整个模块最复杂的部分需要处理各种边界情况 */ bool parsePattern() { // 存储解析后的临时结果pair占位符key, 子规则val // key为空表示是普通字符串val为字符串内容 // key非空表示是格式化占位符val为子规则如%d的时间格式 std::vectorstd::pairstd::string, std::string fmt_order; size_t pos 0; // 当前遍历的位置 std::string key, val; // 临时存储占位符key和子规则val // 遍历整个格式模板字符串 while (pos _pattern.size()) { /************************** 步骤1处理普通字符 ************************** * 逻辑当前字符不是%就作为普通字符加入val继续下一个字符 ***********************************************************************/ if (_pattern[pos] ! %) { val.push_back(_pattern[pos]); continue; } /************************** 步骤2处理转义字符 %% ************************** * 逻辑连续两个%表示要输出一个%本身作为普通字符处理 * 踩坑记录一开始没有处理这种情况导致用户想输出%的时候出现格式错误 ***********************************************************************/ if (pos 1 _pattern.size() _pattern[pos 1] %) { val.push_back(%); pos 2; continue; } /************************** 步骤3处理格式化占位符 ************************** * 逻辑遇到单个%表示后面是格式化占位符 * 1. 先把之前积累的普通字符串保存到fmt_order * 2. 提取占位符key%后面的第一个字符 * 3. 检查是否有子规则如%d{%Y-%m-%d}中的{}部分 ***********************************************************************/ // 3.1 保存之前积累的普通字符串 if (!val.empty()) { fmt_order.push_back(std::make_pair(, val)); val.clear(); } // 3.2 检查%是否在字符串末尾边界情况处理 if (pos _pattern.size()) { std::cout 格式错误%之后没有对应的格式化字符\n; return false; } // 3.3 提取占位符key%后面的第一个字符 key _pattern[pos]; /************************** 步骤4处理占位符的子规则 ************************** * 逻辑如果占位符后面跟着{则{}中的内容是子规则 * 例如%d{%Y-%m-%d} 中子规则是%Y-%m-%d ***********************************************************************/ if (pos _pattern.size() _pattern[pos] {) { pos; // 跳过{字符 // 遍历直到遇到}或字符串末尾 while (pos _pattern.size() _pattern[pos] ! }) { val.push_back(_pattern[pos]); } // 边界情况没有找到对应的} if (pos _pattern.size()) { std::cout 格式错误子规则括号{}不匹配\n; return false; } pos; // 跳过}字符 } // 3.4 将占位符和子规则保存到fmt_order fmt_order.push_back(std::make_pair(key, val)); key.clear(); val.clear(); } /************************** 步骤5处理最后一段普通字符 ************************** * 逻辑循环结束后如果val不为空说明最后一段是普通字符需要保存 * 踩坑记录一开始忘记处理这种情况导致日志末尾的普通字符丢失 ***********************************************************************/ if (!val.empty()) { fmt_order.emplace_back(, val); } /************************** 步骤6创建格式化子项列表 ************************** * 逻辑遍历解析后的fmt_order调用createItem方法创建对应的FormatItem对象 * 并将所有对象存储到_items成员变量中 ***********************************************************************/ for (auto it : fmt_order) { _items.push_back(createItem(it.first, it.second)); } // 解析成功 return true; } private: /** * brief 工厂方法根据占位符key和子规则val创建对应的FormatItem对象 * param key 占位符key如d、p、m等 * param val 子规则如时间格式 * return FormatItem::ptr 创建好的格式化子项智能指针 * note 设计模式简单工厂模式封装对象创建逻辑 * 扩展新增占位符只需要在这里加一行判断即可非常方便 */ FormatItem::ptr createItem(const std::string key, const std::string val) { if (key d) return std::make_sharedTimeFormatItem(val); if (key t) return std::make_sharedThreadFormatItem(); if (key c) return std::make_sharedLoggerFormatItem(); if (key f) return std::make_sharedFileNameFormatItem(); if (key l) return std::make_sharedLineFormatItem(); if (key p) return std::make_sharedLevelFormatItem(); if (key T) return std::make_sharedTabFormatItem(); if (key m) return std::make_sharedMsgFormatItem(); if (key n) return std::make_sharedNLineFormatItem(); // 未知占位符作为普通字符串处理避免程序崩溃 return std::make_sharedOtherFormatItem(val); } private: std::string _pattern; // 用户传入的格式模板字符串 std::vectorFormatItem::ptr _items; // 解析后生成的格式化子项列表 }; } // namespace my_log4完整占位符说明占位符含义示例%d时间支持自定义格式默认 % H:% M:% S%d{%Y-%m-%d %H:%M:%S} → 2024-05-26 15:30:45%p日志等级INFO、WARN、ERR%c日志器名称root、db_logger%f源代码文件名main.cpp%l源代码行号25%t线程 ID140709832386368%m日志消息内容数据库连接成功%T制表符\t%n换行符\n%%输出一个 %%默认的日志格式为[%d{%Y-%m-%d %H:%M:%S}][%p][%c][%f:%l] %m%n输出效果示例[2024-05-26 15:30:45][INFO][root][main.cpp:25] 数据库连接成功5测试代码AI生成#include format.hpp #include iostream int main() { // 1. 构造测试日志消息 my_log::LogMsg msg( my_log::LogLevel::value::INFO, __LINE__, test_format.cpp, root_logger, 这是一条测试日志 ); // 2. 使用默认格式格式化 my_log::Formatter::ptr default_formatter std::make_sharedmy_log::Formatter(); std::string default_log default_formatter-format(msg); std::cout 默认格式日志\n default_log std::endl; // 3. 使用自定义格式格式化 std::string custom_pattern [%d{%Y-%m-%d}][%t][%p] %f:%l - %m%n; my_log::Formatter::ptr custom_formatter std::make_sharedmy_log::Formatter(custom_pattern); std::string custom_log custom_formatter-format(msg); std::cout 自定义格式日志\n custom_log std::endl; return 0; }结果默认格式日志 [2024-05-26 15:30:45][INFO][root_logger][test_format.cpp:12] 这是一条测试日志 自定义格式日志 [2024-05-26][140709832386368][INFO] test_format.cpp:12 - 这是一条测试日志6总结1踩过的坑跨平台时间函数不统一一开始直接用了localtime结果不仅线程不安全而且 Windows 和 Linux 的安全版本函数参数顺序相反最后用宏定义统一了接口格式解析边界处理一开始没有处理%在字符串末尾的情况导致数组越界崩溃还有%%转义的情况一开始没有处理导致输出错误子括号匹配问题一开始没有处理子规则{}不匹配的情况比如%d{%Y-%m-%d没有闭合括号导致解析死循环最后一段普通字符遗漏一开始循环结束后没有处理最后一段普通字符导致日志末尾的内容丢失虚析构函数缺失一开始忘记给FormatItem基类加虚析构函数导致子类对象通过基类指针析构时发生内存泄漏2学到的东西组合模式的实际应用将复杂的格式化任务拆分成多个简单的子任务组合简单工厂模式的应用根据不同的 key 创建不同的格式化子项对象字符串解析的技巧状态机思想、边界情况处理跨平台开发的最佳实践用宏定义统一平台差异避免业务逻辑中出现平台判断面向接口编程的思想依赖抽象而不是具体实现提高代码的扩展性和可维护性
C++工业级日志项目(三)日志格式化消息封装
发布时间:2026/5/27 13:09:11
1上一篇前两篇我们完成了整个日志库的基础支撑模块工具模块util.hpp实现了跨平台的时间获取、文件存在判断、递归目录创建日志等级模块level.hpp采用类嵌套枚举定义了 7 级日志等级提供等级转字符串接口日志消息模块message.hpp封装了包含时间、等级、文件名、行号、线程 ID 等 7 个核心字段的LogMsg结构体现在我们已经有了完整的日志原始数据但是这些零散的结构体字段无法直接输出成可读的日志文本现在我们着手编写日志库里面灵活的日志格式化板块format.hpp支持自定义日志格式将对LogMsg对象换成规整的日志字符串2整体设计思路1为什么不直接用硬编码拼接LogMsg里加一个toString()方法直接把所有字段按固定格式拼接起来。但很快就发现了致命问题完全没有灵活性如果想改日志格式比如调整字段顺序、新增 / 删除字段必须修改LogMsg类的代码违反开闭原则无法满足不同场景需求开发环境可能需要详细的调试信息生产环境只需要核心字段控制台输出和文件输出可能需要不同的格式扩展性差以后想新增自定义字段比如进程 ID、模块名必须修改拼接逻辑2最终方案组合模式占位符解析核心思路是将每个日志字段的格式化逻辑封装成独立的FormatItem子类比如时间格式化、等级格式化、文件名格式化解析用户传入的格式模板字符串如[%d{%Y-%m-%d}][%p] %m%n将其拆分成一系列FormatItem对象的组合格式化日志时按顺序调用每个FormatItem的format方法拼接成最终的日志字符串这种设计的优势非常明显高度灵活通过修改格式字符串即可任意调整日志格式无需修改代码扩展性强新增字段只需要新增一个FormatItem子类原有代码完全不用动逻辑清晰每个类只负责一个字段的格式化符合单一职责原则3核心代码实现1头文件与跨平台宏定义这是整个模块最基础的部分也是最容易踩跨平台坑的地方。我把所有平台相关的代码都集中在这里避免业务逻辑中出现平台判断。#pragma once // 依赖的基础模块头文件 #include level.hpp // 日志等级定义 #include message.hpp // 日志消息结构体定义 // C标准库头文件 #include iostream // 标准输入输出流 #include string // 字符串处理 #include memory // 智能指针shared_ptr #include vector // 动态数组存储格式化子项 #include sstream // 字符串流用于拼接日志 #include ctime // 时间处理 #include cassert // 断言用于调试阶段的错误检查 /************************** 跨平台兼容宏定义 **************************/ #ifdef _WIN32 #define LOCAL_TIME_SAFE(timestamp,tm_ptr) localtime_s(tm_ptr,(timestamp)) #else #define LOCAL_TIME_SAFE(timestamp,tm_ptr) localtime_r((timestamp),tm_ptr) #endif // _WIN32 namespace my_log {一开始我直接用了C标准库的localtime函数结果发现localtime不是线程安全的它使用全局静态变量存储结果多线程环境下会出现数据竞争而且Windows和Linux的线程安全版本接口参数顺序完全相反-Windowslocaltime_s(struct tm*, const time_t*) → 第一个参数是输出第二个是输入-Linuxlocaltime_r(const time_t*, struct tm*) → 第一个参数是输入第二个是输出解决用宏统一两个平台的接口业务代码中直接使用LOCAL_TIME_SAFE即可2格式化子项抽象基类首先定义抽象基类FormatItem所有具体的格式化子项都继承自它/************************** 格式化子项抽象基类 ************************** * 设计模式组合模式Composite Pattern * 作用定义所有格式化子项的统一接口每个子项负责格式化日志的一个特定字段 * 扩展新增字段只需要新增一个继承自FormatItem的子类无需修改原有代码 ***********************************************************************/ class FormatItem{ public: // 智能指针类型别名简化代码书写 using ptr std::shared_ptrFormatItem; /** * brief 纯虚函数格式化日志消息的对应字段到输出流 * param out 输出流可以是std::cout、文件流、字符串流等 * param msg 待格式化的日志消息对象 */ virtual void format(std::ostream out, const LogMsg msg) 0; // 虚析构函数确保子类对象通过基类指针析构时能正确调用子类析构函数 // 如果不写虚析构函数子类对象通过基类指针delete时会发生内存泄漏 virtual ~FormatItem() default; };3具体格式化子项实现每个子类只负责一个字段的格式化严格遵循单一职责原则。大部分子类的实现非常简单只有时间格式化稍微复杂一点。/************************** 具体格式化子项实现 ************************** * 每个子类对应一个日志字段的格式化逻辑实现单一职责原则 * 对应关系 * - MsgFormatItem → %m 日志消息内容 * - LevelFormatItem → %p 日志等级 * - TimeFormatItem → %d 时间支持自定义格式 * - FileNameFormatItem → %f 源代码文件名 * - LineFormatItem → %l 源代码行号 * - ThreadFormatItem → %t 线程ID * - LoggerFormatItem → %c 日志器名称 * - TabFormatItem → %T 制表符 * - NLineFormatItem → %n 换行符 * - OtherFormatItem → 普通字符串非格式化字符 ***********************************************************************/ // 日志消息内容格式化子项 %m class MsgFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { // 直接输出日志消息的有效载荷 out msg._payload; } }; // 日志等级格式化子项 %p class LevelFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { // 调用LogLevel类的静态方法将枚举值转换为字符串 out LogLevel::ToString(msg._level); } }; // 时间格式化子项 %d支持自定义格式默认 %H:%M:%S class TimeFormatItem : public FormatItem { public: /** * brief 构造函数 * param fmt 时间格式字符串遵循strftime函数的格式规范 */ TimeFormatItem(const std::string fmt %H:%M:%S) : _time_fmt(fmt) {} void format(std::ostream out, const LogMsg msg) override { // 定义tm结构体存储分解后的时间信息 struct tm local_tm {}; // 调用跨平台安全的本地时间转换函数 LOCAL_TIME_SAFE(msg._ctime, local_tm); // 定义缓冲区存储格式化后的时间字符串64字节足够存储所有常见时间格式 char buf[64] { 0 }; // 将tm结构体格式化为指定格式的字符串 strftime(buf, sizeof(buf), _time_fmt.c_str(), local_tm); // 输出格式化后的时间字符串 out buf; } private: std::string _time_fmt; // 存储用户自定义的时间格式 }; // 源代码文件名格式化子项 %f class FileNameFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { out msg._filename; } }; // 源代码行号格式化子项 %l class LineFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { out msg._line; } }; // 线程ID格式化子项 %t class ThreadFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { // std::thread::id 支持直接通过流输出非常方便 out msg._tid; } }; // 日志器名称格式化子项 %c class LoggerFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg msg) override { out msg._logger; } }; // 制表符格式化子项 %T class TabFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg) override { // 不需要使用日志消息对象直接输出制表符 out \t; } }; // 换行符格式化子项 %n class NLineFormatItem : public FormatItem { public: void format(std::ostream out, const LogMsg) override { // 不需要使用日志消息对象直接输出换行符 out \n; } }; // 普通字符串格式化子项处理所有非格式化字符 class OtherFormatItem : public FormatItem { public: /** * brief 构造函数 * param str 要输出的普通字符串 */ OtherFormatItem(const std::string str) : _str(str) {} void format(std::ostream out, const LogMsg msg) override { // 直接输出存储的普通字符串 out _str; } private: std::string _str; // 存储普通字符串内容 };4格式化器核心类这是整个模块的核心负责模板解析和日志格式化两大核心功能同时用到了简单工厂模式来创建格式化子项。/************************** 格式化器核心类 ************************** * 作用 * 1. 解析用户传入的格式模板字符串 * 2. 将模板字符串转换为一系列FormatItem对象的组合 * 3. 调用所有FormatItem的format方法拼接成最终的日志字符串 * 设计模式组合模式管理多个FormatItem对象 工厂模式createItem方法 ***********************************************************************/ class Formatter{ public: using ptr std::shared_ptrFormatter; /** * brief 构造函数 * param pattern 日志格式模板字符串默认格式为 * [%d{%Y-%m-%d %H:%M:%S}][%p][%c][%f:%l] %m%n * note 构造函数中会自动调用parsePattern解析模板用assert确保解析成功 */ Formatter(const std::string pattern [%d{%Y-%m-%d %H:%M:%S}][%p][%c][%f:%l] %m%n) : _pattern(pattern) { // 断言模板解析必须成功否则程序终止仅在调试模式生效 assert(parsePattern()); } /** * brief 格式化日志消息返回字符串 * param msg 待格式化的日志消息对象 * return std::string 格式化后的完整日志字符串 */ std::string format(const LogMsg msg) { // 使用字符串流作为中间缓冲区拼接所有格式化子项的输出 // 比直接字符串拼接效率高很多尤其是多个子项拼接的情况 std::stringstream ss; format(ss, msg); // 将字符串流中的内容转换为字符串返回 return ss.str(); } /** * brief 格式化日志消息输出到指定流 * param out 输出流可以是std::cout、文件流、字符串流等 * param msg 待格式化的日志消息对象 * 设计亮点提供流输出接口支持任意输出目标灵活性极强 */ void format(std::ostream out, const LogMsg msg) { // 按顺序遍历所有格式化子项依次调用format方法输出 // 组合模式的核心统一处理单个对象和对象组合 for (auto it : _items) { it-format(out, msg); } } /** * brief 解析格式模板字符串生成格式化子项列表 * return bool 解析成功返回true失败返回false * note 这是整个模块最复杂的部分需要处理各种边界情况 */ bool parsePattern() { // 存储解析后的临时结果pair占位符key, 子规则val // key为空表示是普通字符串val为字符串内容 // key非空表示是格式化占位符val为子规则如%d的时间格式 std::vectorstd::pairstd::string, std::string fmt_order; size_t pos 0; // 当前遍历的位置 std::string key, val; // 临时存储占位符key和子规则val // 遍历整个格式模板字符串 while (pos _pattern.size()) { /************************** 步骤1处理普通字符 ************************** * 逻辑当前字符不是%就作为普通字符加入val继续下一个字符 ***********************************************************************/ if (_pattern[pos] ! %) { val.push_back(_pattern[pos]); continue; } /************************** 步骤2处理转义字符 %% ************************** * 逻辑连续两个%表示要输出一个%本身作为普通字符处理 * 踩坑记录一开始没有处理这种情况导致用户想输出%的时候出现格式错误 ***********************************************************************/ if (pos 1 _pattern.size() _pattern[pos 1] %) { val.push_back(%); pos 2; continue; } /************************** 步骤3处理格式化占位符 ************************** * 逻辑遇到单个%表示后面是格式化占位符 * 1. 先把之前积累的普通字符串保存到fmt_order * 2. 提取占位符key%后面的第一个字符 * 3. 检查是否有子规则如%d{%Y-%m-%d}中的{}部分 ***********************************************************************/ // 3.1 保存之前积累的普通字符串 if (!val.empty()) { fmt_order.push_back(std::make_pair(, val)); val.clear(); } // 3.2 检查%是否在字符串末尾边界情况处理 if (pos _pattern.size()) { std::cout 格式错误%之后没有对应的格式化字符\n; return false; } // 3.3 提取占位符key%后面的第一个字符 key _pattern[pos]; /************************** 步骤4处理占位符的子规则 ************************** * 逻辑如果占位符后面跟着{则{}中的内容是子规则 * 例如%d{%Y-%m-%d} 中子规则是%Y-%m-%d ***********************************************************************/ if (pos _pattern.size() _pattern[pos] {) { pos; // 跳过{字符 // 遍历直到遇到}或字符串末尾 while (pos _pattern.size() _pattern[pos] ! }) { val.push_back(_pattern[pos]); } // 边界情况没有找到对应的} if (pos _pattern.size()) { std::cout 格式错误子规则括号{}不匹配\n; return false; } pos; // 跳过}字符 } // 3.4 将占位符和子规则保存到fmt_order fmt_order.push_back(std::make_pair(key, val)); key.clear(); val.clear(); } /************************** 步骤5处理最后一段普通字符 ************************** * 逻辑循环结束后如果val不为空说明最后一段是普通字符需要保存 * 踩坑记录一开始忘记处理这种情况导致日志末尾的普通字符丢失 ***********************************************************************/ if (!val.empty()) { fmt_order.emplace_back(, val); } /************************** 步骤6创建格式化子项列表 ************************** * 逻辑遍历解析后的fmt_order调用createItem方法创建对应的FormatItem对象 * 并将所有对象存储到_items成员变量中 ***********************************************************************/ for (auto it : fmt_order) { _items.push_back(createItem(it.first, it.second)); } // 解析成功 return true; } private: /** * brief 工厂方法根据占位符key和子规则val创建对应的FormatItem对象 * param key 占位符key如d、p、m等 * param val 子规则如时间格式 * return FormatItem::ptr 创建好的格式化子项智能指针 * note 设计模式简单工厂模式封装对象创建逻辑 * 扩展新增占位符只需要在这里加一行判断即可非常方便 */ FormatItem::ptr createItem(const std::string key, const std::string val) { if (key d) return std::make_sharedTimeFormatItem(val); if (key t) return std::make_sharedThreadFormatItem(); if (key c) return std::make_sharedLoggerFormatItem(); if (key f) return std::make_sharedFileNameFormatItem(); if (key l) return std::make_sharedLineFormatItem(); if (key p) return std::make_sharedLevelFormatItem(); if (key T) return std::make_sharedTabFormatItem(); if (key m) return std::make_sharedMsgFormatItem(); if (key n) return std::make_sharedNLineFormatItem(); // 未知占位符作为普通字符串处理避免程序崩溃 return std::make_sharedOtherFormatItem(val); } private: std::string _pattern; // 用户传入的格式模板字符串 std::vectorFormatItem::ptr _items; // 解析后生成的格式化子项列表 }; } // namespace my_log4完整占位符说明占位符含义示例%d时间支持自定义格式默认 % H:% M:% S%d{%Y-%m-%d %H:%M:%S} → 2024-05-26 15:30:45%p日志等级INFO、WARN、ERR%c日志器名称root、db_logger%f源代码文件名main.cpp%l源代码行号25%t线程 ID140709832386368%m日志消息内容数据库连接成功%T制表符\t%n换行符\n%%输出一个 %%默认的日志格式为[%d{%Y-%m-%d %H:%M:%S}][%p][%c][%f:%l] %m%n输出效果示例[2024-05-26 15:30:45][INFO][root][main.cpp:25] 数据库连接成功5测试代码AI生成#include format.hpp #include iostream int main() { // 1. 构造测试日志消息 my_log::LogMsg msg( my_log::LogLevel::value::INFO, __LINE__, test_format.cpp, root_logger, 这是一条测试日志 ); // 2. 使用默认格式格式化 my_log::Formatter::ptr default_formatter std::make_sharedmy_log::Formatter(); std::string default_log default_formatter-format(msg); std::cout 默认格式日志\n default_log std::endl; // 3. 使用自定义格式格式化 std::string custom_pattern [%d{%Y-%m-%d}][%t][%p] %f:%l - %m%n; my_log::Formatter::ptr custom_formatter std::make_sharedmy_log::Formatter(custom_pattern); std::string custom_log custom_formatter-format(msg); std::cout 自定义格式日志\n custom_log std::endl; return 0; }结果默认格式日志 [2024-05-26 15:30:45][INFO][root_logger][test_format.cpp:12] 这是一条测试日志 自定义格式日志 [2024-05-26][140709832386368][INFO] test_format.cpp:12 - 这是一条测试日志6总结1踩过的坑跨平台时间函数不统一一开始直接用了localtime结果不仅线程不安全而且 Windows 和 Linux 的安全版本函数参数顺序相反最后用宏定义统一了接口格式解析边界处理一开始没有处理%在字符串末尾的情况导致数组越界崩溃还有%%转义的情况一开始没有处理导致输出错误子括号匹配问题一开始没有处理子规则{}不匹配的情况比如%d{%Y-%m-%d没有闭合括号导致解析死循环最后一段普通字符遗漏一开始循环结束后没有处理最后一段普通字符导致日志末尾的内容丢失虚析构函数缺失一开始忘记给FormatItem基类加虚析构函数导致子类对象通过基类指针析构时发生内存泄漏2学到的东西组合模式的实际应用将复杂的格式化任务拆分成多个简单的子任务组合简单工厂模式的应用根据不同的 key 创建不同的格式化子项对象字符串解析的技巧状态机思想、边界情况处理跨平台开发的最佳实践用宏定义统一平台差异避免业务逻辑中出现平台判断面向接口编程的思想依赖抽象而不是具体实现提高代码的扩展性和可维护性