#!/usr/bin/env python3 """ AWOOOI 告警鏈路 E2E Smoke Test ============================== ADR-037 Wave A.6: 告警鏈路端到端驗證腳本 執行方式: python ops/scripts/alert_chain_smoke_test.py 驗證項目: 1. Health Endpoint 可達 2. Alertmanager Webhook 可達 3. Sentry Webhook 可達 4. SignOz Webhook 可達 5. Telegram 連通性 版本: v1.0 建立: 2026-03-29 (台北時區) 建立者: Claude Code (Phase 21 ADR-037) """ import asyncio import os import sys from datetime import datetime import httpx # API 基礎位址 (可透過環境變數覆蓋) API_BASE = os.getenv( "AWOOOI_API_BASE", "http://awoooi-api.awoooi-prod.svc.cluster.local:8000" ) # 本地測試用: # API_BASE = "http://localhost:8000" TIMEOUT = 30 async def test_health_endpoint() -> bool: """測試 Health Endpoint""" print("1. Testing Health Endpoint...") async with httpx.AsyncClient(timeout=TIMEOUT) as client: try: response = await client.get(f"{API_BASE}/api/v1/health") if response.status_code == 200: print(" ✅ Health: OK") return True else: print(f" ❌ Health: HTTP {response.status_code}") return False except Exception as e: print(f" ❌ Health: {e}") return False async def test_alertmanager_webhook() -> bool: """測試 Alertmanager Webhook""" print("2. Testing Alertmanager Webhook...") test_payload = { "version": "4", "status": "firing", "alerts": [{ "status": "firing", "labels": { "alertname": "E2E_SMOKE_TEST", "severity": "info", "service": "smoke-test", "namespace": "test", }, "annotations": { "summary": "E2E Smoke Test - Please Ignore", "description": f"Auto test @ {datetime.now().isoformat()}", }, "startsAt": datetime.now().isoformat() + "Z", }] } async with httpx.AsyncClient(timeout=TIMEOUT) as client: try: response = await client.post( f"{API_BASE}/api/v1/webhooks/alertmanager", json=test_payload, ) if response.status_code == 200: print(" ✅ Alertmanager Webhook: OK") return True else: print(f" ❌ Alertmanager Webhook: HTTP {response.status_code}") print(f" Response: {response.text[:200]}") return False except Exception as e: print(f" ❌ Alertmanager Webhook: {e}") return False async def test_sentry_webhook() -> bool: """測試 Sentry Webhook""" print("3. Testing Sentry Webhook...") # 使用唯一 ID 避免去重 test_id = f"smoke-test-{datetime.now().strftime('%Y%m%d%H%M%S%f')}" test_payload = { "action": "triggered", "data": { "issue": { "id": test_id, "title": "E2E Smoke Test Error", "level": "info", # 使用 info 避免觸發實際告警 "culprit": "smoke_test.py:test", "project": {"slug": "awoooi-api"}, "firstSeen": datetime.now().isoformat(), "count": 1, }, "event": { "message": "E2E Smoke Test - Please Ignore", "platform": "python", }, }, } async with httpx.AsyncClient(timeout=TIMEOUT) as client: try: response = await client.post( f"{API_BASE}/api/v1/webhooks/sentry/error", json=test_payload, ) if response.status_code == 200: result = response.json() status = result.get("status") if status in ["accepted", "deduplicated", "ignored"]: print(f" ✅ Sentry Webhook: OK (status={status})") return True print(f" ❌ Sentry Webhook: HTTP {response.status_code}") return False except Exception as e: print(f" ❌ Sentry Webhook: {e}") return False async def test_signoz_webhook() -> bool: """測試 SignOz Webhook""" print("4. Testing SignOz Webhook...") test_payload = { "alertname": "E2E_SMOKE_TEST", "status": "firing", "labels": { "alertname": "E2E_SMOKE_TEST", "severity": "info", "service_name": "smoke-test", "source": "signoz", }, "annotations": { "summary": "E2E Smoke Test - Please Ignore", "description": f"Auto test @ {datetime.now().isoformat()}", }, "startsAt": datetime.now().isoformat() + "Z", } async with httpx.AsyncClient(timeout=TIMEOUT) as client: try: response = await client.post( f"{API_BASE}/api/v1/webhooks/signoz/alert", json=test_payload, ) if response.status_code == 200: result = response.json() if result.get("status") == "ok": print(" ✅ SignOz Webhook: OK") return True print(f" ❌ SignOz Webhook: HTTP {response.status_code}") return False except Exception as e: print(f" ❌ SignOz Webhook: {e}") return False async def test_signoz_health() -> bool: """測試 SignOz Webhook Health""" print("5. Testing SignOz Webhook Health...") async with httpx.AsyncClient(timeout=TIMEOUT) as client: try: response = await client.get( f"{API_BASE}/api/v1/webhooks/signoz/health" ) if response.status_code == 200: print(" ✅ SignOz Health: OK") return True else: print(f" ❌ SignOz Health: HTTP {response.status_code}") return False except Exception as e: print(f" ❌ SignOz Health: {e}") return False async def test_telegram_connectivity() -> bool: """測試 Telegram 連通性""" print("6. Testing Telegram Connectivity...") async with httpx.AsyncClient(timeout=TIMEOUT) as client: try: response = await client.get(f"{API_BASE}/api/v1/telegram/status") if response.status_code == 200: data = response.json() if data.get("connected"): print(" ✅ Telegram: Connected") return True else: print(" ⚠️ Telegram: Not Connected (endpoint reachable)") return True # 端點可達即可 elif response.status_code == 404: print(" ⚠️ Telegram: Endpoint not found (skipped)") return True # 不影響整體測試 else: print(f" ❌ Telegram: HTTP {response.status_code}") return False except Exception as e: print(f" ⚠️ Telegram: {e} (skipped)") return True # 不影響整體測試 async def main(): """執行所有 Smoke Test""" now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") print("=" * 60) print(" AWOOOI Alert Chain E2E Smoke Test") print(f" Time: {now}") print(f" Target: {API_BASE}") print("=" * 60) print() # 依序執行測試 (非並行,方便除錯) results = [] results.append(await test_health_endpoint()) results.append(await test_alertmanager_webhook()) results.append(await test_sentry_webhook()) results.append(await test_signoz_webhook()) results.append(await test_signoz_health()) results.append(await test_telegram_connectivity()) print() print("=" * 60) passed = sum(results) total = len(results) if passed == total: print(f" ✅ ALL PASSED ({passed}/{total})") print("=" * 60) sys.exit(0) else: failed = total - passed print(f" ❌ FAILED ({failed}/{total} tests failed)") print("=" * 60) sys.exit(1) if __name__ == "__main__": asyncio.run(main())