概述

NGINX 源码中搜索 error_log 可以发现有多处指令定义。比如在 ngx_http_core_module 中指令定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static ngx_command_t  ngx_http_core_commands[] = {
...
{ ngx_string("error_log"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_core_error_log,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
...
};

static char *
ngx_http_core_error_log(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf = conf;

return ngx_log_set_log(cf, &clcf->error_log);
}

观察 ngx_mail_core_modulengx_errlog_module 等其他模块的 error_log 指令处理函数,可以发现最终都会调用到 ngx_log_set_log 函数。其定义如下:

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
char *
ngx_log_set_log(ngx_conf_t *cf, ngx_log_t **head)
{
ngx_log_t *new_log;
ngx_str_t *value, name;
ngx_syslog_peer_t *peer;

if (*head != NULL && (*head)->log_level == 0) {
new_log = *head;

} else {
new_log = ngx_pcalloc(cf->pool, sizeof(ngx_log_t));
if (new_log == NULL) {
return NGX_CONF_ERROR;
}

if (*head == NULL) {
*head = new_log;
}
}

value = cf->args->elts;

if (ngx_strcmp(value[1].data, "stderr") == 0) {
ngx_str_null(&name);
cf->cycle->log_use_stderr = 1;

new_log->file = ngx_conf_open_file(cf->cycle, &name);
if (new_log->file == NULL) {
return NGX_CONF_ERROR;
}
} else if (ngx_strncmp(value[1].data, "memory:", 7) == 0) {
// 内存调试日志,只有 NGX_DEBUG 模式支持
#if (NGX_DEBUG)
size_t size, needed;
ngx_pool_cleanup_t *cln;
ngx_log_memory_buf_t *buf;

value[1].len -= 7;
value[1].data += 7;

needed = sizeof("MEMLOG :" NGX_LINEFEED) + cf->conf_file->file.name.len + NGX_SIZE_T_LEN + NGX_INT_T_LEN + NGX_MAX_ERROR_STR;
size = ngx_parse_size(&value[1]);

if (size == (size_t) NGX_ERROR || size < needed) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid buffer size \"%V\"", &value[1]);
return NGX_CONF_ERROR;
}

buf = ngx_pcalloc(cf->pool, sizeof(ngx_log_memory_buf_t));
if (buf == NULL) {
return NGX_CONF_ERROR;
}

buf->start = ngx_pnalloc(cf->pool, size);
if (buf->start == NULL) {
return NGX_CONF_ERROR;
}

buf->end = buf->start + size;

buf->pos = ngx_slprintf(buf->start, buf->end, "MEMLOG %uz %V:%ui%N", size, &cf->conf_file->file.name, cf->conf_file->line);
ngx_memset(buf->pos, ' ', buf->end - buf->pos);
cln = ngx_pool_cleanup_add(cf->pool, 0);
if (cln == NULL) {
return NGX_CONF_ERROR;
}

cln->data = new_log;
cln->handler = ngx_log_memory_cleanup;

new_log->writer = ngx_log_memory_writer;
new_log->wdata = buf;

#else
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "nginx was built without debug support");
return NGX_CONF_ERROR;
#endif

} else if (ngx_strncmp(value[1].data, "syslog:", 7) == 0) {
peer = ngx_pcalloc(cf->pool, sizeof(ngx_syslog_peer_t));
if (peer == NULL) {
return NGX_CONF_ERROR;
}

if (ngx_syslog_process_conf(cf, peer) != NGX_CONF_OK) {
return NGX_CONF_ERROR;
}

new_log->writer = ngx_syslog_writer;
new_log->wdata = peer;
} else {
// 普通文件日志,关联一个文件对象。此时并未打开文件
// 如果跟踪代码走进去,可以发先会将当前文件存入 cycle->open_files 动态数组中,文件的打开动作在 ngx_init_cycle 阶段将所有动态数组中文件打开
new_log->file = ngx_conf_open_file(cf->cycle, &value[1]);
if (new_log->file == NULL) {
return NGX_CONF_ERROR;
}
}

if (ngx_log_set_levels(cf, new_log) != NGX_CONF_OK) {
return NGX_CONF_ERROR;
}

if (*head != new_log) {
// log 链表是按 log_level 从大到小的顺序排列的,将一个新的 ngx_log_t 对象插入到一个已存在的 log 链表中。
// 一般 log 链表的头是一个固定地址,因此需要插入到头部时需要更改头部指针指向
ngx_log_insert(*head, new_log);
}

return NGX_CONF_OK;
}

ngx_log_error_core

作为 NGINX 的日志输出函数,通过 ngx_log_errorngx_log_debug 能够非常方便打印调试信息(NGINX 提供了 HEX 转换函数也方便打印二进制信息),其实两个函数是宏定义,真正的实现者是 ngx_log_error_core

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
88
89
90
91
92
93
94
95
void
ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...)
{
u_char *p, *last, *msg;
ssize_t n;
ngx_uint_t wrote_stderr, debug_connection;

// 输出缓冲区,当输出信息过长时会导致信息截断,可以调整 NGX_MAX_ERROR_STR 来调整输出信息长度
u_char errstr[NGX_MAX_ERROR_STR];

// 1. 进行输出信息拼接
last = errstr + NGX_MAX_ERROR_STR;
p = ngx_cpymem(errstr, ngx_cached_err_log_time.data, ngx_cached_err_log_time.len);
p = ngx_slprintf(p, last, " [%V] ", &err_levels[level]);

/* pid#tid */
p = ngx_slprintf(p, last, "%P#" NGX_TID_T_FMT ": ", ngx_log_pid, ngx_log_tid);

if (log->connection) {
p = ngx_slprintf(p, last, "*%uA ", log->connection);
}

msg = p;

#if (NGX_HAVE_VARIADIC_MACROS)
va_start(args, fmt);
p = ngx_vslprintf(p, last, fmt, args);
va_end(args);
#else
p = ngx_vslprintf(p, last, fmt, args);
#endif

if (err) {
p = ngx_log_errno(p, last, err);
}

if (level != NGX_LOG_DEBUG && log->handler) {
p = log->handler(log, p, last - p);
}

if (p > last - NGX_LINEFEED_SIZE) {
p = last - NGX_LINEFEED_SIZE;
}

ngx_linefeed(p);

wrote_stderr = 0;
debug_connection = (log->log_level & NGX_LOG_DEBUG_CONNECTION) != 0;

// 遍历 log 进行信息输出
while (log) {
if (log->log_level < level && !debug_connection) {
break;
}
// 可以自定义 writer 行为
if (log->writer) {
log->writer(log, level, errstr, p - errstr);
goto next;
}

if (ngx_time() == log->disk_full_time) {
/*
* on FreeBSD writing to a full filesystem with enabled softupdates
* may block process for much longer time than writing to non-full
* filesystem, so we skip writing to a log for one second
*/
goto next;
}

// 输出信息写入文件
n = ngx_write_fd(log->file->fd, errstr, p - errstr);
if (n == -1 && ngx_errno == NGX_ENOSPC) {
log->disk_full_time = ngx_time();
}

if (log->file->fd == ngx_stderr) {
wrote_stderr = 1;
}

next:

log = log->next;
}

// 输出到终端
if (!ngx_use_stderr || level > NGX_LOG_WARN || wrote_stderr) {
return;
}

// 输出信息调整
msg -= (7 + err_levels[level].len + 3);
(void) ngx_sprintf(msg, "nginx: [%V] ", &err_levels[level]);
(void) ngx_write_console(ngx_stderr, msg, p - msg);
}

阅读上面的代码可以发现输出日志会遍历 log 链表中进行输出。