简介
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。
兼容性说明
- zlog 是基于 POSIX 的。目前我手上有的环境只有AIX和linux。在其他的系统下(FreeBSD, NetBSD, OpenBSD, OpenSolaris, Mac OS X...)估计也能行,有问题欢迎探讨。
- 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
- 有网友提供了如下版本,方便其他平台上安装编译。但个人建议,如果能使用源码编译最好不用其他版本,因为这些可能不是最新版。
auto tools版本: GitHub - bmanojlovic/zlog: A reliable, high-performance, thread safe, flexsible, clear-model, pure C logging library.
windows版本: GitHub - lopsd07/WinZlog: Zlog on Windows
编译和使用
zlog 日志库 并非只有几个文件,一般不好包含到项目文件中一起编译,通常都是编译成动态库的形式使用;
【在 Linux 系统中编译】
$ tar -zxvf zlog-latest-stable.tar.gz
$ cd zlog-latest-stable/
$ make
$ sudo make install
or
$ make PREFIX=/usr/local/
$ sudo make PREFIX=/usr/local/ install
PREFIX 表示 zlog 的安装目标。安装后,刷新动态链接器以确保程序可以找到 zlog 库。其他系统类似。
$ sudo vi /etc/ld.so.conf
/usr/local/lib
$ sudo ldconfig
测试:我们将安装路径设置源码目录下的 __install 目录(新建的测试目录),
$ mkdir __install
$ make PREFIX=$(pwd)/__install
$ sudo make PREFIX=$(pwd)/__install instal
结果如下图所示:
【交叉编译】
修改源码目录下 src/makefile 里的 cc 为交叉编译器的 gcc,如果有必要再修改 ar 为交叉编译器的 ar,如下图所示:
这里我以 RK3568 板卡供应商提供的编译器为例,执行上述测试步骤,结果如下:
【应用程序调用和链接zlog】
应用程序使用 zlog 很简单,只要在C文件里面加一行 #include "zlog.h"
链接 zlog 需要 pthread 库,例如:
$ cc app.c -o app -lpthread -I/usr/local/include -L/usr/local/lib -lzlog
或者
$ cc -c -o app.o app.c -I/usr/local/include #编译
$ cc -o app app.o -L/usr/local/lib -lzlog -lpthread #链接
zlog的使用流程
首先要知道 zlog 中有 3 个重要的概念:
分类(Category):类别用于区分不同类型的日志,它用一个字符串来说明。
格式(Format):用来描述输出日志的格式,比如是否有带有时间戳,是否包含文件位置信息等。
规则(Rule):把分类、级别、输出文件、格式组合起来,决定一条代码中的日志是否输出,输出到哪里,以什么格式输出。
上述的分类、格式、规则等都是写在配置文件中的,配置文件由用户自己编写,我们写一个简单的配置文件 zlog.conf 示例:
[formats]
simple = "%d %m%n"
[rules]
my_cat.DEBUG >stdout; simple
这个配置文件指定了类别为 “my_cat” 且级别 >= DEBUG 的日志将输出到标准输出,格式为 simple。
我们写个C程序测试一下:
#include <stdio.h>
#include "include/zlog.h"
int main(int argc, char** argv)
{
int rc;
zlog_category_t *c;
//根据配置文件初始化
rc = zlog_init("zlog.conf");
if(rc){
printf("init failed\n");
return -1;
}
//将类别信息读取到内存中
c = zlog_get_category("my_cat");
if(!c){
printf("get cat fail\n");
zlog_fini();
return -2;
}
//输出日志信息
zlog_info(c, "hello, zlog");
zlog_fini();
return 0;
}
编译运行结果如下:
由此可见,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)操作】
typedef struct zlog_category_s zlog_category_t
zlog_category_t *zlog_get_category(const char *cname);
zlog_get_category() 从 zlog 的全局分类表里面找到分类,用于以后输出日志。如果没有的话,就建一个。然后它会遍历所有的规则,寻找和 cname 匹配的规则并绑定。
配置文件规则中的分类名匹配 cname 的规则如下:
- * 匹配任意 cname。
- 以下划线_结尾的分类名同时匹配本级分类和下级分类。例如 aa_ 匹配 aa, aa_, aa_bb, aa_bb_cc 这几个 cname。
- 不以下划线_结尾的分类名精确匹配 cname。例如 aa_bb 匹配 aa_bb 这个 cname。
- ! 匹配目前还没有规则的 cname。
这个匹配规则在配置文件中还会讲述到!
每个 zlog_category_t * 对应的规则,在 zlog_reload() 的时候会被自动重新计算。不用担心内存释放,zlog_fini() 最后会清理一切。
【日志输出函数及宏】
void zlog(zlog_category_t * category,
const char *file, size_t filelen,
const char *func, size_t funclen,
long line, int level,
const char *format, ...) ZLOG_CHECK_PRINTF(8,9);
void vzlog(zlog_category_t * category,
const char *file, size_t filelen,
const char *func, size_t funclen,
long line, int level,
const char *format, va_list args);
void hzlog(zlog_category_t * category,
const char *file, size_t filelen,
const char *func, size_t funclen,
long line, int level,
const void *buf, size_t buflen);
这3个函数是实际写日志的函数,输入的数据对应于配置文件中的 %m。
category 通过调用 zlog_get_category() 获得;
file 和 line 填写为__FILE__和__LINE__这两个宏,标识日志是在哪里发生的;
func 填写为 __func__ 或者 __FUNCTION__,如果编译器支持的话,如果不支持,就填写为"";
level 是一个整数,应该是在下面几个里面取值。
typedef enum {
ZLOG_LEVEL_DEBUG = 20,
ZLOG_LEVEL_INFO = 40,
ZLOG_LEVEL_NOT