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'},全部都会被当成字符串来匹配,直到遇到集合外的字符为止;
格式转换"%[^= \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,可以由编译器推导出T的类型。C中,zc_arraylist为了更加通用,其array成员是void **,存放的数据是void*类型。因此, zc_arraylist的使用者必须维护存放的数据类型、空间申请和释放、大小比较等。

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 {
    ...
}

代码更简洁。当然,如果在多次比较的场景中,这样做会重复调用相同的比较函数,效率低,反而不推荐;如果仅仅是单次比较,则推荐这种方式。

posted @ 2022-09-16 17:30  明明1109  阅读(1187)  评论(0编辑  收藏  举报