zlog日志库(笔记) —— 配置文件
配置文件
zlog是一个通用日志库,基本功能跟专用日志库并没有本质区别。通用日志库,通过配置文件,来简化一些列日志使用和配置,以适应各种不同项目需求。而配置文件承载了这角色,为不同项目需求定制不同的功能,主要包括:把日志打印到哪儿去,用什么格式,如何转档等。
例如,一个完整的配置文件内容如下:
# comments
[global]
strict init = true
buffer min = 1024
buffer max = 2MB
rotate lock file = /tmp/zlog.lock
default format = "%d.%us %-6V (%c:%F:%L) - %m%n"
file perms = 600
[levels]
TRACE = 10
CRIT = 130, LOG_CRIT
[formats]
simple = "%m%n"
normal = "%d %m%n"
[rules]
default.* >stdout; simple
*.* "%12.2E(HOME)/log/%c.log", 1MB*12; simple
my_.INFO >stderr;
my_cat.!ERROR "/var/log/aa.log"
my_dog.=DEBUG >syslog, LOG_LOCAL0; 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 byte
注:单位大小写不敏感,1GB <=> 1gb <=> 1Gb <=> 1gB。
全局参数
可选。
全局参数以[global]开头,"[]"代表一个字节开始,四个小节顺序不能变:global -> levels -> formats -> rules。
语法格式:
(key) = (value)
比如上面的"strict init = true"中,"strict init"是key,"true"是value。
- strict init
如果"strict init"值为true,那么zlog_init()将严格检查所有的格式和规则,任何错误都会导致zlog_init()失败并返回-1;如果为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倍,直到进程用完所有内存。
缓存大小默认使用byte作为单位,可以显式加上KB,MB或GB单位。"buffer min"默认值1K,"buffer max默认值2MB。
所谓截断,通常是指将末尾的几个数据替换为如"..."(zlog_buf_new()调用者决定),让用户知道内容发生阶段。
- ratate lock file
该选项指定一个锁文件,用来保证多进程情况下日志安全转档。zlog会在zlog_init()时,以读写权限打开该文件。
请确认你执行APP的用户有权限创建和读写该锁文件。
转档日志的伪代码:
write(log_file, a_log)
if (log_file > 1M)
if (pthread_mutex_lock succ && fcntl_lock(lock_file) succ)
if (log_file > 1M) rotate(log_file);
fcntl_unlock(lock_file);
pthread_mutex_unlock;
mutex 锁(互斥锁)用于多线程,fcntl 锁(POSIX建议锁)用于多进程。进程意外死亡后,OS会释放此进程持有的锁。这是zlog选择用fcntl锁保证安全转档的重要原因。
rotate lock file默认值是/tmp/zlog.lock。
如果rotate lock file = self,此时,zlog不会创建任何锁文件,用配置文件作为锁文件。而fnctl是建议锁(并非强制锁),用户可自由修改、存储配置文件。一般地,单个日志文件不会被不同OS用户的进程转档,所以用配置文件作为锁文件是安全的。
如果用户设置其他路径作为锁文件,如/tmp/zlog.lock,zlog会在zlog_init()时创建该文件。如果有多个OS用户的进程需要转档同一个日志文件,确认该锁文件id多个用户都可读写。
- default format
缺省日志格式,默认值:
%d %V [%p:%F:%L] %m%n"
该格式输出形如:
2022-09-11 12:14:52 INFO [14210:test_hello.c:33] info, zlog
- file perms
指定了创建日志文件的缺省访问权限。必须注意最后产生的日志文件权限为"file perms" & ~umask。umask是进程操作文件的权限的掩码。
默认值为0x600("rw-------"),表示只允许当前用户读写。
- fsync period
每条规则写了一定次数的日志消息到文件后,zlog会调用fsync(3)让OS马上把数据写到硬盘。次数是每条规则单独统计的,并且在zlog_reload()后会清0。需要指出的是,在日志文件名是动态生成或者被转档的情况下,zlog不能保证把所有文件都搞定,zlog只fsync()那个时候刚刚write()的文件描述符。这提供了写日志速度和数据安全性之间的平衡。
例如:
$ time ./test_press_zlog 1 10 100000
real 0m1.806s
user 0m3.060s
sys 0m0.270s
$ wc -l press.log
1000000 press.log
$ time ./test_press_zlog 1 10 100000 # fsync period = 1K
real 0m41.995s
user 0m7.920s
sys 0m0.990s
$ time ./test_press_zlog 1 10 100000 # fsync period = 10K
real 0m6.856s
user 0m4.360s
sys 0m0.550s
如果你非常在乎安全而不是速度,建议用同步IO文件,参见【输出动作】部分。
默认值0,由OS决定何时冲刷缓存到文件。
日志等级
可选。除了支持系统默认的几个日志等级:DEBUG,INFO,NOTICE,WARN,ERROR,FATAL。zlog还支持用户自定义日志等级。语法:
(level string) = (level int), (syslog level, optional)
(level int)必须在[1, 253]之间,值越大表示越重要。
用户自定义等级
如果用户需要自定义一个等级,如TRACE(没有在默认等级中),可以按下面步骤进行配置。
步骤:
1)在配置文件中定义新的等级
$ cat $(top_builddir)/test/test_level.conf
[global]
default format = "%V %v %m%n"
[levels]
TRACE = 30, LOG_DEBUG
[rules]
my_cat.TRACE >stdout;
内置默认等级(不需要写在配置文件里面):
DEBUG = 20, LOG_DEBUG
INFO = 40, LOG_INFO
NOTICEC = 60, LOG_NOTICE
WARN = 80, LOG_WARNING
ERROR = 100, LOG_ERR
FATAL = 120, LOG_ALERT
UNKNOWN = 254, LOG_ERR
自定义的等级数,必须在[1, 253]之间,其他数字非法。数字越大,代表越重要。现在, TRACE等级为30,比DEBUG(等级20)高,比INFO等级低(等级40)。这样,TRACE就能在下面的配置文件中用了。例如:
my_cat.TRACE >stdout;
意味着等级 >= TRACE的,包括INFO,NOTICE,WARN,ERROR,FATAL都会被写到标准输出中。
格式里面的转换字符%V会产生等级字符串的大写输出,%v会产生小写的等级字符串输出。
另外,在等级的定义里面,LOG_DEBUG是指当需要输出到syslog时,自定义的TRACE等级会以LOG_DEBUG输出到syslog。
2)在源代码里面直接用新的等级
注意:这个.c文件和下面的.h文件,相当于是用户程序自定义的,并非库运行时的程序。
zlog(cat, __FILE__, sizeof(__FILE__)-1, \
__func__, sizeof(__func__)-1, __LINE__, \
30, "test %d", 1); // 30 是刚刚自定义的TRACE
为了简单使用,创建一个.h头文件。
$ cat $(top_builder)/test/test_level.h
#ifndef __test_level_h
#define __test_level_h
#include "zlog.h"
enum {
ZLOG_LEVEL_TRACE = 30,
/* must equals conf file setting */
};
#define zlog_trace(cat, format, ...) \
zlog(cat, __FILE__, sizeof(__FILE__)-1, \
__func__, sizeof(__func__)-1, __LINE__, \
ZLOG_LEVEL_TRACE, format, ## __VA_ARGS__)
#endif
3)这样zlog_trace就能在.c文件里面调用了
$ cat $(top_builddir)/test/test_level.c
#include <stdio.h>
#include "test_level.h"
int main(int argc, char *argv[])
{
int rc;
zlog_category_t *zc;
rc = zlog_init("test_level.conf"); /* 以配置文件test_level.conf初始化zlog库 */
if (rc) {
printf("init failed\n");
return -1;
}
zc = zlog_get_category("my_cat");
if (!zc) {
printf("get cat fail\n");
zlog_fini(); /* 反初始化zlog库 */
return -2;
}
zlog_trace(zc, "hello, zlog - trace");
zlog_debug(zc, "hello, zlog - debug");
zlog_info(zc, "hello, zlog - debug");
zlog_fini();
return 0;
}
4)最后,可以观察到输出
$ ./test_level
TRACE trace hello, zlog - trace
INFO info hello, zlog - info
这正是我们所期待的,配置文件只允许 >= TRACE等级的日志输出到屏幕上。配置文件test_level.conf中的%V %v,也显示了正确的结果。
格式(Formats)
以[formats]开始,用来定义日志的格式。语法:
(name) = "(actual formats)"
后面的规则(rules)需要引用时,就通过(name)。(name)必须由数字、字母、下划线("_")组成。(actual format)需要用双引号包裹,可以由转换字符串组成,见【转换格式串】部分。
e.g. 截取formats及rules中引用示例
[formats]
simple = "%m%n"
normal = "%d %m%n"
[rules]
default.* >stdout; simple
这里,"simple"就是格式name,"%m%b"就是转换格式串。
转换格式串
转换格式串的设计是从C标准的printf抄过来的。一个转换格式串由文本字符和转换说明组成。
转换格式串用于规则(rules)的日志文件路径,输出格式(format)中。
你可以把任意的文本字符放到转换格式串里面。
每个转换说明都是以百分号(%)打头,后面跟可选的宽度修饰符,最后以转换字符结尾。转换字符决定了输出什么数据,例如分类名、级别、时间日期、进程号等。宽度修饰符控制了这个字段的最大最小宽度、左右对齐。
例如,如果转换格式串是
"%d(%m-%d %T) %-8V [%p:%F:%L] %m%n"
说明:"%"开头+若干字母,能被zlog识别的,称为转换字符。
像"-" ":" "[" "]" " "(
这里%-8V,表示日志级别要大写,并且左对齐,占用8个字符宽度。
源代码中,写log语句是:
zlog_info(c, "hello, zlog");
将会输出:
09-11 15:37:21 INFO [27608:test_hello.c:33] info, zlog
转换字符
zlog可以识别的转换字符有:
转换字符 | 效果 | 示例 |
---|---|---|
%c | 分类名 | aa_bb |
%d() | 打印日志的时间。后面要跟一对小括号(),内含说明具体的日期格式。 就像%d(%F)或%d(%m-%d %T)。 |
%d(%F) 2022-12-01 %d(%m-%d %T) 12-01 17:17:42 %d(%T) 17:17:42.035 %d 2022-02-14 17:03:12 |
%d() | ||
%E() | 获取环境变量的值 | %E(USER) martin |
%ms | 毫秒,3位数字字符串 取自gettimeofday(2) |
013 |
%us | 微秒,6位数字字符串 取自gettimeofday(2) |
002323 |
%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__宏 | 125 |
%m | 用户日志,用户从zlog接口输入的日志(内容) | hello, zlog |
%M | MDC(mapped diagnostic context),每个线程一张键值对表,输出键相对应的值。后面必需跟一对小括号()内含键。例如,%M(clientNumber),clientNumber。可参见【MDC】 | %M(clientNumber) 123456 |
%n | 换行符,目前还不支持Windows换行符 | \n |
%p | 进程ID,来源于个getpid() | 2134 |
%U | 调用函数名,源于__func__(C99)或__FUNCTION__(gcc) | main |
%V | 日志级别,大写 | INFO |
%v | 日志级别,小写 | info |
%t | 16进制表示线程ID,来源于phtread_self() "0x%x", (unsigned int) pthread_t |
0xba01e70 |
%T | 相当于%t,不过以长整型表示 "%lu", (unsinged long)pthread_t |
140633234859776 |
%% | 一个百分号 | % |
%[其他字符] | 解析为错误,zlog_init()将会失败 |
宽度修饰符
通常,数据按原样输出。不过,因为不同消息的各字段长度不一样,如果要控制不同消息对齐,该怎么办?
可以使用宽度修饰符,控制最小字段宽度、最大字段宽度和左右对齐方式。需要付出一定性能代价。
宽度修饰符,可选,位于%和转换字符之间。
- 左对齐标识
左对齐标识,用减号(-)表示。
- 最小字段宽度
十进制数常量,表示最少有几个字符会被输出。如果数据本来没有那么多字符,将会填充空格(左对齐/右对齐)直到最小宽度为止。默认填充在字段的左边,也就说右对齐。如果使用了左对齐标识,那么就填充在右边。填充字符为空格(
例如,前面的
[formats]
simple = "%d(%m-%d %T) %-8V [%p:%F:%L] %m%n"
%-8V:表示日志级别字段左对齐,最小宽度8.
输出log:
09-11 15:37:21 INFO [27608:test_hello.c:33] info, zlog
即这里日志级别"INFO"后会跟5个空格(8 - 3 = 5),还有1个空格是simple原有固定的空格。
而最大字段宽度,可以截断数据。最大字段宽度是放在一个句点号(.)后面的十进制数常量。如果数据宽度超过最大字段宽度,则尾部多余的字符(超过最大字段宽度的部分)将会被截去。如果最大宽度字段8,数据宽度10,那么最后2个字符会被丢弃。该行为和C的printf一样,把后面的部分截断。
下面是各种宽度修饰符和分类转换字符(%c)配合一起使用的例子。
宽度修饰符 | 左对齐? | 最小字段宽度 | 最大字段宽度 | 附注 |
---|---|---|---|---|
%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个,去掉后面的 |
时间字符
转换字符d 支持的时间字符,即%d()括号中的内容。
所有字符都由strftime(2)生成。在Linux OS上支持的是:
字符 | 效果 | 例子 |
---|---|---|
%a | 一星期中各天的缩写名,根据locale显示 | Wed |
%A | 一星期中各天的全民,根据locale显示 | Wednesday |
%b | 缩写的月份名,根据locale显示 | Mar |
%B | 月份全名,根据locale显示 | March |
%c | 当地时间和日期的全表示,根据locale显示 | Thu Feb 16 14:16:35 2020 |
%C | 世纪(年/100),2位的数字(SU) | 20 |
%d | 一个月中的某一天(01~31) | 06 |
%D | 相当于%m/%d/%y.(美国人专用,建议少用)(SU) | 02/16/22 |
%e | 类似于%d,一个月中的某一天,但头上的0被替换位空格(SU) | 6 |
%F | 相当于%Y-%m-%d(ISO 8601日期格式)(C99) | 2022-02-16 |
%G | 采用%V定义的年,如果那年的前几天不算新年的第一周,就算上一年 | 2022 |
%g | 相当于%G,不过不带世纪(00-99) | 22 |
%h | 相当于%b(SU) | Feb |
%H | 小时,24小时表示(00~23) | 14 |
%I | (大写i)小时,12小时表示(01-12) | 02 |
%j | 一年中的各天(001-366) | 047 |
%k | 小时,24小时表示(0-23);一位的前面为空格(可和%H比较)(TZ) | 15 |
%l | (小写L)小时,12小时表示(0-12);一位的前面为空格(可和% I比较)(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下相当于%l:%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),周日是一周的开始, 一年中第一个周日所在周d是第01周。另见%V和%W |
07 |
%V | ISO 8601星期序号(0-6),周日是0。另见%u | 07 |
%w | 一周的天序号(0-6),周日是0。另见%u | 4 |
%W | 一年中的星期序号(00-53),周一是一周的开始, 一年中第一个周一所在的周是第01周。另见%V和%W |
07 |
%x | 当前locale下的偏好日期 | 02/16/22 |
%X | 当前locale下的偏好时间 | 15:14:47 |
%y | 不带世纪数目的年份(00-99) | 22 |
%Y | 带世纪数目的年份 | 2022 |
%z | 当前时区相对于GMT时间的偏移量。采用RFC 822-conformant来计算 (using "%a,%d %b %Y %H:%M:%S %z")(GNU) |
+0800 |
%Z | 时区名(如果有的话) | CST(美国、澳大利亚、古巴 或中国的标准时间) |
%% | 一个百分号 | % |
规则(Rules)
描述了日志怎么被过滤、格式化以及被输出。语法:
(category).(level) (output), (options, optional); (format name, optional)
当zlog_init()被调用时,所有规则都会被读到内存中。当zlog_get_category()被调用时,规则就会被分配给分类。在实际写日志的时候,例如zlog_info()被调用的时候,就会比较这个INFO和各条规则的等级,来决定这条日志会不会通这条规则输出。当zlog_reload()被调用时,配置文件会被重新读入,包括所有的规则,并且重新计算分类对应的规则。
级别匹配
规则中,level字段决定级别。
zlog有6个默认的级别:DEBUG, INFO, NOTICE, WARN, ERROR, FATAL。像其他的日志函数库,aa.DEBUG意味着任何 >= DEBUG级别的日志会被输出。
表达式 | 含义 |
---|---|
* | 所有等级 |
aa.debug | 代码内等级 >= debug |
aa.=debug | 代码内等级 == debug |
aa.!debug | 代码内等级!=deubg |
分类匹配
分类必须由数字和字母组成,下划线"_"也算字母。
总结 | 配置文件规则分类 | 匹配的代码分类 | 不匹配的代码分类 |
---|---|---|---|
"*"匹配所有 | . | aa, aa_bb, aa_cc, xx, yy ... |
NONE |
以"_"结尾的分类匹配本级以下级分类 | aa_.* | aa, aa_bb, aa_cc, aa_bb_cc |
xx, yy |
不以"_"结尾的精确匹配分类名 | aa.* | aa | aa_bb, aa_cc, aa_bb_cc |
"!"匹配那些没有找到规则的分类 | !.* | xx | aa(as it matches rules above) |
输出动作
目前zlog支持若干种输出,语法:
[输出],[附加选项, 可选]; [format(格式)名, 可选]
动作 | 输出字段 | 附加选项 |
---|---|---|
标准输出 | >stdout | 无意义 |
标准错误输出 | >stderr | 无意义 |
输出到syslog | >syslog | syslog设施(facility): LOG_USER(default), LOG_LOCAL[0-7]必填 |
管道输出 | cat | |
文件 | "文件路径" | 文件转档(见下文) 10M*3 ~ "press.#r.log" |
同步IO文件 | -"文件路径" | |
用户自定义输出 | $name | "path"动态或静态地用于record输出 |
- stdout, stderr, syslog
如表格描述,其中只有syslog的附加选项是有意义并且必须写到。
值得注意的是,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。这会产生未定义行为。如果一定要输出到终端(STDOUT_FILENO),那么用"/dev/tty"代替。
- 管道输出
*.* | /usr/bin/cronolog /www/logs/example_%Y%m%d.log ; normal
这是一个将zlog的输出到管道后接cronolog的例子。实现原理很简单,在zlog_init时,调用popen("/usr/bin/cronolog /www/logs/example_%Y%m%d.log", "w"),后面往这个文件描述符写指定格式的日志。使用cronolog来生成按天分割的日志效率,比zlog自己的动态路径的效率要高,因为通过管道,无需每次都打开、关闭动态路径的文件描述符。
[rules]
*.* "press%d(%Y%m%d).log"
$ time ./test_press_zlog 1 10 100000
real 0m4.240s
user 0m2.500s
sys 0m5.460s
[rules]
*.* / /usr/bin/cronolog press%Y%m%d.log
$ time ./test_press_zlog 1 10 100000
real 0m1.911s
user 0m1.980s
sys 0m1.470s
使用管道也有限制:
1)POSIX.1-2001 保证读写不大于PIPE_BUF大小的内容是原子的。Linux上,PIPE_BUF为4096。
2)单条日志的长度超过PIPE_BUF时,并且有多个有父子关系的进程通zlog写同一个管道,也就说zlog_init之后fork多个子进程,此时只有一个cronolog的进程监听一个管道描述符,日志内容可能会交错。
3)多个进程分别调用zlog_init,启动多个cronolog进程,写拥有同一个文件路径的日志文件,即单条日志长度不超过PIPE_BUF,也有可能导致日志交错,因为cronolog读到的文件流是连续的,它不知道单条日志的边界在哪。
总结下,使用管道来输出到单个日志文件的情况:
1)单进程写,单条日志长度不限制。单进程内的多线程写日志的原子性已经由zlog保证。
2)有父子关系的多进程,单条日志长度不能超过PIPE_BUF(4096)。
3)无父子关系的多进程使用管道同时写一个日志,无论单条日志长度是多少,都有可能导致日志交错。
zlog本身的直接文件输出能保证即使多进程,同时调用zlog写一个日志文件也不会产生交错。
- 文件
1)文件路径
可以说相对路径或绝对路径,被双引号("")包含。转换格式串可以用在文件路径上。例如,文件路径是"%E(HOME)/log/out.log",环境变量$HOME是/home/harry,最后的输出文件是/home/harry/log/output.log。详见【转换格式串】部分。
zlog文件功能强大,例如:
(1)输出到命名管道(FIFO),必须在调用前由mkfifo(1)创建
*.* "/tmp/pipefile"
(2)输出到NULL,也就是不输出
*.* "/dev/null"
(3)在任何情况下输出到终端
*.* "/dev/tty"
(4)每个线程一个日志,在程序运行的目录下
*.* "%T.log"
(5)输出到有进程号区分的日志,每天,在$HOME/log目录,每1GB转档一次,保持5个日志文件。
*.* "%E(HOME)/log/aa.%p.%d(%F).log",1GB * 5
(6)aa_及下级分类,每个分类一个日志
aa_.* "/var/log/%c.log"
- 文件转档
控制文件的大小和个数。zlog根据这个字段来转档,当日志文件太大时,例如
"%E(HOME)/log/out.log", 1M * 3 ~ "%E(HOME)/log/out.log.#r"
这三个参数都不是必填项,zlog转档功能详见【文件转档】。
- 同步IO文件
在文件路径前加一个"-",就打开了同步IO选项 。在打开文件(open)时,会以O_SYNC选项打开,此时,每次写日志操作都会等OS把数据写到硬盘后才返回。该选项极为耗时:
$ time ./test_press_zlog 100 1000
real 0m0.732s
user 0m1.030s
sys 0m1.080s
$ time ./test_press_zlog 100 1000 # synchronous I/O open
real 0m20.646s
user 0m2.570s
sys 0m6.950s
- 格式名
可选。如果不写,用全局配置里面的默认格式
[global]
default format = "%d(%F %T) %V [%p:%F:%L] %m%n"
- 用户自定义输出
详见【用户自定义输出】部分。
文件转档
为什么需要日志文件转档?
因为日志文件过大,可能导致系统硬盘被撑爆;或者,单个日志文件过大,导致grep需要花费很多时间来查找需要匹配的日志。
日志转档,有以下范式:
1)按固定时间段来切分日志
如每天生成一个日志文件
aa.2022-08-02.log
aa.2022-08-03.log
aa.2022-08-04.log
适用场景:管理员大概知道每天生成日志量,然后希望在n个月后,能精确找到某天所有日志。这种日志切分最好由日志库来完成,其次由cronosplit软件分析日志内容的时间字符串后进行切分,最后是用crontab+logrotate或mv来定期移动(但不精确,会造成若干条当前日志被放到上一天的文件里面去)。
zlog完成这种需求,很简单,只需要在(输出)日志文件名里设置时间日期字符串就行了
*.* "aa.%d(%F).log"
如果用cronolog,速度会更快点
*.* | cronolog aa.%F.log
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)
其中,aa.log永远是当前最新的log。Sequence和Rolling的区别,就是后缀0,1,2,谁是次新的,谁是最旧的。
如果只有若干个最新的文件才有意义,需要日志库做主动的删除旧log的工作。由外部程序判断哪些是旧的很难。
最简单的zlog转档配置
[rules]
*.* "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
上面配置可用写更啰嗦一点:
[rules]
*.* "aa.log", 10MB * 0 ~ "aa.log.#r"
逗号后,第一个参数表示文件达到多大后,开始进行转档;
第二个参数表示保留多少个存档文件(0表示不删除任何存档文件);
第三个参数表示转档的文件名,其中#r表示存档文件的序号,r是rolling的缩写。还可以放#s,s是sequence缩写。转档文件名必须包含#r或#s。
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转档。
4)压缩、移动、删除旧的日志
压缩不应该由日志库来完成,因为压缩消耗大量CPU时间。日志库的任务是配合压缩。
对于第一种和第三种,管理较为简单,只要符合某些文件名规则或修改日期的,用shell脚本+crontab轻易压缩、移动和删除。
如果一定要转档的同时进行压缩,只有logrotate能干这活,毕竟是独立的程序,能转档同时搞压缩,不会混淆。
5)zlog对外部转档工具,如logrotate的支持
zlog的转档功能极为强大,有几种是zlog无法处理的,如按时间条件进行转档,转档前后调用一些自制的shell脚本... 这会把zlog配置和表达弄得过于复杂而缺乏美感。
此时,你也许喜欢用一些外部转档工具,如logrotate来完成工作。问题是,Linux OS下,转档工具重命名日志文件名后,APP还是往原来的文件描述符写日志,没办法重新打开日志文件写新的日志。标准做法是给APP一个信号,让其重新打开日志文件,对syslog是
$ kill -SIGHUP 'cat /var/run/syslogd.pid'
对于zlog,因为是个函数库,不适合接受信号。zlog提供函数接口zlog_reload(),该函数会重载配置文件,重新打开所有的日志文件。APP在收到信号logrotate的信号,或者相关命令后,可用调用该函数,重新打开所有日志文件。
配置文件工具
zlog源码src路径下,有个zlog-chk-conf.c源码文件,执行"$ make"命令编译后,会生成zlog-chk-conf目标程序。该程序尝试读取配置文件,检查语法,然后往屏幕上输出这些配置文件是否正确。建议每次创建或改动一个配置文件后,都用这个工具检查一下。
$ ./zlog-chk-conf -h
usage: zlog-chk-conf [conf files]...
-q, suppress non-error message
-h, show help message
zlog version: 1.2.12
用zlog-chk-conf检查配置文件zlog.conf(error):
$ ./zlog-chk-conf zlog.conf
03-08 15:35:44 ERROR (10595:rule.c:391) sscanf [aaa] fail, category or level is null
03-08 15:35:44 ERROR (10595:conf.c:155) zlog_rule_new fail [aaa]
03-08 15:35:44 ERROR (10595:conf.c:258) parse configure file[zlog.conf] line[126] fail
03-08 15:35:44 ERROR (10595:conf.c:306) zlog_conf_read_config fail
03-08 15:35:44 ERROR (10595:conf.c:366) zlog_conf_build fail
03-08 15:35:44 ERROR (10595:zlog.c:66) conf_file[zlog.conf], init conf fail
03-08 15:35:44 ERROR (10595:zlog.c:131) zlog_init_inner[zlog.conf] fail
---[zlog.conf] syntax error, see error message above
可以同时分析多个配置文件(OK):
$ ./zlog-chk-conf ../test/test_hello.conf ../test/test_init.conf
--[../test/test_hello.conf] syntax right
--[../test/test_init.conf] syntax right