C语言日志库zlog总结
一、zlog是什么?
1.1 zlog介绍
zlog是一个可靠性、高性能、线程安全、灵活、概念清晰的纯C日志函数库。
事实上,在C的世界里面没有特别好的日志函数库(就像JAVA里面的log4j,或者C++的log4cxx)。C程序员都喜欢用自己的轮子。printf就是个挺好的轮子,但是没办法通过配置改变日志的格式或者输出文件。syslog是个系同级别的轮子,不过速度慢,而且功能比较单调。而zlog同时具有这些功能。
【zlog的特性】:
zlog在效率、功能、安全性上大大超过了log4c,并且是用c写成的,具有比较好的通用性。zlog有这些特性:
- syslog分类模型,比log4j模型更加直截了当;
- 日志格式定制,类似于log4j的pattern layout;
- 多种输出,包括动态文件、静态文件、stdout、stderr、syslog、用户自定义输出函数;
- 运动时手动、自动刷新配置文件(同时保证安全);
- 高性能,在我的笔记本上达到25万条日志每秒,大概是syslog(3)配合rsyslogd的1000倍速度;
- 用户自定义等级;
- 多线程和多进程环境下保证安全转档;
- 精确到微妙;
- 简单调用包装zlog(一个程序默认只用一个分类);
- MDC,线程键-值对的表,可以扩展用户自定义的字段;
- 自诊断,可以在运行时输出zlog自己的日志和配置状态;
- 不依赖其他库,只要是个POSIX系统就成(当然还要一个C99兼容的vsnprintf);
1.2 zlog兼容性说明
- 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 ...都是好的,不过glibc 2.0不是。在这种情况下,用户需要自己来装一个C99兼容的vsnprintf,来crack这个函数库。我推荐ctrio,或者C99-snprintf。只要改buf.c就行,祝好运!
- zlog目前还没有计划支持windows,毕竟windows下已经有非常成熟的日志函数库。
二、zlog不是什么?
zlog的目标是称为一个简而精的日志函数库,不会直接支持网络输出或者写入数据库,不会直接支持日志内容的过滤和解析。
原因很明显,日志库是被应用程序调用的,所有花在日志库上的时间都是应用程序运行时间的一部分,而上面说的这些操作都很费时间,会拖慢应用程序的速度。这些事儿应该在别的进程或者别的机器上做。如果你需要这些特性,我建议使用rsyslog、hsLogFabric、Logstash,这些日志搜集、过滤、存储软件,当然是单独的进程,不是应用程序的一部分。
目前zlog已经支持用户自定义输出函数,用户可以自己实现一个输出函数,自由的把日志输出到其他进程或者其他机器,而把日志的分配匹配、日志的格式成型的工作交给zlog。
三、Hello World
3.1 应用程序调用和链接zlog
应用程序使用zlog很简单,只要在C文件里面加一行:
#include “hs_log.h”
链接zlog需要pthread库,命令是:
$ cc -c -o app.o app.c -I/usr/local/zlog/include
# -I[where zlog.h is put]
$ cc -o app app.o -L/usr/local/zlog/lib –lzlog –lpthread
# -L[where libzlog.so is put]
3.2 Hello World实现
3.2.1 写一个C程序
$ vim hello.c
#include <stdio.h>
#include "hs_log.h"
int main(int argc, char** argv)
{
int rc;
rc = hs_log_init(); //1 : first init log module
if (rc) {
printf("init zlog module failed\n");
return -1;
}
// 2: call hs-log function print log
hs_log_debug("hello, ha-log %s", "debug");
hs_log_info("hello, hs-log %d", 100);
hs_log_notice("%s %d","hello, hs-log", 100);
hs_log_warn("hello, hs-log");
hs_log_error("hello, hs-log");
hs_log_fatal("hello, hs-log");
hs_log_fini();
return 0;
}
3.2.2 写一个配置文件
[rules]
hs_cat.* >stderr;
将log.conf放在hello.c同一目录下。
3.2.3 编译-运行
$ cc -c -o test_hello.o test_hello.c -I/usr/local/zlog/include
$ cc -o test_hello test_hello.o -L/usr/local/lib –lzlog
$ ./test_hello
2016-06-18 15:06:31 DEBUG [14393:hs_log_hello.c:22] hello, ha-log debug
2016-06-18 15:06:31 INFO [14393:hs_log_hello.c:23] hello, hs-log 100
2016-06-18 15:06:31 NOTICE [14393:hs_log_hello.c:24] hello, hs-log 100
2016-06-18 15:06:31 WARN [14393:hs_log_hello.c:25] hello, hs-log
2016-06-18 15:06:31 ERROR [14393:hs_log_hello.c:26] hello, hs-log
2016-06-18 15:06:31 FATAL [14393:hs_log_hello.c:27] hello, hs-log
四、配置文件
大部分的zlog的行为取决于配置文件:把日志打到哪儿去,用什么格式,怎么转档。配置文件是zlog的黑活,我尽量把这个黑活设计的简单明了。这是个配置文件的例子:
# comments
[global]
strict init = true;
buffer min = 1024;
buffer max = 2MB;
default format = “%d.%us %-6V (%c:%F:%L) - %m%n”;
file perms = 600;
[formats]
simple = “%m%n”
normal = “%d %m%n”
[rules]
default.* >stdout; simple
*.* “%12.2E(HOME)/log/%c.log”, 1MB*12; simple
my_.INFO >stderr;
hs_cat.!ERROR >”/var/log/aa.log”
my_dog.=DEBUG >syslog, LOG_LOCALO; simple
my_mice.* $user_define;
有关单位使用说明:当设置内存大小或大数字时,可以设置1k 5GB 4M这样的单位:
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
单位大小写不敏感,所以1GB 1Gb 1gB是等效的。
4.1 全局参数
全局参数以[global]开头,==[]代表一个节的开始,四个小节的顺序不能变,依次为global-levels-formats-rules==。[global]这一节可以忽略不写,语法为:
(key) = (value)
【strict init】:
如果“strict init”是true,hs_log_init()将会严格检查所有的格式和规则,任何错误都会导致hs_log_init()失败并返回-1。当“strict init”是false的时候,hs_log_init()会忽略错误的格式和规则。这个参数默认为true。
【buffer min/max】:
zlog在堆上为每个线程申请缓存。“buffer min”是单个缓存的最小值,hs_log_init()的时候申请这个长度的内存。写日志的时候,如果单条日志长度大于缓存,缓存会自动扩充,直到“buffer max”。单条日志再长超过“buffer max”就会被截断。如果“buffer max”是0,意味着不限制缓存,每次扩充为原先的2倍,直到这个进程用完所有内存为止。缓存大小可以加上KB,MB或GB这些单位。默认来说“buffer min”是1k,“buffer max”是2MB。
【default format】:
这个参数是缺省的日志格式,默认值为:
“%d %v [%p:%F:%L] %m%n”
这种格式产生的输出类似这样:
2016-06-12 17:03:12 INFO [3758:test_hello.c:39] hello, zlog
4.2 格式(Formats)
这一节以[formats]开始,用来定义日志的格式。语法为:
(name) = “(actual formats)”
很好理解,==(name)被后面的规则使用。(name)必须由数字和字母组成,下划线“_”也算字母==。(actual format)前后需要有双引号。(actual formats)可以由转换字符组成,见下一节。
syslog有一个通配符“*”,匹配所有的设施(facility)。zlog里面也一样,==“*”匹配所有的分类==。这提供了一个很方便的办法来重定向你的系统中各个组织的错误。只要这么写:
[rules]
*.error “/var/log/error.log”
==通配符“_”表示上级分类==。“my_”是“hs_cat”和“my_dog”的上级分类。还有一个通配符“!”,详见5.5.2节。
4.3 转换格式串
转换格式串的设计是从C的printf函数里面抄来的。一个转换格式串由文本字符和转换说明组成。转换格式串用在规则的日志文件路径和输出格式(format)中。你可以把任意的文本字符放到转换格式串里面。
==每个转换说明都是以百分号(%)打头的,后面跟可选的宽度修饰符,最后以转换字符结尾。转化字符决定了输出什么数据,例如分类名、级别、时间日期、进程号等等。宽度修饰符控制了这个字段的最大最小宽度、左右对齐==。下面是简单的例子。
如果转换格式串是:
“%d(%m-%d %T) %-5v [%p:%F:%L] %m%n”
源代码中写日志语句是:
hs_log_info(c,”hello, zlog”);
将会输出:
02-14 17:17:48 INFO [4938:test_hello.c:39] hello, zlog
可以注意到,在文本字符和转换说明之间没有显示的分隔符。zlog解析的时候知道哪里是转换说明的开头和结尾。在这个例子里面%-5p这个转换说明决定了日志级别要被左对齐,占5个字符宽度。
4.3.1 转换字符
可以被辨认的转换字符有:
4.3.2 宽度修饰符
一般来说数据按原样输出。不过,有了宽度修饰符,就能够控制最小字段宽度、最大字段宽度和左右对齐。当然这要付出一定的性能代价。可选的宽度修饰符放在百分号和转换字符之间。
==第一个可选的宽度修饰符是左对齐标识,减号(-)==。++然后是可选的最小字段宽度,这是一个十进制数字常量,表示最少有几个字符会被输出。如果数据本来没有那么多字符,将会填充空格(左对齐或者右对齐)直到最小字段宽度为止。默认是填充在左边也就是右对齐。当然你也可以使用左对齐标志,指定为填充在右边来左对齐。填充字符为空格(space)。如果数据的宽度超过最小字段宽度,则按照数据的宽度输出,永远不会截断数据++。
这种行为可以用最大字段宽度来改变。++最大字符宽度是放在一个句点号(.)后面的十进制数字常量。如果数据的宽度超过了最大字段宽度,则尾部多余的字符(超过最大字段宽度的部分)将会被截取++。最大字段宽度是8,数据的宽度是10,则最后两个字符会被丢弃。这种行为和C的printf是一样的,把后面的部分截断。
下面是各种宽度修饰符和分类转换字符符合一起用的例子。
4.3.3 时间字符
这里是转换字符d支持的时间字符。所有字符都是由strftime(2)生成的,在我的linux操作系统上支持的是:
4.4 规则(Rules)
这一节以[rules]开头。这个描述了日志是怎么被过滤、格式化以及被输出的。这节可以忽略不写,不过这样就没有日志输出了,嘿嘿。语法是:
(category).(level) (output), (options, optional); (format name, optional)
当hs_log_init()被调用的时候,所有规则都会被读到内存中,当hs_log_get_category()被调用,规则就被分配给分类(5.5.2节)。在实际写日志的时候,例如hs_log_info()被调用的时候,就会比较这个INFO和各条规则的等级,来决定这条日志会不会通过这条规则输出。当hs_log_reload()()被调用的时候,配置文件会被重新读入,包括所有的规则,并且重新计算分类对应的规则。
4.4.1 级别匹配
==zlog有6个默认的级别:“DEBUG”、“INFO”、“NOTICE”、“WARN”、“ERROR”和“FATAL”==。就像其他的日志函数库那样,aa.DEBUG意味着任何大于等于DEBUG级别的日志会被输出。当然还有其他表达式。配置文件中的级别是大小写不敏感的。
用户还可以自定义等级,详细见7.3节。
4.4.2 分类匹配
分类必须由数字和字母组成,下划线“_”也算字母。
4.4.3 输出动作
目前zlog支持若干种输出,语法是:
[输出], [附加选型, 可选]; [format(格式)名, 可选]
stdout, stderr, syslog:如表格描述,其中只有syslog的附加选项是有意义并且必须写的。值得注意的是,zlog在写日志的时候会用这样的语句:
write(STDOUT_FILENO, hs_log_buf_str(a_thread->msg_buf), hs_log_buf_len(a_thread->msg_len));
而如果你的程序是个守护进程,在启动的时候把STDOUT_FILENO也就是1的文件描述关掉的话,会发生什么结果呢?
4.5 文档转档
为什么需要将日志文件转档?我已经在实际的运行环境中不止一次的看到过,因为日志文件过大,导致系统硬盘被撑爆,或者单个日志文件过大而即使用grep也要花费很多时间来寻找匹配的日志。对于日志转档,我总结了如下几种范式。
4.5.1 按固定时间段来切分日志
例如每天生成一个日志:
aa.2012-08-02.log
aa.2012-08-03.log
aa.2012-08-04.log
这种日志适合的场景是管理员大概知道每天生产的日质量,然后希望在n个月之后能精确的找出某天的所有日志。这种日志切分最好由日志库来完成,其次的方法是用cronos-plit这种软件来分析日志内容的时间字符串来进行后期的切分,较差的办法是用crontab+logrotate或mv来定期移动(但这并不精确,会造成若干条当天的日志被放到上一天的文件里面去)。
在zlog里面,这种需求不需要用日志转档功能来完成,简单的在日至文件名里面设置时间日期字符串就能解决问题。
*.* “aa.%d(%F).log”
或者用cronolog来完成,速度会更快一点。
*.* | cronolog aa.%F.logs
4.5.2 按照日志大小切分
多用于开发环境,适合的场景是:程序在短时间内生成大量的日志,而用编辑器vi,ue等能快速打开的日志大小是有限的,或者大的日志打开来极慢。同样,这种日志的切分可以在事后用split等工具来完成,但对于开发而言会增加步骤,所以最好的也是由日志库来完成。值得一提的是存档有两种模式,nlog里面称之为Sequence和Rolling,在Sequence情况下:
aa.log (new)
aa.log.2 (less new)
aa.log.1
aa.log.0 (old)
而在Rolling的情况下:
aa.log (new)
aa.log.0 (less new)
aa.log.1
aa.log.2 (old)
很难说哪种更加符合人的直觉。
如果只有若干个最新的文件是有意义的,需要日志库来做主动的删除旧日志的工作。由外部程序是很难判定哪些日志是旧的。最简单的zlog的转档配置为:
*.* “aa.log”, 10MB
这个配置是Rolling的情况,每次aa.log超过10MB的时候,会做这样的重命名:
aa.log.2 -> aa.log.3
aa.log.1 -> aa.log.2
aa.log.0 -> aa.log.1
aa.log -> aa.log.0
上面的配置可以写的更加啰嗦一点:
*.* “aa.log”, 10MB * 0 ~ “aa.log.#r”
逗号后第一个参数表示文件达到多大后开始进行转档;第二个参数表示保留多少个存档文件(0代表不删除任何存档文件);第三个参数表示转档的文件名,其中#r表示存档文件的序号,r是rolling的缩写,如果写成#s,则是sequence的方式。转档文件名必须包含#r或者#s。
4.5.3 按照日志大小切分同时加上时间标签
如下的效果:
aa.log
aa.log-20070305.00.log
aa.log-20070501.00.log
aa.log-20070501.01.log
aa.log-20071008.00.log
这种情况适合于程序本身的日志一般不是很受关注,但是又在某一天想要找出来看的情况。当然,在这种情况下,万一在20070501这一天日志的量超过了指定值,例如100MB,就又要退回到第二种状态,在文件名中加后缀。其对应zlog的配置为:
*.* “aa.log”, 100MB ~ “aa-%d(%Y%m%d).#2s.log”
每到100MB的时候转档,转档文件名也支持转换字符,可以把转档当时的时间串作为转档文件名的一部分。#2s的意思是序号的长度最少为2位,从00开始编号,Sequence转档。这是zlog对转档最复杂的支持了!
4.5.4 压缩-移动-删除旧的日志
首先,压缩不应该由日志库来完成,因为压缩消耗时间和CPU。日志库的任务是配合压缩。对于第一种和第三种,管理较为简单,只要符合某些文件名规则或修改日期的,可以用shell脚本+crontab轻易的压缩、移动和删除。对于第二种,其实不是非常需要压缩,只需要删除就可以了。
如果一定需要转档的同时进行压缩,只有logrotate能干这活,毕竟它是独立的程序,能在转档的同时搞压缩,不会有混淆的问题。
4.5.6 zlog对外部转档工具的支持
zlog的转档功能已经极为强大,当然也有几种情况是zlog无法处理的,例如按时间条件进行转档,转档前后调用一些自制的shell脚本。。。这会把zlog的配置和表达式弄得过于复杂而缺乏美感。
这时候你也许喜欢用一些外部转档工具,例如logrotate来完成这些工作。问题是,在linux操作系统下,转档工具重命名日志文件名后,应用程序还是往原来的文件描述符写日志,没办法重新打开日志文件写新的日志。标准的做法是给应用程序一个信号,让它重新打开日志文件,对于syslogd是:
kill -SIGHUP ‘cat /var/run/syslogd.pid’
对于zlog,因为是个函数库,不适合接收信号。zlog提供了函数接口hs_log_reload(),这个函数会重载配置文件,重新打开所有的日志文件。应用程序在logrotate的信号,或者其他途径,例如客户端命令后,可以调用这个函数,来重新打开所有的日志文件。
五、高阶使用
5.1 MDC
MDC是什么?在log4j里面解释为Mapped Diagnostic Context。听起来是个很复杂的技术,其实MDC就是一个键-值对表。一旦某次你设置了,后面库可以帮你自动打印出来,或者成为文件名的一部分。
$ cat test_mdc.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include “hs_log.h”
int main(int argc, char **argv)
{
int rc;
rc = hs_log_init();
if(rc){
printf(“init zlog module failed\n”);
return -1;
}
hs_log_info(“1.hello, zlog”);
hs_log_put_mdc(“myname”, ”Zhang”);
hs_log_info(“2.hello, zlog”);
hs_log_put_mdc(“myname”, ”Li”);
hs_log_info(“3.hello, zlog”);
hs_log_fini();
return 0;
}
对应的配置文件为:
$ cat test_mdc.conf
[formats]
mdc_format = “%d(%F %X.%ms)” %-6V (%c:%F:%L) [%M(myname)] - %m%n
[rules]
hs_cat.* > stdout; mdc_format
对应的日志输出为:
2012-03-12 09:26:37.740 INFO (hs_cat:test_mdc.c:47) [] - 1.hello. zlog
2012-03-12 09:26:37.740 INFO (hs_cat:test_mdc.c:51) [zhang] - 2.hello, zlog
2012-03-12 09:26:37.740 INFO (hs_cat:test_mdc.c:55) [Li] - 3.hello, zlog
你可以看到hs_log_put_mdc()在表里面设置键“myname”对应值“Zhang”,然后在配置文件里面%M(myname)指出了在日志的哪个位置需要把值打出来。第二次,键“myname”的值被覆盖写成“Li”,然后日志里面也有相应的变化。
MDC什么时候有用呢?往往在用户需要同样的日志行为区分不同业务数据的时候。比如说,C源代码是:
hs_log_put_mdc(“customer_name”, get_customer_name_from_db());
hs_log_info(“get in”);
hs_log_info(“pick product”);
hs_log_info(“pay”);
hs_log_info(“get out”);
在配置文件里面是:
&format “%M(customer_name) %m%n”
当程序同时处理两个用户的时候,打印出来的日志可能是:
Zhang get in
Li get in
Zhang pick product
Zhang pay
Li pick product
Li pay
Zhang get out
Li get out
这样,你就可以用grep命令把这两个用户的日志分开来了。
$ grep Zhang aa.log > Zhang.log
$ grep Li aa.log > Li.log
或者,还有另外一条路,一开始在文件名里面做区分,看配置文件:
*.* “mdc_%M(customer_name).log”;
这就会产生3个日志文件:mdc_.log、mdc_Zhang.log mdc_Li.log。如果用户想知道自己在干什么,这是一条接近。
MDC是每个线程独有的,所以可以把一些线程专有的变量设置进去。如果单单为了区分线程,可以用转换字符里面的%t来搞定。
5.2 诊断zlog本身
OK,至今为止,我假定zlog库本身不出毛病的。zlog帮助用户程序写日志,帮助程序员debug程序。但是如果zlog内部出错了呢?怎么知道在哪里呢?其他的程序可以用日志库来debug,但日志库自己怎么debug?答案很简单,zlog有自己的日志——诊断日志。这个日志通常是关闭的,可以通过环境变量来打开。
$ export ZLOG_PROFILE_DEBUG=/tmp/zlog.debug.log
$ export ZLOG_PROFILE_ERROR=/tmp/zlog.error.log
诊断日志只有两个级别debug和error。设置好环境变量后,再跑test_hello程序,然后debug日志为:
$ more zlog.debug.log
03-13 09:46:56 DEBUG (7503:zlog.c:115) ------hs_log_init start, compile time[Mar 12 2012]
03-13 09:46:56 DEBUG (7503:spec.c:825) spec: [0x7fdf96b7c010][%d(%F %T)][%F %T 29][]
03-13 09:46:56 DEBUG (7503:spec.c:825) spec: [0x7fdf96b7c010][ ][ 0 ][]
. . . . . .
03-13 09:52:40 DEBUG (8139:zlog.c:291) ------hs_log_fini end------
zlog.error.log日志没有产生,因为没有错误发生。你可以看出来,debug日志展示了zlog是怎么初始化和清理的。不过在hs_log_info()执行的时候没有日志打出来,这是为了效率。
如果zlog库有任何问题,都会打印日志到ZLOG_PROFILE_ERROR所指向的错误日志。比如说,在hs_log_info()上用一个错误的printf的语法:
hs_log_info(zc, “%1”, 1);
然后编译执行程序,ZLOG_PROFILE_ERROR的日志会是:
$ cat zlog.error.log
03-13 10:04:58 ERROR (10102:buf.c:189) vsnprintf fail, errno[0]
03-13 10:04:58 ERROR (10102:buf.c:191) nwrite[-1], size_left[1024], format[%1]
03-13 10:04:58 ERROR (10102:spec.c:329) hs_log_buf_vprintf maybe fail or overflow
03-13 10:04:58 ERROR (10102:spec.c:467) z_spec->gen_buf fail
03-13 10:04:58 ERROR (10102:format.c:160) hs_log_spec_gen_msg fail
03-13 10:04:58 ERROR (10102:rule.c:265) hs_log_format_gen_msg fail
03-13 10:04:58 ERROR (10102:category.c:164) hzb_log_rule_output fail
03-13 10:04:58 ERROR (10102:zlog.c:632) hs_log_output fail, scrfile[test_hello.c]
这样,用户就能知道为啥期待的输出没有产生,然后搞定这个问题。运行时诊断会带来一定的性能损失。一般来说,我在生产环境把ZLOG_PROFILE_ERROR打开,LOG_PROFILE_DEBUG关闭。
还有另外一个办法来诊断zlog。我们都知道hs_log_init()会把所有配置信息读入内存,在整个写日志的过程中,这块内存保持不变。如果用户程序因为某种原因损坏了这块内存,那么就会造成问题。还有可能是内存中的信息和配置文件的信息不匹配。所以我设计了一个函数,把内存的信息展现到ZLOG_PROFILE_ERROR指向的错误日志。
代码见$(top_builddir)/test/test_profile.c:
$ cat test_profile.c
#include <stdio.h>
#include <hs_log.h>
int main(int argc, char **argv)
{
int rc;
rc = hs_log_init();
if(rc) {
printf(“init failed\n”);
return -1;
}
hs_log_info(“hello, zlog”);
hs_log_profile();
hs_log_fini();
return 0;
}
hs_log_profile()就是这个函数。配置文件非常简单:
$ cat test_profile.conf
[formats]
simple = “%m%n”
[rules]
hs_cat.* > stdout; simple
然后zlog.error.log会是:
$ cat /tmp/zlog.error.log
06-01 11:21:26 WARN (7063:zlog.c:783) ------hs_log_profile start------
06-01 11:21:26 WARN (7063:zlog.c:784) init_flag:[1]
06-01 11:21:26 WARN (7063:conf.c:75) -conf[0x2333010]-
06-01 11:21:26 WARN (7063:conf.c:76) --global—
06-01 11:21:26 WARN (7063:conf.c:77) ---file[test_profile.conf], mtime[2012-06-01 11:23:12]
06-01 11:21:26 WARN (7063:conf.c:78) --strict init[1]---
06-01 11:21:26 WARN (7063:conf.c:79) ---buffer min[1024]---
06-01 11:21:26 WARN (7063:conf.c:80) ---buffer max[2097152]---
06-01 11:21:26 WARN (7063:conf.c:82) ---default_format---
06-01 11:21:26 WARN (7063:format.c:48) ---format[0x235ef60][default = %d(%F %T) %V [%p:%F:%L] %m%n”]
06-01 11:21:26 WARN (7063:conf.c:85) ---file perms[0600]---
06-01 11:21:26 WARN (7063:conf.c:87) ---rotate lock file[/tmp/zlog.lock]---
06-01 11:21:26 WARN (7063:rotater.c:48) ---rotater[0x233b8d0][0x233b7d0,/tmp/zlog.lock.log]
06-01 11:21:26 WARN (7063:level_list.c:37) --level_list[0x2335490]--
06-01 11:21:26 WARN (7063:level.c:37) ---level[0x23355c0][0, *.*, 1, 6]---
06-01 11:21:26 WARN (7063:level.c:37) ---level[0x23355e0][20, DEBUG, debug, 5, 7]---
06-01 11:21:26 WARN (7063:level.c:37) ---level[0x2339600][40, INFO, info, 4, 6]---
06-01 11:21:26 WARN (7063:level.c:37) ---level[0x233b830][60, NOTICE, notice, 6, 5]---
06-01 11:21:26 WARN (7063:level.c:37) ---level[0x2339600][80, WARN, warn, 4, 4]---
06-01 11:21:26 WARN (7063:level.c:37) ---level[0x2339600][100, ERROR, error, 5, 3]---