Alertmanager 踩坑筆記:不用 Prometheus,也能用 5 行 bash 發告警

Alertmanager 不是 Prometheus 的附屬品,它是獨立的告警閘道,任何東西都能直接推。本文記錄用 bash 看門狗直推告警的做法、endsAt 免寫恢復邏輯的技巧,以及接進 Grafana 時最容易搞錯的方向問題。

Alertmanager 踩坑筆記:不用 Prometheus,也能用 5 行 bash 發告警

事情的起點是某天早上,手機被 Telegram 轟了六則告警。查完之後發現:三則是真的(服務 OOM 崩潰),三則是自己嚇自己(正常停機被當成崩潰)

追這件事的過程中,把 Alertmanager 從頭理解了一遍,也踩了幾個坑。這篇記錄下來。

(同一套 stack 的 Prometheus / Grafana / MLflow 部分,我在 vLLM 推論服務監控實戰 寫過,這篇專講告警。)

最大的誤解:Alertmanager 不是 Prometheus 的一部分

大部分人第一次接觸 Alertmanager,都是透過這條路:

Prometheus 評估 alerting rules → 推給 Alertmanager → 送 Telegram / Slack

看久了很容易以為 Alertmanager 是 Prometheus 的下游附屬品。它不是。

Alertmanager 是一個獨立的服務,跑自己的 process、開自己的 port(預設 9093)。它的 /api/v2/alerts 是公開的 REST endpoint,任何東西都能直接推告警進去 —— Prometheus 可以,你的 bash 腳本也可以。

這個認知差別很重要,因為它直接決定了你要花多少力氣。

為什麼這件事重要:不是所有問題都是 metrics

假設你要監控一個背景 worker:它的 systemd unit 死了沒?主迴圈卡住了沒?

這兩件事不是時序資料。要走「正統」的 Prometheus 路線,你得先架一個 exporter,把「服務活著沒」變成一個 up metric,讓 Prometheus 定時抓、寫進 TSDB、再跑 rule 判斷、才推給 Alertmanager。

繞一大圈,而且你多了一個常駐的 exporter 要顧 —— 它自己也會掛,然後你需要監控你的監控。

直接推 Alertmanager 就沒這些事。 一個 curl 就結束了:

curl -sf -XPOST http://127.0.0.1:9093/api/v2/alerts \
  -H 'Content-Type: application/json' -d '[{
    "labels": {
      "alertname": "MyWorkerDown",
      "severity": "critical",
      "instance": "'$(hostname)'"
    },
    "annotations": {
      "summary": "worker 掛了",
      "description": "查:journalctl -u my-worker -e"
    }
  }]'
Code language: PHP (php)

推進去之後,Alertmanager 那端的分組、節流、靜音、Telegram 推送全部照常運作 —— 因為對它來說,這則告警跟 Prometheus 推的沒有任何區別。

技巧:用 endsAt 讓告警自動恢復,免寫 recovery 邏輯

這是我覺得最漂亮的一招。

Alertmanager 眼中的告警不是一則訊息,而是一個有生命週期的狀態:firing → resolved。每則告警有 startsAt / endsAt過了 endsAt 沒人續命,就自動變成 resolved

利用這點,你的監控腳本可以完全不寫「恢復偵測」:

  1. 每次推告警時,把 endsAt 設成 +10 分鐘
  2. 用 cron / systemd timer 每 3 分鐘跑一次檢查
  3. 問題還在 → 繼續推,endsAt 一直被延後,告警維持 firing
  4. 問題好了 → 沒人推了 → 10 分鐘後 Alertmanager 自己 resolve

搭配 receiver 的 send_resolved: true,恢復通知也是自動的。你一行 recovery 邏輯都沒寫,卻能正確發出「已恢復」。

ends_at=$(date -u -d '+10 minutes' +%Y-%m-%dT%H:%M:%SZ)
# 把 "endsAt": "${ends_at}" 加進 payload 就好
Code language: PHP (php)

等於把狀態管理外包給 Alertmanager 的 TTL 機制。這是純 Telegram Bot API 給不了的 —— 自己 call Telegram 的話,「什麼時候該說恢復了」得你自己記。

