zlog 使用笔记

时间:2024-10-07 19:08:22

简介

GitHub 网址:GitHub - HardySimpson/zlog: A reliable, high-performance, thread safe, flexsible, clear-model, pure C logging library.

zlog 是一个精简、可靠、高性能、线程安全、灵活、模型清晰、纯 C 日志库。

事实上,在 C 的世界里面没有特别好的日志函数库(就像JAVA里面的的log4j,或者C++的log4cxx)。syslog 又专为系统使用而设计,速度慢,功能比较单调。

zlog 在效率、功能、安全性上大大超过了 log4c,并且是用 c 写成的,具有比较好的通用性。

zlog 的目标是成为一个简而精的日志函数库,不会直接支持日志内容的过滤和解析、网络输出、数据库写入等特性。原因很明显,日志库是被应用程序调用的,所有花在日志库上的时间都是应用程序运行时间的一部分,而上面说的这些操作都很费时间,会拖慢应用程序的速度。这些事儿应该在别的进程或者别的机器上做。如果你需要这些特性,我建议使用 rsyslog、zLogFabric、Logstash,这些日志搜集、过滤、存储软件,当然这是单独的进程,不是应用程序的一部分。

zlog有这些特性:

  • syslog 模型,优于 log4j 模型
  • 日志格式自定义
  • 多个输出目标,包括静态文件路径、动态文件路径、stdout、stderr、syslog、用户定义的输出
  • 运行时手动或自动刷新配置(安全)
  • 高性能,每秒 250'000 个日志,比使用 rsyslogd 的 syslog(3) 快约 1000 倍
  • 用户可自定定义日志级别
  • 线程安全和进程安全的日志文件轮换
  • 微秒级精度
  • dzlog,一个默认的类别日志 API,便于使用
  • MDC,log4j 样式的键值映射
  • 可自调试,可在运行时输出ZLOG的自调试和错误日志
  • 没有外部依赖,只是基于 POSIX 系统和符合 C99 的 vsnprintf。

兼容性说明

  1. zlog 是基于 POSIX 的。目前我手上有的环境只有AIX和linux。在其他的系统下(FreeBSD, NetBSD, OpenBSD, OpenSolaris, Mac OS X...)估计也能行,有问题欢迎探讨。
  2. zlog 使用了一个C99兼容的vsnprintf。也就是说如果缓存大小不足,vsnprintf将会返回目标字符串应有的长度(不包括’\0’)。如果在你的系统上vsnprintf不是这么运作的,zlog就不知道怎么扩大缓存。如果在目标缓存不够的时候vsnprintf返回-1,zlog就会认为这次写入失败。幸运的是目前大多数c标准库符合C99标准。glibc 2.1,libc on AIX, libc on freebsd...都是好的,不过glibc2.0不是。在这种情况下,用户需要自己来装一个C99兼容的vsnprintf,来crack这个函数库。我推荐ctrioC99-snprintf
  3. 有网友提供了如下版本,方便其他平台上安装编译。但个人建议,如果能使用源码编译最好不用其他版本,因为这些可能不是最新版。

auto tools版本: GitHub - bmanojlovic/zlog: A reliable, high-performance, thread safe, flexsible, clear-model, pure C logging library.

cmake版本: GitHub - lisongmin/zlog: A reliable, high-performance, thread safe, flexsible, clear-model, pure C logging library.

windows版本: GitHub - lopsd07/WinZlog: Zlog on Windows

编译和使用

zlog 日志库 并非只有几个文件,一般不好包含到项目文件中一起编译,通常都是编译成动态库的形式使用;

【在 Linux 系统中编译】

  1. $ tar -zxvf
  2. $ cd zlog-latest-stable/
  3. $ make
  4. $ sudo make install
  5. or
  6. $ make PREFIX=/usr/local/
  7. $ sudo make PREFIX=/usr/local/ install
  8. PREFIX 表示 zlog 的安装目标。安装后,刷新动态链接器以确保程序可以找到 zlog 库。其他系统类似。
  9. $ sudo vi /etc/
  10. /usr/local/lib
  11. $ sudo ldconfig

测试:我们将安装路径设置源码目录下的 __install 目录(新建的测试目录),

  1. $ mkdir __install
  2. $ make PREFIX=$(pwd)/__install
  3. $ sudo make PREFIX=$(pwd)/__install instal

结果如下图所示:

【交叉编译】

修改源码目录下 src/makefile 里的 cc 为交叉编译器的 gcc,如果有必要再修改 ar 为交叉编译器的 ar,如下图所示:

这里我以 RK3568 板卡供应商提供的编译器为例,执行上述测试步骤,结果如下:

【应用程序调用和链接zlog】

应用程序使用 zlog 很简单,只要在C文件里面加一行 #include ""

链接 zlog 需要 pthread 库,例如:

  1. $ cc -o app -lpthread -I/usr/local/include -L/usr/local/lib -lzlog
  2. 或者
  3. $ cc -c -o -I/usr/local/include #编译
  4. $ cc -o app -L/usr/local/lib -lzlog -lpthread #链接

zlog的使用流程

首先要知道 zlog 中有 3 个重要的概念:

分类(Category):类别用于区分不同类型的日志,它用一个字符串来说明。

格式(Format):用来描述输出日志的格式,比如是否有带有时间戳,是否包含文件位置信息等。

规则(Rule):把分类、级别、输出文件、格式组合起来,决定一条代码中的日志是否输出,输出到哪里,以什么格式输出。

上述的分类、格式、规则等都是写在配置文件中的,配置文件由用户自己编写,我们写一个简单的配置文件 示例:

  1. [formats]
  2. simple = "%d %m%n"
  3. [rules]
  4. my_cat.DEBUG >stdout; simple

这个配置文件指定了类别为 “my_cat” 且级别 >= DEBUG 的日志将输出到标准输出,格式为 simple。

我们写个C程序测试一下:

  1. #include <>
  2. #include "include/"
  3. int main(int argc, char** argv)
  4. {
  5. int rc;
  6. zlog_category_t *c;
  7. //根据配置文件初始化
  8. rc = zlog_init("");
  9. if(rc){
  10. printf("init failed\n");
  11. return -1;
  12. }
  13. //将类别信息读取到内存中
  14. c = zlog_get_category("my_cat");
  15. if(!c){
  16. printf("get cat fail\n");
  17. zlog_fini();
  18. return -2;
  19. }
  20. //输出日志信息
  21. zlog_info(c, "hello, zlog");
  22. zlog_fini();
  23. return 0;
  24. }

编译运行结果如下:

由此可见,zlog 的使用就是先编写配置文件,然后调用日志库的 API 即可。详细的配置和 API 的介绍见下文。

zlog API 详解

