1. 引言
nginx 的日志系统是观察服务状态、排查问题的核心工具。它分为错误日志(error log)和访问日志(access log)两套机制:错误日志记录启动、运行、异常等信息,访问日志则记录每个 http 请求的详情。整个日志体系建立在几组精心设计的 c 结构体之上,理解这些结构体是读懂 nginx 源码、开发自定义模块的前提。本文将结合 src/core/ngx_log.h、src/core/ngx_log.c 以及 src/http/modules/ngx_http_log_module.c,从零开始剖析日志相关的数据结构、输出流程,并结合示例代码演示如何在模块中输出日志,最后归纳常用的日志格式变量。
2. 错误日志核心结构体
nginx 错误日志以 ngx_log_t 为核心,所有模块通过统一的宏和函数输出日志。
2.1 ngx_log_t — 日志对象
typedef struct ngx_log_s ngx_log_t;
struct ngx_log_s {
ngx_uint_t log_level; // 当前日志级别
ngx_open_file_t *file; // 日志文件描述符信息
ngx_atomic_uint_t connection; // 连接数,用于限制日志输出频率
time_t disk_full_time; // 磁盘写满时间戳
ngx_log_handler_pt handler; // 自定义日志处理函数
void *data; // handler 的上下文
ngx_log_writer_pt writer; // 写入函数指针(默认 ngx_log_write)
void *wdata; // writer 的上下文
char *action; // 当前动作描述,如 "request line"
ngx_log_t *next; // 下一个日志对象(链表)
};log_level:由指令error_log设置,可取ngx_log_stderr、ngx_log_emerg、ngx_log_alert、ngx_log_crit、ngx_log_err、ngx_log_warn、ngx_log_notice、ngx_log_info、ngx_log_debug,定义在ngx_log.h中。file:指向ngx_open_file_t,包含文件描述符、文件名、缓冲区等。handler与writer:支持自定义日志处理,默认writer为ngx_log_write,负责将格式化后的字符串写入文件。next:nginx 支持同时记录多个错误日志,通过链表串联。
2.2 ngx_open_file_t — 文件描述信息
typedef struct {
ngx_fd_t fd;
ngx_str_t name;
u_char *buffer; // 日志写入缓冲区
u_char *pos;
u_char *last;
// … 其他 flags 等
} ngx_open_file_t;每个 ngx_log_t 最终通过 ngx_open_file_t 中的 fd 与 buffer 完成 i/o。
2.3 错误日志输出 api 与示例
开发 nginx 模块时,使用一组宏即可输出错误日志:
#define ngx_log_error(level, log, ...) \
if ((log)->log_level >= level) \
ngx_log_error_core(level, log, __va_args__)
void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ...);示例:在 http 模块中输出错误日志
static ngx_int_t
ngx_http_example_handler(ngx_http_request_t *r)
{
ngx_log_t *log;
log = r->connection->log;
ngx_log_error(ngx_log_info, log, 0,
"example module: request to \"%v\" started", &r->uri);
// 业务逻辑 …
ngx_log_error(ngx_log_err, log, 0,
"example module: something went wrong, status=%d",
r->headers_out.status);
return ngx_ok;
}ngx_log_error_core 内部会格式化时间戳、拼接级别、进程 id、action 等内容,最终调用 log->writer(log, ...) 写入文件。错误日志格式大致为:
2024/08/20 09:52:03 [error] 2345#0: *1024 connect() failed (111: connection refused) while connecting to upstream, client: 192.168.1.1, server: localhost, request: "get / http/1.1", upstream: "http://127.0.0.1:8080/", host: "localhost"
3. 访问日志模块源码结构
访问日志由 ngx_http_log_module 实现,除了依赖上述基础错误日志结构外,还定义了一组专用结构体。
3.1 配置结构体:ngx_http_log_loc_conf_t
typedef struct {
ngx_array_t *logs; // ngx_http_log_t 数组(支持多个日志文件)
ngx_open_file_cache_t *open_file_cache;
ngx_uint_t off; // 是否关闭 access_log
} ngx_http_log_loc_conf_t;每条 access_log 指令对应一个 ngx_http_log_t。
3.2 单条日志描述:ngx_http_log_t
typedef struct {
ngx_open_file_t *file; // 打开的日志文件
ngx_http_log_script_t *script; // 日志格式编译后的脚本
time_t disk_full_time;
time_t error_log_time;
ngx_http_log_fmt_t *format; // 日志格式定义(if= 条件等)
ngx_log_t *log; // 指向 error log(用于日志输出错误)
unsigned error:1;
unsigned filter:1; // 是否启用过滤
} ngx_http_log_t;3.3 日志格式定义:ngx_http_log_fmt_t
typedef struct {
ngx_chain_t *op; // 编译后的操作链
ngx_array_t ops; // 原始操作数组
ngx_array_t args; // 参数(变量名等)
// ...
} ngx_http_log_fmt_t;nginx 将 log_format 定义的组合字符串(如 $remote_addr - $request)编译成一系列操作,运行时遍历操作链生成日志行。
3.4 操作与脚本结构
typedef struct {
ngx_http_log_op_t *op; // 当前操作
ngx_str_t *name; // 变量名称
ngx_http_variable_value_t *value; // 运行时变量值
uintptr_t text; // 常量字符串指针
size_t len;
} ngx_http_log_script_t;
typedef enum {
ngx_http_log_op_text = 0, // 固定文本
ngx_http_log_op_var, // 变量
ngx_http_log_op_filter // 过滤条件
} ngx_http_log_op_t;例如 log_format main '$remote_addr - $request $status'; 会被编译为:
- text:
" - "→ 常量 - var:
$remote_addr - text:
" " - var:
$request - text:
" " - var:
$status
4. 日志输出流程与格式
4.1 error log 输出流程
- 模块调用宏
ngx_log_error(level, log, fmt, …)。 - 宏检查
log->log_level,确定是否输出。 - 调用
ngx_log_error_core(),其中:- 使用
ngx_cached_err_log_time格式化时间戳; - 拼接
[level]、pid#tid、action以及用户自定义消息; - 调用
log->writer(log, ...)(默认ngx_log_write)写入; - 若写失败则记录
disk_full_time防止反复写盘。
- 使用
- 最终通过
write()系统调用输出。
4.2 access log 输出流程
- 请求结束时调用
ngx_http_log_handler()(注册在ngx_http_log_phase阶段)。 - 遍历
ngx_http_log_loc_conf_t中的logs数组,对每个ngx_http_log_t:- 检查
filter条件,不满足则跳过; - 遍历
script数组,依次执行每个ngx_http_log_script_t:- 若为
text,直接拷贝固定字符串; - 若为
var,调用ngx_http_get_indexed_variable(r, index)获取变量值并追加;
- 若为
- 将收集到的整行数据写入文件缓冲区;
- 调用
ngx_write_fd()真正输出。
- 检查
- 最终日志行完全是编译后操作链的一次动态执行结果。
经典 combined 格式示例:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';运行时生成类似:
127.0.0.1 - frank [20/aug/2024:09:52:03 +0800] "get /api http/1.1" 200 2326 "https://example.com" "mozilla/5.0 ..."
5. 常用日志格式变量一览
nginx 日志格式中可以使用大量内置变量(也可理解为“格式转义字符”),下面是开发与配置中最常用的一部分:
| 变量 | 含义 | 示例值 |
|---|---|---|
$remote_addr | 客户端 ip 地址 | 192.168.1.1 |
$remote_user | http 基础认证用户名(若有) | frank |
$time_local | 本地时间(格式 [日/月/年:时:分:秒 时区]) | [20/aug/2024:09:52:03 +0800] |
$request | 完整的请求行 | get /api http/1.1 |
$status | 响应状态码 | 200 |
$body_bytes_sent | 发送给客户端的字节数(不含响应头) | 2326 |
$http_referer | referer 头 | https://example.com |
$http_user_agent | user-agent 头 | mozilla/5.0 ... |
$request_time | 处理请求的总时间(秒) | 0.052 |
$upstream_addr | 上游服务器地址 | 192.168.2.1:80 |
$upstream_response_time | 上游服务器响应时间(秒) | 0.051 |
$uri | 当前请求的 uri(不含参数) | /index.html |
$args | 请求参数 | id=1&lang=zh |
$host | 请求的主机名(host 头) | www.example.com |
$server_name | 虚拟主机的 server_name | example.com |
$connection | 连接序列号 | 1024 |
$pipe | 若请求为管道(pipeline)则为 p | p 或空 |
通过在 log_format 中组合这些变量,可以定制出满足业务需求的日志格式。如需自定义变量,可在模块中调用 ngx_http_add_variable() 注册,并在编译日志格式时自动参与操作链生成。
6. 实战配置示例
下面给出两个完整的 nginx.conf 片段,展示如何结合上述知识在实际环境中配置日志。
6.1 多级别错误日志与自定义 access log
worker_processes 1;
error_log logs/error.log warn; # 全局仅输出警告及以上
events {
worker_connections 1024;
}
http {
log_format timing '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_addr"';
access_log logs/access.log timing;
server {
listen 80;
server_name example.com;
# 为当前虚拟主机打开 info 级别错误日志
error_log logs/example_error.log info;
location / {
root html;
index index.html;
}
location /api {
proxy_pass http://backend;
# 仅记录状态码 >= 400 的请求到独立文件
access_log logs/api_error.log timing if=$status >= 400;
}
}
}6.2 按域名分离日志
http {
log_format vhost '$host $remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent';
server {
listen 80;
server_name example.com www.example.com;
# 利用 $host 变量自动分离日志文件
access_log logs/access_$host.log vhost;
error_log logs/error_$host.log warn;
}
server {
listen 80;
server_name api.example.com;
access_log logs/access_api.log vhost;
error_log logs/error_api.log info;
}
}这些配置直接反映了底层 ngx_log_t、ngx_http_log_t 以及操作链的工作方式:error_log 指令创建 ngx_log_t 并设置级别,access_log 指令创建 ngx_http_log_t,格式字符串被编译为脚本链,条件过滤则通过脚本中的 ngx_http_log_op_filter 实现。
7. 开发者实践建议
- 添加自定义变量:通过
ngx_http_add_variable()注册新变量,并在log_format中使用,无需改动日志核心代码。 - 自定义日志过滤:利用
ngx_http_log_t.filter选项,配合if=参数控制日志生成条件,例如只记录慢请求或错误请求。 - 错误日志扩展:如有特殊需求(如发送到 syslog),可设置
log->handler和log->writer接管日志输出。 - 调试时灵活使用
ngx_log_debug宏:在编译 nginx 时开启--with-debug,可以在模块中使用ngx_log_debug()系列宏输出详细的调试信息。
8. 总结
nginx 的日志系统通过清晰的模块化设计和编译期优化,将繁琐的格式化工作转化为高效的操作链遍历。错误日志以 ngx_log_t 为核心,访问日志则围绕 ngx_http_log_t、ngx_http_log_fmt_t 以及操作脚本展开。掌握这些结构体以及它们如何被指令驱动,不仅能深入理解 nginx 的行为,更是定制化模块开发的必备基础。
到此这篇关于nginx 日志模块源码与开发实战指南的文章就介绍到这了,更多相关nginx 日志系统内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论