完整範例:一個 systemd 服務的看門狗

把上面兩件事組合起來,一個實用的看門狗大概長這樣。它檢查兩件事:unit 活著沒,以及心跳有沒有卡住(服務每跑一圈 touch 一個檔案)。

#!/usr/bin/env bash
set -uo pipefail   # 注意:不加 -e,因為 is-active 對非 active 會回非零

UNIT="my-worker"
BEAT="$HOME/.cache/my-worker.beat"
AM_URL="http://127.0.0.1:9093"
STALE_SEC=300

now=$(date +%s)
mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }

state=$(systemctl --user is-active "$UNIT" 2>/dev/null || true)
reason=""

if [[ "$state" != "active" ]]; then
    reason="unit 狀態=${state}"
else
    beat_age=$(( now - $(mtime "$BEAT") ))
    if (( beat_age > STALE_SEC )); then
        reason="心跳 ${beat_age}s 沒更新(行程在,但主迴圈沒動)"
    fi
fi

[[ -z "$reason" ]] && exit 0

ends_at=$(date -u -d '+10 minutes' +%Y-%m-%dT%H:%M:%SZ)
curl -sf -m 10 -XPOST "$AM_URL/api/v2/alerts" \
  -H 'Content-Type: application/json' -d "[{
    \"labels\": {\"alertname\": \"MyWorkerDown\", \"severity\": \"critical\", \"instance\": \"$(hostname)\"},
    \"annotations\": {\"summary\": \"${reason}\"},
    \"endsAt\": \"${ends_at}\"
  }]" || logger -t my-watchdog "告警送不進 Alertmanager:$reason"
Code language: PHP (php)

心跳檔那段是重點,它能抓到 systemd 抓不到的狀況:行程還在、PID 還活著,但主迴圈已經卡死。systemd 只知道 process 在不在,不知道它有沒有在做事。

配上 timer 每 3 分鐘巡邏,再加上 service 的 OnFailure 立即觸發:

# my-worker.service
[Unit]
OnFailure=my-watchdog.service      # 崩潰當下立刻告警,不用等下一輪
StartLimitIntervalSec=300
StartLimitBurst=5                  # 5 分鐘內崩 5 次就停手,別無限重啟

[Service]
Restart=always
RestartSec=5
Code language: PHP (php)
# my-watchdog.timer
[Timer]
OnBootSec=3min
OnUnitActiveSec=3min
Code language: PHP (php)

整套零依賴 —— 純 bash、systemd、curl,沒有 monit、沒有 supervisor、沒有 exporter。watchdog 自己不是常駐 process,所以它不會變成下一個需要被監控的東西。


坑 1:Grafana 接 Alertmanager 有兩個方向,選錯什麼都看不到

這是最容易卡住的一個。

Alertmanager 內建的 web UI(:9093)很陽春,而且沒有任何認證 —— 裸露出去等於任何人都能進去按 Silence 把你的告警全部靜音,而且不會有人通知你。所以通常會想接進 Grafana(至少有帳密)。

問題來了:Grafana 裡有兩個地方都寫著 Alertmanager,方向完全相反。

設定位置 方向 用途
Administration → External Alertmanagers Grafana AM 把 Grafana 自己的 rules 推出去
Connections → Data sources → Alertmanager Grafana AM 讀取外部 AM 的告警與 silence

想在 Grafana 裡既有 Alertmanager 的告警,要的是下面那個(data source)。選錯上面那個,畫面永遠是空的。

正確做法是加一個 Alertmanager 型別的 data source。用 provisioning 的話:

apiVersion: 1
datasources:
  - name: Alertmanager
    uid: alertmanager
    type: alertmanager
    access: proxy
    url: http://alertmanager:9093
    jsonData:
      implementation: prometheus
      handleGrafanaManagedAlerts: false   # Grafana 自己的 rules 不往這裡推
Code language: PHP (php)

接好之後還有第二層陷阱:進到 /alerting/groups/alerting/silences頁面上方有一個下拉選單,預設選的是 Grafana(內建那套,通常是空的)。要手動切成 Alertmanager,才看得到你的告警。

我就是卡在這裡,明明 API 測試全部 200、資料也讀得到,UI 卻空白 —— 因為選單沒切。