zlog 的配置文件是核心,但在学习配置文件之前应该要知道 zlog 库有哪些接口?如何使用?了解了 API 之后再去学习配置文件,结合起来事半功倍。

【初始化和清理操作】

  • int zlog_init(const char *config);

zlog_init() 从配置文件 config 中读取配置。

如果 config 为空,它会查找环境变量 $ZLOG_CONF_PATH 以找到配置文件。如果 $ZLOG_CONF_PATH 也为空,则所有日志将以内部格式输出到 stdout。

每个进程只有第一次调用 zlog_init() 有效,后续调用将失败且不执行任何操作。

成功返回 0,失败返回 -1。详细错误会被写在由环境变量 $ZLOG_PROFILE_ERROR 指定的错误日志里面。

  • int zlog_reload(const char *config);

zlog_reload() 从配置文件 config 中重载配置,并根据这个配置文件来重计算内部的分类规则匹配、重建每个线程的缓存、并设置原有的用户自定义输出函数。

可以在配置文件发生改变后调用这个函数。这个函数使用次数不限。

如果 config 为 NULL,会重载上一次 zlog_init() 或者 zlog_reload() 使用的配置文件。

如果 zlog_reload() 失败,上一次的配置依然有效。所以 zlog_reload() 具有原子性。

成功返回 0,失败返回 -1。详细错误会被写在由环境变量 $ZLOG_PROFILE_ERROR 指定的错误日志里面。

  • void zlog_fini(void);

zlog_fini() 清理所有 zlog API 申请的内存,关闭它们打开的文件。使用次数不限。

【分类(Category)操作】

  1. typedef struct zlog_category_s zlog_category_t
  2. zlog_category_t *zlog_get_category(const char *cname);

zlog_get_category() 从 zlog 的全局分类表里面找到分类,用于以后输出日志。如果没有的话,就建一个。然后它会遍历所有的规则,寻找和 cname 匹配的规则并绑定。

配置文件规则中的分类名匹配 cname 的规则如下:

  1. * 匹配任意 cname。
  2. 以下划线_结尾的分类名同时匹配本级分类和下级分类。例如 aa_ 匹配 aa, aa_, aa_bb, aa_bb_cc 这几个 cname。
  3. 不以下划线_结尾的分类名精确匹配 cname。例如 aa_bb 匹配 aa_bb 这个 cname。
  4. ! 匹配目前还没有规则的 cname。

这个匹配规则在配置文件中还会讲述到!

每个 zlog_category_t * 对应的规则,在 zlog_reload() 的时候会被自动重新计算。不用担心内存释放,zlog_fini() 最后会清理一切。

【日志输出函数及宏】

  1. void zlog(zlog_category_t * category,
  2. const char *file, size_t filelen,
  3. const char *func, size_t funclen,
  4. long line, int level,
  5. const char *format, ...) ZLOG_CHECK_PRINTF(8,9);
  6. void vzlog(zlog_category_t * category,
  7. const char *file, size_t filelen,
  8. const char *func, size_t funclen,
  9. long line, int level,
  10. const char *format, va_list args);
  11. void hzlog(zlog_category_t * category,
  12. const char *file, size_t filelen,
  13. const char *func, size_t funclen,
  14. long line, int level,
  15. const void *buf, size_t buflen);

这3个函数是实际写日志的函数,输入的数据对应于配置文件中的 %m。

category 通过调用 zlog_get_category() 获得;

file 和 line 填写为__FILE__和__LINE__这两个宏,标识日志是在哪里发生的;

func 填写为 __func__ 或者 __FUNCTION__,如果编译器支持的话,如果不支持,就填写为"";

level 是一个整数,应该是在下面几个里面取值。

  1. typedef enum {
  2. ZLOG_LEVEL_DEBUG = 20,
  3. ZLOG_LEVEL_INFO = 40,
  4. ZLOG_LEVEL_NOTICE = 60,
  5. ZLOG_LEVEL_WARN = 80,
  6. ZLOG_LEVEL_ERROR = 100,
  7. ZLOG_LEVEL_FATAL = 120
  8. } zlog_level;

zlog() 和 vzlog() 根据 format 输出,就像 printf(3) 和 vprintf(3)。

vzlog() 相当于 zlog(),只是它用一个 va_list 类型的参数 args,而不是一堆类型不同的参数。vzlog() 内部使用了 va_copy 宏,args 的内容在 vzlog() 后保持不变,可以参考 stdarg(3)。

hzlog() 有点不一样,它产生下面这样的输出,长度为 buf_len 的内存 buf 以16进制的形式表示出来。

hex_buf_len=[5365]

              0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F      0123456789ABCDEF

0000000001   23 21 20 2f 62 69 6e 2f 62 61 73 68 0a 0a 23 20   #! /bin/bash..#

0000000002   74 65 73 74 5f 68 65 78 20 2d 20 74 65 6d 70 6f   test_hex - tempo

0000000003   72 61 72 79 20 77 72 61 70 70 65 72 20 73 63 72   rary wrapper scr

我们一般不会直接使用这么复杂的接口,每个函数都有对应的简单使用的宏(具体可以查看 头文件):

  1. /* zlog macros */
  2. zlog_fatal(cat, format, ...)
  3. zlog_error(cat, format, ...)
  4. zlog_warn(cat, format, ...)
  5. zlog_notice(cat, format, ...)
  6. zlog_info(cat, format, ...)
  7. zlog_debug(cat, format, ...)
  8. /* vzlog macros */
  9. vzlog_fatal(cat, format, args)
  10. vzlog_error(cat, format, args)
  11. vzlog_warn(cat, format, args)
  12. vzlog_notice(cat, format, args)
  13. vzlog_info(cat, format, args)
  14. vzlog_debug(cat, format, args)
  15. /* hzlog macros */
  16. hzlog_fatal(cat, buf, buf_len)
  17. hzlog_error(cat, buf, buf_len)
  18. hzlog_warn(cat, buf, buf_len)
  19. hzlog_notice(cat, buf, buf_len)
  20. hzlog_info(cat, buf, buf_len)
  21. hzlog_debug(cat, buf, buf_len)

这些函数不返回。如果有错误发生,详细错误会被写在由环境变量 ZLOG_PROFILE_ERROR 指定的错误日志里面。

【dzlog 接口】

  1. int dzlog_init(const char *confpath, const char *cname);
  2. int dzlog_set_category(const char *cname);
  3. void dzlog(const char *file, size_t filelen,
  4. const char *func, size_t funclen,
  5. long line, int level,
  6. const char *format, ...) ZLOG_CHECK_PRINTF(7,8);
  7. void vdzlog(const char *file, size_t filelen,
  8. const char *func, size_t funclen,
  9. long line, int level,
  10. const char *format, va_list args);
  11. void hdzlog(const char *file, size_t filelen,
  12. const char *func, size_t funclen,
  13. long line, int level,
  14. const void *buf, size_t buflen);

