应用日志监控擅长发现异常,却很少覆盖异常发生后的诊断与处置。关键词命中次数、错误率和响应时间一旦超过阈值,监控系统可以迅速发出告警;但错误是否来自同一根因、影响了哪些服务、是否需要立即控制影响,通常仍要由工程师接手判断。
墨天轮原有的日志监控也是如此。2026 年 7 月 10 日,外部监控只告诉我后端程序出现了 74 条 ERROR。我根据这条告警手工创建了 issue_49,初始信息只有报错数量和日志目录:
接到告警说后端程序 ERROR 报错 74 个,
请看看是什么报错。
issue_49 随后通过 Agent 连接两台应用节点,重新统计完整日志:一个节点有 113 条 ERROR,另一个节点有 80 条,合计 193 条。因此,Issue 标题中的 74 和分析结果中的 193 属于不同统计口径:74 是外部监控传来的初始告警值,193 是 Agent 扫描两台服务器后得到的完整 ERROR 总量。分析过程中又识别出 74 条短信验证码高频请求,但这是错误分类结果,不能代替完整日志总量。Opsx 继续完成错误分类和根因分析,但触发链路仍然存在断层:第三方平台发现异常,人把告警数量转述给 Opsx,再手工创建 Issue。只要人没有及时转述,后续分析和处理就不会开始。
单一的报错数量也不足以回答生产处置真正关心的问题:
- 74 条 ERROR 是 74 个不同问题,还是同一个问题重复了 74 次?
- 错误来自哪个应用副本、哪个接口和哪个用户或 IP?
- 是程序 Bug、数据库故障、外部依赖异常,还是攻击流量?
- 服务是否已经恢复,是否需要立即介入?
- 如果属于已知问题,能否直接复用历史结论和处置方法?
因此,这次实践不只是增强告警后的分析,而是替换这条依赖第三方监控和人工转述的入口:由 Opsx 的 Shell Schedule 每 10 分钟直接扫描两台应用服务器的增量日志;ERROR 超过阈值后自动创建资产关联 Issue,对满足明确规则的已知问题先执行处置,再由 Agent 分析根因、验证结果,并把发现、动作和结论保存在同一个运维上下文中。

监控的终点,应该是运维流程的起点
真正困难的不是判断 emcs-app ERROR > 500,而是理解这个数字背后的系统上下文:
svc-emcs-app
├── 运行在两台应用主机、两个服务副本
├── 前置 svc-emcs-nginx
├── 源码来自 repo-emcs-app
├── 依赖 MogDB 主备库
├── 依赖 Redis
└── 调用短信等外部服务
如果平台没有保存这些关系,Agent 只能依据 Prompt 中有限的日志片段推断。它可以解释某个异常类,却无法确认应该从哪个环境取得完整堆栈、源码位于哪个仓库、数据库在同一时段是否异常,也无法安全地操作生产 NGINX。
当前机制不再等待外部监控信号进入 Opsx。Shell Schedule 直接绑定 svc-emcs-app Asset:Asset 提供稳定身份、两个运行节点、前置服务、源码和依赖关系;Asset Memory、Discovery Report 与 Knowledge 提供已经确认的接口特点、日志格式和同类事件;Runtime 决定 Shell 和 Claude Code 应该在哪些环境执行。
当增量日志超过阈值,Schedule 自动创建持续存在的 Issue:触发窗口、目标资产、两个节点返回的日志、Agent 判断、生产动作、验证证据和最终结论都保存在同一个 Workspace 中。
这也是 Opsx 与在监控系统中直接接入 LLM 的区别。后者获得的上下文通常局限于指标和日志;Opsx 则通过 Asset、关系和 Runtime,把应用、主机、源码、数据库以及生产执行环境连接到同一个运维控制面。
用 Shell 承担每 10 分钟一次的确定性检测
当前 emcs-app 日志巡检由一个 Shell Schedule 承担,每 10 分钟运行一次。

