zlog學習隨筆


zlog1使用手冊

 

Contents

Chapter 1  zlog是什么?

zlog是一個高可靠性、高性能、線程安全、靈活、概念清晰的純C日志函數庫。

事實上,在C的世界里面沒有特別好的日志函數庫(就像JAVA里面的的log4j,或者C++的log4cxx)。C程序員都喜歡用自己的輪子。printf就是個挺好的輪子,但沒辦法通過配置改變日志的格式或者輸出文件。syslog是個系統級別的輪子,不過速度慢,而且功能比較單調。

所以我寫了zlog。

zlog在效率、功能、安全性上大大超過了log4c,並且是用c寫成的,具有比較好的通用性。

zlog有這些特性:

  • syslog分類模型,比log4j模型更加直接了當
  • 日志格式定制,類似於log4j的pattern layout
  • 多種輸出,包括動態文件、靜態文件、stdout、stderr、syslog、用戶自定義輸出函數
  • 運行時手動、自動刷新配置文件(同時保證安全)
  • 高性能,在我的筆記本上達到25萬條日志每秒, 大概是syslog(3)配合rsyslogd的1000倍速度
  • 用戶自定義等級
  • 多線程和多進程環境下保證安全轉檔
  • 精確到微秒
  • 簡單調用包裝dzlog(一個程序默認只用一個分類)
  • MDC,線程鍵-值對的表,可以擴展用戶自定義的字段
  • 自診斷,可以在運行時輸出zlog自己的日志和配置狀態
  • 不依賴其他庫,只要是個POSIX系統就成(當然還要一個C99兼容的vsnprintf)

相關鏈接:

主頁:http://hardysimpson.github.com/zlog/

下載:https://github.com/HardySimpson/zlog/releases

郵箱:HardySimpson1984@gmail.com

1.1  兼容性說明

  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這個函數庫。我推薦ctrio, 或者C99-snprintf。只要改buf.c就行,祝好運!
  3. 有網友提供了如下版本,方便其他平台上安裝編譯,非常感謝!

    auto tools版本: https://github.com/bmanojlovic/zlog

    cmake版本: https://github.com/lisongmin/zlog

    windows版本: https://github.com/lopsd07/WinZlog

1.2  zlog 1.2 發布說明

  1. zlog 1.2 新增了這些功能
    1. 對管道的支持,從此zlog可以外接cronolog這樣的日志過濾程序來輸出
    2. 全面的日志轉檔支持,詳見5.6
    3. 其他兼容性的代碼改動
  2. zlog 1.2 在庫方面是和zlog 1.0/1.1二進制兼容的,區別在於:
    1. 所有的宏改為小寫,ZLOG_INFO->zlog_info,方便開發者手工輸入。這是一個巨大的改變,如果zlog1.1/1.0的用戶要用zlog 1.2的話,需要寫一個腳本,把源代碼中的大寫批量替換為小寫,然后重新編譯你的程序。我提供了一個腳本:
      sed -i -e ’s/\b\w * ZLOG\w * \b/\L&\E/g’ aa.c
    2. 取消了auto tools的使用,也就是說,不論你在任何平台,都需要gcc和gnu make才能編譯安裝zlog。主流的操作系統(Aix, OpenSolaris..)都能安裝gcc和gnu make。當然也可以自行修改makefile來完成編譯,對於平台稍有經驗的Geek都可以自行完成!

Chapter 2  zlog不是什么?

zlog的目標是成為一個簡而精的日志函數庫,不會直接支持網絡輸出或者寫入數據庫,不會直接支持日志內容的過濾和解析。

原因很明顯,日志庫是被應用程序調用的,所有花在日志庫上的時間都是應用程序運行時間的一部分,而上面說的這些操作都很費時間,會拖慢應用程序的速度。這些事兒應該在別的進程或者別的機器上做。

如果你需要這些特性,我建議使用rsyslog、zLogFabric、Logstash,這些日志搜集、過濾、存儲軟件,當然這是單獨的進程,不是應用程序的一部分。

目前zlog已經支持7.4,可以自己實現一個輸出函數,自由的把日志輸出到其他進程或者其他機器。而把日志的分類匹配、日志格式成型的工作交給zlog。

目前我的想法是實現一個zlog-redis客戶端,用自定義輸出功能,把日志存儲到本機或者遠程的redis服務器內,然后用其他進程(也使用zlog庫)來把日志寫到文件里面,不知大家以為這個想法如何?歡迎和我聯系探討。

Chapter 3  Hello World

3.1  編譯和安裝zlog

下載zlog-latest-stable.tar.gz

$ tar -zxvf zlog-latest-stable.tar.gz

$ cd zlog-latest-stable/

$ make 

$ sudo make install

or

$ sudo make PREFIX=/usr/local/ install

PREFIX指明了安裝的路徑,安轉完之后為了讓你的程序能找到zlog動態庫

$ sudo vi /etc/ld.so.conf

/usr/local/lib

$ sudo ldconfig