dzlog 是忽略分类 (zlog_category_t) 的一组简单 zlog 接口。它采用内置的一个默认分类,这个分类置于锁的保护下。这些接口也是线程安全的。忽略了分类,意味着用户不需要操心创建、存储、传输 zlog_category_t 类型的变量。当然也可以在用 dzlog 接口的同时用一般的 zlog 接口函数,这样会更爽。

dzlog_init() 和 zlog_init() 一样做初始化,就是多需要一个默认分类名 cname 的参数。zlog_reload()、 zlog_fini() 可以和以前一样使用,用来刷新配置,或者清理。

dzlog_set_category() 是用来改变默认分类用的。上一个分类会被替换成新的。同样不用担心内存释放的问题,zlog_fini()最后会清理。

dzlog 的宏也定义在 里面。更简单的写法:

  1. dzlog_fatal(format, ...)
  2. dzlog_error(format, ...)
  3. dzlog_warn(format, ...)
  4. dzlog_notice(format, ...)
  5. dzlog_info(format, ...)
  6. dezlog_debug(format, ...)
  7. vdzlog_fatal(format, args)
  8. vdzlog_error(format, args)
  9. vdzlog_warn(format, args)
  10. vdzlog_notice(format, args)
  11. vdzlog_info(format, args)
  12. vdzlog_debug(format, args)
  13. hdzlog_fatal(buf, buf_len)
  14. hdzlog_error(buf, buf_len)
  15. hdzlog_warn(buf, buf_len)
  16. hdzlog_noticebuf, buf_len)
  17. hdzlog_info(buf, buf_len)
  18. hdzlog_debug(buf, buf_len)

成功情况下 dzlog_init() 和 dzlog_set_category() 返回0。失败情况下dzlog_init()和 dzlog_set_category()返回-1。详细错误会被写在由环境变量ZLOG_PROFILE_ERROR指定的错误日志里面。

【MDC 操作】

  1. //向 MDC 中添加一个新的键值对。成功返回0,失败返回-1。
  2. int zlog_put_mdc(const char *key, const char *value);
  3. //从 MDC 中检索与给定键相关联的值。成功返回value的指针,失败或者没有相应的key返回NULL。
  4. char *zlog_get_mdc(const char *key);
  5. //从 MDC 中删除指定的键值对。
  6. void zlog_remove_mdc(const char *key);
  7. //清理 MDC,移除其中所有的键值对。在程序执行完毕或MDC不再需要时,调用此函数可以确保MDC被正确清理,避免内存泄漏。
  8. void zlog_clean_mdc(void);

MDC(Mapped Diagnostic Context)是一个线程本地(thread-local)的键-值表,用于存储诊断信息。

这些函数通常与日志记录框架一起使用,以便在记录日志时包含有关当前执行上下文的信息。例如,在处理Web请求时,你可能会将用户ID、请求ID或其他相关信息放入MDC,然后当日志被记录时,这些信息会自动附加到日志消息中,从而帮助你在后续分析时更好地理解发生了什么。

key 和 value 是字符串,长度不能超过 MAXLEN_PATH(1024),否则会被截断。

MDC 是线程本地的,这意味着每个线程都有自己独立的 MDC 实例。这允许您在多线程环境中为每个线程存储不同的诊断上下文。

MDC 使用键-值对的形式存储信息。您可以向 MDC 添加任意数量的键值对,每个键都是唯一的。

在使用完 MDC 后,应该清理其内容以避免内存泄漏。通常,这可以通过在代码执行完毕时调用 MDC 的清理方法来实现。

添加的键值对如何输出到日志信息中呢?很简单,请看下文的配置文件详解!

如果有错误发生,详细错误会被写在由环境变量ZLOG_PROFILE_ERROR指定的错误日志里面。

【用户自定义输出】

  1. typedef struct zlog_msg_s {
  2. char *buf;
  3. size_t len;
  4. char *path;
  5. } zlog_msg_t;
  6. //输出函数的格式
  7. typedef int (*zlog_record_fn)(zlog_msg_t *msg);
  8. int zlog_set_record(const char *rname, zlog_record_fn record);

zlog允许用户自定义输出函数。输出函数需要绑定到某条特殊的规则上。这种规则的格式在配置文件解析中能够看到。 

zlog_set_record() 用于将输出函数 record 和规则中的输出段 $name 做绑定。

输出函数 record 的具体内容由用户定义。它的参数 msg 用于传递日志信息,具体字段描述如下:

buf 和 len 是 zlog 格式化后的日志信息和长度;

path 来自规则的逗号后的字符串,这个字符串会被动态的解析,输出当前的path,就像动态文件路径一样。

所有zlog_set_record()做的绑定在zlog_reload()使用后继续有效。

zlog_set_record() 成功返回0,失败返回-1。详细错误会被写在由环境变量ZLOG_PROFILE_ERROR指定的错误日志里面。

用户自定义输出的意义是 zlog 放弃一些权力。zlog 只负责动态生成单条日志和文件路径,但怎么输出、转档、清理等等工作由用户按照自己的需求自行写函数完成。写完函数只要绑定到某个规则就可以。这里我把用户自定义输出的几个步骤写下来。

首先需要在配置文件里面定义规则:

  1. $ cat
  2. [formats]
  3. simple = "%m%n"
  4. [rules]
  5. my_cat.* $myoutput, "mypath %c %d";simple
  1. #include <>
  2. #include "include/"
  3. //用户自定义的输出函数
  4. int output(zlog_msg_t *msg)
  5. {
  6. printf("[mystd]:[%s][%s][%ld]\n", msg->path, msg->buf, (long)msg->len);
  7. return 0;
  8. }
  9. int main(int argc, char** argv)
  10. {
  11. int rc;
  12. zlog_category_t *zc;
  13. rc = zlog_init("");
  14. if(rc){
  15. printf("init failed\n");
  16. return -1;
  17. }
  18. zlog_set_record("myoutput", output);
  19. zc = zlog_get_category("my_cat");
  20. if(!zc) {
  21. printf("get cat fail\n");
  22. zlog_fini();
  23. return -2;
  24. }
  25. zlog_info(zc, "hello, zlog");
  26. zlog_fini();
  27. return 0;
  28. }

运行结果如下:

正如你所见,msglen 是12,zlog 生成的 msg 在最后有一个换行符。

用户自定义输出可以干很多神奇的事情,例如:

    1. 日志文件名为
    2. 如果 超过 100M,则生成一个新文件,其中包含的就是 目前的内容 而 则变为空,重新开始增长
    3. 如果距离上次生成文件时已经超过 5 分钟,则即使不到 100M,也应当生成一个新文件
    4. 新文件名称可以自定义,比如加上设备名作为前缀、日期时间串作为后缀
    5. 我希望这个新文件可以被压缩,以节省磁盘空间或者网络带宽。

