# RUNBOOK-OLLAMA-FAILOVER.md
# Ollama 容災監控 Runbook
# 2026-04-26 P2.3 by Claude Sonnet 4.6 (tool-expert)
# 2026-06-04 Codex 更新：ADR-110 現行順序為 GCP-A → GCP-B → 111 local → Gemini；111 經 110:11437 Nginx proxy 進入，不直接由 K8s Pod 打 111。
# 對應告警規則: ops/monitoring/ollama_health_rules.yaml
# 對應 Dashboard: ops/monitoring/grafana/dashboards/ollama_failover.json

---

## 現行拓樸與一鍵診斷

現行 AI 推理順序：

| 順位 | Provider | 入口 |
|------|----------|------|
| 1 | GCP-A | `http://192.168.0.110:11435` |
| 2 | GCP-B | `http://192.168.0.110:11436` |
| 3 | 111 local fallback | `http://192.168.0.110:11437` → `192.168.0.111:11434` |
| 4 | Gemini | 只在三層 Ollama 都不可用後作最後備援 |

111 是 Mac / LaunchAgent 管理的 local fallback；不要用舊的 `systemctl` / `nvidia-smi` 手順判斷它。若 `11437` 回 `502`，優先分辨是 110 proxy 本身壞掉，還是 110 到 111 的 LAN/ARP 不通。

只讀診斷：

```bash
bash scripts/ops/ollama111-fallback-proxy-diagnose.sh
```

常見判讀：

| 證據 | 判讀 | 行動 |
|------|------|------|
| `ollama_gcp_a=up` 或 `ollama_gcp_b=up`，但 `ollama_local=down` | 第三層 fallback 單點故障，核心 AI lane 仍可用 | 不要改 provider order；先修 111 主機或 LAN |
| 110 `ip neigh 192.168.0.111 INCOMPLETE` | 110 找不到 111 MAC，通常是 111 關機、睡眠、Wi-Fi/LAN 斷線或 IP 漂移 | 恢復 111 電源/網路，再重跑診斷 |
| 110 direct `192.168.0.111:11434` OK，但 `127.0.0.1:11437` 502 | 110 Nginx proxy 配置或程序異常 | 檢查 `110-ollama-proxy.conf` 與 Nginx reload |
| SSH 到 `ollama-111-gpu` OK，但 `127.0.0.1:11434` 失敗 | 111 本機 Ollama 或 allowlist LaunchAgent 異常 | 檢查 `com.momo.ollama111-allow-proxy` 與 Ollama |

---

## Grafana Dashboard 使用說明

Dashboard 路徑：`Ollama 容災監控`（uid: `ollama-failover-p23`）
匯入方式：Grafana UI → Dashboards → Import → Upload JSON file → 選 `ops/monitoring/grafana/dashboards/ollama_failover.json`

### Panel 1 — Ollama 可用性 (Stat)

**看什麼**：`up{job=~"ollama_gcp_a|ollama_gcp_b|ollama_local|ollama_111"}` × 100，顯示每個 Ollama provider endpoint 的 scrape 存活狀態。

| 顏色 | 意義 |
|------|------|
| 綠色 100% | Prometheus 探測正常，主機在線 |
| 黃色 | 部分 endpoint 離線，系統應進入容災 |
| 紅色 0% | Ollama provider pool 全離線，高風險 |

**注意**：此面板反映 Prometheus scrape 狀態，需要 scrape job 命名對齊 `ollama_gcp_a` / `ollama_gcp_b` / `ollama_local`。
設定檔位於 `ops/monitoring/generated/prometheus-scrape-generated.yaml`。

---

### Panel 2 — 推理延遲 P50 / P99 (Time Series)

**看什麼**：推理延遲分位數。

| 門檻 | 含義 |
|------|------|
| < 10s (P50) | HEALTHY — 正常使用 111 |
| 10–30s (P50) | SLOW — 系統已切至 Gemini |
| > 30s (P99) | DEGRADED — 應觸發 failover |

**⚠️ BACKLOG 警告**：`ollama_inference_duration_seconds_bucket` 尚未在 API 暴露（需在 `_check_inference()` 加 Histogram.observe()）。
面板顯示 "No Data" 是正常的，等 backlog 補完後啟用。

---

### Panel 3 — AI Provider 路由分布 (Pie Chart)

**看什麼**：過去 5 分鐘各 provider 被選中的請求比例。

