Libevent 如何定義自己的日志輸出格式
Libevent 的日志系統(tǒng)中定義了四個日志的輸出等級,默認情況下會將日志信息輸出到終端檬某。不過 Libevent 允許用戶設(shè)置自定義的日志輸出回調(diào)函數(shù)以及異常日志輸出函數(shù)替梨,以提供日志信息的進一步解釋和輸出重定向钓试。回調(diào)函數(shù)的指針定義如下:
//event.h
//日志輸出等級
#define EVENT_LOG_DEBUG 0
#define EVENT_LOG_MSG 1
#define EVENT_LOG_WARN 2
#define EVENT_LOG_ERR 3
//用戶自定義的日志輸出回調(diào)函數(shù)副瀑,其中 severity 代表日志輸出等級
typedef void (*event_log_cb)(int severity, const char *msg);
typedef void (*event_fatal_cb)(int err);
Libevent 默認將這兩個回調(diào)函數(shù)置為 NULL弓熏,用戶可以通過函數(shù)event_set_log_callback
和event_set_fatal_callback
進行設(shè)置
//log.c
static event_log_cb log_fn = NULL;
void event_set_log_callback(event_log_cb cb)
{
log_fn = cb;
}
static event_fatal_cb fatal_fn = NULL;
void event_set_fatal_callback(event_fatal_cb cb)
{
fatal_fn = cb;
}
若用戶沒有自定義日志輸出函數(shù),那么 Libevent 會根據(jù) severity 的值調(diào)用相應(yīng)的默認日志輸出函數(shù)糠睡,并向 stderr 輸出日志信息挽鞠。具體的實現(xiàn)如下:
//log.c
static void event_log(int severity, const char *msg)
{
if (log_fn)
log_fn(severity, msg);
else {
const char *severity_str;
switch (severity) {
case EVENT_LOG_DEBUG:
severity_str = "debug";
break;
case EVENT_LOG_MSG:
severity_str = "msg";
break;
case EVENT_LOG_WARN:
severity_str = "warn";
break;
case EVENT_LOG_ERR:
severity_str = "err";
break;
default:
severity_str = "???";
break;
}
//(void)的作用是減少編譯器因為“未被使用的返回值”而產(chǎn)生的警告信息。
(void)fprintf(stderr, "[%s] %s\n", severity_str, msg);
}
}
static void event_exit(int errcode)
{
if (fatal_fn) {
fatal_fn(errcode);
exit(errcode); /* should never be reached */
} else if (errcode == EVENT_ERR_ABORT_)
abort();
else
exit(errcode);
}
日志系統(tǒng)的底層函數(shù)
看完了 Libevent 的日志系統(tǒng)留給用戶的兩個接口定義后狈孔,我們可以來看看 Libevent 默認的日志輸出函數(shù)信认。在源碼當(dāng)中,日志系統(tǒng)相關(guān)的文件主要有兩個: log.c 和 log-internal.h均抽。我們先來看 log-internal.h 文件
// log-internal.h
#ifdef __GNUC__
#define EV_CHECK_FMT(a,b) __attribute__((format(printf, a, b)))
#define EV_NORETURN __attribute__((noreturn))
#else
#define EV_CHECK_FMT(a,b)
#define EV_NORETURN
#endif
//EVENT_DEBUG_LOGGING_ENABLED 為調(diào)試模式的開關(guān)
#ifdef EVENT_DEBUG_LOGGING_ENABLED
EVENT2_CORE_EXPORT_SYMBOL extern ev_uint32_t event_debug_logging_mask_;
#define event_debug_get_logging_mask_() (event_debug_logging_mask_)
#else
#define event_debug_get_logging_mask_() (0)
#endif
//EVENT_ERR_ABORT_ 代表終止程序時的錯誤碼
#define EVENT_ERR_ABORT_ ((int)0xdeaddead)
/* EVENT2_EXPORT_SYMBOL是定義在 include/event2/visibility.h 下的一個宏
* define EVENT2_EXPORT_SYMBOL __attribute__ ((visibility("default")))
*/
EVENT2_EXPORT_SYMBOL
void event_err(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3) EV_NORETURN;
EVENT2_EXPORT_SYMBOL
void event_warn(const char *fmt, ...) EV_CHECK_FMT(1,2);
EVENT2_EXPORT_SYMBOL
void event_sock_err(int eval, evutil_socket_t sock, const char *fmt, ...) EV_CHECK_FMT(3,4) EV_NORETURN;
EVENT2_EXPORT_SYMBOL
void event_sock_warn(evutil_socket_t sock, const char *fmt, ...) EV_CHECK_FMT(2,3);
EVENT2_EXPORT_SYMBOL
void event_errx(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3) EV_NORETURN;
EVENT2_EXPORT_SYMBOL
void event_warnx(const char *fmt, ...) EV_CHECK_FMT(1,2);
EVENT2_EXPORT_SYMBOL
void event_msgx(const char *fmt, ...) EV_CHECK_FMT(1,2);
EVENT2_EXPORT_SYMBOL
void event_debugx_(const char *fmt, ...) EV_CHECK_FMT(1,2);
EVENT2_EXPORT_SYMBOL
void event_logv_(int severity, const char *errstr, const char *fmt, va_list ap)
EV_CHECK_FMT(3,0);
在 log-internal.h 中嫁赏,我們可以看到 Libevent 中的日志系統(tǒng)其實運用到了編譯器屬性 __attribute__ 的一些技巧。
__attribute__ 是 GNU C 下的一種機制油挥,可以用來設(shè)置函數(shù)屬性潦蝇、變量屬性以及類型屬性款熬,它的語法格式如下
__attribute__((attribute-list))
其中 attribute-list 是用逗號分隔開的列表。在 Libevent 的日志系統(tǒng)中攘乒,主要應(yīng)用到的有以下三種 __attribute__ 屬性:
1. __attribute__((format(archetype, string-index, first-to-check)))
該屬性可以為函數(shù)添加上類似于 print贤牛、scanf、strftime 以及 strfmon 風(fēng)格的格式化字符串则酝,從而可以幫助我們完成對格式化字符串的類型檢查殉簸。其中,archetype 代表參數(shù)的格式化風(fēng)格堤魁,string-index 代表格式化字符串位于形參列表中的哪個位置瓶佳,first-to-check 代表可變參數(shù)列表在形參列表中的哪個位置刷喜。結(jié)合一個 event_warn 的例子:
void event_warn(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
__attribute__((format(printf, 1, 2))) 代表按照 printf 的風(fēng)格檢查格式化字符串,其中參數(shù)列表的第 1 個參數(shù)代表格式化字符串隐绵,第 2 個參數(shù)代表可變參數(shù)列表洞坑。
2. __attribute__((noreturn))
noreturn 是一個函數(shù)屬性盲链,用來通知編譯器該函數(shù)在執(zhí)行后不會將控制權(quán)返回給它的調(diào)用者。以函數(shù) event_err
為例迟杂,當(dāng)程序使用該日志函數(shù)時刽沾,說明程序已然產(chǎn)生了嚴重的錯誤,不能再繼續(xù)執(zhí)行下去排拷,除了拋出異常侧漓,或者調(diào)用如exit()
或abort()
函數(shù)終止程序以外,可以利用 noreturn 來處理监氢。當(dāng)函數(shù)執(zhí)行 event_err 時布蔗,主程序會將控制權(quán)交給 event_err 函數(shù),當(dāng) event_err 執(zhí)行完畢后浪腐,控制流將不會返回主程序纵揍,而是停留在此處。
3. __attribute__ ((visibility("default")))
visibility屬性可以將從動態(tài)庫中導(dǎo)出可見符號议街,default 代表可見泽谨,而 hidden 代表隱藏。新版本的 Linux 內(nèi)核限制了動態(tài)庫的符號可見性特漩,因為這樣做可以提高程序的模塊性吧雹,同時動態(tài)庫裝載和識別的符號越少,程序啟動和運行的速度也就越快涂身。Libevent 中被 EVENT2_EXPORT_SYMBOL 修飾的函數(shù)符號都將會被導(dǎo)出吮炕,因此使用該動態(tài)庫程序都可以使用到這些函數(shù)。
對于event_err
访得,event_sock_err
龙亲,event_debugx_
等函數(shù)陕凹,它們的實現(xiàn)大同小異,區(qū)別僅是部分參數(shù)的不同鳄炉,以下選取部分函數(shù)進行討論
void event_err(int eval, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
event_logv_(EVENT_LOG_ERR, strerror(errno), fmt, ap);
va_end(ap);
event_exit(eval);
}
void event_sock_err(int eval, evutil_socket_t sock, const char *fmt, ...)
{
va_list ap;
int err = evutil_socket_geterror(sock);
va_start(ap, fmt);
event_logv_(EVENT_LOG_ERR, evutil_socket_error_to_string(err), fmt, ap);
va_end(ap);
event_exit(eval);
}
void event_debugx_(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
event_logv_(EVENT_LOG_DEBUG, NULL, fmt, ap);
va_end(ap);
}
void event_logv_(int severity, const char *errstr, const char *fmt, va_list ap)
{
char buf[1024];
size_t len;
//當(dāng) event_debug_get_logging_mask_() == 0 時關(guān)閉調(diào)試日志的輸出
if (severity == EVENT_LOG_DEBUG && !event_debug_get_logging_mask_())
return;
if (fmt != NULL)
//將日志信息按照格式字符串進行格式化杜耙,并存入 buf 中
evutil_vsnprintf(buf, sizeof(buf), fmt, ap);
else
buf[0] = '\0';
//若有額外的錯誤信息,則將其一起放入 buf
if (errstr) {
len = strlen(buf);
if (len < sizeof(buf) - 3) {
evutil_snprintf(buf + len, sizeof(buf) - len, ": %s", errstr);
}
}
event_log(severity, buf);
}
上述的日志函數(shù)大體的結(jié)構(gòu)是相同的拂盯,都采用了可變參數(shù)列表的形式實現(xiàn)佑女,最終通過event_logv_
以及event_log
兩個函數(shù)將日志輸出。
一些語法糖
在 log-internal.h 當(dāng)中還定義了一個 event_debug
的宏谈竿,可以用于在調(diào)試期間打印出調(diào)試信息团驱。利用這個宏來代替event_debugx_
函數(shù),一方面可以使得調(diào)試語義更加清晰空凸,另一方面也可以非常方便地實現(xiàn)調(diào)試模式的開關(guān)嚎花。它的定義如下:
#ifdef EVENT_DEBUG_LOGGING_ENABLED
#define event_debug(x) do { \
if (event_debug_get_logging_mask_()) { \
event_debugx_ x; \
} \
} while (0)
#else
#define event_debug(x) ((void)0)
#endif
總結(jié)
- Libevent 的日志系統(tǒng)為用戶提供了兩個接口
event_log_cb
和event_fatal_cb
,以便用戶實現(xiàn)自定義的日志處理功能呀洲,在需要的時候可以使用event_set_log_callback
和event_set_fatal_callback
進行設(shè)置 - 如果用戶沒有定義自己的日志處理回調(diào)函數(shù)紊选,則采用 Libevent 默認的日志處理系統(tǒng),它將日志輸出等級定義為四個道逗,并會將日志輸出到終端兵罢。
- 在日志系統(tǒng)的開發(fā)過程當(dāng)中,利用好 GNU C 的一些機制滓窍,如 __attribute__ 等卖词,可以極大簡化一些繁瑣的檢查過程,不僅有利于調(diào)試吏夯,幫助找到一些難以發(fā)現(xiàn)的錯誤此蜈,還有助于編譯器進行優(yōu)化。