要让Nginx日志真正支撑全链路追踪,需四步闭环:统一trace-id来源(用map映射多源header)、补全span-id与parent-id、记录request_time及upstream三时间字段实现耗时归因、上下游日志字段严格对齐。

要让 Nginx 日志真正支撑全链路追踪,不能只加一个 $request_id 就算完事。核心是把请求上下文“锚定”下来,并与上下游系统对齐字段、格式和生命周期。下面分四个关键环节讲清楚怎么做。
统一 trace-id 来源:用 map 消除头命名差异
不同服务传来的 trace-id 名字五花八门:X-B3-TraceId、uber-trace-id、traceparent、X-Trace-ID……Nginx 不会自动识别,必须手动映射成统一变量。
- 在
http块开头定义 map:
map $http_x_b3_traceid $trace_id {
"" $http_trace_id;
~. $http_x_b3_traceid;
}
map $http_uber_trace_id $trace_id {
"" $trace_id;
~. $http_uber_trace_id;
}
map $http_traceparent $trace_id {
"" $trace_id;
~^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16}) $2;
}
- 这样无论上游用哪种 header,最终都落到
$trace_id这个变量里; - 后续所有日志、header 透传、甚至 Lua 脚本都只认这一个名字,避免逻辑分支;
- 注意:若 header 含下划线(如
X-Request-ID),需配置underscores_in_headers on;,否则 Nginx 默认忽略。
补全 span 层级:生成本机 span-id 并关联 parent
仅记录 trace-id 只能知道“属于哪条链”,无法定位“当前是链中哪一段”。需要为 Nginx 本次转发生成自己的 span-id,并明确标注 parent-id。
- 用
set_by_lua_block在 location 中生成:
set_by_lua_block $span_id {
return string.sub(ngx.md5(ngx.time() .. ngx.var.msec .. ngx.var.pid), 1, 16)
}
set_by_lua_block $parent_id {
return ngx.var.trace_id or ""
}
- 或更轻量方式:直接复用
$request_id作 span-id(前提是后端也接受该 ID 作为 span 标识); - 关键是把
$span_id和$parent_id都写进 log_format,且通过proxy_set_header透传给下游; - 例如:
proxy_set_header X-B3-SpanId $span_id;、proxy_set_header X-B3-ParentSpanId $parent_id;。
构建可分析的时间链:request_time + upstream 时间三件套
单看 $request_time 只知道总耗时,看不出瓶颈在哪。必须和 upstream 的三个时间变量并列记录,才能做差值归因。
- 推荐日志字段组合:
log_format full_trace
'$remote_addr "$request" $status '
'$request_time $upstream_connect_time $upstream_header_time $upstream_response_time '
'$upstream_addr $upstream_status '
'trace_id="$trace_id" span_id="$span_id" parent_id="$parent_id" '
'request_id="$request_id"';
-
$upstream_connect_time:连接建立耗时(网络/SSL); -
$upstream_header_time:从连接完成到收到响应头(含后端处理); -
$upstream_response_time:从连接完成到收完全部响应体(含传输); - 用差值快速定位:比如
$upstream_header_time - $upstream_connect_time > 200ms→ 后端逻辑慢;$request_time - $upstream_response_time > 300ms→ 客户端弱网或 Nginx gzip 压缩开销大。
打通上下游:日志字段与后端日志严格对齐
再好的 Nginx 日志,如果后端不按同样字段打日志,链路就断在第一跳。必须约定好字段名、格式、注入方式。
- 前端 / 网关层应注入标准头:
X-Request-ID、traceparent、X-B3-TraceId等; - Nginx 提取后,用
proxy_set_header全部透传,不改名、不丢弃; - 后端服务(Java/Go/Python)需在日志框架中显式读取这些 header,并作为固定字段输出,例如:
# Java Logback 配置
# Go Zap 日志
logger.With(zap.String("request_id", r.Header.Get("X-Request-ID")))
- 验证是否对齐:找一条异常请求,在 Nginx 日志里复制
$request_id或$trace_id,去 ELK / Grafana 日志系统中搜索,看能否串起 Nginx → API 网关 → 用户服务 → 订单服务的完整日志流。


