当然这个需求更多的是涉及到文件转档,相关内容详见后文。

【调试和诊断】

void zlog_profile(void);

环境变量 ZLOG_PROFILE_ERROR 指定 zlog 本身的错误日志。

环境变量 ZLOG_PROFILE_DEBUG 指定 zlog 本身的调试日志。

zlog_profile() 在运行时打印所有内存中的配置信息到 ZLOG_PROFILE_ERROR,可以把这个和配置文件比较,看看有没有问题。

zlog 帮助用户程序写日志,帮助程序员 debug 程序。但是如果 zlog 内部出错了呢?怎么知道错在哪里呢?其他的程序可以用日志库来debug,但日志库自己怎么debug?答案很简单,zlog 有自己的日志——诊断日志。这个日志通常是关闭的,可以通过环境变量来打开。

  1. $ export ZLOG_PROFILE_DEBUG=/tmp/
  2. $ export ZLOG_PROFILE_ERROR=/tmp/

诊断日志只有两个级别debug和error。设置好环境变量后再跑程序,可以看到 debug 日志类似如下:

  1. $ more
  2. 03-13 09:46:56 DEBUG (7503::115) ------zlog_init start, compile time[Mar 13 2012 11:28:56]------
  3. 03-13 09:46:56 DEBUG (7503::825) spec:[0x7fdf96b7c010][%d(%F %T)][%F %T 29][]
  4. 03-13 09:46:56 DEBUG (7503::825) spec:[0x7fdf96b52010][ ][ 0][]
  5. ......
  6. 03-13 09:52:40 DEBUG (8139::291) ------zlog_fini end------

日志没产生,因为没有错误发生。

你可以看出来,debug日志展示了zlog是怎么初始化还有清理的。不过在zlog_info()执行的时候没有日志打出来,这是为了效率。

如果zlog库有任何问题,都会打日志到 ZLOG_PROFILE_ERROR 所指向的错误日志。

比如说,在zlog_info()上用一个错误的printf的语法:

zlog_info(zc, "%l", 1);

然后编译执行程序,ZLOG_PROFILE_ERROR 的日志类似如下:

  1. $ cat
  2. 03-13 10:04:58 ERROR (10102::189) vsnprintf fail, errno[0]
  3. 03-13 10:04:58 ERROR (10102::191) nwrite[-1], size_left[1024], format[%l]
  4. 03-13 10:04:58 ERROR (10102::329) zlog_buf_vprintf maybe fail or overflow
  5. 03-13 10:04:58 ERROR (10102::467) a_spec->gen_buf fail
  6. 03-13 10:04:58 ERROR (10102::160) zlog_spec_gen_msg fail
  7. 03-13 10:04:58 ERROR (10102::265) zlog_format_gen_msg fail
  8. 03-13 10:04:58 ERROR (10102::164) hzb_log_rule_output fail
  9. 03-13 10:04:58 ERROR (10102::632) zlog_output fail, srcfile[test_hello.c], srcline[41]

这样,用户就能知道为啥期待的输出没有产生,然后搞定这个问题。

运行时诊断会带来一定的性能损失。一般来说,我在生产环境把ZLOG_PROFILE_ERROR打开,ZLOG_PROFILE_DEBUG关闭。

还有另外一个办法来诊断zlog。我们都知道,zlog_init() 会把配置信息读入内存。在整个写日志的过程中,这块内存保持不变。如果用户程序因为某种原因损坏了这块内存,那么就会造成问题。还有可能是内存中的信息和配置文件的信息不匹配。所以我设计了一个函数 zlog_profile(),把内存的信息展现到 ZLOG_PROFILE_ERROR 指向的错误日志。使用示例如下:

  1. $ cat test_profile.conf
  2. [formats]
  3. simple = "%m%n"
  4. [rules]
  5. my_cat.* >stdout; simple
  1. #include <>
  2. #include ""
  3. int main(int argc, char** argv)
  4. {
  5. int rc;
  6. rc = dzlog_init("test_profile.conf", "my_cat");
  7. if (rc) { printf("init failed\n");return -1;}
  8. dzlog_info("hello, zlog");
  9. zlog_profile();
  10. zlog_fini();
  11. return 0;
  12. }

然后 会是:

  1. $ cat /tmp/
  2. 06-01 11:21:26 WARN (7063::783) ------zlog_profile start------
  3. 06-01 11:21:26 WARN (7063::784) init_flag:[1]
  4. 06-01 11:21:26 WARN (7063::75) -conf[0x2333010]-
  5. 06-01 11:21:26 WARN (7063::76) --global--
  6. 06-01 11:21:26 WARN (7063::77) ---file[test_profile.conf],mtime[2012-06-01 11:20:44]---
  7. 06-01 11:21:26 WARN (7063::78) ---strict init[1]---
  8. 06-01 11:21:26 WARN (7063::79) ---buffer min[1024]---
  9. 06-01 11:21:26 WARN (7063::80) ---buffer max[2097152]---
  10. 06-01 11:21:26 WARN (7063::82) ---default_format---
  11. 06-01 11:21:26 WARN (7063::48) ---format[0x235ef60][default = %d(%F %T) %V [%p:%F:%L] %m%n(0x233b810)]---
  12. 06-01 11:21:26 WARN (7063::85) ---file perms[0600]---
  13. 06-01 11:21:26 WARN (7063::87) ---rotate lock file[/tmp/]---
  14. 06-01 11:21:26 WARN (7063::48) --rotater[0x233b7d0][0x233b7d0,/tmp/,4]--
  15. 06-01 11:21:26 WARN (7063:level_list.c:37) --level_list[0x2335490]--
  16. 06-01 11:21:26 WARN (7063::37) ---level[0x23355c0][0,*,*,1,6]---
  17. 06-01 11:21:26 WARN (7063::37) ---level[0x23375e0][20,DEBUG,debug,5,7]---
  18. 06-01 11:21:26 WARN (7063::37) ---level[0x2339600][40,INFO,info,4,6]---
  19. 06-01 11:21:26 WARN (7063::37) ---level[0x233b830][60,NOTICE,notice,6,5]---
  20. 06-01 11:21:26 WARN (7063::37) ---level[0x233d850][80,WARN,warn,4,4]---
  21. 06-01 11:21:26 WARN (7063::37) ---level[0x233fc80][100,ERROR,error,5,3]---

【用户自定义等级】

这个功能涉及到源码的修改,个人认为目前 zlog 提供的等级已经完全够用,无需再去新增。有兴趣的朋友在提供的其他资料中可以找到解决办法。

