学习一个极简的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)
{
/* 默认会输出到 stderr */
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");

/*
* 添加日志文件 demo.log。
*
* 文件中记录 DEBUG 及以上日志;
* stderr 仍然只显示 INFO 及以上日志。
*/
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");

/*
* quiet 只关闭 stderr 的输出。
* 不会关闭已经注册的文件 callback。
*/
log_set_quiet(true);

log_warn("This appears only in demo.log");

fclose(fp);

return 0;
}