dsh-log-triage:讓 DSH 自己在服務器上定位容器報錯

前言

如果你維護過內網 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)

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