zlog 配置文件详解

配置文件决定了把日志打到哪里去,用什么格式,怎么转档,下面是一个配置文件的例子:

  1. [global]
  2. strict init = true
  3. reload conf period = 0
  4. buffer min = 1024
  5. buffer max = 2MB
  6. rotate lock file = self
  7. default format = "%d.%us %-6V (%c:%F:%L) - %m%n"
  8. file perms = 600
  9. fsync period = 0
  10. [levels]
  11. TRACE = 10
  12. CRIT = 130, LOG_CRIT
  13. [formats]
  14. simple = "%m%n"
  15. normal = "%d %m%n"
  16. [rules]
  17. default.* >stdout; simple
  18. *.* "%12.2E(HOME)/log/%", 1MB*12; simple
  19. my_.INFO >stderr;
  20. my_cat.!ERROR "/var/log/"
  21. my_dog.=DEBUG >syslog, LOG_LOCAL0; simple
  22. my_mice.* $user_define;

注意:

  • 有关单位:当设置内存大小或者大数字时,可以设置1k、5GB、4M这样的单位,单位是大小写不敏感的,所以1GB 1Gb 1gB是等效的:
  1. # 1k => 1000 bytes
  2. # 1kb => 1024 bytes
  3. # 1m => 1000000 bytes
  4. # 1mb => 1024*1024 bytes
  5. # 1g => 1000000000 bytes
  6. # 1gb => 1024*1024*1024 byte

【global 全局参数】

全局参数以 [global] 开头。[] 代表一个节的开始,四个小节的顺序不能变,依次为 global-levels-formats-rules。

这一节可以忽略不写。如果写,语法为:(key) = (value)。

  • strict init

如果 strict init = true,zlog_init() 将会严格检查所有的格式和规则,任何错误都会导致zlog_init() 失败并且返回-1。

如果 strict init = false,zlog_init() 会忽略错误的格式和规则。 这个参数默认为 true。

  • reload conf period

这个选项让 zlog 能在一段时间间隔后自动重载配置文件。重载的间隔以每进程写日志的次数来定义。当写日志次数到了一定值后,内部将会调用 zlog_reload() 进行重载。每次 zlog_reload() 或者 zlog_init() 之后重新计数累加。因为 zlog_reload() 是原子性的,重载失败继续用当前的配置信息,所以自动重载是安全的。默认值是0,自动重载是关闭的。

  • buffer min
  • buffer max

zlog 在堆上为每个线程申请缓存。"buffer min" 是单个缓存的最小值,zlog_init() 的时候申请这个长度的内存。写日志的时候,如果单条日志长度大于缓存,缓存会自动扩充,直到到 "buffer max"。 单条日志再长超过 "buffer max" 就会被截断。如果 "buffer max" 是 0,意味着不限制缓存,每次扩充为原先的2倍,直到这个进程用完所有内存为止。缓存大小可以加上 KB, MB 或 GB这些单位。默认 "buffer min"是 1K , "buffer max" 是2MB。

  • rotate lock file

这个选项指定了一个锁文件,用来保证多进程情况下日志安全转档。zlog 会在 zlog_init() 时候以读写权限打开这个文件。确认你执行程序的用户有权限创建和读写这个文件。转档日志的伪代码是:

  1. write(log_file, a_log)
  2. if (log_file > 1M)
  3. if (pthread_mutex_lock succ && fcntl_lock(lock_file) succ)
  4. if (log_file > 1M) rotate(log_file);
  5. fcntl_unlock(lock_file);
  6. pthread_mutex_unlock;

mutex_lock 用于多线程, fcntl_lock 用于多进程。fcntl_lock 是 POSIX 建议锁。详见 man 3 fcntl。这个锁是全系统有效的。在某个进程意外死亡后,操作系统会释放此进程持有的锁。这就是我为什么用 fcntl 锁来保证安全转档。进程需要对锁文件有读写权限。

默认来说,rotate lock file = self。在这种情况下,zlog 不会创建任何锁文件,用配置文件作为锁文件。fcntl 是建议锁,所以用户可以*的修改存储他们的配置文件。一般来说,单个日志文件不会被不同操作系统用户的进程转档,所以用配置文件作为锁文件是安全的。

如果你设置其他路径作为锁文件,例如 /tmp/,zlog 会在 zlog_init() 的时候创建这个文件。如果有多个操作系统用户的进程需要转档同一个日志文件,确认这个锁文件对于多个用户都可读写。

  • default format

这个参数是缺省的日志格式,如果在规则中不指定格式,则以默认格式输出。默认值为:"%d %V [%p:%F:%L] %m%n"

这种格式产生的输出类似这样:

2012-02-14 17:03:12 INFO [3758:test_hello.c:39] hello, zlog

  • file perms

这个指定了创建日志文件的缺省访问权限。必须注意的是最后的产生的日志文件的权限为"file perms"& ~umask。默认为600,只允许当前用户读写。

  • fsync period

在每条规则写了一定次数的日志到文件后,zlog 会调用 fsync(3) 来让操作系统马上把数据写到硬盘。次数是每条规则单独统计的,并且在zlog_reload() 后会被清0。默认值是0,由操作系统来决定什么时候刷缓存到文件。

必须指出的是,在日志文件名是动态生成或者被转档的情况下,zlog 不能保证把所有文件都搞定,zlog 只 fsync()那个时候刚刚 write() 的文件描述符。这提供了写日志速度和数据安全性之间的平衡。

如果你极度在乎安全而不是速度的话,可以将输出动作定义为同步IO文件,见下文的规则(Rules)描述。

【levels 日志等级自定义】

这一节以[levels]开始。用于定义用户自己的日志等级,建议和用户自定义的日志记录宏一起使用。这一节可以忽略不写。语法为:

(level string) = (level int), (syslog level, optional)

(level int)必须在[1,253]这个范围内,越大越重要。(syslog level)是可选的,如果不设默认为LOG_DEBUG。

如上文所述,该功能基本用不上。

【formats 格式定义】

这一节以 [formats] 开始。用来定义日志的格式。语法为:(name) = "(actual formats)"

很好理解,(name) 被后面的规则使用。(name) 必须由数字和字母组成,下划线"_"也算字母。(actual format)前后需要有双引号。 (actual formats)可以由转换字符组成。

【转换格式串】

转换格式串的设计是从 C 的 printf 函数里面抄来的。一个转换格式串由文本字符和转换说明组成。

转换格式串用在规则(rules)的日志文件路径和输出格式(format)中。

每个转换说明都是以百分号(%)打头的,后面跟可选的宽度修饰符,最后以转换字符结尾。

你可以把任意的文本字符放到转换格式串里面。转换字符决定了输出什么数据,例如分类名、级别、时间日期、进程号等等。