在你的程序運行之前,保證libzlog.so在系統的動態鏈接庫加載器可以找到的目錄下。上面的命令適用於linux,別的系統自己想辦法。

  • 除了一般的make以外,還可以
$ make 32bit # 32bit version on 64bit machine, libc6-dev-i386 is needed

$ make noopt # without gcc optimization

$ make doc   # lyx and hevea is needed

$ make test  # test code, which is also good example for zlog

  • makefile是用GNU make的格式寫的,所以在你的平台上需要預裝gnu make和gcc。或者,手工修改一個自己平台的makefile也行。

3.2  應用程序調用和鏈接zlog

應用程序使用zlog很簡單,只要在C文件里面加一行。

#include "zlog.h"

鏈接zlog需要pthread庫,命令是:

$ cc -c -o app.o app.c -I/usr/local/include

  # -I[where zlog.h is put]

$ cc -o app app.o -L/usr/local/lib -lzlog -lpthread

  # -L[where libzlog.so is put]

3.3  Hello World 代碼

這些代碼在$(top_builddir)/test/test_hello.c, test_hello.conf

  1. 寫一個C文件:
    $ vi test_hello.c

    #include <stdio.h> 

    #include "zlog.h"

     

    int main(int argc, char** argv)

    {

    int rc;

    zlog_category_t *c;

    rc = zlog_init("test_hello.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;

  2. 寫一個配置文件,放在和test_hello.c同樣的目錄下:
    $ vi test_hello.conf

    [formats]

    simple = "%m%n"

    [rules]

    my_cat.DEBUG    >stdout; simple

  3. 編譯、然后運行!
    $ cc -c -o test_hello.o test_hello.c -I/usr/local/include

    $ cc -o test_hello test_hello.o -L/usr/local/lib -lzlog

    $ ./test_hello

    hello, zlog

3.4  更簡單的Hello World

這個例子在$(top_builddir)/test/test_default.c, test_default.conf. 源代碼是:

#include <stdio.h>

#include "zlog.h"

int main(int argc, char** argv)

{

int rc;

rc = dzlog_init("test_default.conf", "my_cat");

if (rc) {

printf("init failed\n");

return -1;

}

dzlog_info("hello, zlog");

zlog_fini();

return 0;

配置文件是test_default.conf,和test_hello.conf一模一樣,最后執行程序的輸出也一樣。區別在於這里用了dzlog API,內含一個默認的zlog_category_t。詳見6.5

Chapter 4  Syslog 模型

4.1  分類(Category)、規則(Rule)和格式(Format)

zlog有3個重要的概念:分類(Category)、規則(Rule)和格式(Format)。

分類(Category)用於區分不同的輸入。代碼中的分類變量的名字是一個字符串,在一個程序里面可以通過獲取不同的分類名的category用來后面輸出不同分類的日志,用於不同的目的。

格式(Format)是用來描述輸出日志的格式,比如是否有帶有時間戳,是否包含文件位置信息等,上面的例子里面的格式simple就是簡單的用戶輸入的信息+換行符。

規則(Rule)則是把分類、級別、輸出文件、格式組合起來,決定一條代碼中的日志是否輸出,輸出到哪里,以什么格式輸出。

所以,當程序執行下面的語句的時候

zlog_category_t  * c;

c = zlog_get_category("my_cat");

zlog_info(c, "hello, zlog");

zlog會找到c的名字是"my_cat",對應的配置文件中的規則是

[ rules ]

my_cat.DEBUG    >stdout; simple

然后庫會檢查,目前這條日志的級別是否符合規則中的級別來決定是否輸出。因為INFO>=DEBUG,所以這條日志會被輸出。並且根據這條規則,會被輸出到stdout(標准輸出) ,輸出的格式是simple,在配置文件中定義是

[ formats ]

simple = "%m%n" 

最后在屏幕上打印

hello, zlog

這就是整個過程。用戶要做就是寫自己的信息。日志往哪里輸出,以什么格式輸出,都是庫和配置文件來完成的。

4.2  syslog模型和log4j模型的區別

好,那么目前這個模型和syslog有什么關系呢?至今為止,這個模型還是比較像log4j。log4j的模型里面有logger, appender和layout。區別在於,在log4j里面,代碼中的logger和配置中的logger是一一對應的,並且一個logger有唯一的級別。一對一關系是log4j, log4cxx, log4cpp, log4cplus, log4net的唯一選擇。

但這種模型是不靈活的,他們發明了過濾器(filters)來彌補,但這只能把事情弄得更加混亂。所以讓我們把目光轉回syslog的模型,這是一個設計的很簡易正確的模型。

繼續上一節的例子,如果在zlog的配置文件中有這么2行規則:

[ rules ]

my_cat.DEBUG     >stdout; simple

my_cat.INFO      >stdout;

然后,一行代碼會產生兩行輸出:

hello, zlog

2012-05-29 10:41:36 INFO [11288:test_hello.c:41] hello, zlog

現在一個代碼中的分類對應配置文件中的兩條規則。log4j的用戶可能會說:"這很好,但是只要在log4j里面放兩個appender也能做的一樣。"所以繼續看下一個例子:

[ rules ]

my_cat.WARN     "/var/log/aa.log"

my_cat.DEBUG    "/var/log/bb.log"

代碼是:

zlog_info(c, "info, zlog");

zlog_debug(c, "debug, zlog");

最后,在aa.log中只有一條日志

2012-05-29 10:41:36 INFO  [ 11288:test_hello.c:41 ]  info, zlog

但在bb.log里面有兩條

2012-05-29 10:41:36 INFO  [ 11288:test_hello.c:41 ]  info, zlog

2012-05-29 10:41:36 DEBUG [11288:test_hello.c:42] debug, zlog

從這個例子能看出來區別。log4j無法輕易的做到這一點。在zlog里面,一個分類可以對應多個規則,每個規則有自己的級別、輸出和格式。這就讓用戶能按照需求過濾、多渠道輸出自己的所有日志。

4.3  擴展syslog模型

所以到現在你能看出來zlog的模型更像syslog的模型。不幸的是,在syslog里面,設施(facility)是個int型,而且必須從系統定義的那幾種里面選擇。zlog走的遠一點,用一個字符串來標識分類。

syslog有一個通配符"*",匹配所有的設施(facility)。zlog里面也一樣,"*"匹配所有分類。這提供了一個很方便的辦法來重定向你的系統中各個組件的錯誤。只要這么寫:

[ rules ]

*.error    "/var/log/error.log"

zlog強大而獨有的特性是上下級分類匹配。如果你的分類是這樣的:

c = zlog_get_category("my_cat");

然后配置文件是這樣的

[ rules ]

my_cat.*      "/var/log/my_cat.log"

my_.NOTICE    "/var/log/my.log"

這兩條規則都匹配c分類"my_cat"。通配符"_" 表示上級分類。 "my_"是"my_cat"和"my_dog"的上級分類。還有一個通配符是"!",詳見5.5.2

Chapter 5  配置文件

大部分的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是等效的。

5.1  全局參數

全局參數以[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()時候以讀寫權限打開這個文件。確認你執行程序的用戶有權限創建和讀寫這個文件。轉檔日志的偽代碼是:

    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_lock用於多線程, fcntl_lock用於多進程。fcntl_lock是POSIX建議鎖。詳見man 3 fcntl。這個鎖是全系統有效的。在某個進程意外死亡后,操作系統會釋放此進程持有的鎖。這就是我為什么用fcntl鎖來保證安全轉檔。進程需要對鎖文件有讀寫權限。

    默認來說,rotate lock file = self。在這種情況下,zlog不會創建任何鎖文件,用配置文件作為鎖文件。fcntl是建議鎖,所以用戶可以自由的修改存儲他們的配置文件。一般來說,單個日志文件不會被不同操作系統用戶的進程轉檔,所以用配置文件作為鎖文件是安全的。

    如果你設置其他路徑作為鎖文件,例如/tmp/zlog.lock,zlog會在zlog_init()的時候創建這個文件。如果有多個操作系統用戶的進程需要轉檔同一個日志文件,確認這個鎖文件對於多個用戶都可讀寫。默認值是/tmp/zlog.lock。

  • 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。必須指出的是,在日志文件名是動態生成或者被轉檔的情況下,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文件,見5.5.3。默認值是0,由操作系統來決定什么時候刷緩存到文件。

5.2  日志等級自定義

這一節以[levels]開始。用於定義用戶自己的日志等級,建議和用戶自定義的日志記錄宏一起使用。這一節可以忽略不寫。語法為:

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

(level int)必須在[1,253]這個范圍內,越大越重要。(syslog level)是可選的,如果不設默認為LOG_DEBUG。

詳見7.3

5.3  格式(Formats)

這一節以[formats]開始。用來定義日志的格式。語法為:

(name) = "(actual formats)"

很好理解,(name)被后面的規則使用。(name)必須由數字和字母組成,下划線"_"也算字母。(actual format)前后需要有雙引號。 (actual formats)可以由轉換字符組成,見下一節。

5.4  轉換格式串

轉換格式串的設計是從C的printf函數里面抄來的。一個轉換格式串由文本字符和轉換說明組成。

轉換格式串用在規則的日志文件路徑和輸出格式(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個字符寬。

5.4.1  轉換字符

可以被辨認的轉換字符是

 


字符
效果
例子
%c
分類名
aa_bb
%d()
打日志的時間。這個后面要跟一對小括號()內含說明具體的日期格式。就像%d(%F)或者%d(%m-%d %T)。如果不跟小括號,默認是%d(%F %T)。括號內的格式和 strftime(2)的格式一致。詳見 5.4.3
%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()

%E()
獲取環境變量的值
%E(USER) simpson
%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__宏
135
%m
用戶日志,用戶從zlog函數輸入的日志。
hello, zlog
%M
MDC (mapped diagnostic context),每個線程一張鍵值對表,輸出鍵相對應的值。后面必需跟跟一對小括號()內含鍵。例如 %M(clientNumber) ,clientNumbe是鍵。 詳見  7.1
%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()將會失敗
 
 

5.4.2  寬度修飾符

一般來說數據按原樣輸出。不過,有了寬度修飾符,就能夠控制最小字段寬度、最大字段寬度和左右對齊。當然這要付出一定的性能代價。

可選的寬度修飾符放在百分號和轉換字符之間。

第一個可選的寬度修飾符是左對齊標識,減號(-)。然后是可選的最小字段寬度,這是一個十進制數字常量,表示最少有幾個字符會被輸出。如果數據本來沒有那么多字符,將會填充空格(左對齊或者右對齊)直到最小字段寬度為止。默認是填充在左邊也就是右對齊。當然你也可以使用左對齊標志,指定為填充在右邊來左對齊。填充字符為空格(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個字符,去掉后面的。

5.4.3  時間字符

這里是轉換字符d支持的時間字符。

所有字符都是由strftime(2)生成的,在我的linux操作系統上支持的是:

 


字符
效果
例子
%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
%%
一個百分號
%
 

5.5  規則(Rules)

這一節以[rules]開頭。這個描述了日志是怎么被過濾、格式化以及被輸出的。這節可以忽略不寫,不過這樣就沒有日志輸出了,嘿嘿。語法是:

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

當zlog_init()被調用的時候,所有規則都會被讀到內存中。當zlog_get_category()被調用,規則就被被分配給分類(5.5.2)。在實際寫日志的時候,例如zlog_info()被調用的時候,就會比較這個INFO和各條規則的等級,來決定這條日志會不會通過這條規則輸出。當zlog_reload()被調用的時候,配置文件會被重新讀入,包括所有的規則,並且重新計算分類對應的規則。

5.5.1  級別匹配

zlog有6個默認的級別:"DEBUG", "INFO", "NOTICE", "WARN", "ERROR"和"FATAL"。就像其他的日志函數庫那樣, aa.DEBUG意味着任何大於等於DEBUG級別的日志會被輸出。當然還有其他的表達式。配置文件中的級別是大小寫不敏感的。

表達式 含義
* 所有等級
aa.debug 代碼內等級>=debug
aa.=debug 代碼內等級==debug
aa.!debug 代碼內等級!=debug

用戶可以自定義等級,詳見7.3

5.5.2  分類匹配

分類必須由數字和字母組成,下划線"_"也算字母。

總結
配置文件規則分類
匹配的代碼分類
不匹配的代碼分類
*匹配所有
*.*
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)

5.5.3  輸出動作

目前zlog支持若干種輸出,語法是:

[輸出], [附加選項, 可選]; [format(格式)名, 可選]

動作 輸出字段
附加選項
標准輸出 >stdout
無意義
標准錯誤輸出 >stderr
無意義
輸出到syslog >syslog
syslog設施(facilitiy):

LOG_USER(default), LOG_LOCAL[0-7]

必填

管道輸出 |cat
無意義
文件 "文件路徑"
文件轉檔,詳見 5.6

10M * 3 ~ "press.#r.log"

同步IO文件 -"文件路徑"  
用戶自定義輸出 $name
"path" 動態或者靜態的用於record輸出
  • stdout, stderr, syslog

    如表格描述,其中只有sylog的附加選項是有意義並必須寫的。

    值得注意的是,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"代替。

  • 管道輸出
    * . *     | /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

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

    • 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)
    • 無父子關系的多進程使用管道同時寫一個日志,無論單條日志長度是多少,都有可能導致日志交錯。

    zlog本身的直接文件輸出能保證即使是多進程,同時調用zlog寫一個日志文件也不會產生交錯,見下。

  • 文件
    • 文件路徑

      可以是相對路徑或者絕對路徑,被雙引號"包含。轉換格式串可以用在文件路徑上。例如文件路徑是 "%E(HOME)/log/out.log",環境變量$HOME是/home/harry,那最后的輸出文件是/home/harry/log/output.log。轉換格式串詳見 5.4

      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的轉檔功能詳見5.6

    • 同步IO文件

      在文件路徑前加上一個"-"就打開了同步IO選項。在打開文件(open)的時候,會以O_SYNC選項打開,這時候每次寫日志操作都會等操作系統把數據寫到硬盤后才返回。這個選項極為耗時:

      $ 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"

  • 用戶自定義輸出詳見7.4

5.6  文件轉檔

為什么需要將日志文件轉檔?我已經在實際的運行環境中不止一次的看到過,因為日志文件過大,導致系統硬盤被撐爆,或者單個日志文件過大而即使用grep也要花費很多時間來尋找匹配的日志。對於日志轉檔,我總結了如下幾種范式:

  1. 按固定時間段來切分日志。

    例如,每天生成一個日志

    aa.2012-08-02.log 

    aa.2012-08-03.log

    aa.2012-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)

    很難說哪種更加符合人的直覺。

    如果只有若干個最新的文件是有意義的,需要日志庫來做主動的刪除舊的工作。由外部程序是很難判定哪些日志是舊的。

    最簡單的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。

  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. 壓縮、移動、刪除舊的日志

    首先,壓縮不應該由日志庫來完成,因為壓縮消耗時間和CPU。日志庫的任務是配合壓縮。

    對於第一種和第三種,管理較為簡單,只要符合某些文件名規則或修改日期的,可以用shell腳本+crontab輕易的壓縮、移動和刪除。

    對於第二種,其實不是非常需要壓縮,只需要刪除就可以了。

    如果一定需要轉檔的同時進行壓縮,只有logrotate能干這活兒,畢竟他是獨立的程序,能在轉檔同時搞壓縮,不會有混淆的問題。

  5. zlog對外部轉檔工具,例如logrotate的支持

    zlog的轉檔功能已經極為強大,當然也有幾種情況是zlog無法處理的,例如按時間條件進行轉檔,轉檔前后調用一些自制的shell腳本……這會把zlog的配置和表達弄得過於復雜而缺乏美感。

    這時候你也許喜歡用一些外部轉檔工具,例如logrotate來完成工作。問題是,在linux操作系統下,轉檔工具重命名日志文件名后,應用進程還是往原來的文件描述符寫日志,沒辦法重新打開日志文件寫新的日志。標准的做法是給應用程序一個信號,讓他重新打開日志文件,對於syslogd是

    kill -SIGHUP ‘cat /var/run/syslogd.pid‘

    對於zlog,因為是個函數庫,不適合接受信號。zlog提供了函數接口zlog_reload(),這個函數會重載配置文件,重新打開所有的日志文件。應用程序在logrotate的信號,或者其他途徑,例如客戶端的命令后,可以調用這個函數,來重新打開所有的日志文件。

5.7  配置文件工具

$ zlog-chk-conf -h 

Useage: zlog-chk-conf [conf files]... 

-q, suppress non-error message 

-h, show help message

zlog-chk-conf 嘗試讀取配置文件,檢查語法,然后往屏幕上輸出這些配置文件是否正確。我建議每次創建或者改動一個配置文件之后都用一下這個工具。輸出可能是這樣:

$ ./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

這個告訴你配置文件zlog.conf的126行,是錯的。第一行進一步告訴你[aaa]不是一條正確的規則。

zlog-chk-conf可以同時分析多個配置文件,舉例:

$ zlog-chk-conf zlog.conf ylog.conf 

--[zlog.conf] syntax right 

--[ylog.conf] syntax right 

Chapter 6  zlog接口(API)

zlog的所有函數都是線程安全的,使用的時候只需要

#include "zlog.h"

6.1  初始化和清理

總覽
 
int zlog_init(const char  * confpath );

int zlog_reload(const char *confpath);

void zlog_fini(void);

描述
 

zlog_init()從配置文件confpath中讀取配置信息到內存。如果confpath為NULL,會尋找環境變量ZLOG_CONF_PATH的值作為配置文件名。如果環境變量ZLOG_CONF_PATH也沒有,所有日志以內置格式寫到標准輸出上。每個進程只有第一次調用zlog_init()是有效的,后面的多余調用都會失敗並不做任何事情。

zlog_reload()從confpath重載配置,並根據這個配置文件來重計算內部的分類規則匹配、重建每個線程的緩存、並設置原有的用戶自定義輸出函數。可以在配置文件發生改變后調用這個函數。這個函數使用次數不限。如果confpath為NULL,會重載上一次zlog_init()或者zlog_reload()使用的配置文件。如果zlog_reload()失敗,上一次的配置依然有效。所以zlog_reload()具有原子性。

zlog_fini()清理所有zlog API申請的內存,關閉它們打開的文件。使用次數不限。

返回值
 

如果成功,zlog_init()和zlog_reload()返回0。失敗的話,zlog_init()和zlog_reload()返回-1。詳細錯誤會被寫在由環境變量ZLOG_PROFILE_ERROR指定的錯誤日志里面。

6.2  分類(Category)操作

總覽
 
typedef struct zlog_category_s zlog_category_t;

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() 最后會清理一切。

返回值
 

如果成功,返回zlog_category_t的指針。如果失敗,返回NULL。詳細錯誤會被寫在由環境變量ZLOG_PROFILE_ERROR指定的錯誤日志里面。

6.3  寫日志函數及宏

總覽
 
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, ...); 

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()。

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

參數file和line填寫為__FILE__和__LINE__這兩個宏。這兩個宏標識日志是在哪里發生的。參數func 填寫為__func__或者__FUNCTION__,如果編譯器支持的話,如果不支持,就填寫為"<unkown>"。

level是一個整數,應該是在下面幾個里面取值。

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;

每個函數都有對應的宏,簡單使用。例如:

#define zlog_fatal(cat, format, args...) \         

zlog(cat, __FILE__, sizeof(__FILE__)-1, \

__func__, sizeof(__func__)-1, __LINE__, \ 

ZLOG_LEVEL_FATAL, format, ##args) 

所有的宏列表:

/ *  zlog macros  * /

zlog_fatal(cat, format, ...)

zlog_error(cat, format, ...)

zlog_warn(cat, format, ...)

zlog_notice(cat, format, ...)

zlog_info(cat, format, ...)

zlog_debug(cat, format, ...)

 

/* vzlog macros */

vzlog_fatal(cat, format, args)

vzlog_error(cat, format, args)

vzlog_warn(cat, format, args)

vzlog_notice(cat, format, args)

vzlog_info(cat, format, args)

vzlog_debug(cat, format, args)

 

/* hzlog macros */

hzlog_fatal(cat, buf, buf_len)

hzlog_error(cat, buf, buf_len)

hzlog_warn(cat, buf, buf_len)

hzlog_notice(cat, buf, buf_len)

hzlog_info(cat, buf, buf_len)

hzlog_debug(cat, buf, buf_len) 

返回值
 

這些函數不返回。如果有錯誤發生,詳細錯誤會被寫在由環境變量ZLOG_PROFILE_ERROR指定的錯誤日志里面。

6.4  MDC操作

總覽
 
int zlog_put_mdc(const char  * key , const char  * value );

char *zlog_get_mdc(const char *key);

void zlog_remove_mdc(const char *key);

void zlog_clean_mdc(void);

描述
 

MDC(Mapped Diagnostic Context)是一個每線程擁有的鍵-值表,所以和分類沒什么關系。

key和value是字符串,長度不能超過MAXLEN_PATH(1024)。如果超過MAXLEN_PATH(1024)的話,會被截斷。

記住這個表是和線程綁定的,每個線程有自己的表,所以在一個線程內的調用不會影響其他線程。

返回值
 

zlog_put_mdc()成功返回0,失敗返回-1。zlog_get_mdc()成功返回value的指針,失敗或者沒有相應的key返回NULL。如果有錯誤發生,詳細錯誤會被寫在由環境變量ZLOG_PROFILE_ERROR指定的錯誤日志里面。

6.5  dzlog接口

總覽
 
int dzlog_init(const char  * confpath , const char  * cname );

int dzlog_set_category(const char *cname); 

void dzlog(const char *file, size_t filelen,

           const char *func, size_t funclen,

           long line, int level,

           const char *format, ...); 

void vdzlog(const char *file, size_t filelen,

            const char *func, size_t funclen,

            long line, int level,

            const char *format, va_list args); 

void hdzlog(const char *file, size_t filelen,

            const char *func, size_t funclen,

            long line, int level,

            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的宏也定義在zlog.h里面。更簡單的寫法。

dzlog_fatal(format, ...)

dzlog_error(format, ...)

dzlog_warn(format, ...)

dzlog_notice(format, ...)

dzlog_info(format, ...)

dezlog_debug(format, ...)

 

vdzlog_fatal(format, args)

vdzlog_error(format, args)

vdzlog_warn(format, args)

vdzlog_notice(format, args)

vdzlog_info(format, args)

vdzlog_debug(format, args)

 

hdzlog_fatal(buf, buf_len)

hdzlog_error(buf, buf_len)

hdzlog_warn(buf, buf_len)

hdzlog_noticebuf, buf_len)

hdzlog_info(buf, buf_len)

hdzlog_debug(buf, buf_len)

返回值
 

成功情況下dzlog_init()和dzlog_set_category()返回0。失敗情況下dzlog_init()和 dzlog_set_category()返回-1。詳細錯誤會被寫在由環境變量ZLOG_PROFILE_ERROR指定的錯誤日志里面。

6.6  用戶自定義輸出

總覽
 
typedef struct zlog_msg_s {

        char *buf;

        size_t len;

        char *path;

} zlog_msg_t;

typedef int (*zlog_record_fn)(zlog_msg_t *msg); 

int zlog_set_record(const char *rname, zlog_record_fn record); 

描述
 

zlog允許用戶自定義輸出函數。輸出函數需要綁定到某條特殊的規則上。這種規則的例子是:

* . *      $name, "record path %c %d"; simple

zlog_set_record()做綁定動作。規則中輸出段有$name的,會被用來做用戶自定義輸出。輸出函數為record。這個函數需要為zlog_record_fn的格式。

zlog_msg_t結構的各個成員描述如下:

path來自規則的逗號后的字符串,這個字符串會被動態的解析,輸出當前的path,就像動態文件路徑一樣。

buf和len 是zlog格式化后的日志信息和長度。

所有zlog_set_record()做的綁定在zlog_reload()使用后繼續有效。

返回值
 

成功情況下zlog_set_record()返回0。失敗情況下zlog_set_record()返回-1。詳細錯誤會被寫在由環境變量ZLOG_PROFILE_ERROR指定的錯誤日志里面。

6.7  調試和診斷

總覽
 
void zlog_profile(void);
描述
 

環境變量ZLOG_PROFILE_ERROR指定zlog本身的錯誤日志。

環境變量ZLOG_PROFILE_DEBUG指定zlog本身的調試日志。

zlog_profile()打印所有內存中的配置信息到ZLOG_PROFILE_ERROR,在運行時。可以把這個和配置文件比較,看看有沒有問題。

Chapter 7  高階使用

7.1  MDC

MDC是什么?在log4j里面解釋為Mapped Diagnostic Context。聽起來是個很復雜的技術,其實MDC就是一個鍵-值對表。一旦某次你設置了,后面庫可以幫你自動打印出來,或者成為文件名的一部分。讓我們看一個例子,來自於$(top_builddir)/test/test_mdc.c.

$ cat test_mdc.c

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <unistd.h>

#include <string.h>

#include "zlog.h"

int main(int argc, char** argv)

{

int rc;

zlog_category_t *zc;

rc = zlog_init("test_mdc.conf");

if (rc) { 

printf("init failed\n");

return -1;

}

zc = zlog_get_category("my_cat");

if (!zc) {

printf("get cat fail\n");

zlog_fini();

return -2;

}

zlog_info(zc, "1.hello, zlog");

zlog_put_mdc("myname", "Zhang");

zlog_info(zc, "2.hello, zlog");

zlog_put_mdc("myname", "Li");

zlog_info(zc, "3.hello, zlog"); 

zlog_fini(); 

return 0;

配置文件

$ cat test_mdc.conf

[formats]

mdc_format=     "%d(%F %X.%ms) %-6V (%c:%F:%L) [%M(myname)] - %m%n"

[rules]

*.*             >stdout; mdc_format

輸出

$ ./test_mdc

2012-03-12 09:26:37.740 INFO   (my_cat:test_mdc.c:47) [] - 1.hello, zlog 

2012-03-12 09:26:37.740 INFO   (my_cat:test_mdc.c:51) [Zhang] - 2.hello, zlog 

2012-03-12 09:26:37.740 INFO   (my_cat:test_mdc.c:55) [Li] - 3.hello, zlog

你可以看到zlog_put_mdc()在表里面設置鍵"myname"對應值"Zhang",然后在配置文件里面%M(myname)指出了在日志的哪個位置需要把值打出來。第二次,鍵"myname"的值被覆蓋寫成"Li",然后日志里面也有相應的變化。

MDC什么時候有用呢?往往在用戶需要在同樣的日志行為區分不同的業務數據的時候。比如說,c源代碼是

zlog_put_mdc("customer_name", get_customer_name_from_db() );

zlog_info("get in"); 

zlog_info("pick product"); 

zlog_info("pay");

zlog_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來搞定。

 

7.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程序3.3,然后debug日志為

$ more zlog.debug.log 

03-13 09:46:56 DEBUG (7503:zlog.c:115) ------zlog_init start, compile time[Mar 13 2012 11:28:56]------ 

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:[0x7fdf96b52010][ ][ 0][] 

......

03-13 09:52:40 DEBUG (8139:zlog.c:291) ------zlog_fini end------

zlog.error.log日志沒產生,因為沒有錯誤發生。

你可以看出來,debug日志展示了zlog是怎么初始化還有清理的。不過在zlog_info()執行的時候沒有日志打出來,這是為了效率。

如果zlog庫有任何問題,都會打日志到ZLOG_PROFILE_ERROR所指向的錯誤日志。比如說,在zlog_info()上用一個錯誤的printf的語法:

zlog_info(zc, "%l", 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[%l] 

03-13 10:04:58 ERROR (10102:spec.c:329) zlog_buf_vprintf maybe fail or overflow 

03-13 10:04:58 ERROR (10102:spec.c:467) a_spec->gen_buf fail 

03-13 10:04:58 ERROR (10102:format.c:160) zlog_spec_gen_msg fail 

03-13 10:04:58 ERROR (10102:rule.c:265) zlog_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) zlog_output fail, srcfile[test_hello.c], srcline[41]

這樣,用戶就能知道為啥期待的輸出沒有產生,然后搞定這個問題。

運行時診斷會帶來一定的性能損失。一般來說,我在生產環境把ZLOG_PROFILE_ERROR打開,ZLOG_PROFILE_DEBUG關閉。

還有另外一個辦法來診斷zlog。我們都知道,zlog_init()會把配置信息讀入內存。在整個寫日志的過程中,這塊內存保持不變。如果用戶程序因為某種原因損壞了這塊內存,那么就會造成問題。還有可能是內存中的信息和配置文件的信息不匹配。所以我設計了一個函數,把內存的信息展現到ZLOG_PROFILE_ERROR指向的錯誤日志。

代碼見$(top_builddir)/test/test_profile.c

$ cat test_profile.c

#include <stdio.h>

#include "zlog.h"

 

int main(int argc, char** argv)

{

int rc;

rc = dzlog_init("test_profile.conf", "my_cat");

if (rc) { 

printf("init failed\n");

return -1;

}

dzlog_info("hello, zlog");

zlog_profile();

zlog_fini(); 

return 0;

zlog_profile()就是這個函數。配置文件很簡單。

$ cat test_profile.conf 

[formats] 

simple = "%m%n"   

[rules]

my_cat.*                >stdout; simple

然后zlog.error.log會是

$ cat /tmp/zlog.error.log

06-01 11:21:26 WARN  (7063:zlog.c:783) ------zlog_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:20:44]--- 

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(0x233b810)]--- 

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[0x233b7d0][0x233b7d0,/tmp/zlog.lock,4]-- 

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[0x23375e0][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[0x233d850][80,WARN,warn,4,4]--- 

06-01 11:21:26 WARN  (7063:level.c:37) ---level[0x233fc80][100,ERROR,error,5,3]--- 

7.3  用戶自定義等級

這里我把用戶自定義等級的幾個步驟寫下來。

  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

    NOTICE = 60, LOG_NOTICE

    WARN = 80, LOG_WARNING

    ERROR = 100, LOG_ERR

    FATAL = 120, LOG_ALERT

    UNKNOWN = 254, LOG_ERR

    這樣在zlog看來,一個整數(30)還有一個等級字符串(TRACE)代表了等級。這個整數必須位於[1,253]之間,其他數字是非法的。數字越大代表越重要。現在TRACE比DEBUG重要(30>20),比INFO等級低(30<40)。在這樣的定義后,TRACE就可以在下面的配置文件里面用了。例如這句話:

    my_cat.TRACE >stdout; 

    意味着等級>=TRACE的,包括INFO, NOTICE, WARN, ERROR, FATAL會被寫到標准輸出。

    格式里面的轉換字符%V會產生等級字符串的大寫輸出,%v會產生小寫的等級字符串輸出。

    另外,在等級的定義里面,LOG_DEBUG是指當需要輸出到syslog的時候,自定義的TRACE等級會以LOG_DEBUG輸出到syslog。

  2. 在源代碼里面直接用新的等級是這么搞的
    zlog(cat, __FILE__, sizeof(__FILE__)-1, \

    __func__, sizeof(__func__)-1,__LINE__, \

    30, "test %d", 1);

    為了簡單使用,創建一個.h頭文件

    $ cat $(top_builddir)/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");

    if (rc) {

    printf("init failed\n");

    return -1;

    }

    zc = zlog_get_category("my_cat");

    if (!zc) {

    printf("get cat fail\n");

    zlog_fini();

    return -2;

    }

    zlog_trace(zc, "hello, zlog - trace");

    zlog_debug(zc, "hello, zlog - debug");

    zlog_info(zc, "hello, zlog - info");

    zlog_fini(); 

    return 0;

  4. 最后我們能看到輸出
    $ ./test_level

    TRACE trace hello, zlog - trace 

    INFO info hello, zlog - info 

    正是我們所期待的,配置文件只允許>=TRACE等級的日志輸出到屏幕上。%V和%v也顯示了正確的結果。

7.4  用戶自定義輸出

用戶自定義輸出的意義是zlog放棄一些權力。zlog只負責動態生成單條日志和文件路徑,但怎么輸出、轉檔、清理等等工作由用戶按照自己的需求自行寫函數完成。寫完函數只要綁定到某個規則就可以。這里我把用戶自定義輸出的幾個步驟寫下來。

  1. 在配置文件里面定義規則
    $ cat test_record.conf

    [formats]

    simple = "%m%n"

    [rules]

    my_cat.*      $myoutput, " mypath %c %d";simple

  2. 綁定一個函數到$myoutput,並使用之
    #include <stdio.h>

    #include "zlog.h"

    int output(zlog_msg_t *msg)

    {

    printf(" [ mystd ] : [ %s ] [ %s ] [ %ld ] \n", msg->path, msg->buf, (long)msg->len);

    return 0;

    }

     

    int main(int argc, char** argv)

    {

    int rc;

    zlog_category_t *zc;

    rc = zlog_init("test_record.conf");

    if (rc) {

    printf("init failed\n");

    return -1;

    }

    zlog_set_record("myoutput", output);

    zc = zlog_get_category("my_cat");

    if (!zc) {

    printf("get cat fail\n");

    zlog_fini();

    return -2;

    }

    zlog_info(zc, "hello, zlog");

    zlog_fini();

    return 0;

  3. 最后我們發現用戶自定義輸出的函數能用了!
    $ ./test_record  

    [mystd]:[ mypath my_cat 2012-07-19 12:23:08][hello, zlog

    ][12] 

    正如你所見,msglen是12,zlog生成的msg在最后有一個換行符。

  4. 用戶自定義輸出可以干很多神奇的事情,就像某個用戶(flw@newsmth.net)的需求
    1. 日志文件名為 foo.log
    2. 如果 foo.log 超過 100M,則生成一個新文件,其中包含的就是 foo.log 目前的內容 而 foo.log 則變為空,重新開始增長
    3. 如果距離上次生成文件時已經超過 5 分鍾,則即使不到 100M,也應當生成一個新文件
    4. 新文件名稱可以自定義,比如加上設備名作為前綴、日期時間串作為后綴
    5. 我希望這個新文件可以被壓縮,以節省磁盤空間或者網絡帶寬。

    但願他能順利寫出這個需求的代碼!在多進程或者多線程的情況下!上帝保佑他!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM