前言¶
如果你维护过内网 Linux 机器上的容器服务,大概会遇到这个局面:日志没有落文件,docker logs(容器 stdout/stderr)是唯一来源,而它有两个硬伤——受 log driver 轮转限制,容器重建后彻底丢失。出了问题,你得 SSH 上去一条条翻几万行日志,找到报错,再把栈粘回对话里让模型分析。
dsh-log-triage 就是为这个流程做的自动化:插件把一条 log_triage 工具暴露给模型,模型在对话里直接调用,通过 SSH 到目标机跑只读探针,取回结构化快照,本地聚类成错误指纹、抽取最深的栈根因,最后只给模型几百 token 的结论,而不是几万行日志。
这是什么¶
dsh-log-triage 是 xinstart-1 维护的 DeepSeek Harness 插件,运行在 DSH 0.2.1 线上,属于联网工具分类。它在社区目录页的星标数是 0(截至本文写作时),README 与安装文档已经写得比较完整,下面的内容以这两处为准。
插件的核心立场是只读:整条链路只跑 docker ps/inspect/logs/stats、dmesg、df、free 和 systemctl 状态查询,不重启容器、不改配置、不写服务器。服务器上不装 agent、不装 cron、不起常驻进程,探针执行结果走 base64 编码通过 stdin 传回,远端不落文件,临时文件用完即删。
调用链是这样的:
模型调用 log_triage 工具
→ SSH 到目标机执行只读探针
→ 返回「帧化」的结构化快照(容器状态 / 退出码 / OOM / 磁盘 / 负载 / 日志)
→ 本地聚类成错误指纹(计数 + 首次/末次时间)
→ 从 Java / Python / Node 栈里抽最深的 Caused by 作根因
→ 脱敏(内网 IP、账号、token)
→ 给模型结论,而不是原始日志
关键点是:整份日志从不进上下文。工具只回摘要、指纹表和有行号引用的证据。
安装与启用¶
插件不发布到 npm(package.json 里 "private": true),所以不能用裸包名装。要求 DSH >=0.2.1-rc.1 <0.3.0-0,Node >=20,本机 SSH 客户端为 OpenSSH(填密码认证时需要 OpenSSH ≥ 8.4,走 SSH_ASKPASS;Windows 10/11 自带的满足)。不需要构建,lib/ 是纯 ESM 源码直接入库。
dsh plugin --profile web add github:xinstart-1/dsh-log-triage
桌面应用(Electron 版)用插件页的图形界面:侧栏「插件」页 →「添加插件」→ 填 Git 地址 → 安装。
装完必须重启 DSH:host 半侧每个进程只加载一次插件,当前进程里新装的东西不存在,面板会报 404。重启后到 设置 → 容器日志巡检 填 SSH 主机。
内网 / 离线机器用打包方式,零依赖是这个场景的结构性优势:
npm pack
# 得到 dsh-log-triage-<version>.tgz(约 223KB,31 个文件)
dsh plugin --profile web add file:C:/path/to/dsh-log-triage-0.2.0.tgz
文档里实测过:--offline 下 pnpm add file:<tgz> 成功,输出 downloaded 0, reused 1, added 1,真正零下载。注意 GitHub 的 Download ZIP 不能用——安装器只认 git 地址、file: 路径和 .tgz,没有 zip 分支。
装完用这条验证(不需要真机,服务不可达时自动跳过的脚本见 README):
curl -s http://127.0.0.1:<GUI端口>/plugins/dsh-log-triage/env
# 返回 {"ok":true,"platform":"win32","version":"0.2.0","config":{...}}
这个接口的密码已剥离,且只服务回环地址,非 127.0.0.1 访问一律 403。
配置 SSH 主机¶
配置落在 ~/.dsh/log-triage/config.json(环境变量 DSH_LOG_TRIAGE_HOME 可改目录):
{
"hosts": [
{ "name": "app-01", "ssh": "root@10.0.0.11", "since": "3h", "tail": 4000, "containers": [] }
],
"intervalMinutes": 0
}
几种写法与含义:
1、ssh: "user@host" 是紧凑字符串,也可以拆成结构化字段 host / user / port / password,两种形态双向兼容。
2、containers: [] 表示该机全部容器;填具体名字则只看这些。
3、password 留空走本机密钥免密;非空走 SSH_ASKPASS,密码只进 ssh 子进程的环境变量,不进命令行、不落远端。
4、intervalMinutes: 0 表示定时巡检关闭,也是默认值。改成大于 0 才启用,且上一轮没跑完会跳过本轮,不会重叠执行。
面板里的「测连通」只是一次 SSH 往返(约 400ms),不跑探针,适合先确认网络和凭据。
核心功能:指纹、根因、证据¶
插件给模型暴露两个工具:log_triage 负责定位报错,log_source_read 负责在报错定位到「文件:行号」后按需读取源码片段。侧栏的「日志巡检」面板(立即分析 / 测连通 / 只看变化 / 历史报告)和模型走的是同一条代码路径,所以界面看到的报告和模型读到的摘要是同一份。
聚类和根因抽取是确定性的规则,这一步要处理两类常见问题。
指纹聚类。 同一类报错按指纹归并,带计数和首末时间。折叠 Traceback (most recent call last): 这类结构行,避免它自己占一个指纹槽位;DNS/网络类根因(gaierror、EAI_AGAIN、ENOTFOUND、no such host 等)会被排到最前,severity 提到 3。
链式 traceback。 Python 的 raise X from Y 会让一条日志里出现三到四段 Traceback,用 The above exception was the direct cause of the following exception: 串起来,最深根因可能埋在第 40 行。插件按「链」而不是按「块」处理:按 Traceback 切块、按桥接行建链,取最深一环,用 exceptionChain[].causeRelation 区分 cause 和 context。README 给了实测 fixture(test/fixtures/chained-python-openwebui.log):根因取到 socket.gaierror: [Errno -3] Temporary failure in name resolution,链 L0→L3 完整。
errno 是有意保留而非归一化的:[Errno -3](DNS 挂了)和 [Errno 111](端口没人听)是两种故障、两种修复动作,归一化成 [Errno -<n>] 会把它们误合并;而 attempt 41 这类每次变的数字照旧归一化。
源码只给地址,不给正文。 早期版本会把出错位置上下 ±6 行原文塞进报告,既费 token,也替模型决定了读多少代码——真正要看的往往是整个函数或者相邻的调用者。现在拆成两步:
log_triage → source: /app/.../retrieval/utils.py:425 (1200 lines total)
nearby definitions: def embed_texts() @ 395 · def get_model_path() @ 410
read it yourself: log_source_read(container: "open-webui", file: "...", start_line: 410, end_line: 454)
log_source_read → 只返回你要的那一段,带绝对行号
工具只回传文件行数、定义的行号和名字(无签名无代码)、出错那一行的锚点;窗口由 log_source_read 按参数取,max_lines 硬上限 400。报告建议的窗口取外围 def 的跨度,模型想放宽就自己改参数。
「只看变化」与轮询的盲区¶
巡检每隔几分钟跑一次时,一台正常机器的指纹表每轮都一样。每次都重读那十条老指纹,人很快会学会无视这个面板——这是巡检功能最常见的死法。所以有「只看变化」:
立即分析 → 本轮的全部指纹(完整证据)
只看变化 → 过滤最新报告,只留「刚出现的」「次数突增的」「容器刚坏掉的」
它是过滤器,不是第二次巡检。早期版本把它做成了第二个按钮,点一下就完整跑一轮 SSH 探针只为看上一轮的数据,每次点击都重置指纹基线,同一个老错误下一轮又会被当成新指纹报警。现在它只是把最新报告按 diff 过滤一遍——瞬时、零服务器开销,判定口径与告警用的是同一份 diff。判定写死在 diffHost() 里,只有四种算变化:新指纹(按容器隔离)、突增(本轮次数 >= max(3, 上轮 × 2))、容器刚坏、错误连续 N 轮不消失(默认 3,只在跨过阈值那一轮提醒一次)。
日志类报错不会因为 5 分钟间隙而错过。 --since 3h --tail 4000 的语义是回看 3 小时内的日志,不是只看这 5 分钟。持续性错误(DNS 挂、连接拒绝)下一轮一定还在;瞬时错误只要还在 3 小时窗口内就能看到;容器状态变化曾经是真盲区——两轮之间崩溃又被 Docker 自动拉起,下一轮读到的是 healthy 的当前状态,所有规则都沉默。修复靠持久化基线:把 Docker 的 RestartCount 存进 state.json 下一轮对比,计数上涨即触发 critical 告警「在两轮巡检之间重启过」,详情给出 3 -> 4 的跳变。
两条边界值得知道:仍然 broken 的容器走「刚坏」规则,一次死亡只报一次;告警 key 带上计数值,死两次报两次,不会被冷却期吞掉。如果需要秒级感知(docker events 流式订阅),那是常驻连接 + 常驻进程的架构,与「服务器上不装任何东西」的设计原则冲突,目前刻意不做。
实测数据(5 容器 / openEuler 虚拟机,来自 README):一轮完整巡检约 2.7s、传输 933KB;2 容器白名单下约 2.5s、133KB;巡检期间被测容器 CPU 峰值 <2%。每 5 分钟一轮的长期代价是每天 288 次 SSH 会话,服务器侧每次只读约 3MB 的 json log 文件头部。轮询式监听对这台虚拟机没有可感知影响。
AI 分析:规则定位,模型解释¶
聚类、栈解析、源码定位是确定性的,能回答「哪里」「什么」,回答不了「为什么」「该改什么」。那部分交给模型。
指纹表 + 根因 + 源码地址 → [设置里选的模型] → 根因判断 + 依据引用 + 处置建议
几个默认行为决定了它能不能放心开:
1、默认关闭。每轮巡检花一次模型调用,不该由插件替用户决定,要在设置页「AI 分析」里显式开启。
2、prompt 只喂摘要,绝不喂日志。
3、禁止编造。system prompt 要求引用输入里真实出现的异常类名 / 指纹 / 文件:行号,信息不足就说「不确定」。
4、失败降级为一行。供应商超时、key 过期、空回答,一律变成「分析未完成:…」,巡检本身不会因为模型不可用而失败。
5、模型清单与宿主同源。设置页下拉框读 /models(ctx.llm.listProviders + listModels),与对话框右下角的模型选择器是同一份清单。
配置形态:
"analysis": { "enabled": false, "provider": "", "model": "" }
只有 enabled === true(严格布尔)才算开启;provider / model 只有成对出现才生效,半截配置会被丢掉,否则每轮都会去请求一条解析不了的路由。
容器白名单与无人值守告警¶
一台机器跑几十个容器时,「全部检查」既慢又吵。实测单容器日志读取约 0.37s,而早期版本每个容器还要跑一次 docker stats(约 2s/个),7 个容器就是 14 秒,这批数据在任何地方都没被使用。现在移除了无消费方的 docker stats(改为按需 INCLUDE_STATS=1),7 容器一轮从约 19s 降到约 2.5s。设置页会从主机拉取实际容器列表并勾选,留空等于全部容器。
无人值守告警默认关闭,开启后每轮把值得惊动人的变化记进面板的告警列表(带未读徽标),不发邮件、短信或任何外部 webhook——提醒就在插件里,紧挨着证据。告警分五档:容器刚进入异常状态(严重)、出现新指纹(警告)、错误量突增(警告)、错误连续 N 轮不消失(警告)、主机读不到(严重)。
「一直不消失」这条是有意加的:前三条规则结构上抓不到一种最常见的真实故障——服务一直报错但容器活着。第一次出现按新指纹报,之后计数涨不涨都沉默,直到跨过 N 轮阈值才提醒一次,同一个错误之后每轮重复报警的噪音也被砍掉了。
适用场景与注意¶
适合:内网 Linux 批量机器上的容器服务巡检,尤其是日志只剩 docker logs、没有落盘、没有集中式日志系统的环境;想通过 DSH 在对话里直接问「这台机器怎么了」的团队。
需要注意的几点:
1、插件运行在当前 DSH 进程权限下,会以你的账号 SSH 到目标机执行只读命令。安装前建议先读一遍源码(仓库不大,lib/ 是纯 ESM)并确认许可证是 MIT。
2、面板路由只服务回环地址,设计上就是不让局域网内其他机器打到面板,不是 bug。
3、唯一真实影响是巡检那一刻的负载(实测 7 容器一轮 12–19s)。业务高峰期建议手动触发,或把定时巡检设在低谷。
4、docker stats 移出默认路径后,需要容器实时资源数据时要显式设 INCLUDE_STATS=1。
5、卸载:
dsh plugin --profile web remove dsh-log-triage
然后删掉 ~/.dsh/log-triage/(配置、指纹基线、历史报告都在这里,不随卸载自动删除)。
结尾¶
dsh-log-triage 的价值不在「又多了一个看日志的界面」,而是把定位过程搬到模型能调用的工具链里:SSH 只读探针、指纹聚类、最深的根因链、按需读取的源码窗口,每一步都有明确的边界。它默认不打扰(定时巡检和 AI 分析默认关闭),也不往服务器上装任何东西。
目录页:https://www.skillhub.cn/plugins/xinstart-1/dsh-log-triage
仓库与文档:https://github.com/xinstart-1/dsh-log-triage(README 为主文档,安装细节见 docs/INSTALL.md)