宽度修饰符控制了这个字段的最大最小宽度、左右对齐。

例如,如果转换格式串是:"%d(%m-%d %T) %-5V [%p:%F:%L] %m%n".

源代码中的写日志语句是:zlog_info(c, "hello, zlog");

将会输出:02-14 17:17:42 INFO  [4935:test_hello.c:39] hello, zlog

可以注意到,在文本字符和转换说明之间没有显式的分隔符。zlog 解析的时候知道哪里是转换说明的开头和结尾。在这个例子里面 %-5p 这个转换说明决定了日志级别要被左对齐,占5个字符宽。

  • 可以被辨识的转换字符有:

字符

效果

例子

%c

分类名

aa_bb

%E()

获取环境变量的值

%E(USER) simpson

%ms

毫秒,3位数字字符串

取自gettimeofday(2)

013

%us

微秒,6位数字字符串

取自gettimeofday(2)

002323

%d()

打日志的时间。括号()内含具体的日期格式,就像%d(%m-%d %T)。如果不跟小括号,默认是%d(%F %T)。括号内的格式和 strftime(2)的格式一致。时间格式详见下面的时间字符说明。

%d(%F) 2011-12-01

%d(%m-%d %T) 12-01 17:17:42

%d(%T) 17:17:42.035

%d 2012-02-14 17:03:12

%d()

%F

源代码文件名,来源于__FILE__宏。在某些编译器下 __FILE__是绝对路径。用%f来去掉目录只保留文件名,或者编译器有选项可以调节

test_hello.c

或者在某些编译器下

/home/zlog/src/test/test_hello.c

%f

源代码文件名,输出$F最后一个’/’后面的部分。当然这会有一定的性能损失

test_hello.c

%H

主机名,来源于 gethostname(2)

zlog-dev

%L

源代码行数,来源于__LINE__宏

135

%m

用户日志,用户从zlog函数输入的日志。

hello, zlog

%M

MDC (mapped diagnostic context),每个线程一张键值对表,输出键相对应的值。后面必需跟跟一对小括号()内含键。例如 %M(clientNumber) ,clientNumbe是键。

%M(clientNumber) 12345

%n

换行符,目前还不支持windows换行符

\n

%p

进程ID,来源于getpid()

2134

%U

调用函数名,来自于__func__(C99)或者__FUNCTION__(gcc),如果编译器支持的话。

main

%V

日志级别,大写

INFO

%v

日志级别,小写

info

%t

16进制表示的线程ID,来源于pthread_self()

"0x%x",(unsigned int) pthread_t

0xba01e700

%T

相当于%t,不过是以长整型表示的

"%lu", (unsigned long) pthread_t

140633234859776

%%

一个百分号

%

%[其他字符]

解析为错误,zlog_init()将会失败

  • 可以被辨识的时间字符有:

字符

效果

例子

%a

一星期中各天的缩写名,根据locale显示

Wed

%A

一星期中各天的全名,根据locale显示

Wednesday

%b

缩写的月份名,根据locale显示

Mar

%B

月份全名,根据locale显示

March

%c

当地时间和日期的全表示, 根据locale显示

Thu Feb 16 14:16:35 2012

%C

世纪 (年/100),2位的数字(SU)

20

%d

一个月中的某一天 (01-31)

06

%D

相当于%m/%d/%y. (呃,美国人专用,美国人要知道在别的国家%d/%m/%y 才是主流。也就是说在国际环境下这个格式容易造成误解,要少用) (SU)

02/16/12

%e

就像%d,一个月中的某一天,但是头上的0被替换成空格(SU)

6

%F

相当于%Y-%m-%d (ISO 8601日期格式)(C99)

2012-02-16

%G

The ISO 8601 week-based year (see NOTES) with century as a decimal number. The 4-digit year corre‐ sponding to the ISO week number (see %V). This has the same format and value as %Y, except that if the ISO week number belongs to the previous or next year, that year is used instead. (TZ)

大意是采用%V定义的年,如果那年的前几天不算新年的第一周,就算上一年

2012

%g

相当于%G,就是不带世纪 (00-99). (TZ)

12

%h

相当于%b(SU)

Feb

%H

小时,24小时表示(00-23)

14

%I

小时,12小时表示(01-12)

02

%j

一年中的各天(001-366)

047

%k

小时,24小时表示( 0-23); 一位的前面为空格 (可和%H比较) (TZ)

15

%l

小时,12小时表示( 0-12); 一位的前面为空格 (可和%比较)(TZ)

3

%m

月份(01-12)

02

%M

分钟(00-59)

11

%n

换行符 (SU)

\n

%p

"AM" 或 "PM",根据当时的时间,根据locale显示相应的值,例如"上午"、"下午" 。 中午是"PM",凌晨是"AM"

PM

%P

相当于%p不过是小写,根据locale显示相应的值 (GNU)

pm

r

时间+后缀AM或PM。在POSIX locale下相当于%I:%M:%S %p. (SU)

03:11:54 PM

%R

小时(24小时制):分钟 (%H:%M) (SU) 如果要带秒的,见%T

15:11

%s

Epoch以来的秒数,也就是从1970-01-01 00:00:00 UTC. (TZ)

1329376487

%S

秒(00-60). (允许60是为了闰秒)

54

%t

制表符tab(SU)

%T

小时(24小时制):分钟:秒 (%H:%M:%S) (SU)

15:14:47

%u

一周的天序号(1-7),周一是1,另见%w (SU)

4

%U

一年中的星期序号(00-53),周日是一周的开始,一年中第一个周日所在的周是第01周。另见%V和%W

07

%V

ISO 8601星期序号(01-53),01周是第一个至少有4天在新年的周。另见%U 和%W(SU)

07

%w

一周的天序号(0-6),周日是0。另见%u

4

%W

一年中的星期序号(00-53),周一是一周的开始,一年中第一个周一所在的周是第01周。另见%V和%W

07

%x

当前locale下的偏好日期

02/16/12

%X

当前locale下的偏好时间

15:14:47

%y

不带世纪数目的年份(00-99)

12

%Y

带世纪数目的年份

2012

%z

当前时区相对于GMT时间的偏移量。采用RFC 822-conformant来计算(话说我也不知道是啥) (using "%a, %d %b %Y %H:%M:%S %z"). (GNU)

+0800

%Z

时区名(如果有的话)

CST

%%

一个百分号

%

  • 可以被辨识的宽度修饰符有:

宽度修饰符能够控制最小字段宽度、最大字段宽度和左右对齐。当然这要付出一定的性能代价。

宽度修饰符放在百分号和转换字符之间。

第一个可选的宽度修饰符是左对齐标识,减号(-)。

