学习一个极简的C语言日志库 rxi/log.c ,这个库的亮点不是复杂,而是极简,用不到两百行的代码就完成了一个极简日志库。(虽然但是,这个库是写法是基于 C99 的,后续也没有继续维护)
源码学习
写得比较乱,没有严格的前后逻辑顺序,源码本身就非常短,一看就懂。
这个日志库的基础接口很标准:
提供各种日志等级:”TRACE”, “DEBUG”, “INFO”, “WARN”, “ERROR”, “FATAL”(FATAL 不会中断程序本身)
类似 printf 的语法;
使用宏封装来获取文件名和行号信息;
宏提供等级参数,最后调用统一接口 log_log 函数。
1 2 3 4 5 6 #define log_trace(...) log_log(LOG_TRACE, __FILE__, __LINE__, __VA_ARGS__) #define log_debug(...) log_log(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__) #define log_info(...) log_log(LOG_INFO, __FILE__, __LINE__, __VA_ARGS__) #define log_warn(...) log_log(LOG_WARN, __FILE__, __LINE__, __VA_ARGS__) #define log_error(...) log_log(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__) #define log_fatal(...) log_log(LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__)
使用例如
1 2 log_info("n = %d, x = %.4f" , n, x); log_error("Something went wrong" );
控制台输出例如
1 2 3 4 5 6 7 8 01:42:08 TRACE test.c:8: This is a trace message 01:42:08 DEBUG test.c:9: This is a debug message 01:42:08 INFO test.c:10: Program started 01:42:08 INFO test.c:15: n = 42, x = 3.1416 01:42:08 INFO test.c:21: This info message will be shown 01:42:08 WARN test.c:22: Something may be wrong 01:42:08 ERROR test.c:23: Something went wrong 01:42:08 INFO test.c:40: This appears in both terminal and demo.log
日志等级如下
1 2 3 4 5 6 enum { LOG_TRACE, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL };static const char *level_strings[] = {"TRACE" , "DEBUG" , "INFO" , "WARN" , "ERROR" , "FATAL" }; const char *log_level_string (int level) { return level_strings[level]; }
可以设置控制台的日志等级,只会把调用语句等级超过这个等级的日志输出到 stderr
1 void log_set_level (int level) { L.level = level; }
提供了一个全局的 quiet 模式,开启后所有控制台输出都被关闭。
1 void log_set_quiet (bool enable) { L.quiet = enable; }
除了控制台输出,日志库还允许用户自动设置多个 callback,存到全局的数组中,每一个 callback 自带一个日志等级,不会受到前面 log_set_level 的日志等级过滤(那个只会影响控制台输出)
1 2 3 4 5 6 7 8 9 int log_add_callback (log_LogFn fn, void *udata, int level) { for (int i = 0 ; i < MAX_CALLBACKS; i++) { if (!L.callbacks[i].fn) { L.callbacks[i] = (Callback){fn, udata, level}; return 0 ; } } return -1 ; }
最常见的是加一个日志文件输出,直接实现了 file_callback 并提供了一个接口
1 2 3 int log_add_fp (FILE *fp, int level) { return log_add_callback(file_callback, fp, level); }
与之类似的是控制台输出函数 stdout_callback(这里 stdout 并不是说输出到 stdout,其实是输出到 stderr)。
这个库使用一个结构体 L 存储全局状态,包括日志等级,是否开启 quiet 模式,以及回调函数等
1 2 3 4 5 static struct { int level; bool quiet; Callback callbacks[MAX_CALLBACKS]; } L;
结构体 log_Event 是日志事件结构体,包括参数、格式、文件名、时间、用户数据、行号和日志等级
1 2 3 4 5 6 7 8 9 typedef struct { va_list ap; const char *fmt; const char *file; struct tm *time ; void *udata; int line; int level; } log_Event;
还有一个一般性的回调函数声明
1 typedef void (*log_LogFn) (log_Event *ev) ;
输出到控制台和文件的回调函数很类似,都接收一个日志事件结构体,只是输出目的地不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void stdout_callback (log_Event *ev) { char buf[16 ]; buf[strftime(buf, sizeof (buf), "%H:%M:%S" , ev->time)] = '\0' ; fprintf (ev->udata, "%s %-5s %s:%d: " , buf, level_strings[ev->level], ev->file, ev->line); vfprintf (ev->udata, ev->fmt, ev->ap); fprintf (ev->udata, "\n" ); fflush(ev->udata); } static void file_callback (log_Event *ev) { char buf[64 ]; buf[strftime(buf, sizeof (buf), "%Y-%m-%d %H:%M:%S" , ev->time)] = '\0' ; fprintf (ev->udata, "%s %-5s %s:%d: " , buf, level_strings[ev->level], ev->file, ev->line); vfprintf (ev->udata, ev->fmt, ev->ap); fprintf (ev->udata, "\n" ); fflush(ev->udata); }
然后就是最核心的 log_log 函数,这个函数会根据日志等级和 quiet 模式来判断是否输出日志,以及调用所有回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static void init_event (log_Event *ev, void *udata) { if (!ev->time) { time_t t = time(NULL ); ev->time = localtime(&t); } ev->udata = udata; } void log_log (int level, const char *file, int line, const char *fmt, ...) { log_Event ev = {.fmt = fmt, .file = file, .line = line, .level = level}; if (!L.quiet && level >= L.level) { init_event(&ev, stderr ); va_start(ev.ap, fmt); stdout_callback(&ev); va_end(ev.ap); } for (int i = 0 ; i < MAX_CALLBACKS && L.callbacks[i].fn; i++) { Callback *cb = &L.callbacks[i]; if (level >= cb->level) { init_event(&ev, cb->udata); va_start(ev.ap, fmt); cb->fn(&ev); va_end(ev.ap); } } }
其中函数 init_event 进行了一些初始化,包括获取当前时间和设置输出目的地。
这里有两个 udata:log_Event 的 udata 和回调函数的 udata,在这里都是指代输出目的地的,通常是 stderr 或 FILE * 指针。
源码 为了便于查看,这里附上 rxi/log.c 的完整源码,移除了头部的版权注释和一些不必要的细节,包括:
终端色彩支持
线程安全(其实这个库也没有提供线程安全支持,只是提供了线程同步 hook,用户可以加锁)
log.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #ifndef LOG_H #define LOG_H #include <stdarg.h> #include <stdbool.h> #include <stdio.h> #include <time.h> #define LOG_VERSION "0.1.0" typedef struct { va_list ap; const char *fmt; const char *file; struct tm *time ; void *udata; int line; int level; } log_Event; typedef void (*log_LogFn) (log_Event *ev) ;enum { LOG_TRACE, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL };#define log_trace(...) log_log(LOG_TRACE, __FILE__, __LINE__, __VA_ARGS__) #define log_debug(...) log_log(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__) #define log_info(...) log_log(LOG_INFO, __FILE__, __LINE__, __VA_ARGS__) #define log_warn(...) log_log(LOG_WARN, __FILE__, __LINE__, __VA_ARGS__) #define log_error(...) log_log(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__) #define log_fatal(...) log_log(LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__) const char *log_level_string (int level) ;void log_set_level (int level) ;void log_set_quiet (bool enable) ;int log_add_callback (log_LogFn fn, void *udata, int level) ;int log_add_fp (FILE *fp, int level) ;void log_log (int level, const char *file, int line, const char *fmt, ...) ;#endif
log.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include "log.h" #define MAX_CALLBACKS 32 typedef struct { log_LogFn fn; void *udata; int level; } Callback; static struct { int level; bool quiet; Callback callbacks[MAX_CALLBACKS]; } L; static const char *level_strings[] = {"TRACE" , "DEBUG" , "INFO" , "WARN" , "ERROR" , "FATAL" }; static void stdout_callback (log_Event *ev) { char buf[16 ]; buf[strftime(buf, sizeof (buf), "%H:%M:%S" , ev->time)] = '\0' ; fprintf (ev->udata, "%s %-5s %s:%d: " , buf, level_strings[ev->level], ev->file, ev->line); vfprintf (ev->udata, ev->fmt, ev->ap); fprintf (ev->udata, "\n" ); fflush(ev->udata); } static void file_callback (log_Event *ev) { char buf[64 ]; buf[strftime(buf, sizeof (buf), "%Y-%m-%d %H:%M:%S" , ev->time)] = '\0' ; fprintf (ev->udata, "%s %-5s %s:%d: " , buf, level_strings[ev->level], ev->file, ev->line); vfprintf (ev->udata, ev->fmt, ev->ap); fprintf (ev->udata, "\n" ); fflush(ev->udata); } const char *log_level_string (int level) { return level_strings[level]; }void log_set_level (int level) { L.level = level; }void log_set_quiet (bool enable) { L.quiet = enable; }int log_add_callback (log_LogFn fn, void *udata, int level) { for (int i = 0 ; i < MAX_CALLBACKS; i++) { if (!L.callbacks[i].fn) { L.callbacks[i] = (Callback){fn, udata, level}; return 0 ; } } return -1 ; } int log_add_fp (FILE *fp, int level) { return log_add_callback(file_callback, fp, level); } static void init_event (log_Event *ev, void *udata) { if (!ev->time) { time_t t = time(NULL ); ev->time = localtime(&t); } ev->udata = udata; } void log_log (int level, const char *file, int line, const char *fmt, ...) { log_Event ev = {.fmt = fmt, .file = file, .line = line, .level = level}; if (!L.quiet && level >= L.level) { init_event(&ev, stderr ); va_start(ev.ap, fmt); stdout_callback(&ev); va_end(ev.ap); } for (int i = 0 ; i < MAX_CALLBACKS && L.callbacks[i].fn; i++) { Callback *cb = &L.callbacks[i]; if (level >= cb->level) { init_event(&ev, cb->udata); va_start(ev.ap, fmt); cb->fn(&ev); va_end(ev.ap); } } }
一份示例代码
main.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <stdio.h> #include "log.h" int main (void ) { log_trace("This is a trace message" ); log_debug("This is a debug message" ); log_info("Program started" ); int n = 42 ; double x = 3.1415926 ; log_info("n = %d, x = %.4f" , n, x); log_set_level(LOG_INFO); log_debug("This debug message will not be shown" ); log_info("This info message will be shown" ); log_warn("Something may be wrong" ); log_error("Something went wrong" ); FILE *fp = fopen("demo.log" , "a" ); if (fp == NULL ) { log_error("Failed to open demo.log" ); return 1 ; } log_add_fp(fp, LOG_DEBUG); log_debug("This appears only in demo.log" ); log_info("This appears in both terminal and demo.log" ); log_set_quiet(true ); log_warn("This appears only in demo.log" ); fclose(fp); return 0 ; }