Nginx默认access-log不单独记录接口路径,需自定义log_format使用$uri字段提取解码后无参数的路径,配合精准过滤状态码、UA和路径才能准确统计TOP接口。

Access-Log 默认格式不包含接口路径?
Nginx 默认的 log_format(如 combined)只记录 $request 的完整请求行(例如 GET /api/v1/users?id=123 HTTP/1.1),但没单独剥离出「接口路径」。直接用 awk 或 cut 按空格切分容易被 query string 或特殊字符干扰,导致统计失真。
解决办法是自定义日志格式,显式提取路径:
log_format api_path '$remote_addr - $remote_user [$time_local] '
'"$request_method $uri $http_version" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" $request_time';注意这里用的是 $uri 而非 $request_uri —— 前者已解码且不含 query string,更适合统计「接口」维度;后者保留原始编码和参数,适合调试但会把 /search?q=a 和 /search?q=b 当作不同接口。
- 在
http或server块中定义该格式,并在access_log中引用:access_log /var/log/nginx/api_access.log api_path; - 重载 Nginx:
nginx -s reload,旧日志不会自动切换格式,需等新请求写入 - 若已有大量
$request_uri日志,可用sed 's/ \?.*$//'粗略截取路径,但遇空格参数(如POST /upload HTTP/1.1后带 body)仍不可靠
用 awk + sort + uniq 统计 TOP 接口时为什么结果不准?
高频接口统计本质是「按路径聚合计数」,但常见命令链 awk '{print $7}' | sort | uniq -c | sort -nr 有三个硬伤:
-
$7是默认combined格式的第七字段,实际对应的是$request整体,不是路径 —— 若未改日志格式,它可能是"GET或/api/v1/users,取决于空格数量,完全不可控 - 未过滤掉健康检查、静态资源、404 等噪音请求,比如
/healthz或/favicon.ico可能冲淡真实业务接口排名 - 未排除爬虫 UA 或恶意扫描(如
User-Agent: sqlmap),它们常高频访问/admin类路径,但不属于正常业务流量
推荐组合命令(假设已启用 api_path 格式):
awk '$9 >= 200 && $9 < 400 && $7 !~ /^\/(healthz|favicon\.ico|robots\.txt)$/ {print $7}' /var/log/nginx/api_access.log \
| sort | uniq -c | sort -nr | head -20其中 $7 对应 $uri 字段,$9 是状态码字段 —— 字段序号取决于你定义的 log_format 字段顺序,务必先用 head -1 查一眼日志确认。
实时监控高频接口是否必须用 ELK?
不需要。ELK(Elasticsearch + Logstash + Kibana)适合长期归档与多维下钻,但查「此刻过去 5 分钟谁最忙」,用 tail -f 流式处理更轻量:
- 用
tail -F实时追加新日志,配合awk过滤+计数,再用sort -nr滚动刷新:
tail -F /var/log/nginx/api_access.log | \
awk '$9>=200 && $9<400 {count[$7]++}
NR%100==0 {for (i in count) print count[i], i; delete count}' | \
sort -k1,1nr | head -10这段脚本每读 100 行就输出一次当前累计 Top 10,避免内存无限增长。注意 NR%100==0 是粗略窗口,不等于精确时间窗口;如需严格按秒聚合(如每 60 秒清零),得用 awk 记录 $time_local 并解析时间戳,复杂度陡增 —— 此时不如上 goaccess 或 lnav 这类专用日志分析工具。
另外,Nginx 自身支持 limit_req 配合 log_format 标记限流请求,可单独统计被限频的接口,这比事后分析更前置。
为什么 /api/v1/users 和 /api/v1/users/123 被算作两个接口?
因为 $uri 是精确路径匹配,Nginx 不做路由模式抽象。RESTful 接口的动态 ID 段(如 /users/:id)在日志里就是字面值,导致每个 ID 单独计数,无法反映「用户查询接口整体热度」。
解决思路分两层:
- 简单聚合:用
sed替换数字 ID 段为通配符,例如sed -E 's:/[0-9]+(/|$):/{id}\1:g',再统计。适用于 ID 规律明确(全数字、固定位置)的场景 - 精准路由映射:在应用层(如 Spring Boot 的 Actuator、或 Gin 的 middleware)注入统一 trace header(如
X-Route-Name: user-get-by-id),然后在 Nginxlog_format中加入$http_x_route_name。这样日志里直接有逻辑接口名,不依赖路径解析 - 注意:正则替换要避开非 ID 字段(如
/v1/2024中的年份),建议先采样验证替换效果
路径泛化本身有信息损失 —— 如果你要定位「哪个用户 ID 被刷得最多」,就必须保留原始 $uri;如果目标只是评估模块负载,那聚合后的逻辑接口名更有意义。别指望一个日志格式同时满足所有分析目标。

