驗證的話,直接打 Grafana UI 自己在用的那個 API 最快:

curl -s -u admin:密碼 \
  'http://localhost:3000/api/datasources/proxy/uid/alertmanager/api/v2/alerts'
Code language: JavaScript (javascript)

200 就是通的。

坑 2:Alertmanager 沒有歷史,resolve 就消失

我一開始以為告警列表是空的代表壞了,其實空白才是正常 —— 那個 UI 只顯示「此刻正在燒」的告警

這代表:已經恢復的告警,你在 Alertmanager 和 Grafana 裡都查不到了。 想知道「上週三到底燒過沒」,那兩個 UI 都給不了答案。

歷史得自己留,實務上有幾個地方:

  • 通知管道的聊天記錄(Telegram / Slack)—— 最直接,但難搜尋
  • 你自己腳本裡的 logger —— 我在 watchdog 加了 logger -t my-watchdog,結果這變成最有用的一份,journalctl -t my-watchdog --since '30 days ago' 直接把整個月的事件撈出來,後來就是靠它還原現場的
  • Prometheus 的 ALERTS 時序 —— 只涵蓋 Prometheus rules 觸發的告警,你用 curl 直推的它不知道

所以如果你打算直推 Alertmanager,務必在腳本裡加一行 logger,成本一行,價值很高。

坑 3:systemd 的 143 —— 正常停機被當成崩潰

這是我那六則告警裡「假警報」的來源,也是最陰的一個。

Stopping my-worker.service...
my-worker.service: Main process exited, code=exited, status=143/n/a
my-worker.service: Failed with result 'exit-code'.
my-worker.service: Triggering OnFailure= dependencies.     ← 告警發出去了
Code language: JavaScript (javascript)

143 = 128 + 15 = SIGTERM。也就是說:這是一次正常的 systemctl stop,服務乖乖收信號退出。

但 systemd 認為「非零退出碼 = 失敗」,於是 Failed → 觸發 OnFailure= → 看門狗發告警。每次你正常停機維護,就收到一則假崩潰通知。

修法一行:

[Service]
SuccessExitStatus=143

或者更根本地,讓程式自己攔 SIGTERM 然後 exit 0

**判讀速查**:status=143 = 被 SIGTERM(正常 stop)、status=137 = 被 SIGKILL(-9 或 OOM killer)、status=1/FAILURE = 程式自己異常退出(**這種才是真的崩了**,要去看它的 stderr)。

我那次就是混了兩種:143 是維護停機(假警報),1/FAILURE 才是真的 CUDA OOM。分不清楚的話,會浪費很多時間查錯方向。

坑 4:Compose 的 inline config 改了不會重建容器

這個坑我在上一篇寫過,結果這次又踩了一次,所以再提一句。

configs:content: 寫 inline 設定時,改了內容重新 upDocker 不認為需要重建容器,你的新設定不會生效。

# 確認容器內拿到的是不是新版
docker exec <容器> cat /path/to/config.yml

# 沒換的話,砍掉重建(具名 volume 的資料會留著)
docker rm -f <容器> && docker compose up -d
Code language: HTML, XML (xml)

判斷方法:docker inspect <容器> --format '{{.Created}}',如果建立時間是好幾天前,那它用的就是舊設定。

小結

重點 一句話
Alertmanager 的定位 獨立的告警匯流排,不是 Prometheus 的附屬品
什麼時候直推 監控對象不是 metrics(服務死活、心跳、檔案狀態)時,別為它架 exporter
endsAt + 定期續火 免寫 recovery 邏輯,恢復通知自動來
要留歷史 Alertmanager 沒有,靠 logger 自己記
接 Grafana data source(讀),不是 External Alertmanagers(推);記得切下拉選單
systemd 143 是正常 stop,不是崩潰,加 SuccessExitStatus=143

整套下來最大的心得是:「偵測問題」和「通知人類」本來就該分開。 Alertmanager 把後者做成了一個誰都能接的服務 —— 你的 5 行 bash 和正牌的 Prometheus,在它眼裡地位完全相同。認清這點,很多監控需求就不用繞遠路了。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *