Nginx的访问日志是排查线上问题最重要的数据来源之一,但默认的combined格式只记录了最基本的请求信息,缺少请求耗时、上游响应时间、POST请求体等关键数据。当需要分析慢接口、定位异常流量或者对接日志采集系统时,就必须掌握log_format指令的用法,按自己的需求定制日志字段。

log_format基础语法与配置位置
log_format指令属于http核心模块,只能写在http块中,不能放到server或location里。它的基本语法是log_format name [escape=default|json|none] string ...;,其中name是格式的名称,后面跟日志内容的模板字符串,多个字符串会自动拼接。
一个典型的自定义配置如下,写在nginx.conf的http块内:
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log main;
}
注意几点细节:模板中用单引号包裹是为了方便换行书写,Nginx会自动拼接;如果字符串里要出现单引号本身,需要用双引号包裹该段;$time_local这类变量外面套的方括号是纯文本,会原样输出到日志里。
配置完成后执行nginx -t检查语法,再执行nginx -s reload让配置生效,不需要重启服务。如果日志格式改了但日志文件里没变化,先确认access_log指令引用的格式名和log_format定义的名字是否一致,这是新手最容易踩的坑。
常用内置变量及其实际含义
自定义日志的核心就是挑选合适的内置变量。下面这些变量在实际工作中使用频率最高,建议逐个理解清楚。
| 变量 | 含义 | 典型用途 |
|---|---|---|
| $remote_addr | 客户端IP(直连IP) | 统计来源IP |
| $http_x_forwarded_for | 请求头X-Forwarded-For的值 | 多层代理下获取真实IP |
| $request_time | 请求完整处理时间(秒) | 定位慢请求 |
| $upstream_response_time | 上游服务器响应时间 | 区分Nginx耗时和后端耗时 |
| $upstream_addr | 实际转发到的上游地址 | 排查负载均衡命中情况 |
| $request_length | 请求总长度(含请求行、头、体) | 分析大请求 |
| $body_bytes_sent | 发送给客户端的响应体字节数 | 流量统计 |
| $request_body | POST请求体内容 | 排查接口参数问题 |
这里重点说两个变量。$request_time是从收到第一个字节到发送完响应的总耗时,包含了客户端慢速传输的时间;而$upstream_response_time只统计上游处理的时间。如果两者差距很大,说明耗时主要消耗在客户端网络传输或Nginx自身,而不是后端服务。
另外要注意$http_x_forwarded_for和$remote_addr的区别。当Nginx前面还有一层LB或者CDN时,$remote_addr记录的是上一跳代理的IP,真实客户端IP要从X-Forwarded-For请求头里取,取出来的值可能是逗号分隔的多个IP,第一个通常才是真实客户端。如果业务要求日志里的$remote_addr直接就是真实IP,需要配合realip模块设置set_real_ip_from和real_ip_header。
$request_body这个变量默认只在proxy_pass等场景下有值,而且它只在日志阶段读取请求体时需要Nginx已经把请求体缓存下来。开启client_body_buffer_size调大缓冲区可以避免请求体写临时文件,但记录完整请求体会带来日志膨胀和敏感信息泄露的风险,生产环境要谨慎评估。
escape参数与JSON格式输出
默认情况下log_format的escape模式是default,变量值中的特殊字符会按\xXX形式转义。如果你的日志要交给ELK、Loki这类系统解析,推荐直接输出JSON格式,并将escape设置为json,这样变量值里的引号、换行符会按JSON规范转义,解析端不会出错。
log_format json_log escape=json
'{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time",'
'"referer":"$http_referer",'
'"user_agent":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.json json_log;
上面例子中$status、$body_bytes_sent这类本身就是数字的变量没有加引号,输出后是JSON数字类型;而$upstream_response_time在多上游重试时可能输出成逗号分隔的多个值,比如"0.005, 0.010",所以必须按字符串处理,否则会破坏JSON结构。
escape=json只对变量值生效,模板里手写的引号、冒号原样输出。还有一个escape=none选项会关闭所有转义,除非你能保证变量值里不会出现引号和换行,否则不建议使用,否则一条带换行的User-Agent就可能让你的日志解析全部报错。
按域名和条件拆分日志文件
实际项目中经常需要把不同站点、不同类型的请求写到不同日志文件。可以在server块内分别声明access_log,同一个log_format可以被多处复用:
log_format with_host '$time_local host=$host $remote_addr "$request" '
'$status rt=$request_time';
server {
server_name www.ipipp.com;
access_log /var/log/nginx/site_a.log with_host;
}
server {
server_name api.ipipp.com;
access_log /var/log/nginx/site_b.log with_host;
}
还可以用map配合条件变量实现按状态码分流,比如把4xx和5xx的错误请求单独记录,方便快速扫描:
map $status $error_log_flag {
default 0;
~^[45] 1;
}
server {
access_log /var/log/nginx/access.log main;
access_log /var/log/nginx/error_only.log main if=$error_log_flag;
}
if参数接收一个非空且不为0的值时才写该日志文件,一条请求可以同时写多个日志文件,两者互不影响。此外,日志文件路径中也可以使用变量,例如access_log /var/log/nginx/$host.log main;,但要特别注意:路径含变量时Nginx不会自动创建目录,而且每次写日志都要打开文件句柄,建议配合open_log_file_cache缓存以降低开销。
日志性能与磁盘占用建议
访问日志虽然重要,但高频站点每秒可能写入几千条记录,日志量会迅速膨胀。字段不是越多越好,像$request_body、完整查询串这类大字段,只有在明确有分析需求时才记录。合理设置日志轮转(logrotate按天切割并压缩)能有效控制磁盘占用。
如果日志量极大,还可以考虑buffer参数,例如access_log /var/log/nginx/access.log main buffer=32k flush=5s;,日志会先写入内存缓冲区,攒够32KB或超过5秒再落盘,能明显减少磁盘IO。代价是Nginx异常退出时缓冲区内未落盘的日志会丢失,对日志完整性要求极高的场景要权衡使用。
最后提醒一点,排查问题时如果发现日志里时间对不上,检查一下是否使用了$time_local(本地时间)和$time_iso8601(带时区的标准格式)混用的情况,统一用$time_iso8601对接下游系统是最稳妥的做法。
Nginxaccess_loglog_format修改时间:2026-09-06 16:46:41