zlog日志库源码解析 —— 日志等级level及其列表level_list
日志等级
从zlog帮助文档以及zlog.h源码文件,容易知道zlog默认支持下面几个日志等级(优先级从低到高):DEBUG、INFO、NOTICE、WARN、ERROR、FATAL。
// zlog.h
/* 日志消息等级 */
typedef enum {
ZLOG_LEVEL_DEBUG = 20,
ZLOG_LEVEL_INFO = 40,
ZLOG_LEVEL_NOTICE = 60,
ZLOG_LEVEL_WARN = 80,
ZLOG_LEVEL_ERROR = 100,
ZLOG_LEVEL_FATAL = 120,
} zlog_level;
// 对应内置默认等级(未在配置文件中)
DEBUG = 20, LOG_DEBUG
INFO = 40, LOG_INFO
NOTICE = 60, LOG_NOTICE
WARN = 80, LOG_WARNING
ERROR = 100, LOG_ERR
FATAL = 120, LOG_ALERT
UNKNOWN = 254, LOG_ERR
添加一个自定义日志等级
APP如果想要调用API打印日志消息,可以调用这几个接口zlog_debug、zlog_info、zlog_notice、zlog_warn、zlog_error、zlog_fatal。但是,如果我们需要添加一个自定义等级,比如"TRACE",优先级(或成日志等级值)为30,要如何处理?
答案是可以通过修改配置文件(.conf),添加新的自定义等级(优先级需要位于[1,253]),比如:
[global]
default format = "%V %v %m%n"
[levels]
TRACE = 30, LOG_DEBUG
[rules]
my_cat.TRACE >stdout;
zlog内部就会生成一个新的日志等级:
TRACE = 30, LOG_DEBUG
而在APP中,为了使用新建的日志等级,需要添加一个.h文件,为该日志等级定义接口。
#include "zlog.h"
// APP自定义源码
enum {
ZLOG_LEVEL_TRACE = 30, /* 必须与.conf配置文件一致 */
};
#define zlog_trace(cat, format, ...) \
zlog(cat, __FILE__, sizeof(__FILE__)-1, \
__func__, sizeof(__func)-1, __LINE__, \
ZLOG_LEVEL_TRACE, format, ## __VA_ARGS__)
这样,在用户的.c文件中,就能调用zlog_trace打印日志等级为TRACE的日志了。
level日志等级的表示
zlog中如何表示一个日志等级的呢?
Re:使用结构体zlog_level_t,包含等级数值,日志等级字符串名称(如"TRACE",支持大小和小写),syslog日志等级等。
/**
* 自定义日志等级
*
* 完整日志等级字符串,
* e.g.
* "TRACE = 30, LOG_DEBUG"
* "INFO = 40, LOG_INFO"
*/
typedef struct zlog_level_s {
int int_level; /* 日志等级数值, 来源于日志等级字符串 */
char str_uppercase[MAXLEN_PATH + 1]; /* 大写日志等级名字符串 */
char str_lowercase[MAXLEN_PATH + 1]; /* 小写日志等级名字符串 */
size_t str_len; /* str_uppercase字符串长度 */
int syslog_level; /* syslog日志等级, 来源于日志等级字符串 */
} zlog_level_t;
思考:既然str_uppercase/str_lowercase以字符串表示,为何还额外维护str_len表示其长度呢?
Re:因为str_uppercase/str_lowercase的数据来源于外部,有可能会溢出,此时,str_len就能起到作用。另外,profile打印对象信息时,可以很快速知道字符串长度,不用重复计算。
level日志等级的接口
zlog_level_t的接口很简单,只有构造、析构、profile(打印整个对象的栈信息):
zlog_level_t *zlog_level_new(char *line);
void zlog_level_del(zlog_level_t *a_level);
void zlog_level_profile(zlog_level_t *a_level, int flag);
level日志接口的实现
构造函数
zlog_level_new通过一个配置行,如上面.conf文件的"TRACE = 30, LOG_DEBUG",来构造构造一个zlog_level_t对象。配置文件内容有3个关键内容:日志等级名"TRACE",日志等级值"30",syslog日志等级名"LOG_DEBUG"。构造函数主要工作,就是提前关键内容,然后转换为相应的类型存储到对象中。
另外,构造函数必须遵循原子性的规则,即构造错误立即回滚(rollback)。在zlog中,这点通过goto + 析构函数来实现。
/**
* 由自定义日志等级字符串构造zlog_level_t对象
*
* 自定义日志等级, 示例:
* [levels]
* TRACE = 30, LOG_DEBUG
*
* 此时, line = "TRACE = 30, LOG_DEBUG", 那么,
* "TRACE"是用户自定义等级名称, "30"是对应等级值(int_level),
* "LOG_DEBUG"是syslog level名称(str_uppercase/str_lowercase, 长str_len)
*/
zlog_level_t *zlog_level_new(char *line)
{
zlog_level_t *a_level = NULL;
int i;
int nscan;
char str[MAXLEN_CFG_LINE + 1]; // 自定义日志等级名
int lv = 0; // 自定义日志等级数值
char sl[MAXLEN_CFG_LINE + 1]; // 自定义syslog 等级名
zc_assert(line, NULL);
memset(str, 0x00, sizeof(str));
memset(sl, 0x00, sizeof(sl));
// 从line读取日志等级名str, 数值lv, syslog 等级名sl
// FIXME: cppcheck warning: sscanf() without field width limits can crash with huge input data.
// nscan = sscanf(line, " %" MAXLEN_CFG_LINE "[^= \t] = %d ,%" MAXLEN_CFG_LINE" s", str, &l, sl);
nscan = sscanf(line, " %4096[^= \t] = %d ,%1024s", str, &lv, sl);
if (nscan < 2) {
zc_error("level[%s], syntax wrong", line);
return NULL;
}
/* check level and str */
if ((lv < 0) || (lv > 255)) {
zc_error("lv[%d] not in [0,255], wrong", lv);
return NULL;
}
if (str[0] == '\0') {
zc_error("str[0] = 0");
return NULL;
}
a_level = calloc(1, sizeof(zlog_level_t));
if (!a_level) {
zc_error("calloc fail, errno[%d]", errno);
return NULL;
}
/* 注意lv来源于自定义日志等级字符串, 将等级字符串转换为整型后的数值 */
a_level->int_level = lv;
/* fill syslog level */
if (sl[0] == '\0') { /* syslog等级字符串未填写时, 设置默认的syslog level */
a_level->syslog_level = LOG_DEBUG;
} else {
a_level->syslog_level = syslog_level_atoi(sl); /* 将syslog等级字符串转换为数值 */
if (a_level->syslog_level == -187) {
zc_error("syslog_level_atoi fail");
goto err;
}
}
/* 拷贝日志等级名字符串, 将其转换为全大小和全小写 */
/* strncpy and toupper(str), tolower(str) */
for (i = 0; (i < sizeof(a_level->str_uppercase) - 1) && str[i] != '\0'; i++) {
(a_level->str_uppercase)[i] = toupper(str[i]);
(a_level->str_lowercase)[i] = tolower(str[i]);
}
if (str[i] != '\0') {
/* overflow */
zc_error("not enough space for str, str[%s] > %d", str, i);
goto err;
} else { /* 末尾添加NUL-terminated byte */
(a_level->str_uppercase)[i] = '\0';
(a_level->str_lowercase)[i] = '\0';
}
a_level->str_len = i; /* str_uppercase 字符串长度 */
// zlog_level_profile(a_level, ZC_DEBUG);
return a_level;
err:
zc_error("line[%s]", line);
zlog_level_del(a_level);
return NULL;
}
- sscanf从line中截取数据
利用sscanf读取数据时,有一个非常巧妙的用法。4096、1024是为了限制字符串长度,避免溢出以及CppCheck警告,当然这也会让代码的移植性降低。
格式转换"%[= \t]",表示匹配"[]"(表示集合)中的任意字符,即{'=', ' '(
格式转换"%[^= \t]",表示匹配任意字符,但除了"[]"中的字符,也就是说,str会从line首部开始匹配任意字符,一直到遇到{'=', ' ', '\t'}中任意字符为止。如果line = "TRACE = 30, LOG_DEBUG",那么str = "TRACE"(遇到空格停止了);
后面lv会跟"30"匹配,转换成整型;
sl会跟"LOG_DEBUG"匹配。
nscan = sscanf(line, " %4096[^= \t] = %d ,%1024s", str, &lv, sl);
- 字符串长度计算
充分利用字符串大小写转换时遍历字符串的过程,将下标i赋值给长度str_len,而不是再重新调用strlen计算长度,能节省时间。
int i;
...
/* strncpy and toupper(str), tolower(str) */
for (i = 0; (i < sizeof(a_level->str_uppercase) - 1) && str[i] != '\0'; i++) {
(a_level->str_uppercase)[i] = toupper(str[i]);
(a_level->str_lowercase)[i] = tolower(str[i]);
}
...
a_level->str_len = i; /* str_uppercase 字符串长度 */
- 将sl(syslog 日志等级字符串)转换为整型
zlog_level_t是用整型syslog_level存放日志等级,而syslog_level支持的日志等级是固定的(如宏LOG_EMERG、LOG_ALERT etc.)。如何转换?level模块专门提供了这样一个函数syslog_level_atoi。
/**
* 将syslog level字符串转换为整型
* @param str 要转换的字符串
* @details syslog只支持几个固定的level
* @return 转换成功返回 >= 0的值, 即{LOG_EMERG,LOG_ALERT,LOG_CRIT,LOG_ERR,LOG_WARNING,
* LOG_NOTICE,LOG_INFO,LOG_DEBUG } 中的一个; 失败, 返回-187.
* @see
* https://man7.org/linux/man-pages/man3/syslog.3.html
*/
static int syslog_level_atoi(const char *str)
{
/* guess no unix system will choose -187 (a magic number defined by zlog)
* as its syslog level, so it is a safe return value
*/
zc_assert(str, -187);
if (STRICMP(str, ==, "LOG_EMERG")) // 忽略大小写, 比较字符串str与"LOG_EMERG"是否相等
return LOG_EMERG;
if (STRICMP(str, ==, "LOG_ALERT"))
return LOG_ALERT;
if (STRICMP(str, ==, "LOG_CRIT"))
return LOG_CRIT;
if (STRICMP(str, ==, "LOG_ERR"))
return LOG_ERR;
if (STRICMP(str, ==, "LOG_WARNING"))
return LOG_WARNING;
if (STRICMP(str, ==, "LOG_NOTICE"))
return LOG_NOTICE;
if (STRICMP(str, ==, "LOG_INFO"))
return LOG_INFO;
if (STRICMP(str, ==, "LOG_DEBUG"))
return LOG_DEBUG;
zc_error("wrong syslog level[%s]", str);
return -187;
}
- 错误处理及回滚
当申请了系统资源后,但后面过程出错,一定要记得回滚,确保资源及时释放,以免造成内存泄露。zlog是怎么做的呢?且看构造函数的一部分:
a_level = calloc(1, sizeof(zlog_level_t));
if (!a_level) {
zc_error("calloc fail, errno[%d]", errno);
return NULL;
}
...
/* fill syslog level */
if (sl[0] == '\0') { /* syslog等级字符串未填写时, 设置默认的syslog level */
...
} else {
a_level->syslog_level = syslog_level_atoi(sl); /* 将syslog等级字符串转换为数值 */
if (a_level->syslog_level == -187) {
zc_error("syslog_level_atoi fail");
goto err;
}
}
...
return a_level;
err:
zc_error("line[%s]", line);
zlog_level_del(a_level);
return NULL;
在calloc申请内存资源成功以后,如果再发生错误时,巧妙地利用goto到错误处理代码块(label err)。err代码块利用自诊断error log打印error信息,析构level对象,返回NULL。
析构函数
zlog_level_t对象的析构非常简单,因为各成员都是基本类型,只需要释放a_level本身即可。
/**
* 释放a_level对象空间
*/
void zlog_level_del(zlog_level_t *a_level)
{
zc_assert(a_level,);
zc_debug("zlog_level_del[%p]", a_level);
free(a_level);
}
profile打印栈信息
debug或排错时,我们经常需要查看对象的栈信息,打印出对象地址、各成员的值等信息,这需要模块本身支持。zlog为每个具有实体意义的模块,定义了profile接口。level模块的profile实现如下:
/**
* 打印level对象信息
*/
void zlog_level_profile(zlog_level_t *a_level, int flag)
{
zc_assert(a_level, );
zc_profile(flag, "---level[%p][%d,%s,%s,%d,%d]---",
a_level,
a_level->int_level,
a_level->str_uppercase,
a_level->str_lowercase,
(int)a_level->str_len,
a_level->syslog_level);
}
原则:如果基本类型的成员,用zc_profile按其类型的格式化后打印;
如果是对象类型,就调用对应模块的profile接口。
level_list日志等级列表
实践中,我们可能会定义多个自定义日志等级,而level只能表示一个日志等级,多个日志等级需要用level_list。level_list本质上是一个zc_arraylist(前面文章【zlog日志库源码解析 —— 数据结构:动态列表 zc_arraylist】提到的动态列表),因此,level_list并没有单独定义数据结构,只提供了接口。
思考:为什么不直接用zc_arraylist来表示levelt_list,而是要单独划分一个模块level_list?
Re:因为zc_arraylist不同于C++中的vector
level_list接口
level_list的接口中,除了常规的构造与析构、profile,还提供了部分arraylist特有的接口:set设置(或添加)指定位置的level,get获取指定日志等级值的level。还有一个是level_list特有的:atoi根据日志等级名称查找对应日志等级值。
/**
* 日志等级列表
* 相当于zc_arraylist_t<zlog_level_t*>
*/
zc_arraylist_t *zlog_level_list_new(void);
void zlog_level_list_del(zc_arraylist_t *levels);
void zlog_level_list_profile(zc_arraylist_t *levels, int flag);
/* conf init use, slow
* if line is wrong or str=="", return -1
*/
int zlog_level_list_set(zc_arraylist_t *levels, char *line);
/* spec output use, fast
* rule output use, fast
* if not found, return levels[254]
*/
zlog_level_t *zlog_level_list_get(zc_arraylist_t *levels, int lv);
/* rule init use, slow
* if not found, return -1
*/
int zlog_level_list_atoi(zc_arraylist_t *levels, char *str);
level_list实现
构造与析构
构造函数本质上是构造一个zc_arraylist对象levels,然后为其添加一些初始数据。
想一想:
1)在构造zc_arraylist对象时,为什么要提供删除器zlog_level_del呢?
Re:因为zc_arraylist中的元素是void类型,zc_arraylist并不知道要如何释放其空间,而该什么时候释放确由zc_arraylist负责,因此可以通过回调元素的删除器来删除元素。
对于level_list,元素是level(zlog_level_t类型),因此删除器就是level对象的析构函数zlog_level_del。
/**
* 相当于C++:
* zlog_level_list = zc_arraylist_t<zlog_level_t*>
*/
zc_arraylist_t *zlog_level_list_new(void)
{
zc_arraylist_t *levels;
levels = zc_arraylist_new((zc_arraylist_del_fn)zlog_level_del);
if (!levels) {
zc_error("zc_arraylist_new fail");
return NULL;
}
if (zlog_level_list_set_default(levels)) {
zc_error("zlog_level_list_set_default fail");
goto err;
}
// zlog_level_list_profile(levels, ZC_DEBUG);
return levels;
err:
zlog_level_list_del(levels);
return NULL;
}
2)level_list需要什么样的初始数据?
level_list表示日志等级列表,而zlog内置了DEBUG、INFO、NOTICE、WARN、ERROR、FATAL这几个等级,而无需配置文件再额外定义。函数zlog_level_list_set_default 提供了这部分功能,还额外提供了两个额外的匹配"* = 0"(匹配任意日志等级名称),"! = 255"(匹配垃圾日志等级名称,即没有与其他日志等级名称匹配的)。
/**
* 以字符串形式, 往levels列表中添加默认的内置日志等级
* @details 内置的日志等级, conf配置文件无需边界
*/
static int zlog_level_list_set_default(zc_arraylist_t *levels)
{
return zlog_level_list_set(levels, "* = 0, LOG_INFO")
|| zlog_level_list_set(levels, "DEBUG = 20, LOG_DEBUG")
|| zlog_level_list_set(levels, "INFO = 40, LOG_INFO")
|| zlog_level_list_set(levels, "NOTICE = 60, LOG_NOTICE")
|| zlog_level_list_set(levels, "WARN = 80, LOG_WARNING")
|| zlog_level_list_set(levels, "ERROR = 100, LOG_ERR")
|| zlog_level_list_set(levels, "FATAL = 120, LOG_ALERT")
|| zlog_level_list_set(levels, "UNKNOWN = 254, LOG_ERR")
|| zlog_level_list_set(levels, "! = 255, LOGI_NFO");
}
level_list的析构则非常简单,只需要释放在new中申请的资源即可。当然,通过插入接口添加的资源,也能通过zc_arraylist_del析构函数释放。
void zlog_level_list_del(zc_arraylist_t *levels)
{
zc_assert(levels, );
zc_arraylist_del(levels);
zc_debug("zlog_level_list_del[%p]", levels);
}
profile打印栈信息
level_list的profile函数同样很简单,由于没有独立的数据结构及成员,只需要遍历levels列表的每个成员,调用其profile即可。
void zlog_level_list_profile(zc_arraylist_t *levels, int flag)
{
int i;
zlog_level_t *a_level;
zc_assert(levels, );
zc_profile(flag, "--level_list[%p]--", levels);
zc_arraylist_foreach(levels, i, a_level) {
/* skip empty slots */
if (a_level) zlog_level_profile(a_level, flag);
}
}
插入数据
既然level_list也是zc_arraylist,那么如何插入数据呢?数组的索引代表什么呢?
对于level_list,zlog是将其日志等级数值([1,253])作为数组索引,而不是一个无意义数字。这样做的好处是,查找速度达O(1);代价是需要用额外的数组空间来存储空位置,而且必须对日志等级范围做限制。
简单来说,动态列表level_list:索引 —— 日志等级值,元素值 —— level对象(zlog_level_t*)。
往zc_arraylist的指定位置插入数据,这就需要用到set接口。而该接口会自动对zc_arraylist进行扩容,以达到在指定位置插入元素的目的。
/**
* 向列表levels中插入一个日志等级字符串line, 列表位置为line字符串中"="右边的数值.
* @param levels 日志等级列表
* @param line 一行日志等级字符串, 通常来源于conf文件[levels]部分
* @return 插入成功返回0; 失败返回-1
*/
int zlog_level_list_set(zc_arraylist_t *levels, char *line)
{
zlog_level_t *a_level;
a_level = zlog_level_new(line);
if (!a_level) {
zc_error("zlog_level_new fail");
return -1;
}
/* int_level在构造a_level时, 初始化为line字符串的日志等级值 */
if (zc_arraylist_set(levels, a_level->int_level, a_level)) {
zc_error("zc_arraylist_set fail");
goto err;
}
return 0;
err: /* 异常处理, 确保原子性 */
zc_error("line[%s]", line);
zlog_level_del(a_level);
return -1;
}
根据日志等级值查找元素
动态列表存储数据的部分是一个数组,而数组中查找数据只需要提供索引即可。level_list中,索引就是日志等级值。
/**
* 根据日志等级值lv查找日志等级, 因为lv就是列表索引, 因此时间复杂度O(1)
*/
zlog_level_t *zlog_level_list_get(zc_arraylist_t *levels, int lv)
{
zlog_level_t *a_level;
a_level = zc_arraylist_get(levels, lv);
if (a_level) {
return a_level;
} else {
/* empty slot */
zc_error("lv[%d] not in (0,254), or has no level defined,"
"see configure file define, set to UNKOWN", lv);
return zc_arraylist_get(levels, 254); // UNKNOWN = 254
}
}
根据日志等级名查找等级值
假设只知道日志等级的名字,如"INFO" "TRACE",如何知道它们的日志等级值呢?
可以通过zlog_level_list_atoi,在levels列表中,顺序查找第一个名称相同的日志等级对应的等级值。
/**
* 在列表levels中查找str字符串对应的日志等级
* @param levels 自定义日志等级列表
* @param str 待查找字符串
* @details levels的类型相当于C++ zc_arraylist_t<zlog_level_t>
*/
int zlog_level_list_atoi(zc_arraylist_t *levels, char *str)
{
int i;
zlog_level_t *a_level;
if (str == NULL || *str == '\0') {
zc_error("str is [%s], can't find level", str);
return -1;
}
zc_arraylist_foreach(levels, i, a_level) {
/* 忽略大小写的字符串比较 */
if (a_level && STRICMP(str, ==, a_level->str_uppercase)) {
return i;
}
}
zc_error("str[%s] can't found in level list", str);
return -1;
}
知识点
宏函数STRCMP比较字符串
C为我们提供了字符串比较函数strcmp系列函数,但使用起来较为繁琐,而且不直观。比如,我们要比较"aabb","abbb" 两个字符串(字典)大小,需要这样做:
const char *str1 = "aabb";
const char *str2 = "abbb";
int res = strcmp(str1, str2);
if (res == 0) { // str1 == str2
...
} else if (res < 0) { // str1 < str2
...
} else {// str1 > str2
...
}
有没有一种办法,可以直观比较str1, str2,比如 "cmp(str1, fstr2, <)"?
zlog在zc_xplatform.h文件中,提供了这样一组宏函数:
/* 字符串比较 */
#define STRCMP(_a_, _C_, _b_) ( strcmp(_a_, _b_) _C_ 0 )
#define STRNCMP(_a_, _C_, _b_, n) ( strncmp(_a_, _b_, _n_) _C_ 0 )
#define STRICMP(_a_, _C_, _b_) ( strcasecmp(_a_, _b_) _C_ 0 )
#define STRNICMP(_a_, _C_, _b_, _n_) ( strncasecmp(_a_, _b_, _n_) _C_ 0 )
这样,上面繁琐的比较代码可以改写成:
const char *str1 = "aabb";
const char *str2 = "abbb";
if (STRCMP(str1, ==, str2)) {
...
} else if (STRCMP(str1, <, str2)) {
...
} else {
...
}
代码更简洁。当然,如果在多次比较的场景中,这样做会重复调用相同的比较函数,效率低,反而不推荐;如果仅仅是单次比较,则推荐这种方式。