編寫合格的C代碼(2):實現簡易日誌庫

需求

最簡單暴力的調試方法是printf()輸出變量的值,對於檢查發現異常情況很有幫助。

但並非所有時候都需要這些打印出來的信息,例如:太多的打印信息影響算法性能,暴露算法或業務邏輯細節機密,Release模式希望關閉log信息保持乾淨,etc。

手動增刪printf()語句是一種刀耕火種的做法,費力、不容易管理、影響coding狀態。換言之,用於調試的打印信息應當可控,想輸出就輸出,想不輸出就不輸出:

  • 能夠輸出信息到屏幕或文件:用printf、fprintf可以做到
  • 能夠控制何時輸出何時不輸出:需要封裝打印功能,根據Debug/Release模式或其他條件來控制是否打印
  • 能夠增加更多的打印信息:除了打印需要的變量的值,還能打印運行時間、行號、文件名、log等級等信息
  • 能夠輸出到文件,並且多線程安全

通過幾個步驟,漸進的實現一個簡易的logging庫。

step1: 打印功能的封裝

可以通過宏定義的方式封裝printf(),但宏定義寫起來並不如函數好寫。

在函數中調用vfprintf()則可以實現打印功能的封裝,支持任意多個參數,相當於自己實現了一個printf(),好處是可以定製。
(需要注意的是,並不能在函數中調用printf()來實現一個自己的printf(),因爲__VA_ARGS__(...)和va_list並不一樣。)

nc_log()函數近似實現了printf()的功能:

#include <stdio.h>
#include <stdarg.h>

void nc_log(const char* fmt, ...) {

    printf("[Nc Log] "); //定製輸出:增加[Nc Log]作爲log的TAG,區別於其他printf輸出

    va_list args;
    va_start(args, fmt); //解析fmt後的可變參數

    vfprintf(stdout, fmt, args); //以fmt作爲格式川,打印可變參數

    va_end(args);
}

int main(){

    nc_log("hello nc log, %s\n", "nice"); //調用logging函數

    printf("hello nc log, %s\n", "nice"); //調用標準的printf()

    return 0;
}

測試nc_log函數和標準的printf()函數的輸出:

[Nc Log] hello nc log, nice
hello nc log, nice

step2:定製logging的輸出行格式

考慮每一行logging輸出的格式,除了用戶調用時提供的打印參數,通常還可以添加的額外信息可以包括:

  • 不同的錯誤等級,顯示不同的顏色
  • 當前運行時刻
  • 調用logging處的行號、文件名

例如:

具體的格式可以自行定製,這裏分別考慮每種額外信息的打印實現。

step2.1 不同logging等級顯示不同顏色