然后是可选的最小字段宽度,这是一个十进制数字常量,表示最少有几个字符会被输出。如果数据本来没有那么多字符,将会填充空格(左对齐或者右对齐)直到最小字段宽度为止。默认是填充在左边也就是右对齐。当然你也可以使用左对齐标志,指定为填充在右边来左对齐。填充字符为空格(space)。如果数据的宽度超过最小字段宽度,则按照数据的宽度输出,永远不会截断数据。

这种行为可以用最大字段宽度来改变。最大字段宽度是放在一个句点号(.)后面的十进制数字常量。如果数据的宽度超过了最大字段宽度,则尾部多余的字符(超过最大字段宽度的部分)将会被截去。 最大字段宽度是8,数据的宽度是10,则最后两个字符会被丢弃。这种行为和C的printf是一样的,把后面的部分截断。

下面是各种宽度修饰符和分类转换字符配合一起用的例子:

宽度修饰符

左对齐

最小字段宽度

最大字段宽度

附注

%20c

20

左补充空格,如果分类名小于20个字符长。

%-20c

20

右补充空格,如果分类名小于20个字符长。

%.30c

30

如果分类名大于30个字符长,取前30个字符,去掉后面的。

%20.30c

20

30

如果分类名小于20个字符长,左补充空格。如果在20-30之间,按照原样输出。如果大于30个字符长,取前30个字符,去掉后面的。

%-20.30c

20

30

如果分类名小于20个字符长,右补充空格。如果在20-30之间,按照原样输出。如果大于30个字符长,取前30个字符,去掉后面的。

【rules 规则定义】

这一节以 [rules] 开头。描述日志是怎么被过滤、格式化以及被输出的。这节可以忽略不写,不过这样就没有日志输出了。语法是:

(category).(level)    (output), (options, optional); (format name, optional)

当 zlog_init() 被调用的时候,所有规则都会被读到内存中。当 zlog_get_category() 被调用,规则就被被分配给分类。在实际写日志的时候,例如 zlog_info() 被调用的时候,就会比较这个 INFO 和各条规则的等级,来决定这条日志会不会通过这条规则输出。当zlog_reload()被调用的时候,配置文件会被重新读入,包括所有的规则,并且重新计算分类对应的规则。

  • 关于分类的匹配方式如下:

说明

配置文件规则分类

匹配的代码分类

不匹配的代码分类

*匹配所有

*.*

aa, aa_bb, aa_cc, ...

NONE

以_结尾的分类匹配本级及下级分类

aa_.*

aa, aa_bb, aa_bb_cc

xx, yy

不以_结尾的精确匹配分类名

aa.*

aa

aa_bb, aa_cc, aa_bb_cc

!匹配那些没有找到规则的分类

!.*

xx

aa(as it matches rules above)

  • 关于级别的匹配方式如下:

zlog 有 6 个默认的级别:"DEBUG", "INFO", "NOTICE", "WARN", "ERROR", "FATAL"。配置文件中的级别是大小写不敏感的。

表达式

含义

category.*

所有等级的日志会被输出

代码内等级 >=debug 的日志会被输出

category.=debug

代码内等级 ==debug 的日志会被输出

category.!debug

代码内等级 !=debug 的日志会被输出

  • 关于输出的匹配方式如下:

zlog 支持若干种输出,这里讨论的就是 (output), (options, optional); 字段。

  • output 为 >stdout>stderr

就是标准输出和标准错误输出。此时附加选项是无意义的,不用写。

值得注意的是,zlog 在写日志的时候会用这样的语句:

write(STDOUT_FILENO, zlog_buf_str(a_thread->msg_buf), zlog_buf_len(a_thread->msg_buf))

而如果你的程序是个守护进程,在启动的时候把 STDOUT_FILENO,也就是1的文件描述符关掉的话,会发生什么结果呢?

日志会被写到新的1的文件描述符所代表的文件里面!我收到过邮件,说 zlog 把日志写到自己的配置文件里面去了!

所以,千万不要在守护进程的规则里面加上 >stdout 或 >stderr。这会产生不可预料的结果,如果一定要输出到终端,用"/dev/tty"代替。

  • output 为 >syslog

此时附加选项必须要写,可以是:LOG_USER(default), LOG_LOCAL[0-7]

  • output 为 管道输出

此时附加选项是无意义的,不用写。例如:

*.*  |  /usr/bin/cronolog /www/logs/example_%Y%m% ; normal

这是一个将 zlog 的输出到管道后接 cronolog 的例子。实现的原理很简单,在 zlog_init 的时候调用 popen("/usr/bin/cronolog /www/logs/example_%Y%m%", "w"),后面往这个文件描述符里面写指定格式的日志。使用cronolog来生成按天分割的日志效率比zlog自己的动态路径的效率要高,因为通过管道,无须每次打开关闭动态路径的文件描述符。

不过,使用管道也是有限制的:

  • POSIX.1-2001保证读写不大于PIPE_BUF大小的内容是原子的。linux上PIPE_BUF为4096。
  • 单条日志的长度超过PIPE_BUF的时候并且有多个有父子关系的进程写通过zlog写同一个管道,也就是在zlog_init之后fork多个子进程,此时只有一个cronolog的进程监听一个管道描述符,日志内容可能会交错。
  • 多个进程分别zlog_init,启动多个cronolog进程,写拥有同一个文件路径的日志文件,即使单条日志长度不超过PIPE_BUF,也有可能导致日志交错,因为cronolog读到的文件流是连续的,它不知道单条日志的边界在哪里。

所以,总结一下,使用管道来输出到单个日志文件的情况是:

  • 单进程写,单条日志长度不限制。单进程内内的多线程写日志的原子性已经由zlog保证了。
  • 有父子关系的多进程,单条日志长度不能超过PIPE_BUF(4096)
  • 无父子关系的多进程使用管道同时写一个日志,无论单条日志长度是多少,都有可能导致日志交错。

个人认为:管道的使用必须小心,要实际测试才行,一般不会用到管道。

  • output 为 "文件路径" 或者 -"文件路径"

zlog 本身的直接文件输出能保证即使是多进程,同时调用 zlog 写一个日志文件也不会产生交错。

可以是相对路径或者绝对路径,被双引号 " 包含。

文件路径可以包含转换格式串。例如文件路径是 "%E(HOME)/log/",环境变量$HOME是 /home/harry,那最后的输出文件是 /home/harry/log/。

zlog的文件功能极为强大,例如:

  • 输出到命名管道(FIFO),必须在调用前由mkfifo(1)创建:

*.*  "/tmp/pipefile"

  • 输出到NULL,也就是不输出:

*.*  "/dev/null"

  • 在任何情况下输出到终端:

*.*  "/dev/tty"

  • 每线程一个日志,在程序运行的目录下:

