春江暮客

春江暮客的个人学习分享网站

Python 日志排障实战:用 rg 和 uv 快速定位 Nginx 5xx 错误

2026-06-03 技术
Python 日志排障实战:用 rg 和 uv 快速定位 Nginx 5xx 错误

线上服务出问题时,最常见的第一步不是写复杂平台,而是先回答三个问题:什么时候开始报错、哪些 URL 报错最多、是不是集中在某些客户端或上游接口

如果日志很大,直接用编辑器打开会很慢。更实用的方式是先用 rg 把可疑行筛出来,再用一个很小的 Python 脚本做统计。本文用 Nginx access log 做例子,搭一套可以复制到服务器上的排障流程。

完成后,你会得到:

  1. 一组常用 rg 日志检索命令
  2. 一个 uv run 即可执行的 Python 分析脚本
  3. 一份 5xx 状态码、URL 和 IP 排名摘要
  4. 常见报错的直接修复方法

适合什么场景

这套方法适合临时排障和轻量自动化:

  1. Nginx、Apache、应用 access log 体积较大
  2. 需要快速定位 500502503504
  3. 还没有接入完整日志平台,或者日志平台查询不方便
  4. 想把一次性排查命令沉淀成可重复脚本

如果你已经有完善的 ELK、Loki 或云日志服务,仍然可以保留这个方法:服务器本地排查时,它通常更直接。

方法 1:先用 rg 缩小日志范围

先准备一个测试目录:

mkdir nginx-log-triage
cd nginx-log-triage

写入一份小型示例日志:

cat > access.log <<'EOF'
203.0.113.10 - - [03/Jun/2026:07:40:01 +0800] "GET / HTTP/1.1" 200 612 "-" "curl/8.0"
203.0.113.11 - - [03/Jun/2026:07:40:03 +0800] "GET /api/orders HTTP/1.1" 502 173 "-" "Mozilla/5.0"
203.0.113.12 - - [03/Jun/2026:07:40:08 +0800] "POST /api/login HTTP/1.1" 500 91 "-" "Mozilla/5.0"
203.0.113.11 - - [03/Jun/2026:07:41:15 +0800] "GET /api/orders HTTP/1.1" 504 173 "-" "Mozilla/5.0"
203.0.113.13 - - [03/Jun/2026:07:42:20 +0800] "GET /assets/app.css HTTP/1.1" 200 2048 "-" "Mozilla/5.0"
203.0.113.14 - - [03/Jun/2026:07:43:11 +0800] "GET /api/orders HTTP/1.1" 502 173 "-" "Mozilla/5.0"
EOF

查所有 5xx 行:

rg -n '" 5[0-9]{2} ' access.log

只看 502504

rg -n '" (502|504) ' access.log

查看匹配行前后各 1 行,方便看错误前后的请求:

rg -n -C 1 '" 5[0-9]{2} ' access.log

如果日志分散在多个文件里:

rg -n '" 5[0-9]{2} ' /var/log/nginx -g '*.log'

这一步的目标不是生成最终报告,而是快速确认:错误是否真实存在、集中在哪些文件、是否需要进一步统计。

方法 2:用 uv 运行 Python 统计脚本

rg 适合快速筛选,但如果要统计“哪个 URL 最多、哪个 IP 最多”,Python 会更稳。

创建一个脚本:

uv init --script log_report.py --python 3.12

log_report.py 改成下面这样:

# /// script
# requires-python = ">=3.12"
# ///

from __future__ import annotations

import argparse
import re
from collections import Counter
from pathlib import Path


LOG_PATTERN = re.compile(
    r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] '
    r'"(?P<method>\S+) (?P<path>\S+) [^"]+" '
    r'(?P<status>\d{3}) (?P<size>\S+)'
)


def iter_records(path: Path):
    with path.open(encoding="utf-8", errors="replace") as file:
        for line_number, line in enumerate(file, start=1):
            match = LOG_PATTERN.search(line)
            if not match:
                continue
            record = match.groupdict()
            record["line"] = str(line_number)
            yield record