是說在終端下讓logging輸出具有各種顏色,原理就是在需要打印的內容之外,用轉義字符來包圍,終端本身會將這些轉義字符解釋爲顏色然後輸出。現在的Linux/MacOS的終端都支持ANSI顏色轉義規則,也就是使用\x1b[%dm作爲起始、用\x1b[0m作爲結束。看看所有的ASCII字符都能被轉義爲什麼樣子:

#include <stdio.h>
int main() {
    for(int i=0; i<256; i++) {
        printf("\x1b[%dm %3d \x1b[0m ", i, i);
        if (i%16==15) {
            printf("\n");
        }
    }
    return 0;
}

顯然,有顏色的是少數,沒有顏色的是多數;顏色又包括前景字體顏色、背景顏色,並且有普通彩色和加亮彩色。挑選自己喜歡的顏色,然後定義幾個自己覺得必要的logging等級,每個等級分別對應一種顏色(對應的顏色轉義碼),則容易得到每種logging等級的顏色輸出:

#include <stdio.h>
#include <stdarg.h>

typedef enum NcLogLevel {
    NC_LOG_LEVEL_BEGIN=-1,

    TRACE,
    DEBUG,
    INFO,
    WARN,
    ERROR,
    FATAL,

    NC_LOG_LEVEL_END
} NcLogLevel;

static const char* level_names[] = {
    "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"
};

static const char* level_colors[] = {
    "\x1b[94m", "\x1b[36m",  "\x1b[32m","\x1b[33m", "\x1b[31m", "\x1b[35m"
};

void nc_log(NcLogLevel level, const char* fmt, ...) {
    if (level<=NC_LOG_LEVEL_BEGIN || level>=NC_LOG_LEVEL_END) {
        return;
    }

    fprintf(stdout, "%s[%-5s]\x1b[0m", level_colors[level], level_names[level]);

    va_list args;
    va_start(args, fmt);

    vfprintf(stdout, fmt, args);

    va_end(args);
}

int main(){

    for(int level=NC_LOG_LEVEL_BEGIN+1; level<NC_LOG_LEVEL_END; level++) {
        nc_log(level, "test trace\n");
    }

    return 0;
}

終端顏色輸出的通用性
考慮到通用性,測試發現我的Win10的cmd已經默認支持ANSI顏色轉義了,而如果是Win7(也許包括老一些版本的win10?),則可以通過安裝ANSICON來解決。

step2.2 顯示當前運行時刻

使用C標準庫函數localtime()獲取當前時刻,通過C標準庫函數strftime()格式化當前時刻爲指定格式的字符串輸出,格式說明見strftime。個人認爲必要的格式包括:時區、年月日、時分秒。這裏需要注意的是,時區的顯示如果使用了locale則不容易處理,因此直接顯示數字格式的時區偏移量(使用%z替代%Z)。嘗試輸出:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    time_t t = time(NULL);
    struct tm* lt = localtime(&lt);
    char now[100];
    now[strftime(now, sizeof(now), "%z %Y-%m-%d %H:%M:%S", lt)] = '\0';
    printf("now: %s\n", now);
}

集成到nc_log()函數中:

now[strftime(now, sizeof(now), "%Y-%m-%d %H:%M:%S", lt)] = '\0';
fprintf(stdout, "%s %s[%-5s]\x1b[0m ", now, level_colors[level], level_names[level])

step2.3 顯示行號和文件名

原理是:利用C語言內置宏__LINE__表示行號,__FILE__表示文件名。
需要注意的是,需要把“調用logging打印函數的那行代碼的所在行、所在文件”輸出,而不是在logging函數中的vfprintf()調用的那一行、那個文件輸出。因此應該把__FILE____LINE__作爲參數傳給logging函數:

void nc_log(NcLogLevel level, const char* file, int line, const char* fmt, ...);

調用logging函數的地方傳入行號、文件名:

c_log(level, __FILE__, __LINE__, "test log\n");

輸出效果:

每次打log需要手動傳__FILE____LINE__未免效率低下,考慮到用宏封裝。對於傳入不定個數參數的宏,用...__VA_ARGS__分別表示需要替代的不定個數參數、傳給對應函數的不定個數參數;爲了方便,將原來的nc_log函數重命名爲nc_log_log函數,定義nc_log,nc_log_trace等宏:

#define nc_log(level, ...)  nc_log_log(level, __FILE__, __LINE__, __VA_ARGS__)
#define nc_log_trace(...)   nc_log_log(NC_LOG_TRACE, __FILE__, __LINE__, __VA_ARGS__)
#define nc_log_debug(...)   nc_log_log(NC_LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__)
#define nc_log_info(...)    nc_log_log(NC_LOG_INFO,  __FILE__, __LINE__, __VA_ARGS__)
#define nc_log_warn(...)    nc_log_log(NC_LOG_WARN,  __FILE__, __LINE__, __VA_ARGS__)
#define nc_log_error(...)   nc_log_log(NC_LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__)
#define nc_log_fatal(...)   nc_log_log(NC_LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__)

step3: 控制輸出

按前面的代碼,vfprintf(stdout,...),顯然是輸出到屏幕終端上。一個合格的logging庫應當能夠控制輸出:

  • 是否輸出到屏幕:
    vsprintf(stdout,...)即可
  • 是否輸出到文件:
    vsprintf(logger.fp,...)即可,注意fflush
  • 控制輸出level的粒度:
    粒度可以設定level範圍:只運行[min, max]範圍內level的logging打印;粒度也可以設置爲單個level。

我採用的是單個level控制粒度,支持如下函數:

//默認不設定level,會開啓所有level的log

//設定level開啓的範圍:範圍內的level被開啓,範圍外的level都被關閉
nc_log_set_level_range(int min_level, int max_level);

//開啓單個level
nc_log_set_level_on(int level);

//關閉單個level
nc_log_set_level_off(int level);

輸出到文件的設定:

//默認不輸出到文件

//設定輸出到文件
nc_log_set_fp(FILE* fp);

輸出到屏幕終端的設定:

// 默認是logging到屏幕的,開啓quiet則不輸出到屏幕
void nc_log_set_quiet(int enable);

step4 多線程安全

當logging到文件時,需要考慮線程安全。

ref: https://github.com/rxi/log.c/issues/1

reference

stdlib and colored output in C
log.c
Getting colored output working on Windows

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章