Files
awoooi/scripts/ops/deploy-alerts.sh

231 lines
10 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# scripts/ops/deploy-alerts.sh
# 部署統一告警規則到 110 Prometheus
# 2026-04-05 Claude Code: Sprint 1 自動化部署
# 用法: bash scripts/ops/deploy-alerts.sh [--dry-run]
set -euo pipefail
ALERT_RULES_FILE="ops/monitoring/alerts-unified.yml"
SLO_RULES_FILE="ops/monitoring/slo-rules.yml"
DRIFT_GUARD_SCRIPT="scripts/ops/prometheus-rule-drift-guard.sh"
TARGET_HOST="192.168.0.110"
TARGET_ALERTS_PATH="/home/wooo/monitoring/alerts.yml"
TARGET_ALERTS_CANONICAL_PATH="/home/wooo/monitoring/alerts-unified.canonical.yml"
TARGET_SLO_PATH="/home/wooo/monitoring/slo-rules.yml"
TARGET_DRIFT_GUARD_PATH="/home/wooo/scripts/prometheus-rule-drift-guard.sh"
PROMETHEUS_URL="http://${TARGET_HOST}:9090"
PROMETHEUS_CONTAINER="${PROMETHEUS_CONTAINER:-prometheus}"
PROMETHEUS_RUNTIME_RULES="${PROMETHEUS_RUNTIME_RULES:-/etc/prometheus/alerts.yml}"
DRY_RUN="${1:-}"
DEPLOY_STAMP="$(date +%Y%m%d%H%M%S)"
REMOTE_CANDIDATE_PATH="/tmp/awoooi-alerts-unified.${DEPLOY_STAMP}.yml"
CONTAINER_CANDIDATE_PATH="/tmp/awoooi-alerts-unified.${DEPLOY_STAMP}.yml"
TARGET_ALERTS_BACKUP_PATH="${TARGET_ALERTS_PATH}.bak.${DEPLOY_STAMP}"
TARGET_ALERTS_CANONICAL_BACKUP_PATH="${TARGET_ALERTS_CANONICAL_PATH}.bak.${DEPLOY_STAMP}"
TARGET_SLO_BACKUP_PATH="${TARGET_SLO_PATH}.bak.${DEPLOY_STAMP}"
TARGET_DRIFT_GUARD_BACKUP_PATH="${TARGET_DRIFT_GUARD_PATH}.bak.${DEPLOY_STAMP}"
APPLY_STARTED=0
DEPLOY_VERIFIED=0
log() { echo "[$(date '+%H:%M:%S')] $*"; }
cleanup_candidate() {
ssh wooo@${TARGET_HOST} "rm -f '${REMOTE_CANDIDATE_PATH}'; docker exec -u 0 '${PROMETHEUS_CONTAINER}' rm -f '${CONTAINER_CANDIDATE_PATH}' >/dev/null 2>&1 || true" >/dev/null 2>&1 || true
}
rollback_deploy() {
log "部署驗證失敗,開始還原 ${DEPLOY_STAMP} 前規則"
ssh wooo@${TARGET_HOST} "set -euo pipefail
cp '${TARGET_ALERTS_BACKUP_PATH}' '${TARGET_ALERTS_PATH}'
cp '${TARGET_ALERTS_CANONICAL_BACKUP_PATH}' '${TARGET_ALERTS_CANONICAL_PATH}'
cp '${TARGET_SLO_BACKUP_PATH}' '${TARGET_SLO_PATH}'
cp '${TARGET_DRIFT_GUARD_BACKUP_PATH}' '${TARGET_DRIFT_GUARD_PATH}'
chmod 0755 '${TARGET_DRIFT_GUARD_PATH}'
curl -fsS -X POST '${PROMETHEUS_URL}/-/reload' >/dev/null
sleep 2
curl -fsS '${PROMETHEUS_URL}/-/ready' >/dev/null"
log "已還原規則並完成 Prometheus readiness readback"
}
on_exit() {
local rc=$?
cleanup_candidate
if [ "$rc" -ne 0 ] && [ "$APPLY_STARTED" -eq 1 ] && [ "$DEPLOY_VERIFIED" -eq 0 ]; then
rollback_deploy || log "ERROR: 自動 rollback 失敗,保留精確 backup paths 供獨立恢復"
fi
exit "$rc"
}
remote_promtool_preflight() {
scp "$ALERT_RULES_FILE" "wooo@${TARGET_HOST}:${REMOTE_CANDIDATE_PATH}"
ssh wooo@${TARGET_HOST} "set -euo pipefail
docker cp '${REMOTE_CANDIDATE_PATH}' '${PROMETHEUS_CONTAINER}:${CONTAINER_CANDIDATE_PATH}'
docker exec '${PROMETHEUS_CONTAINER}' promtool check rules '${CONTAINER_CANDIDATE_PATH}'"
cleanup_candidate
log "✅ remote promtool preflight 通過"
}
trap on_exit EXIT
file_sha() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}
remote_file_sha() {
ssh wooo@${TARGET_HOST} "\
if command -v sha256sum >/dev/null 2>&1; then \
sha256sum '$1' | awk '{print \$1}'; \
else \
shasum -a 256 '$1' | awk '{print \$1}'; \
fi"
}
# 確認檔案存在
for file in "$ALERT_RULES_FILE" "$SLO_RULES_FILE" "$DRIFT_GUARD_SCRIPT"; do
if [ ! -f "$file" ]; then
echo "ERROR: $file not found"
exit 1
fi
done
# 驗證 YAML 語法
for file in "$ALERT_RULES_FILE" "$SLO_RULES_FILE"; do
if python3 -c "import yaml; yaml.safe_load(open('$file'))" 2>/dev/null; then
:
elif ruby -e "require 'yaml'; YAML.load_file('$file')" 2>/dev/null; then
:
else
echo "ERROR: YAML syntax error or no YAML parser available: $file"
exit 1
fi
done
log "✅ YAML 語法驗證通過"
# Dry run 模式
if [ "$DRY_RUN" = "--dry-run" ]; then
remote_promtool_preflight
log "DRY RUN: would deploy $ALERT_RULES_FILE to ${TARGET_HOST}:${TARGET_ALERTS_PATH}"
log "DRY RUN: would deploy $ALERT_RULES_FILE to ${TARGET_HOST}:${TARGET_ALERTS_CANONICAL_PATH}"
log "DRY RUN: would deploy $SLO_RULES_FILE to ${TARGET_HOST}:${TARGET_SLO_PATH}"
log "DRY RUN: would deploy $DRIFT_GUARD_SCRIPT to ${TARGET_HOST}:${TARGET_DRIFT_GUARD_PATH}"
ALERT_COUNT=$(grep -c "alert:" "$ALERT_RULES_FILE")
SLO_RECORD_COUNT=$(grep -c "record:" "$SLO_RULES_FILE")
SLO_ALERT_COUNT=$(grep -c "alert:" "$SLO_RULES_FILE")
log "告警規則數量: $ALERT_COUNTSLO recording: $SLO_RECORD_COUNTSLO alerts: $SLO_ALERT_COUNT"
exit 0
fi
# 使用 production Prometheus 的 promtool 驗證候選規則,通過後才能寫 active path。
remote_promtool_preflight
# 備份現有規則;任一 source 缺失時 fail closed不開始 apply。
ssh wooo@${TARGET_HOST} "set -euo pipefail
cp '${TARGET_ALERTS_PATH}' '${TARGET_ALERTS_BACKUP_PATH}'
cp '${TARGET_ALERTS_CANONICAL_PATH}' '${TARGET_ALERTS_CANONICAL_BACKUP_PATH}'
cp '${TARGET_SLO_PATH}' '${TARGET_SLO_BACKUP_PATH}'
cp '${TARGET_DRIFT_GUARD_PATH}' '${TARGET_DRIFT_GUARD_BACKUP_PATH}'"
log "✅ 現有規則已備份"
APPLY_STARTED=1
# 部署新規則
scp "$ALERT_RULES_FILE" wooo@${TARGET_HOST}:${TARGET_ALERTS_PATH}
scp "$ALERT_RULES_FILE" wooo@${TARGET_HOST}:${TARGET_ALERTS_CANONICAL_PATH}
scp "$SLO_RULES_FILE" wooo@${TARGET_HOST}:${TARGET_SLO_PATH}
scp "$DRIFT_GUARD_SCRIPT" wooo@${TARGET_HOST}:${TARGET_DRIFT_GUARD_PATH}
ssh wooo@${TARGET_HOST} "chmod 0755 ${TARGET_DRIFT_GUARD_PATH}"
log "✅ 規則已複製到 ${TARGET_HOST}"
LOCAL_ALERTS_SHA="$(file_sha "$ALERT_RULES_FILE")"
REMOTE_ALERTS_SHA="$(remote_file_sha "$TARGET_ALERTS_PATH")"
REMOTE_ALERTS_CANONICAL_SHA="$(remote_file_sha "$TARGET_ALERTS_CANONICAL_PATH")"
LOCAL_SLO_SHA="$(file_sha "$SLO_RULES_FILE")"
REMOTE_SLO_SHA="$(remote_file_sha "$TARGET_SLO_PATH")"
LOCAL_DRIFT_GUARD_SHA="$(file_sha "$DRIFT_GUARD_SCRIPT")"
REMOTE_DRIFT_GUARD_SHA="$(remote_file_sha "$TARGET_DRIFT_GUARD_PATH")"
if [ "$LOCAL_ALERTS_SHA" != "$REMOTE_ALERTS_SHA" ]; then
echo "ERROR: 遠端 alerts.yml hash 不一致 local=${LOCAL_ALERTS_SHA} remote=${REMOTE_ALERTS_SHA}"
exit 1
fi
if [ "$LOCAL_ALERTS_SHA" != "$REMOTE_ALERTS_CANONICAL_SHA" ]; then
echo "ERROR: 遠端 alerts-unified.canonical.yml hash 不一致 local=${LOCAL_ALERTS_SHA} remote=${REMOTE_ALERTS_CANONICAL_SHA}"
exit 1
fi
if [ "$LOCAL_SLO_SHA" != "$REMOTE_SLO_SHA" ]; then
echo "ERROR: 遠端 slo-rules.yml hash 不一致 local=${LOCAL_SLO_SHA} remote=${REMOTE_SLO_SHA}"
exit 1
fi
if [ "$LOCAL_DRIFT_GUARD_SHA" != "$REMOTE_DRIFT_GUARD_SHA" ]; then
echo "ERROR: 遠端 prometheus-rule-drift-guard.sh hash 不一致 local=${LOCAL_DRIFT_GUARD_SHA} remote=${REMOTE_DRIFT_GUARD_SHA}"
exit 1
fi
log "✅ 遠端規則 hash 驗證通過"
# Reload Prometheus
ssh wooo@${TARGET_HOST} "curl -fsS -X POST ${PROMETHEUS_URL}/-/reload >/dev/null"
sleep 3
# 驗證規則數量
RULE_COUNT=$(ssh wooo@${TARGET_HOST} "curl -s ${PROMETHEUS_URL}/api/v1/rules | python3 -c \"import sys,json; r=json.load(sys.stdin); print(sum(len(g['rules']) for g in r['data']['groups']))\"")
log "Prometheus 已載入 ${RULE_COUNT} 條規則"
if [ "$RULE_COUNT" -lt 25 ]; then
echo "ERROR: 規則數量異常 ($RULE_COUNT < 25),請檢查"
exit 1
fi
NO_ALERTS_QUERY=$(ssh wooo@${TARGET_HOST} "curl -s ${PROMETHEUS_URL}/api/v1/rules" | python3 -c "import sys,json; r=json.load(sys.stdin); print(next((x.get('query','') for g in r['data']['groups'] for x in g['rules'] if x.get('name') == 'NoAlertsReceived2Hours'), ''))")
if [[ "$NO_ALERTS_QUERY" != *'source="alertmanager"'* ]]; then
echo "ERROR: NoAlertsReceived2Hours query 未限制 alertmanager 主鏈路: ${NO_ALERTS_QUERY}"
exit 1
fi
log "✅ NoAlertsReceived2Hours query 已限制 alertmanager 主鏈路"
SOURCE_PROVIDER_STALE_QUERY=$(ssh wooo@${TARGET_HOST} "curl -s ${PROMETHEUS_URL}/api/v1/rules" | python3 -c "import sys,json; r=json.load(sys.stdin); print(next((x.get('query','') for g in r['data']['groups'] for x in g['rules'] if x.get('name') == 'SourceProviderIngestionStale'), ''))")
if [[ "$SOURCE_PROVIDER_STALE_QUERY" != *'source=~"sentry|signoz"'* ]]; then
echo "ERROR: SourceProviderIngestionStale query 未限制 Sentry/SignOz provider freshness: ${SOURCE_PROVIDER_STALE_QUERY}"
exit 1
fi
log "✅ SourceProviderIngestionStale query 已限制 Sentry/SignOz provider freshness"
# 驗證關鍵規則存在
KEY_RULES=("SentryDown" "HarborDown" "GiteaDown" "OpenClawDown" "AlertmanagerDown" "AlertChainUnhealthy" "SourceProviderIngestionStale")
for rule in "${KEY_RULES[@]}"; do
EXISTS=$(ssh wooo@${TARGET_HOST} "curl -s ${PROMETHEUS_URL}/api/v1/rules | python3 -c \"import sys,json; r=json.load(sys.stdin); names=[x['name'] for g in r['data']['groups'] for x in g['rules']]; print('OK' if '$rule' in names else 'MISSING')\"")
if [ "$EXISTS" = "OK" ]; then
log "$rule"
else
echo "$rule 未找到"
exit 1
fi
done
KEY_SLO_RULES=("sli:autonomy_rate:5m" "sli:decision_accuracy:5m" "sli:confidence_calibration:1h" "sli:km_growth_rate:24h" "SLO_KMGrowthRate_Critical")
for rule in "${KEY_SLO_RULES[@]}"; do
EXISTS=$(ssh wooo@${TARGET_HOST} "curl -s ${PROMETHEUS_URL}/api/v1/rules | python3 -c \"import sys,json; r=json.load(sys.stdin); names=[x['name'] for g in r['data']['groups'] for x in g['rules']]; print('OK' if '$rule' in names else 'MISSING')\"")
if [ "$EXISTS" = "OK" ]; then
log "$rule"
else
echo "$rule 未找到"
exit 1
fi
done
REMOTE_RUNTIME_SHA=$(ssh wooo@${TARGET_HOST} "docker exec '${PROMETHEUS_CONTAINER}' sha256sum '${PROMETHEUS_RUNTIME_RULES}' | awk '{print \$1}'")
if [ "$LOCAL_ALERTS_SHA" != "$REMOTE_RUNTIME_SHA" ]; then
echo "ERROR: Prometheus runtime rules hash 不一致 local=${LOCAL_ALERTS_SHA} runtime=${REMOTE_RUNTIME_SHA}"
exit 1
fi
log "✅ Prometheus runtime rules hash 與 canonical source 一致"
bash scripts/reboot-recovery/verify-cold-start-monitor-deploy.sh
log "✅ Cold-start producer / active rules / Agent99 consumer scope verifier 通過"
DEPLOY_VERIFIED=1
log "Rollback receipts: ${TARGET_ALERTS_BACKUP_PATH}, ${TARGET_ALERTS_CANONICAL_BACKUP_PATH}"
log "🎉 部署完成!所有關鍵規則已生效"