def main() -> None:
    parser = argparse.ArgumentParser(description="Summarize Nginx 5xx access log entries.")
    parser.add_argument("log_file", type=Path)
    parser.add_argument("--status", default="5", help="Status prefix, for example 5 or 50")
    parser.add_argument("--top", type=int, default=5)
    args = parser.parse_args()

    status_count: Counter[str] = Counter()
    path_count: Counter[str] = Counter()
    ip_count: Counter[str] = Counter()
    first_seen: str | None = None
    last_seen: str | None = None

    for record in iter_records(args.log_file):
        status = record["status"]
        if not status.startswith(args.status):
            continue

        status_count[status] += 1
        path_count[record["path"]] += 1
        ip_count[record["ip"]] += 1
        first_seen = first_seen or record["time"]
        last_seen = record["time"]

    total = sum(status_count.values())
    print(f"Total matched requests: {total}")
    print(f"Time range: {first_seen or 'n/a'} -> {last_seen or 'n/a'}")

    print("\nStatus:")
    for status, count in status_count.most_common():
        print(f"  {status}: {count}")

    print("\nTop paths:")
    for path, count in path_count.most_common(args.top):
        print(f"  {count:>4}  {path}")

    print("\nTop client IPs:")
    for ip, count in ip_count.most_common(args.top):
        print(f"  {count:>4}  {ip}")


if __name__ == "__main__":
    main()

运行脚本:

uv run log_report.py access.log

验证输出

正常输出类似下面这样:

Total matched requests: 4
Time range: 03/Jun/2026:07:40:03 +0800 -> 03/Jun/2026:07:43:11 +0800

Status:
  502: 2
  500: 1
  504: 1

Top paths:
     3  /api/orders
     1  /api/login

Top client IPs:
     2  203.0.113.11
     1  203.0.113.12
     1  203.0.113.14

这个结果已经能支持下一步排查:

  1. /api/orders 是最集中的错误 URL
  2. 502500 更多,应该优先检查上游服务或反向代理
  3. 错误集中在 07:4007:43,可以继续对比应用日志和部署时间

方法 3:接入真实服务器日志

在服务器上可以先复制脚本,再对真实日志执行:

uv run log_report.py /var/log/nginx/access.log

只统计 502

uv run log_report.py /var/log/nginx/access.log --status 502

多看几个排名:

uv run log_report.py /var/log/nginx/access.log --top 20

如果你只想分析最近追加的一部分日志,可以先用 tail 生成临时文件:

tail -n 20000 /var/log/nginx/access.log > recent-access.log
uv run log_report.py recent-access.log

这样做的好处是不会反复扫描完整大文件,排障时响应更快。

排障思路

拿到统计结果后,可以按下面顺序继续查:

  1. 500 多:优先看应用错误日志、异常堆栈、数据库连接错误
  2. 502 多:检查 upstream 是否存活、端口是否正确、反向代理超时
  3. 503 多:检查限流、维护模式、服务池是否没有可用实例
  4. 504 多:检查慢查询、外部 API、上游响应时间和 Nginx timeout 配置

例如先查 Nginx error log:

rg -n "upstream|timeout|connect\\(\\) failed|refused" /var/log/nginx/error.log

再查应用日志里的同一时间段:

rg -n "07:4[0-3]|ERROR|Traceback|Exception" /path/to/app.log

常见问题

1. rg 找不到任何 5xx

先确认日志格式里状态码前后是否有空格。本文命令匹配的是 Nginx combined log 里这样的片段:

"GET /api/orders HTTP/1.1" 502 173

如果你的日志是 JSON,应该直接搜索 JSON 字段:

rg -n '"status":50[0-9]' access.jsonl

2. Python 脚本统计为 0

原因通常是日志格式不匹配。先打印一行真实日志:

head -n 1 access.log

如果字段顺序和示例不同,就需要调整 LOG_PATTERN。临时排障时也可以先用 rgawk 解决,不必一开始就追求通用解析器。

3. 权限不够读取日志

如果当前用户不能读 /var/log/nginx/access.log,先检查权限:

ls -l /var/log/nginx/access.log

临时排查可以用:

sudo tail -n 20000 /var/log/nginx/access.log > recent-access.log
sudo chown "$USER":"$USER" recent-access.log
uv run log_report.py recent-access.log

4. 日志已经被压缩

rg 默认不直接搜索 .gz 内容。先用 zgrep 快速确认:

zgrep -n '" 5[0-9][0-9] ' /var/log/nginx/access.log.1.gz | head

需要做 Python 统计时,先解压到临时文件:

gzip -dc /var/log/nginx/access.log.1.gz > old-access.log
uv run log_report.py old-access.log

总结

排查日志时,不一定要先上复杂平台。rg 负责从大文件里快速定位可疑行,Python 负责把这些线索整理成状态码、URL 和 IP 排名,uv 让脚本可以在新机器上直接运行。

这套流程适合服务器临时排障,也适合沉淀成团队内部的小工具。下一次遇到 5xx 峰值时,先用这几条命令把问题范围缩小,再决定要查应用、数据库还是上游服务。

友情链接

其它