在 Day 18 中,我們成功利用 GitHub Actions 排程實現了 24/7 的無人值守巡航。然而,在真正的生產維運(Production Ops)中,完全無人干預的系統就像一艘靜音潛水艇——你不知道它正順暢前進,還是已經在海底拋錨。
當遇到 Google API 速率限制(Quota Exceeded)、來源網址無效、或是 LLM 產生不在 Schema 約束內的畸變輸出時,若缺乏即時回饋,系統故障可能數天都未被發現。
本篇將建構雷達系統的巡航哨兵(Sentinel Monitor):
src/adapters/notifier.py)依循六角架構,通知管道屬於系統的主動驅動轉接器(Driven Adapter)。我們封裝 WebhookNotifier,只需透過標準函式庫 requests,就能以 Markdown Rich Embed 格式推送情報統計或報錯日誌:
# src/adapters/notifier.py
import logging
import os
import requests
logger = logging.getLogger(__name__)
class WebhookNotifier:
"""輕量級 Webhook 告警轉接器 (相容 Discord / Slack Webhook 格式)"""
def __init__(self):
self.webhook_url = os.getenv("ALERT_WEBHOOK_URL")
def send_pipeline_report(self, processed_count: int, archived_count: int, error_count: int):
"""推送每日巡航成功總結報告"""
if not self.webhook_url:
logger.info("未設定 ALERT_WEBHOOK_URL,跳過推播。")
return
payload = {
"embeds": [
{
"title": "📡 Intelligence Radar 巡航巡邏完畢",
"color": 3066993 if error_count == 0 else 15158332, # 綠色或紅色
"fields": [
{"name": "通過精煉 (PROCESSED)", "value": f"**{processed_count}** 則", "inline": True},
{"name": "雜訊熔斷 (ARCHIVED)", "value": f"**{archived_count}** 則", "inline": True},
{"name": "異常中斷 (ERROR)", "value": f"**{error_count}** 則", "inline": True},
],
"footer": {"text": "Google ADK x Hexagonal Intelligence Radar 2026"}
}
]
}
self._dispatch(payload)
def send_critical_alert(self, error_message: str, context: str = "Pipeline Failure"):
"""管線發生未預期崩潰時的緊急告警"""
if not self.webhook_url:
return
payload = {
"content": "🚨 **【緊急告警】情報雷達管線發生崩潰異常!**",
"embeds": [
{
"title": f"系統錯誤情境: {context}",
"color": 15158332, # 醒目紅
"description": f"```python\n{error_message[:1500]}\n```",
"footer": {"text": "請盡速檢查 GitHub Actions 執行記錄"}
}
]
}
self._dispatch(payload)
def _dispatch(self, payload: dict):
try:
resp = requests.post(self.webhook_url, json=payload, timeout=10)
resp.raise_for_status()
except Exception as e:
logger.error(f"發送 Webhook 告警失敗: {e}")
run_pipeline.py)我們微調 Day 16 的進入點 run_pipeline.py,加入健康度計數器與全域崩潰攔截器:
# run_pipeline.py
import logging
import os
import sys
from google import genai
from src.adapters.sheets_adapter import SheetsAdapter
from src.adapters.notifier import WebhookNotifier
from src.core.models import ItemStatus
from src.services.pipeline import IntelligencePipeline
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
def main():
notifier = WebhookNotifier()
try:
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
raise ValueError("找不到 GEMINI_API_KEY 環境變數")
client = genai.Client(api_key=api_key)
sheets_adapter = SheetsAdapter()
pipeline = IntelligencePipeline(client)
pending_items = sheets_adapter.fetch_pending_items(auto_lock=True)
logger.info(f"成功鎖定待處理情報數量: {len(pending_items)}")
stats = {ItemStatus.PROCESSED: 0, ItemStatus.ARCHIVED: 0, ItemStatus.ERROR: 0}
for item in pending_items:
try:
processed_item = pipeline.process_item(item)
sheets_adapter.update_item_status(processed_item)
stats[processed_item.status] = stats.get(processed_item.status, 0) + 1
except Exception as item_err:
logger.error(f"條目 {item.entry_id} 處理異常: {item_err}")
item.status = ItemStatus.ERROR
item.metadata["pipeline_error"] = str(item_err)
sheets_adapter.update_item_status(item)
stats[ItemStatus.ERROR] += 1
# 若有處理到項目,發布巡航總結推播
if pending_items:
notifier.send_pipeline_report(
processed_count=stats.get(ItemStatus.PROCESSED, 0),
archived_count=stats.get(ItemStatus.ARCHIVED, 0),
error_count=stats.get(ItemStatus.ERROR, 0),
)
except Exception as fatal_err:
logger.critical(f"管線遭遇致命崩潰: {fatal_err}", exc_info=True)
notifier.send_critical_alert(str(fatal_err), context="Fatal Unhandled Exception")
sys.exit(1)
if __name__ == "__main__":
main()
.github/workflows/patrol.yml)除了 Python 內部捕獲的異常,若虛擬機在「安裝依賴失敗」或「Pytest 測試沒過」時中斷,Python 腳本根本還沒執行,這時該怎麼辦?
我們利用 GitHub Actions 的 if: failure() 條件,在工作流最後掛上一道「守護神告警 Step」:
# 在 .github/workflows/patrol.yml 的 jobs.patrol.steps 最後追加:
- name: Notify CI/CD Pipeline Crash
if: failure() # 只有當前面任何一個步驟失敗亮紅燈時才觸發
env:
ALERT_WEBHOOK_URL: ${{ secrets.ALERT_WEBHOOK_URL }}
run: |
if [ -n "$ALERT_WEBHOOK_URL" ]; then
curl -H "Content-Type: application/json" \
-X POST \
-d '{"content": "🚨 **[GitHub Actions 巡航失敗]** 情報雷達在測試或環境建置階段中斷,請立即前往檢視!"}' \
$ALERT_WEBHOOK_URL
fi
在實作雲端哨兵時,有兩大容易引發「次生災難」的經典陷阱:
for item in pending_items: 迴圈裡面,每發生一次錯誤就發送一則 Webhook。若一次批次有 50 筆錯誤,手機會在 3 秒內被震動癱瘓,甚至觸發 Discord / Slack 的發信頻率限制(Rate Limit 429)。stats**,整個批次跑完後只彙總推送一次單一 Embed 報告**。_dispatch 方法中,發送 HTTP 請求必須加上 try...except 隔離,不能因為網路抖動導致通知發不出去而把原本正常回填到 Sheets 的流程中斷。完成本篇設定後,只要在 GitHub Secrets 填入 ALERT_WEBHOOK_URL:
明天 Day 20,我們將正式進入系列文章的第五階段:戰情室視覺化與情資落地,探討如何運用 Google Sheets 的樞紐分析與前端輕量儀表板,將這些 PROCESSED 的多維情報轉化為一目了然的科研與投資戰情雷達!
src/adapters/notifier.py。run_pipeline.py 與 .github/workflows/patrol.yml。ALERT_WEBHOOK_URL。ruff check . 與 pytest tests/ -v 確保全綠後 git push!在實作哨兵監控與驗證過程中,有兩個關鍵現象值得深入剖析:
在手動除錯時,讀者常發現把資料設為 PENDING 後,工作流一旦跑過,該資料在試算表上會自動變成 ANALYZING。
這證明了 fetch_pending_items(auto_lock=True) 的原子性搶佔鎖(Pessimistic Lock) 正在發揮作用:
ANALYZING,防止多節點重複處理。PENDING 即可重新排隊。當我們刻意將 ALERT_WEBHOOK_URL 設為無效的 404 網址時,在 GitHub Actions 執行細節中可觀察到:
發送 Webhook 告警失敗: 404 Client Error,並被 _dispatch 的 try...except 吞吐吸收。PROCESSED,整體 GitHub Actions 依舊順暢綠燈收尾。stats 計數,整個批次跑完後只發送一次單一彙總卡片,杜絕短時間內轟炸 Discord 伺服器引發 429 限流。