它不是请求到达时立即判断的实时防火墙,但最迟会在下一个 10 分钟窗口发现异常,把过去依赖人工转述的响应压缩到分钟级。对于已经能够用阈值和来源集中度明确表达的高频攻击,Shell 会在派发 LLM 分析前直接执行隔离。
日志增量读取、ERROR 计数和阈值比较都属于规则明确的确定性工作,适合由 Shell 持续执行。正常窗口内,执行链路保持在最小范围:
Schedule 按周期触发
→ 根据 svc-emcs-app 找到两个 Runtime
→ 读取上次 marker 之后的新增日志
→ 统计 ERROR 数量和高频来源
→ 未超过阈值:记录结果并结束
→ 不创建分析 Issue
→ 不调用 LLM,也不消耗 Token
Shell 用字节 marker 保存每个节点上次读到的位置,只扫描新增内容;日志轮转后文件变小时重置位置,单次最多读取 5 MB,避免异常增长的日志占用过多执行资源。
THR=500
WIN_MIN=10
COUNT=$(printf '%s\n' "$NEW" | grep -cE ' ERROR ' || true)
if [ "$COUNT" -le "$THR" ]; then
# 正常窗口到此结束:不创建 Issue,不派发 LLM
exit 0
fi
# 异常窗口内,找出至少出现 20 次的来源 IP
TOP_IPS=$(printf '%s\n' "$NEW" \
| grep -E ' ERROR ' \
| grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' \
| sort | uniq -c | sort -rn \
| awk '$1 >= 20 {print $2}')
这条 Schedule 绑定的是应用 Asset,而不是一段散落在某台服务器上的 crontab。Opsx 为每次运行保留 Runtime 和执行记录,资产关系改变时也有明确的调整入口。Agent、LLM 甚至具体 Shell 实现都可以替换,平台持有的资产、Issue、证据和运行历史不会随之消失。
ERROR 爆发时,先隔离高频来源,再判断根因
墨天轮的 /code/getSmsCode 是一个只接受 POST 的验证码接口。过去多次出现同一种攻击:未登录来源不断用 GET 请求访问这个接口,参数高度重复,有时还使用明显的探测手机号。
Spring 正确地拒绝了请求,却抛出 HttpRequestMethodNotSupportedException;全局异常处理又把它记录为“未知异常”ERROR。攻击没有真正发出短信,但可以在很短时间内制造成百上千条错误日志,污染告警并占用应用资源。
这类问题第一次出现时需要 AI 和人共同确认。经过多次事件以后,它已经有稳定特征:
- 单一或少量来源 IP;
- 未登录请求,用户 ID 为空;
- 重复访问
/code/getSmsCode; - 使用错误的 GET 方法;
- 异常类型高度集中;
- 短时间内 ERROR 快速爆发。
当前 Schedule 不会让高频来源的隔离等待 LLM。ERROR 超过 500 条且窗口内存在至少出现 20 次的来源 IP 时,Shell 会先调用受控的 NGINX 黑名单脚本隔离候选地址,再更新 svc-emcs-app 的健康状态;主节点随后创建 Issue 并派发 Claude Code。
这里需要区分“快速隔离”和“根因判定”。Shell 依据的是 ERROR 爆发与来源集中度,并不在这一刻声称已经识别出验证码攻击。它负责执行条件明确的处置动作;请求是否属于已知 wrong-method flood、是否影响短信业务、是否还存在数据库或代码问题,仍由后续跨资产分析确认。
2026 年 8 月 16 日的一次告警初始值为 588。Agent 后续聚合两个应用副本,确认同类 ERROR 已达到 5,712 条,全部集中在同一个攻击来源和同一个验证码接口;NGINX 自动封禁生效以后,相同错误停止增长。
8 月 23 日再次出现 588 条告警时,Claude 在一个应用副本的完整窗口中复核到 1,759 条 ERROR,仍然是相同接口、相同异常类型和单一来源。攻击者换了 IP,但已有资产上下文和历史知识让分析不必从一条陌生 ERROR 重新开始。
这条自动隔离规则仍有明确边界:它不会凭一条 ERROR 封禁地址,只有 ERROR 总量和单 IP 频次同时达到阈值才执行;封禁通过统一管理脚本进入 NGINX,并保留解封路径。更细的接口签名、白名单,以及是否修改生产限流策略,仍然需要 Agent 分析和人的审批。

Shell 处理确定部分,异常窗口才调用 LLM
ERROR 超过阈值后,主节点会自动创建新的 Issue,把本轮时间、错误总数、候选来源、NGINX 处置状态和少量日志样本放进去,然后派发 Claude Code。另一个节点参与日志采集和封禁,但不会重复创建 Issue 和发送通知。正常窗口不调用 LLM;异常窗口仍会调用,因为仅凭来源集中度还不能安全区分已知攻击、真实代码异常和依赖故障。
Claude Code 拿到的不是孤立日志,而是一份由 Opsx 组装的、以资产为中心的执行上下文:
本次 Issue
├── Schedule 触发窗口和错误样本
├── svc-emcs-app Asset Memory
├── 历史攻击 Knowledge
├── 两个应用 Runtime 的完整日志
├── repo-emcs-app 的接口和异常处理源码
├── svc-emcs-nginx 的活动配置
└── MogDB、Redis 等依赖的现场状态
以 8 月 23 日的事件为例,Claude Code 继续核对了应用日志、请求路径、HTTP 方法、异常类型、源码接口定义、匿名访问配置、NGINX 当前 deny 规则、封禁后的错误增量,以及同一时刻 MogDB 是否存在 ERROR/FATAL。该次 Execution 从派发到完成约 129 秒;8 月 16 日形成 kw_167 的首次完整分析约 161 秒。两次 Execution 都已完成,对应 Issue 当前仍处于 In Review,等待人确认后关闭。
最终结论不止于“疑似 CC 攻击”,而是形成了可以验证的判断:
- 这是针对 POST-only 验证码接口的 wrong-method flood;
- 不是数据库故障,也不是本次新出现的代码 Bug;
- 攻击没有进入正常短信发送流程;
- 当前 IP 封禁已经止住日志增长;
- 结构性缺口仍在,包括边界层缺少更精细的
limit_req、应用层缺少 per-IP/per-phone 限流,以及 405 客户端错误被记录为 ERROR。
如果另一次 ERROR 爆发来自 OSS 对象不存在、数据库唯一键冲突、连接池等待或真实代码异常,Agent 会沿对应资产关系继续调查,而不是套用验证码攻击结论。
这正是 LLM 应该发挥价值的地方:理解语义、比较多份证据、排除错误方向并提出下一步计划;而不是承担每 10 分钟一次的重复计数。
企业微信先收到处置状态,完整结论留在 Issue
过去企业微信里的第三方告警只有 ERROR 数量,维护者收到后还要手工创建任务。当前版本由 Opsx 直接扫描日志,再把异常窗口、ERROR 数量、候选 IP、NGINX 封禁状态和自动创建的 Issue 推送到企业微信。企业微信从自动化入口变成结果通知渠道;维护者收到消息时,处置已经开始,并且可以沿 Issue 继续查看证据:
发生了什么
→ 哪个资产、哪个接口、哪类异常
已经做了什么
→ 是否自动封禁、资产状态是否更新
证据在哪里
→ 对应 Issue、Execution 和关联资产
Claude 的完整根因、影响判断和后续建议会在 Execution 完成后写回 Issue。当前 Shell 异步派发 Claude 后立即发送企业微信消息,因此即时通知包含处置状态和 Issue 链接,不等待两分钟左右的完整分析。
在这条应用日志链路中,Shell Schedule 负责分钟级发现,Issue 承载分析与处置,企业微信负责通知结果;不同 Runtime 上的 Agent 与 Shell 共同工作,处理过程成为持久的运维记录。即使最终仍需人工修改代码或批准生产配置,工程师面对的也已经是经过证据收敛的问题,而不再只是一项异常计数。
从单次结论沉淀为可复用的攻击知识
8 月 16 日的分析后来形成了 kw_167,记录这类 wrong-method flood 的端点、请求方法、异常类型、用户态、历史事件、处置结果和当前防护缺口。svc-emcs-app 的 Asset Memory 则继续保存服务运行节点、前置 NGINX、源码仓库、数据库、Redis 和短信服务等关系。
于是,系统开始形成一个不断演进的分工:
第一次遇到的新问题
→ 人补充业务背景
→ LLM 跨资产分析
→ Agent 获取现场证据
→ 人审核处置边界
验证后的经验
→ 写入 Memory / Knowledge
→ 能确定表达的部分固化为 Shell
→ Schedule 持续运行
下一次相同问题
→ 自动隔离高频来源
→ 带着历史结论快速复核
→ 围绕本次新增证据调整判断
这就是 Opsx 所说的 Self-Evolving:不是让 LLM 自己拥有生产权限,而是让每次执行的结果继续改善资产认知、Knowledge 和确定性流程。平台负责事实、范围、状态和证据,Agent 在明确边界内行动。
当前机制仍有可以继续加强的地方,例如更细的业务签名、自动解封策略、下游失败重放,以及把应用限流建议真正变成经过审核的 Playbook。但与过去相比,最大的变化已经发生:监控不再以“我发现了 ERROR”作为终点,而是开始回答“为什么、影响什么、已经处理什么、接下来该做什么”。