| 分布 | 意義 |
|------|------|
| ollama / ollama_gcp_a 佔 >90% | 正常，GCP-A 健康 |
| ollama_gcp_b 佔多數 | GCP-A SLOW/DEGRADED/OFFLINE，容災到 GCP-B |
| ollama_local 出現 | GCP-A/B 均不可用，容災到 111 local |
| gemini 佔多數 | Ollama provider pool 全部不可用，使用付費備援 |
| 全部 nemotron/claude | 極端情況，所有主力 provider 失敗 |

---

### Panel 4 — Failover / Recovery 觸發次數 (Bar Chart)

**看什麼**：每小時 failover（橘）和 recovery（綠）的觸發次數。

| 模式 | 意義 |
|------|------|
| 兩條都接近 0 | 正常，111 穩定運行中 |
| 橘色上升後綠色跟上 | Auto recovery 正常：切出後又切回 |
| 橘色上升，綠色不動 | **`OllamaRecoveryStuck` alert**，見下方 runbook |
| 橘色持續高頻（>5/h） | **`OllamaFailoverFrequent` alert**，111 不穩定 |

---

## Alert Runbook

### `OllamaInstanceDown` — Ollama 主機離線

**觸發條件**：`up{job=~"ollama_gcp_a|ollama_gcp_b|ollama_local|ollama_111"} == 0` 持續 2 分鐘。

**影響評估**：
- 系統應依序切至 GCP-B / 111 local / Gemini（查 Panel 3 與 `/api/v1/health` 確認）
- 查 Panel 4 是否有 Failover 計數上升

**排查步驟**：

```bash
# 步驟 1：跑完整只讀診斷，先分辨是 110 proxy、111 LAN、或 111 本機服務
bash scripts/ops/ollama111-fallback-proxy-diagnose.sh

# 步驟 2：若 110 到 111 是 No route to host / ip neigh INCOMPLETE
# 代表 111 主機或網路不可達；先恢復 111 電源/網路，不要重啟 API。

# 步驟 3：若 SSH 到 111 可行，再查本機 Ollama 與 LaunchAgent
ssh ollama-111-gpu 'curl -sS -m 5 http://127.0.0.1:11434/api/tags'
ssh ollama-111-gpu 'launchctl print gui/501/com.momo.ollama111-allow-proxy | head -80'
```

**恢復確認**：
`/api/v1/health` 的 `ollama_local.status=up`，且 `scripts/ops/ollama111-fallback-proxy-diagnose.sh` 中 110 `proxy_11437` 回 HTTP 200。

---

### `OllamaFailoverFrequent` — Failover 頻率過高

**觸發條件**：`rate(ollama_failover_triggered_total[1h]) > 5` 持續 10 分鐘（每小時超過 5 次切換）。

**影響評估**：
- 服務本身仍可用（Gemini 在接手）
- 但 Gemini 配額消耗加速，有觸發 `GeminiQuotaApproaching` 的風險

**排查步驟**：

```bash
# 步驟 1：確認 111 近況（反覆 OFFLINE/HEALTHY 之間跳動？）
bash scripts/ops/ollama111-fallback-proxy-diagnose.sh

# 步驟 2：查 API log 找 failover 原因
kubectl logs -n awoooi-prod deploy/api --since=30m | grep "ollama_failover_triggered"

# 步驟 3：查推理延遲（是否長期在 SLOW 邊界？）
kubectl logs -n awoooi-prod deploy/api --since=30m | grep "ollama_health_checked"

# 步驟 4：如果是 111 本機 Ollama 問題，需依 LaunchAgent runbook 執行維護；不可直接用 Linux systemctl 手順。
```

---

### `OllamaRecoveryStuck` — Auto Recovery 停滯

**觸發條件**：`ollama_health_status{host="111"} == 1 AND ollama_current_primary_is_ollama == 0` 持續 5 分鐘。
（111 已 HEALTHY 但路由仍走 Gemini）

**影響評估**：
- API 功能正常（Gemini 在服務）
- 但 Gemini 配額持續消耗，111 GPU 資源浪費

**排查步驟**：

```bash
# 步驟 1：確認 OllamaAutoRecoveryService 是否在運行
kubectl logs -n awoooi-prod deploy/api --since=10m | grep "ollama_auto_recovery"

# 步驟 2：查 recovery service 狀態
kubectl logs -n awoooi-prod deploy/api --since=10m | grep -E "ollama_auto_recovery_started|ollama_auto_recovery_stopped|ollama_auto_recovery_loop_error"

# 步驟 3：查 current_primary Redis key
kubectl exec -n awoooi-prod deploy/api -- python -c "
import asyncio
from src.core.redis_client import get_redis
async def check():
    r = get_redis()
    val = await r.get('ollama:current_primary')
    print('current_primary:', val)
asyncio.run(check())
"

# 步驟 4：如果 recovery service 掛了，重啟 API pod（會重新啟動 lifespan）
kubectl rollout restart deployment/api -n awoooi-prod
kubectl rollout status deployment/api -n awoooi-prod
```