*.* "%"

  • 输出到有进程号区分的日志,每天,在$HOME/log目录,每1GB转档一次,保持5个日志文件:

*.*  "%E(HOME)/log/aa.%p.%d(%F).log",1GB *5

  • aa_及下级分类,每个分类一个日志:

*.*  "/var/log/%"

  • 同步IO文件:

在文件路径前加上一个"-"就打开了同步IO选项。在打开文件(open)的时候,会以O_SYNC选项打开,这时候每次写日志操作都会等操作系统把数据写到硬盘后才返回。这个选项极为耗时

  • 文件转档:

控制文件的大小和个数。当输出为文件时,zlog 根据逗号后面的附加字段来转档文件,例如:

"%E(HOME)/log/", 1M * 3 ~ "%E(HOME)/log/.#r"

关于文件转档的细节描述见下文

  • 关于格式的匹配方式如下:

输出格式是可选的,如果不写,则用全局配置里面的默认格式:

  1. [global]
  2. default format = "%d(%F %T) %V [%p:%F:%L] %m%n"

【文件转档详解】

为什么需要将日志文件转档?我已经在实际的运行环境中不止一次的看到过,因为日志文件过大,导致系统硬盘被撑爆,或者单个日志文件过大而即使用 grep 也要花费很多时间来寻找匹配的日志。对于日志转档,我总结了如下几种范式:

  • 按固定时间段来切分日志

例如,每天生成一个日志

aa. 

aa.

aa.

这种日志适合的场景是,管理员大概知道每天生成的日志量,然后希望在n个月之后能精确的找出某天的所有日志。

这种日志切分最好的办法是由日志库来完成,其次的方法是用 cronosplit 这种软件来分析日志内容的时间字符串来进行后期的切分,较差的办法是用 crontab+logrotate 或 mv 来定期移动(这并不精确,会造成若干条当天的日志被放到上一天的文件里面去)。

在 zlog 里面,这种需求不需要用日志转档功能来完成,简单的在日志文件名里面设置时间日期字符串就能解决问题:

*.* "aa.%d(%F).log"

或者用cronolog来完成,速度会更快一点:

*.* |  cronolog aa.%

  • 按照日志大小切分

当程序在短时间内生成大量的日志或者就是想减少日志的文件数量,而大的日志文件打开极慢,可能希望按照日志大小切分,这种日志的切分可以在事后用 split 等工具来完成,但对于开发而言会增加步骤,所以最好也是由日志库来完成。值得一提的是存档有两种模式,nlog 里面称之为 Sequence 和 Rolling,

在 Sequence 情况下:

 (new) 

.2 (less new) 

.1 

.0 (old)

在 Rolling 的情况下:

 (new) 

.0 (less new) 

.1 

.2 (old)

两种模式看个人喜好选择。

最复杂的配置格式为:

*.*  "", 10MB * 0 ~ "aa-%d(%Y%m%d).log.#2r"

逗号后第一个参数表示文件达到多大后开始进行转档。

第二个参数表示保留多少个存档文件(0代表不删除任何存档文件)。

第三个参数表示转档的文件名,其中

%d(%Y%m%d) 表示把转档当时的时间串作为转档文件名的一部分;

#2r 表示存档文件的序号,2 的意思是序号的长度最少为2位,从00开始编号;r 表示 rolling,s 表示 sequence。转档文件名必须包含#r或者#s。

* ~ 都是参数的分隔符。

例如:

 

-20070305. 

-20070501. 

-20070501.

-20071008.

  • 对外部工具的支持

当需要对日志文件进行压缩、移动、删除等操作时,只能依靠 shell 脚本+外部工具 的方式实现。例如常用的有 crontab、logrotate等。

需要注意的是:如果外部工具会对日志文件进行重命名操作,切记此时应用进程还是往原来的文件描述符写日志的,应用进程并没有打开新文件来进行读写操作,需要格外注意应用进程和外部工具操作的是不是同一个文件。

zlog_reload() 函数会重载配置文件,重新打开所有的日志文件。应用程序在 logrotate 的信号或者其他途径,例如客户端的命令后,可以调用这个函数,来重新打开所有的日志文件,这也许能提供一个解决办法。

【配置文件工具】

在编译源码文件后,安装目录/bin 目录下会有 zlog-chk-conf 程序文件:

zlog-chk-conf 尝试读取配置文件,检查语法,然后往屏幕上输出这些配置文件是否正确。我建议每次创建或者改动一个配置文件之后都用一下这个工具。输出可能是这样:

  1. $ ./zlog-chk-conf
  2. 03-08 15:35:44 ERROR (10595::391) sscanf [aaa] fail, category or level is null
  3. 03-08 15:35:44 ERROR (10595::155) zlog_rule_new fail [aaa]
  4. 03-08 15:35:44 ERROR (10595::258) parse configure file[] line[126] fail
  5. 03-08 15:35:44 ERROR (10595::306) zlog_conf_read_config fail
  6. 03-08 15:35:44 ERROR (10595::366) zlog_conf_build fail
  7. 03-08 15:35:44 ERROR (10595::66) conf_file[], init conf fail
  8. 03-08 15:35:44 ERROR (10595::131) zlog_init_inner[] fail
  9. ---[] syntax error, see error message abo

这个告诉你配置文件的126行,是错的。第一行进一步告诉你[aaa]不是一条正确的规则。

zlog-chk-conf 可以同时分析多个配置文件,举例:

  1. $ zlog-chk-conf
  2. --[] syntax right
  3. --[] syntax right

注意事项及问题记录

  • 用起来确实很简单,我感觉 zlog 比较适合用于程序的性能分析,即程序长时间运行时,记录程序运行状态,然后分析程序性能,以便于后期优化;另外zlog有个地方需要注意下,就是在日志文件达到设置的大小时,切换日志文件,如果这时程序输出日志过多间隔也比较短(100毫秒)左右时,程序会挂掉,这时 zlog 的一个bug。一般程序输出日志没那么多时,不会出现。(这段文字来自别处,最好引起重视)

示例

配置文件 :

  1. [global]
  2. strict init = true
  3. buffer min = 400
  4. buffer max = 1kb
  5. rotate lock file = /tmp/
  6. default format = "%d %m%n"
  7. file perms = 600
  8. [rules]
  9. # default.* >stdout
  10. default.* "/work/log/", 5MB*1 ~ ".#s"

应用程序:

  1. static int syslog(const char *str)
  2. {
  3. int rc = dzlog_init("", "default");
  4. if(!rc) {
  5. dzlog_info(str);
  6. zlog_fini();
  7. }
  8. return rc;
  9. }
  10. int main(int argc, char const *argv[])
  11. {
  12. syslog("system starting...\n");
  13. exit(0);
  14. }