ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零实现 C++ Json-Rpc(六):分级日志宏

从零实现 C++ Json-Rpc(六):分级日志宏 目录前言一、为什么有 GDB 了还需要日志二、先从 printf 开始三、日志至少要告诉我们“从哪里打印的”四、再给日志加上时间五、为什么还要区分 Debug、Info、Error六、统一的 LOG 宏七、从一条 ELOG() 看宏到底做了什么八、用当前测试代码验证一下写到最后前言系列C RPC 框架从设计到实现第五篇项目源码JSON-RPChttps://gitee.com/kuang-zhenting/json-rpc前面几篇我们已经把项目从整体设计推进到了公共字段和网络抽象层。到这里后面的代码会越来越多消息对象、协议解析、网络封装、Dispatcher、RPC、服务注册发现和 Topic 都会逐步接进来。这时有一个很实际的问题必须先解决程序没有崩但结果不对时我们怎么快速判断问题出在哪一步当然可以使用 GDB但如果只是想知道“消息有没有解析成功”“请求有没有进入 Dispatcher”“某个业务处理是否失败”每次都从头单步调试并不划算。因此这一篇先补一个很小的工程工具分级日志宏。它不是什么完整的工业级日志库只解决当前项目最需要的几件事统一日志入口 printf 风格格式化 时间 文件名和行号 Debug / Info / Error 等级过滤后面的源码里我们就可以统一写成DLOG(connection established); ILOG(listen port %d, 8080); ELOG(parse failed: method%s, rcode%d, Add, -1);需要注意的是当前源码中的source/common/detail.hpp不只包含日志宏还包含 JSON 辅助函数和 UUID 生成工具。本篇只展开日志这一部分避免把几个不同的小工具混在一起讲。后面真正用到它们时再结合对应模块说明。一、为什么有 GDB 了还需要日志GDB 和日志解决的问题并不完全一样。如果程序直接崩溃例如出现Segmentation fault这时候我们通常关心崩在哪一行调用栈是什么当前变量是什么状态GDB 很适合处理这类问题。但框架里更多时候遇到的是“程序还能跑只是逻辑不对”。例如服务注册请求发出去了但注册中心没有保存成功消息已经从网络层读出来了但没有进入预期 HandlerRPC 响应回来了但请求 ID 没有匹配到等待中的调用。这时如果关键节点有日志收到数据 → 解析完整消息 → 进入 Dispatcher → 找到 Handler → 业务处理失败。我们往往扫一眼输出就能迅速缩小问题范围。所以可以简单理解成工具更适合处理GDB崩溃、调用栈、内存访问、现场变量日志程序流程、状态变化、业务错误定位它们不是互相替代而是配合使用。二、先从 printf 开始最普通的输出当然是printf(hello world\n);需要带变量时printf(listen port %d\n, 8080);但后面整个项目都会反复打印日志。如果一直直接写printf很快会出现有的忘记换行有的格式不统一有的只打印parse failed却不知道从哪里打印出来。所以第一步不是造一个复杂日志库而是先把这些重复工作收进一个统一入口。最简单的想法是#define LOG(format, ...) \ printf(format \n, ##__VA_ARGS__)这样既可以打印固定文本LOG(hello world);也可以像printf一样传入格式化参数LOG(method %s, port %d, Add, 8080);这里的...表示宏还能接收数量不固定的参数而__VA_ARGS__表示把这些参数继续展开到宏内部。当前源码使用的是##__VA_ARGS__这是 GCC / GNU 环境中常见的写法用来兼容没有额外可变参数的调用例如LOG(hello world);我们的项目本身就在 Linux GCC 环境中开发因此这里直接沿用当前实现即可。三、日志至少要告诉我们“从哪里打印的”只有正文还不够。如果项目运行时输出parse failed我们马上还会问哪个文件哪一行C/C 已经提供了两个很实用的预定义宏__FILE__ __LINE__其中__FILE__ → 当前源文件__LINE__ → 当前代码所在行号于是日志就可以组织成printf([%s:%d]\t format \n, __FILE__, __LINE__, ##__VA_ARGS__);最终输出会类似[rpc_router.hpp:128] parse failed相比只打印一句错误这已经更适合真正的项目调试了。四、再给日志加上时间服务端不是运行几秒就退出的小 Demo。以后连接建立、请求处理、服务上线下线等日志会不断出现。如果没有时间信息我们很难判断事件发生的先后和间隔。当前实现使用的是标准 C 时间接口time_t t time(nullptr); struct tm *lt localtime(t); char time_tmp[32] {0}; strftime(time_tmp, 31, %m-%d %T, lt);这里的处理过程可以看成time() ↓ 拿到当前系统时间 ↓ localtime() ↓ 拆成年、月、日、时、分、秒 ↓ strftime() ↓ 整理成字符串当前格式%m-%d %T最终会得到类似09-06 02:58:10然后再和文件名、行号组合到一起[09-06 02:58:10][test_log.cpp:5] parse failed对于当前这个轻量级日志工具来说这些信息已经足够用了。五、为什么还要区分 Debug、Info、Error开发阶段我们往往希望知道程序每一步在做什么开始解析消息读取消息类型创建消息对象进入 Dispatcher这些内容很适合调试但项目稳定以后如果每个细节都打印出来终端很快就会被刷满。所以当前源码定义了三个日志等级#define LDBG 0 #define LINFO 1 #define LERROR 2对应关系很简单等级用途LDBG开发阶段观察内部流程LINFO记录正常运行中的重要状态LERROR记录需要重点关注的错误同时定义当前最低输出等级#define LDEFAULT LDBG真正的过滤逻辑只有一句if ((level) LDEFAULT)例如当前#define LDEFAULT LDBG那么三个等级都会输出。如果改成#define LDEFAULT LINFO那么Debug 0 1 → false不输出 Info 1 1 → true输出 Error 2 1 → true输出所以LDEFAULT表示的不是“当前这条日志是什么等级”而是最低允许输出到什么等级。六、统一的 LOG 宏把前面的东西组合起来就是当前源码里的核心实现#define LOG(level, format, ...) \ { \ if ((level) LDEFAULT) \ { \ time_t t time(nullptr); \ struct tm *lt localtime(t); \ char time_tmp[32] {0}; \ strftime(time_tmp, 31, %m-%d %T, lt); \ printf([%s][%s:%d]\t format \n, \ time_tmp, __FILE__, __LINE__, ##__VA_ARGS__); \ } \ }它一共完成了四件事按日志等级决定是否输出获取当前时间自动加入文件名和行号使用 printf 风格输出正文然后再在上面包装三个真正给业务代码使用的接口#define DLOG(format, ...) LOG(LDBG, format, ##__VA_ARGS__) #define ILOG(format, ...) LOG(LINFO, format, ##__VA_ARGS__) #define ELOG(format, ...) LOG(LERROR, format, ##__VA_ARGS__)这样业务代码就不用每次自己写日志等级DLOG(recv message); ILOG(listen port %d, 8080); ELOG(invalid message type: %d, mtype);从使用者角度看只需要记住DLOG → 调试信息;ILOG → 正常的重要状态;ELOG → 错误信息.七、从一条 ELOG() 看宏到底做了什么假设后面的业务代码写ELOG(parse failed: mtype%d, 5);第一层会展开成LOG(LERROR, parse failed: mtype%d, 5);进入LOG后再依次完成检查 LERROR 是否达到 LDEFAULT ↓ 获取当前时间 ↓ 格式化时间字符串 ↓ 取出 __FILE__ / __LINE__ ↓ printf 输出正文所以一条很短的ELOG(...);背后实际上已经统一处理了等级过滤 时间 源码位置 格式化正文这也是我们封装这一层的主要价值。八、用当前测试代码验证一下当前仓库里已经有一个最小测试#include ./commom/detail.hpp #include iostream int main() { DLOG(hello debug); ILOG(listen port %d, 8080); ELOG(parse failed: method%s, rcode%d, Add, -1); return 0; }日志相关宏单独编译运行后输出格式类似实际时间和行号会随着运行环境变化。如果想观察等级过滤可以把#define LDEFAULT LDBG临时改成#define LDEFAULT LINFO再次运行时DLOG就不会再打印而ILOG和ELOG仍然保留。写到最后这一篇没有推进 RPC 主流程却补上了后面会频繁使用的一块基础工具。现在我们已经可以在项目里统一使用DLOG(...); ILOG(...); ELOG(...);来记录程序走到哪里 关键状态发生了什么变化 错误从哪一个文件、哪一行产生下一篇我们回到消息主线把第五篇里的抽象BaseMessage真正落成具体消息对象JsonMessage ↓ Request / Response ↓ RPC / Topic / Service 消息 ↓ MessageFactory到那时前面定义的公共字段和这一篇的日志工具都会真正开始参与框架实现。
返回列表