---

### `GeminiQuotaApproaching` — Gemini 配額 >80%

**觸發條件**：`gemini_daily_call_count / gemini_daily_quota > 0.8` 持續 5 分鐘。

**注意**：`gemini_daily_quota` 來自 `settings.GEMINI_DAILY_QUOTA`（預設 1000）。
`gemini_daily_call_count` 從 Redis key `ollama:gemini_daily_count:{YYYY-MM-DD}` 讀取並刷新 Gauge。

**影響評估**：
- 當日 Gemini 配額即將耗盡
- 耗盡後系統會自動切至 188 CPU-only 備援（qwen2.5:7b-instruct），速度較慢

**行動步驟**：

```bash
# 步驟 1：確認當日 Gemini 使用量
kubectl exec -n awoooi-prod deploy/api -- python -c "
import asyncio, datetime
from src.core.redis_client import get_redis
async def check():
    r = get_redis()
    today = datetime.date.today().isoformat()
    val = await r.get(f'ollama:gemini_daily_count:{today}')
    print(f'gemini_daily_count[{today}]:', val)
asyncio.run(check())
"

# 步驟 2：確認 111 是否能快速恢復（讓流量切回 Ollama）
bash scripts/ops/ollama111-fallback-proxy-diagnose.sh

# 步驟 3：如需增加配額，修改 settings
# k8s/awoooi-prod/04-configmap.yaml.patch-* 找 GEMINI_DAILY_QUOTA
# 改完後 kubectl apply + rollout restart

# 步驟 4：緊急手動重置計數（謹慎使用，只在確認誤計時才用）
# kubectl exec -n awoooi-prod deploy/api -- redis-cli DEL "ollama:gemini_daily_count:$(date +%Y-%m-%d)"
```

---

## Metric 清單

| Metric | 類型 | 狀態 | 說明 |
|--------|------|------|------|
| `up{job="ollama_gcp_a"}` | Gauge | ✅ 現有 | Prometheus scrape 存活 |
| `up{job="ollama_gcp_b"}` | Gauge | ✅ 現有 | Prometheus scrape 存活 |
| `up{job="ollama_local"}` | Gauge | ✅ 現有 | Prometheus scrape 存活 |
| `ollama_failover_triggered_total` | Counter | ✅ P2.3 補入 | failover 切換次數，labels: from_provider, to_provider |
| `ollama_recovery_triggered_total` | Counter | ✅ P2.3 補入 | recovery 切回次數，labels: from_provider |
| `ollama_health_status{host}` | Gauge | ✅ P2.3 補入 | 健康狀態 1=healthy, 0=not_healthy |
| `ollama_current_primary_is_ollama` | Gauge | ✅ P2.3 補入 | 1=primary 是 ollama, 0=failover 中 |
| `ai_router_selected_provider_total` | Counter | ✅ P2.3 補入 | AI router 選擇次數，labels: provider |
| `gemini_daily_call_count` | Gauge | ✅ P2.3 補入 | 今日 Gemini 呼叫次數 |
| `gemini_daily_quota` | Gauge | ✅ P2.3 補入 | Gemini 每日配額 |
| `ollama_inference_duration_seconds` | Histogram | ⏳ BACKLOG | 推理延遲分布，需在 `_check_inference()` 加 observe |
| `post_execution_verification_total` | Counter | ⏳ BACKLOG | Verifier 執行次數，需 auto_repair_service.py 補入 |
| `post_execution_verification_failed_total` | Counter | ⏳ BACKLOG | Verifier 失敗次數，需 auto_repair_service.py 補入 |

## Backlog 補完指引

### `ollama_inference_duration_seconds`

在 `apps/api/src/services/ollama_health_monitor.py` 的 `_check_inference()` 方法結尾，加：

```python
from src.core.metrics import OLLAMA_INFERENCE_DURATION  # 需先在 metrics.py 加 Histogram
OLLAMA_INFERENCE_DURATION.labels(host=host_label).observe(latency_ms / 1000)
```

### `post_execution_verification_*`

在 `apps/api/src/services/auto_repair_service.py` 的 verifier 路徑，加 Counter inc()。
需先確認 verifier 執行點（grep `post_execution` 或 `verif` 找入口）。
