早上到公司的第一件事,不用猜,大部分团队都一样:打开 Sentry,看昨晚又挂了什么。我这边业务线略多,项目加起来十几个,Sentry 里堆栈看半天,经常是同一个报错在不同版本、不同用户里反复出现,新问题被旧问题淹没。每天光是筛选一遍新 bug 就要花二十几分钟,更麻烦的是看了也不一定判断得出该立刻修还是能缓缓。
后来我琢磨了一下,能不能把“拉日志”和“读日志”这两件事完全交给程序跑。正好 Codex 这个终端里的 AI 智能体成熟了不少,支持非交互模式,可以像命令一样被别的脚本调用。于是就有了今天这套东西:每天定时让 Codex 拉取 Sentry 最近 24 小时的日志,自动做聚合、分析、定位,最后生成一份“今天有哪些 bug 值得看、最可能的原因是什么、修复建议是什么”的日报,直接推到团队群。
这篇文章我分成四块写:整体设计思路、核心准备、落地脚本、以及我在实操中踩过的坑。涉及的代码我会直接贴出来,你可以照着改一版用在自己项目上。
1. 整体设计与思路拆解
1.1 这活儿真正难的不是“拉日志”,是“读日志”
如果要给这件事拆步骤,原来的手动流程大概是这样的:
- 打开 Sentry 项目首页,点 Issues。
- 按“最近出现”排序,扫一遍列表里新冒出来的报错。
- 点进每一个看起来陌生的 issue,看堆栈的栈顶几行。
- 对照源码定位是哪个模块抛出来的。
- 根据影响人数、出现频率、是不是最近上线的新代码,判断优先级。
这套流程里,第 1 步和第 2 步本质上就是“拉日志”,用 Sentry API 十分钟就能搞定。真正耗费精力的是第 3 到第 5 步——“读日志”和“判断优先级”。一个堆栈可能横跨十几个调用帧,里面还穿插着框架内部的方法,如果对代码不熟,光是把“哪个函数是真正业务侧的”提取出来就得花不少时间。
所以我的核心思路是:把“拉日志”交给脚本,把“读日志”交给 Codex。脚本负责按时按点把数据准备好,Codex 负责像一个不睡觉的同事一样,把堆栈信息消化掉,输出人能直接看懂、能直接拍板的结果。
这个定位很重要。如果你也想搭这么一套,别把重心放在“怎么把日志拉下来”,那只是最初级的体力活;把思路放在“怎么让 Codex 分析得更准、输出更稳定”,这决定了这套自动化到底是真的提效,还是另一个玩具。
1.2 为什么选 Codex 而不是自己写一套规则脚本
在决定用 Codex 之前,我其实先评估过另外两条路:Sentry 自带的 Alert 通知,和自己写规则过滤脚本。
Sentry 的 Alert 能做的是:当某个 issue 满足条件时,发邮件、发 webhook。它只告诉你“有问题”,不会告诉你“这个问题的堆栈指向哪个模块、大概率是什么原因、要不要叫后端同事看一眼”。而且多项目、多环境下的告警规则维护起来非常琐碎,规则写松了吧,天天轰炸;写紧了吧,真正的问题又漏了。
自己写规则脚本则是另一个极端。你可以按标题关键字分组、按用户量排序、定期对比新旧 issue 集合。这套东西确实能做,但有一个致命问题:规则是死的。今天服务端抛了一个新串格式的错误,明天前端某个 SDK 升级后堆栈风格变了,你的关键词过滤规则就失效了,又得去加规则。等于这套“自动化”的长尾维护成本,比它省下来的时间还高。
Codex 的路子不太一样。它是语言模型,不需要你预先写死什么“规则”,你只要把堆栈信息丢给它,它自己理解上下文。更关键的在于 Codex 是一个 CLI 程序,支持非交互式调用,天然适合嵌进脚本链里。我可以用 cron 定时触发一个 Python 脚本,脚本拉完数据后直接调 codex exec,把分析结果拿回来,再推送出去。整个链路上没有人在中间拦一下,真正的全自动。
1.3 方案选型:把“能用”和“好维护”分开看
技术栈我最终选的是 Python + Shell 的组合。Sentry 日志拉取用 Python 的 requests 库,分析环节调 Codex CLI,定时任务用系统自带的 crontab。这个组合最大的好处是“每一环都能单独调试”。
我见过不少团队一上来就上 Jenkins、上 K8s CronJob、上容器编排,把一个很轻的需求搞得很重。定时拉日志分析这件事,最重的依赖其实是 Sentry API 和 Codex 的调用,逻辑本身不复杂,没必要为了“显得正规”引入一堆基础设施。开发期内你在本地手动跑,验证稳定了再扔进 crontab,这才是最快的路径。
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| Sentry Alert | 配置简单,官方支持 | 只能通知,不能分析,多项目规则繁琐 | 做补充可以,做主力不行 |
| 自研规则脚本 | 可控性强,不依赖外部模型 | 规则写死,堆栈一变就失灵,维护成本高 | 适合当滤波层,不适合当分析层 |
| Codex 分析 | 能理解上下文,输出像人话,适配新报错 | 需要调 prompt,偶尔输出不稳定 | 主力方案 |
架构上就四条线:cron 定时触发、Python 拉取日志、Codex 分析、webhook 推送。下面一步一步展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心环节准备:Sentry API 与 Codex CLI
2.1 申请 Sentry API Token 并确认项目标识
先准备一个能读取 Sentry 数据的 Token。如果你是 Sentry SaaS 用户,打开组织首页后进 Settings,找到 Auth Tokens(在 Developer Settings 下),点 Create New Token。如果是自建 Sentry,路径类似,只是域名换成你自己的。
Token 的权限不能乱给。我们这个场景只需要读取项目事件和 issue,勾上 project:read 和 event:read 就够了,最多再加一个 org:read。不要勾 admin 权限,机器上留一个能删项目的 token 是给自己埋雷。
还需要确认两个标识:organization_slug 和 project_slug。最简单的方式是登录 Sentry 网页端,看浏览器地址栏。比如地址是 https://sentry.io/organizations/my-company/projects/backend-api/,那 org slug 就是 my-company,project slug 就是 backend-api。记住,Sentry 的 API 路径里用的是 slug 不是显示名称,显示名称带空格、带中文都可能,slug 一定是小写加连字符那种格式。
拿到 token 后别急着写 Python,先用 curl 冲一下接口,确认权限和路径都正确:
bash复制curl -s -H "Authorization: Bearer $SENTRY_TOKEN" \
"https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/issues/?statsPeriod=24h&query=is:unresolved" \
| head -c 2000
能返回一段 JSON 数组,说明这一步通了。返回 403 就回去检查 token 权限;返回 404 基本是 org slug 或 project slug 写错了。
2.2 安装 Codex CLI 并完成鉴权配置
Codex CLI 的安装并不复杂,官方推荐用 npm 全局安装:
bash复制npm install -g @openai/codex
装完执行 codex --version,能输出版本号就算成功了。如果你不想走 npm,GitHub 上也有编译好的二进制可以直接下载,放到 /usr/local/bin 下就能用,这在一些不让装 Node 的生产机器上反而更方便。
鉴权是第二个关键点。Codex 支持两种方式:一种是在终端里执行 codex login,走浏览器 OAuth 流程,这种方式适合人坐在电脑前交互使用;另一种是设置环境变量,适合无人值守的脚本场景。我这里用的是后者,在脚本环境里设置:
bash复制export OPENAI_API_KEY="sk-your-key"
如果你的 Codex 接的是兼容 OpenAI 协议的服务,还可以加一个 OPENAI_BASE_URL 指向你自己的网关地址,这样密钥管理和计量都能走公司内部统一通道。这一步没有固定答案,取决于你手里实际可用的凭证类型。
装好后建议先手动跑一个最简单的命令验证链路:
bash复制codex exec --full-auto --skip-git-repo-check "你好,简短回复即可"
能正常输出一段文本,说明 CLI 装好、鉴权也通了。
2.3 手动跑通最小闭环
在写完整脚本之前,我强烈建议先手动把最小闭环跑通:拉数据 → 存文件 → 调 Codex 分析 → 看结果。先用一条命令验证数据链路,再验证分析链路,出了问题更容易定位。
bash复制# 第一步:拉取 Sentry 问题列表,保存到本地
curl -s -H "Authorization: Bearer $SENTRY_TOKEN" \
"https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/issues/?statsPeriod=24h&query=is:unresolved" \
-o /tmp/sentry-issues.json
# 第二步:让 Codex 读取文件并分析
codex exec --full-auto --skip-git-repo-check \
"读取 /tmp/sentry-issues.json 中的 Sentry 问题列表,按影响大小排序列出前 5 个,并给出每个问题最可能的修复建议。"
这里我用 --skip-git-repo-check 是因为 Codex 默认会检查当前目录是否在 Git 仓库里,我们的脚本跑在临时目录或 /opt 下,没必要让它做这个检查。--full-auto 表示全自动执行,不需要我逐步确认。这两个参数在无人值守场景里是必须的。
跑通了这一条链路,后面的工作就变成工程化的细节了:怎么分页、怎么定时、怎么推送。下面进入正式脚本阶段。
3. 实操落地:写一个真正能用的日志分析脚本
3.1 用 Python 拉取 Sentry 最近 24 小时的日志
先写数据拉取模块。Sentry 的 Issues API 有一个比较烦的地方是分页用 cursor 机制,而不是常见的页数偏移。我一开始偷懒没做分页,只拿第一页 100 条,结果好几个低频报错根本没出现在日报里。后来老老实实按 Link 响应头里的游标走了。
python复制import json
import os
import requests
from typing import Any
SENTRY_TOKEN = os.environ["SENTRY_TOKEN"]
SENTRY_ORG = os.environ["SENTRY_ORG"]
SENTRY_PROJECT = os.environ["SENTRY_PROJECT"]
BASE_URL = f"https://sentry.io/api/0/projects/{SENTRY_ORG}/{SENTRY_PROJECT}"
def parse_cursor(link_header: str) -> str | None:
# Link 头格式类似:
# <https://...>; rel="next"; results="true", <https://...>; rel="previous"; results="false"
for part in link_header.split(","):
if 'rel="next"' in part:
return part.split("&")[-1].split("=")[-1].split(">")[0]
return None
def fetch_recent_issues(hours: int = 24) -> list[dict[str, Any]]:
issues = []
cursor = None
while True:
params = {
"statsPeriod": f"{hours}h",
"query": "is:unresolved",
"limit": 100,
}
if cursor:
params["cursor"] = cursor
resp = requests.get(
f"{BASE_URL}/issues/",
headers={"Authorization": f"Bearer {SENTRY_TOKEN}"},
params=params,
timeout=30,
)
resp.raise_for_status()
issues.extend(resp.json())
link = resp.headers.get("Link", "")
# 没有下一页就退出
if 'rel="next"' not in link or 'results="false"' in link:
break
cursor = parse_cursor(link)
return issues
这里解释一下几个参数。statsPeriod=24h 是让 Sentry 只返回最近 24 小时有更新的 issue,而不是全部历史 issue。is:unresolved 是过滤掉已经标记为已解决的,避免日报里反复出现已经处理掉的旧问题。limit=100 是单页最大条数,Sentry 的上限也就是 100。
用 statsPeriod 而不是自己算 start 和 end,是我踩过的一个坑:Sentry 的时间参数默认是 UTC,如果你用本地时间算范围,在中国时区下会让数据整体偏移 8 小时。statsPeriod 是相对时间,交给 Sentry 自己算,反而最省心。
考虑到真实场景中突发流量会导致 issue 数量很大,我在脚本里还会对结果做一个二次排序:按 count * userCount 降序排,只保留前 30 条进入分析环节。这样可以避免 Codex 的上下文被几百个低价值 issue 塞满。
3.2 设计 Prompt:让 Codex 输出稳定可用的分析结果
Codex 的发挥水平,七成取决于 prompt 怎么设计。我最初试过直接把 JSON 内容贴在 prompt 里,效果很不稳定。后来改成了“文件路径 + 严格的输出要求”模式,效果好很多。
我的 prompt 模板大致是这样:
text复制你是一个负责线上质量的技术负责人。请分析 Sentry 最近 24 小时的问题列表。
JSON 文件路径:{json_path}
要求:
1. 只看 count * userCount 影响最大的前 8 个问题。
2. 如果列表为空或影响很小,直接输出“今日无高风险问题”。
3. 对每个问题输出以下内容:
- 问题标题
- 首次出现时间、最近出现时间
- 影响估算:事件数、用户数
- 最可疑的堆栈调用帧(提取业务代码部分,忽略框架内部帧)
- 修复建议:包括可能的原因、需要检查的模块、建议的修复方向
4. 用 Markdown 格式输出,不要寒暄,不要输出额外解释。
如果信息不足,直接说“信息不足”,不要编造。
之所以强调“把业务代码部分从堆栈里提取出来,忽略框架内部帧”,是因为真实堆栈里 90% 的帧都是各种框架、SDK、中间件内部的方法,直接塞给模型看,容易让它分心。Codex 确实有这个能力去过滤,但它需要一个明确的指令。
“不要编造”这句话也很有用。模型在信息不足时容易顺着上下文编一个看着合理的修复建议,这会误导处理问题的人。明确告诉它信息不足就承认,能显著降低幻觉概率。
输出格式我故意用 Markdown 而不是 JSON。原因很简单:这个日报最终是给人看的,在群里用 Markdown 展示更直接。如果将来想让某个系统自动消费这份结果,再让 Codex 额外输出一份 JSON 也不迟。
3.3 把分析结果组装成日报并推送到团队群
分析结果拿到了,下一步是推送到团队群。我这里以企业微信机器人为例,其他平台的 webhook 大同小异,只是 payload 结构略有不同。
python复制import requests
WEBHOOK_URL = os.environ["WECHAT_WEBHOOK_URL"]
def send_markdown(content: str) -> None:
# 企业微信 markdown 消息长度限制是 4096 字节,超了会被拒
if len(content.encode("utf-8")) > 4000:
content = content[:1500] + "\n\n...(内容过长已截断,请查看完整报告)"
payload = {
"msgtype": "markdown",
"markdown": {
"content": content,
},
}
resp = requests.post(WEBHOOK_URL, json=payload, timeout=10)
resp.raise_for_status()
这里有个细节:企业微信的 markdown 消息有长度限制,而 Codex 生成的分析如果问题很多,很容易超。我处理的方式是限制分析的前 8 个问题,并把完整报告同时落盘到本地文件,群里的日报只是一个摘要。这样既不会因为长度问题发送失败,也保留了完整审计记录。
推送之前的最后一步,是在报告开头加上日期和时效信息,比如“2025-XX-XX 每日 Sentry 分析报告”,这样大家扫一眼就知道是哪天的数据,不会跟昨天的报告混在一起。
我还会把 Codex 返回的原始报告同时保存一份到本地 reports/ 目录,文件名带日期。这样做一方面便于追溯“当时 Codex 为什么给这个结论”,另一方面也给后续做数据积累留了原料。
3.4 crontab 定时执行与运行锁
所有逻辑都跑通之后,最后一步是挂定时任务。我用的是 crontab,最简单的做法:
cron复制0 9 * * * cd /opt/sentry-codex && /usr/bin/python3 run_daily.py >> /var/log/sentry-codex.log 2>&1
这条规则的意思是:每天早上 9 点整,进入 /opt/sentry-codex 目录,用绝对路径的 Python 执行脚本,把标准输出和错误输出都追加写进日志文件。
这里有两个新手很容易踩的点,值得单独说:
第一,cron 环境下 PATH 环境变量非常精简,很可能只有 /usr/bin:/bin。如果你在脚本里调用 codex,而 codex 安装在 npm 全局目录 /usr/local/bin,cron 会直接报 command not found。我习惯在脚本开头主动把 PATH 加上:
bash复制#!/bin/bash
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
第二,crontab 里的环境变量不会自动继承你 shell 里的设置。Sentry token、API key 这些敏感信息,我通常写在一个 .env 文件里,脚本启动时用 python-dotenv 加载,cron 任务只负责调用脚本,不直接承载敏感信息。
另外一个容易被忽略的问题是“运行锁”。如果某天 Sentry 响应特别慢,或者 Codex 分析耗时过长,上一次任务还没跑完,下一次 cron 又触发了,两个进程同时读写同一个报告文件,结果会乱。我在脚本里加了一个基于文件锁的保护:
python复制import fcntl
with open("/tmp/sentry-codex.lock", "w") as lock_file:
try:
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
print("上一次任务还在运行,跳过本次执行")
exit(0)
# 真正执行任务的代码放这里
Windows 用户如果要用任务计划程序,逻辑也是一样的,只要把“定时触发”和“执行命令”两件事配置好即可。但说实话,这种轻量自动化脚本放在 Linux/Mac 上跑会更顺手,Windows 上各种路径和环境变量问题会多一些。
4. 常见问题与排查技巧实录
4.1 cron 任务明明挂了,却什么都没发生
这是我最开始遇到的问题之一。cron 日志里能看到任务触发了,但根本没有日报推送。排查下来主要有三种原因:
第一种是脚本里的环境变量没设。cron 不会读你 .bashrc 里那些 export,所以脚本里如果没有主动加载 .env,Sentry token 就是空的,脚本直接抛异常。
第二种是 PATH 问题。前面说过,cron 的 PATH 精简到令人发指,codex 命令找不到,脚本就在 subprocess.run(["codex", ...]) 这一步抛 FileNotFoundError。
第三种是输出丢失。cron 默认会把标准输出和标准错误用邮件发送给当前用户,但很多服务器根本没配邮件服务,于是异常信息就悄悄丢了。这就是为什么我一定会在 crontab 里加上 >> /var/log/sentry-codex.log 2>&1,让日志落盘。
排查这类问题我的顺序是:先手动执行一次 bash run_daily.sh,确认能跑通;再 crontab -l 看看任务有没有挂上;最后再看日志文件里有没有报错。按照这个顺序,百分之九十的问题都能定位。
4.2 Codex CLI 报错与鉴权问题
Codex 相关的报错,我遇到频率最高的有两个。
一个是 command not found: codex。这个基本就是 PATH 问题或者 npm 全局安装目录你没放进 PATH。解决办法是用 npm config get prefix 查看全局安装路径,把它加到 ~/.bashrc 里,或者直接在脚本开头 export。
另一个是 unable to locate the codex cli binary。这个报错主要出现在 Codex 桌面端应用里,是图形界面在启动时找不到 codex 可执行文件的提示。它不会影响命令行直接调用 codex exec,但如果你跟桌面端配合使用,需要在应用的设置页里手动指定 codex_cli_path,指向你机器上 codex 二进制的实际位置。这个不算 bug,是配置文件路径没配好。
还有一类是鉴权失败。脚本里设置了 OPENAI_API_KEY 但值不对或已过期,codex exec 会明确报鉴权相关错误。这种情况我会先回到终端手动执行一次,确认同一个 key 能正常用,再怀疑是 cron 环境没把环境变量带过去。
4.3 Sentry API 返回 403 或拿到空数据
403 大概率是 token 权限不够。Sentry 的 Auth Token 是生成时就固定权限的,不给你中途勾选的机会,所以只要权限不对,就得重新生成一个。这个重新生成的流程很快,但往往容易忽略,因为你可能跟别人共用同一个 token,那个人只给了一个低权限 scope。
空数据的情况也常见,但原因完全不同。最常见的是 org slug 或 project slug 填错,导致 URL 指向了一个不存在的资源。Sentry 有时候对不存在的项目返回空数组而不是 404,会让人误以为“系统没报错”。我排查时会先 curl 一下接口,直接看 HTTP 状态码和响应内容。
另外注意 query=is:unresolved 这个参数。如果你在 Sentry 页面里用了其他过滤条件,API 层不会继承你的页面设置,必须自己写清楚。如果你只关心特定环境的报错,比如生产环境,可以追加 environment=production。这些条件拼在 query 里是空格分隔,URL 编码后发送。
4.4 Codex 分析结果偶尔“跑偏”
语言模型的分析结果不可能 100% 稳定,这点要有心理预期。我遇到过的跑偏包括:把 StackOverflow 风格的通用建议当修复方案、忽略了我明确要求的“只看前 8 个问题”、甚至把两个不同 issue 的内容混淆在一起。
应对思路有几个。第一个是在 prompt 里把约束写得非常具体,包括数量、格式、忽略框架内部帧等,而不是笼统地说“帮我分析一下”。第二个是在脚本里做后置校验。比如我要求输出里必须包含 issue 标题,如果 Codex 返回的结果里一个标题都没有,脚本就把原始 JSON 保存下来并输出一条“分析异常”的告警,而不是把错的内容推送出去。
第三个是控制输入范围。如果一次塞给它几百个 issue,本身就超出了模型的有效关注力。我把数量限制在 30 个候选里再筛前 8 个,就是为了给模型“减负”。信息量小一点,跑偏的概率就低一点。
4.5 重复告警与噪音控制
每天 9 点把同样的 5 个历史问题再推一遍,谁也受不了。这个问题比前面的更影响体验。
我的做法是在脚本里维护一个“已知问题指纹”文件。每次分析前,把当前 issue 列表的 id 集合跟上次记录的对比,只挑出“新增的”或者“事件数或用户数明显上升的”问题进入分析。这个逻辑很简单,用 set 做差集就够了:
python复制import json
import pathlib
KNOWN_FILE = pathlib.Path("data/known_issue_ids.json")
def load_known_ids():
if KNOWN_FILE.exists():
return set(json.loads(KNOWN_FILE.read_text()))
return set()
def save_known_ids(ids):
KNOWN_FILE.parent.mkdir(exist_ok=True)
KNOWN_FILE.write_text(json.dumps(sorted(ids)))
current_ids = {issue["id"] for issue in issues}
known_ids = load_known_ids()
new_issues = [issue for issue in issues if issue["id"] not in known_ids]
# 然后对新问题做分析
# 每天执行完后,不管是否推送了日报,都更新 known_ids
save_known_ids(current_ids)
这套逻辑上线之后,日报的“含金量”明显提升。因为群里收到的都是真正值得关注的新问题,而不是每天重复出现的旧账。
到这里,整个自动化的链路就完整了:定时触发、拉取日志、Codex 分析、推送报告、记录历史。这套东西我实际跑了两个多月,最直观的感受是每天早上的“清报错”时间从二十多分钟缩短到了扫一眼群消息。更重要的是,以前那些“不翻到第三页根本不会发现”的慢热问题,现在当天上午就会出现在日报里,处理响应速度比之前快了不止一个量级。
最后再分享一个我后来补上的小技巧:给脚本加一个 --dry-run 参数。加了之后,脚本只生成报告并保存到本地,不推送 webhook。调试 prompt、调整过滤逻辑的时候,这一个参数能帮你省掉无数次打扰同事的尴尬。等你把 prompt 调得足够稳定了,再把这个参数摘掉,让日报开始“打扰”所有人。
