上一篇把流程分為資料來源、查詢內容、固定規則與 AI 調查,最後再整理成報告。
今天會做一個 cli.py 作為巡檢程式的執行入口,讀取查詢設定、執行固定規則,再產生 result.json。
將所有巡檢項目放在 config/checks.yaml。以後要新增項目、調整順序或修改 query,都先改這個檔案。
檔案最上層分成 meta 和 layers:
meta:
cluster: lab
timezone: Asia/Taipei
layers:
- id: data-trust
title: Data Trustworthiness
blocking: true
checks:
# 資料可信度的巡檢項目
- id: control-plane
title: Control Plane
checks:
# Control Plane 的巡檢項目
meta 記錄叢集名稱與時區。layers 用來將巡檢項目分組,例如資料可信度、Control Plane、Node、Workload、監控系統與 Kubernetes Event。
layers 的排列順序也是報告的章節順序。我把資料可信度放在最前面,因為 Prometheus 根本沒有收到完整資料時,後面的綠燈也沒有多少參考價值。blocking: true 會讓報告在這一層未通過時,直接顯示資料可信度警示。
以 Node Ready 為例,取出主要欄位後長這樣:
- id: node-ready
# type: promql(預設值,可以省略)
title: 節點是否全程正常
risk: 節點 NotReady 期間,上面的 Pod 會被驅逐或停止服務。
advice: 確認該節點的 kubelet 狀態與網路連通性。
unit: bool
identity: [node]
expr: 'min_over_time(kube_node_status_condition{condition="Ready",status="true"}[{{window}}])'
fail_below: 1
這個 check 包含幾個重點:
id 是程式辨識這個項目的名稱,title 會顯示在報告上。type 指定這個 check 的查詢執行方式。上面用註解標出 # type: promql,因為 promql 是預設值,實際設定可以省略。expr 是實際執行的 query。Node 處於 Ready 時,這個 metric 的值是 1;Node 處於 NotReady 時,值是 0。min_over_time 會取出整個巡檢期間的最低值,所以全程 Ready 會得到 1,期間內只要曾經出現 NotReady 就會得到 0。{{window}} 會在執行時換成本次巡檢期間,例如 24h 或 72h。identity 指定要用哪些 metric label 組成查詢對象的名稱,不會改變 PromQL。Node Ready 會回傳多台 Node 的結果,每筆資料都有 node label,因此設定為 identity: [node]。報告就能顯示 worker-01 = 1、worker-02 = 0,直接指出哪一台 Node 曾經 NotReady。Container 類型的資料則可以使用 [namespace, pod, container],避免不同 namespace 或 Container 的結果混在一起。unit 說明數值的單位,讓後面整理結果時知道該顯示百分比、次數、秒數或 bool。risk 和 advice 由人預先寫在 checks.yaml。同一個巡檢項目的風險與基本處理方向不會每天改變。把這些內容放在同一個 check 裡,之後修改 query 或門檻時,也能一起確認風險與建議是否需要調整。AI 後面只負責補充這次巡檢實際發生的狀況。warn_above、fail_above、warn_below、fail_below 是判斷門檻。above 代表結果高於設定值時觸發,below 代表結果低於設定值時觸發,warn 和 fail 則決定狀態。例如:Node Ready 使用 fail_below: 1,只要查詢結果低於 1,這個項目就會判定為 FAIL。這樣做之後,query、對象、單位、風險和建議都放在同一個 check 裡,清楚表示這個數字屬於誰、代表什麼。
大部分 check 可以直接執行 PromQL,有些資料要從 Loki 查詢,還有一些項目需要程式執行多次查詢後再合併結果。這些差異會透過 type 區分。沒有設定 type 時,程式預設執行 PromQL。
Kubernetes Event 儲存在 Loki,這類查詢會指定 type: logql:
- id: warning-events
type: logql
empty_means: pass
expr: 'sum by (reason,namespace) (count_over_time({job="kube-events"} | logfmt | type="Warning" [{{window}}]))'
warn_above: 0
查不到 Warning Event 時,empty_means: pass 會讓這個 check 判定為 PASS。只要查到一筆以上,warn_above: 0 就會將結果判定為 WARN。
這種查詢只會回傳產生問題的對象,沒有查到資料時才可以使用 empty_means: pass。CPU、Memory 這類平常就應該有數值的查詢,空結果仍然要判定為 UNKNOWN。
使用率類型的 check 會使用 type: usage。程式會分別查詢巡檢期間的最高值、產生報告時的目前值,以及超過警告門檻的時間。
- id: node-cpu-usage
type: usage
unit: ratio
identity: [instance]
expr: '1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))'
warn_above: 0.80
fail_above: 0.95
報告產生時仍然超標,會依原本的門檻判定。巡檢期間曾經超標、後來已經恢復,會將當時最高值的判定降低一級。這樣可以在報告保留這次尖峰,也不會把已經恢復的問題列成當下仍在發生的 FAIL。
工作負載副本需要分別查詢 Deployment、DaemonSet 與 StatefulSet,再合併就緒數量與期望數量,因此使用專用的 workload_ready:
- id: workload-replicas
type: workload_ready
identity: [kind, namespace, workload]
程式讀到 type 後,就知道這個 check 要執行 PromQL、LogQL,或交給專用的查詢流程處理。
下一步說明查回來的 metrics 如何整理成可以判斷的結果。
每個 check 查詢完成後,程式會取出回傳結果,逐筆套用 checks.yaml 裡的門檻。同一個 check 有多筆結果時,再取其中最嚴重的狀態作為這個 check 的最後結果。
以 Container restart 為例,取出與判斷有關的設定:
- id: container-restarts
identity: [namespace, pod, container]
empty_means: pass
expr: 'sum by (namespace,pod,container) (increase(kube_pod_container_status_restarts_total[{{window}}])) > 0'
warn_above: 0
fail_above: 5
這條 query 會依 namespace、pod、container 分組,每個 Container 的重啟次數都會成為一筆查詢結果。empty_means: pass 代表沒有查到任何結果時,就代表沒有 Container 在巡檢期間重啟,這個 check 可以判定為 PASS。
CPU、Memory 這類應該持續有數值的查詢,空結果代表資料不足,會判定為 UNKNOWN。warn_above: 0 代表只要曾經重啟就要注意,fail_above: 5 代表重啟超過五次就判定為 FAIL。
假設一個 Container 重啟一次,另一個 Container 重啟六次,程式會先將兩筆結果判定為 WARN 與 FAIL,再將整個 container-restarts 判定為 FAIL。查詢失敗或回傳的數值無法使用時,程式會將這個 check 標記為 UNKNOWN。
程式會依照 checks.yaml 的順序重複這個流程,直到所有 check 都完成,再把每個 check 的查詢與判斷結果組成這次巡檢的結果。
查詢內容與門檻都準備好後,就可以設定 Prometheus、Loki 的位置並執行巡檢:
PROM_URL=http://localhost:19090 \
LOKI_URL=http://localhost:13100 \
./.venv/bin/python cli.py report
所有 check 完成後,程式會將結果寫入:
reports/YYYY-MM-DD/result.json
result.json 會保留本次巡檢期間、每個 check 的狀態、實際執行的 query、查詢對象、判斷門檻,以及每一筆查詢結果(samples)。以 Container restart 為例,下面使用簡化的示意資料說明 result.json 的結構:
{
"cluster": "lab",
"window_start": "2026-08-19T00:00:00+00:00",
"window_end": "2026-08-20T00:00:00+00:00",
"data_trustworthy": true,
"results": [
{
"id": "container-restarts",
"layer": "workloads",
"title": "容器重啟次數",
"status": "FAIL",
"summary": "2 項未通過",
"identity": [
"namespace",
"pod",
"container"
],
"unit": "count",
"thresholds": {
"warn_above": 0,
"fail_above": 5
},
"evidence": {
"query": "sum by (namespace,pod,container) (increase(...)) > 0",
"query_type": "promql",
"samples": [
{
"labels": {
"namespace": "lite-bank",
"pod": "user-service-123",
"container": "user-service"
},
"value": 1,
"status": "WARN"
},
{
"labels": {
"namespace": "kube-system",
"pod": "kube-vip-456",
"container": "kube-vip"
},
"value": 6,
"status": "FAIL"
}
]
},
"investigation": null
}
]
}
這些欄位分別代表:
cluster、window_start、window_end 記錄這份結果屬於哪個叢集,以及本次巡檢涵蓋的時間。data_trustworthy 代表資料可信度相關的 check 是否全部通過。results 保存所有 check 的結果,順序與 checks.yaml 相同。status、summary 是固定規則產生的狀態與簡短說明。identity 指出組成查詢對象名稱的 labels。unit、thresholds 記錄數值單位與本次使用的判斷門檻。evidence 保存實際執行的 query、query 類型與每一筆查詢結果(samples)。labels、value、status 分別表示這筆結果屬於哪個對象、查到的數值,以及套用門檻後的狀態。investigation 是保存 AI 調查結果的位置。這個階段的 investigation 還是 null,因為目前只完成固定規則的判斷,AI 還沒有開始調查。接下來 AI 會先找出 WARN、FAIL 與 UNKNOWN,再使用工具補查其他資料,最後將結果寫回 investigation。
到這裡,巡檢程式已經完成查詢、套用門檻,並將所有結果寫入 result.json。每個狀態都來自固定規則,使用相同資料重跑就會得到相同結果。
這時 investigation 還是 null。下一篇會繼續實作 AI 調查流程,說明 Skill 如何使用提供的調查工具、補查未通過的項目,再將調查結果寫回 result.json 並產生最後的巡檢報告。
今天就先寫到這,我們明天見!