zoukankan      html  css  js  c++  java
  • 编写合格的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
    ", "nice"); //调用logging函数
    
        printf("hello nc log, %s
    ", "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("
    ");
            }
        }
        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
    ");
        }
    
        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)] = '';
        printf("now: %s
    ", now);
    }
    

    集成到nc_log()函数中:

    now[strftime(now, sizeof(now), "%Y-%m-%d %H:%M:%S", lt)] = '';
    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
    ");
    

    输出效果:

    每次打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

    Greatness is never a given, it must be earned.
  • 相关阅读:
    20180320作业2:进行代码复审训练
    20180320作业1:源代码管理工具调查
    软工作业PSP与单元测试练习
    软工课后作业01-P18第四题
    20180320作业2:进行代码复审训练
    判断传入的电子邮箱账号的正确性
    软工课后作业01-00365
    实现模块判断传入的电子邮箱账号的正确性
    个人介绍
    20180320作业2:进行代码复审训练
  • 原文地址:https://www.cnblogs.com/zjutzz/p/11333334.html
Copyright © 2011-2022 走看看