前言
现在几乎每个软件在开发和运维时,都需要借助log进行调试和监控。我们一般会使用开源的log库,或自己内部实现一个专用的log库,然后业务代码调用这些库提供的头文件中的日志输出接口进行日志输出。那么,如果当前库提供的日志格式不适合业务使用,或业务需要添加一些定制功能,可能就需要修改这些log头文件进行适配了。为了便于修改和扩展,我们可以使用预编译期编程的方式(宏函数、constexpr、模板函数等)实现代码实现log
但是考虑到这些库的版本可能会不断更新升级,且我们想要添加的“定制功能”只是当前模块使用,并不是全部系统共同使用的等原因,我们尽量不要去修改这些log头文件,而是基于这些头文件中原有接口,我们再封装一层log接口进行定制实现。
因为现在的系统普遍性的对性能有比较高的要求,至少不能有过多的性能浪费,而log又不可避免的要进行输出,所以我们在编写或使用log相关代码时,尽量避免可能导致性能变差的函数或操作,如果避免不了的话也要尽量减少这些操作的使用频率。
对于既要考虑代码扩展性和预编译期编写代码,又要考虑log函数对性能的影响,那么我们怎样实现在原有log上实现特殊业务的扩展呢?
下面我就以通过宏拼接的方式介绍一个范例的设计方案和编码demo,供大家参考分析。
范例需求
比如我们现在有个xx系统,但是基于现有日志都是英文,尤其是ERR日志不便于运维查看和应急,所以客户要求能在监控系统中看到ERR日志的中文说明。
范例设计分析
- 日志长度是有限制的,所以不能英文和中文一起输出,而且一起输出的话显示也比较乱。
- 现有代码中的ERR日志比较多,也不能对每一行ERR日志都输出一行中文说明。否则和英文输出内容输出重复输出了。而且很多日志是有一定共性的。
- 中文说明格式需要统一管理
综上:我们根据日志进行梳理、分类,在原有日志代码的基础上输出对应的日志分类标签,并使用特殊字符标注出标签信息。监控系统监控日志时,根据解析出的标签到对应的数据字典中查看提前录入的中文提示。
涉及的知识点
1.使用c++的string类或字符串拼接函数(strcat、snprintf等)是比较耗费性能的,所以需要使用不影响性能的形式进行编写,比如使用宏的符号替换特性。
2.非常常见的一个宏的拼接和展示字符串的宏定义:
#define CONTACT(x,y) x##y //拼接
#define STR(x) #x //转字符串
编码实现
#include <iostream>
//---------------底层log头文件范例,实际代码的buf创建和printf不在这里,且实际会比这复杂的多(不能修改)-----------------------------
//底层log头文件中的宏函数定义
#define FUN_TEST_LABEL(log_level,...)do{\
if (log_level > 1)\
{\
char buf[1024];\
snprintf(buf, 1024, __VA_ARGS__);\
printf("%s", buf);\
}\
}while(0)
//---------------业务封装的自己使用的log头文件范例-----------------------------
//本次添加的“中转字符用的宏”
#define __TO_STR(LABEL) @LABEL@ // 字符串拼接
//下面两行的效果一样,注意:低版本编译器使用第一个时会提示“does not give a valid preprocessing token”错误
//#define PRINTF_TEST(LABEL, ...) #LABEL ## __VA_ARGS__// 宏参数拼接成符号
#define PRINTF_TEST(LABEL, ...) #LABEL __VA_ARGS__// 宏参数拼接成符号
#define PRINTF_TEST_TEMP(LABEL, ...) PRINTF_TEST(LABEL, __VA_ARGS__)// 宏参数拼接成符号
//业务使用log
#define LOG_PRINTF_TEST(log_level,LABEL, ...) FUN_TEST_LABEL(log_level,PRINTF_TEST_TEMP(__TO_STR(LABEL),__VA_ARGS__))// 宏参数拼接成符号
//日志标签定义----当前我们在日志中输出这些变量对应的名称(字符串),数值在log中不使用,其他日志扩展功能使用。
const int LOG_LABEL_ERROR = 1001;//问题产生原因:xxx 涉及范围:xxx 应急建议:xxx
//---------------业务代码中使用log的cpp代码范例-----------------------------
int main()
{
int a = 666;
LOG_PRINTF_TEST(5,LOG_LABEL_ERROR, "%s() hello! %d \n", __FUNCTION__,a);
return 0;
}
运行结果:
@LOG_LABEL_ERROR@main() hello! 666
宏代码展开效果
备注:
可以通过https://cppinsights.io/网站提供的在线编译工具查看预编译展开代码,也可以通过预处理编译命令(“$ g++ -o test.i -E test.cpp -std=c++11”)生成.i文件进行查看。
详细操作方法可以参考我以前的文章:《简单易用的C++在线编译工具,你值得拥有!》《探秘!编译器眼中不一样的变量名》
补充说明
- 宏是在预编译期进行的字符替换,如果代码编译不通过的话,也是无法生成.i文件进行查看。
- 宏对应的代码不方便调试和查看,在预编译期进行展开后,也会看到影响了原有代码的内容和代码行。
所以,大家在使用宏函数前,最好在cppinsight上编写个demo,看看代码展开后的效果,这样开发效率能高一些。
关于宏的特点,大家可以参考我以前的文章:《「C++」define、const、constexpr分别有哪些特点?》《代码使用宏和const,到底哪个好?》