fix(aiops): suppress repeated llm alert loops
Some checks failed
CD Pipeline / tests (push) Successful in 1m37s
Code Review / ai-code-review (push) Successful in 28s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-05-01 13:02:07 +08:00
parent 3691402561
commit 9db87f177e
9 changed files with 244 additions and 24 deletions

View File

@@ -0,0 +1,45 @@
"""Alertmanager LLM storm guards.
Service-layer Redis helpers used by webhook routers to avoid spawning duplicate
LLM analysis tasks for the same Alertmanager fingerprint.
"""
from src.core.logging import get_logger
from src.core.redis_client import get_redis
logger = get_logger("awoooi.alertmanager_llm_guard")
ALERTMANAGER_LLM_INFLIGHT_LOCK_TTL_SECONDS = 600
def alertmanager_llm_inflight_key(fingerprint: str) -> str:
"""Return the Redis lock key for one Alertmanager fingerprint entering AI analysis."""
return f"alertmanager:llm_inflight:{fingerprint}"
async def try_acquire_alertmanager_llm_lock(
fingerprint: str,
alert_id: str,
*,
ttl_seconds: int = ALERTMANAGER_LLM_INFLIGHT_LOCK_TTL_SECONDS,
) -> bool:
"""Prevent same-second duplicate Alertmanager deliveries from spawning LLM calls."""
try:
redis = get_redis()
acquired = await redis.set(
alertmanager_llm_inflight_key(fingerprint),
alert_id,
ex=ttl_seconds,
nx=True,
)
return bool(acquired)
except Exception as exc:
logger.warning(
"alertmanager_llm_inflight_lock_failed_fail_open",
fingerprint=fingerprint,
alert_id=alert_id,
error=str(exc),
)
return True

View File

@@ -124,6 +124,21 @@ def _backfill_kubectl_command(proposal: dict, tools: list) -> None:
# OpenClaw Service
# =============================================================================
def _build_alert_cache_context_hash(alert_context: dict | None) -> str:
"""Build a stable LLM cache scope for repeat alerts without dynamic annotations."""
if not alert_context:
return ""
alertname = alert_context.get("alertname") or alert_context.get("alert_type", "")
category = alert_context.get("alert_category", "")
namespace = alert_context.get("namespace", "")
target = alert_context.get("target_resource", "")
severity = alert_context.get("severity", "")
fingerprint = alert_context.get("fingerprint", "")
return f"{alertname}:{category}:{namespace}:{target}:{severity}:{fingerprint}"
class OpenClawService:
"""
OpenClaw AI 決策服務 - True LLM + SignOz Integration
@@ -727,9 +742,19 @@ class OpenClawService:
"""
生成 LLM 快取鍵
使用 prompt 內容的 SHA256 作為快取鍵,確保相同問題不重複呼叫 LLM
有告警上下文時,使用 prompt family + 穩定告警維度,避免 annotations /
SignOz 即時數值讓同一告警每 20 秒打穿快取;沒有上下文時仍用完整 prompt。
"""
content = f"{prompt}:{context_hash}"
if context_hash:
prompt_family_source = (
"openclaw_alert_analysis"
if "## Alert Data:" in prompt
else prompt[:512]
)
prompt_family = hashlib.sha256(prompt_family_source.encode()).hexdigest()[:8]
content = f"{prompt_family}:{context_hash}"
else:
content = prompt
hash_digest = hashlib.sha256(content.encode()).hexdigest()[:16]
return f"llm_cache:{hash_digest}"
@@ -760,12 +785,7 @@ class OpenClawService:
# 2026-04-16 ogt + Claude Sonnet 4.6: 修復 — alertname 才是主要識別符
# 舊版用 alert_type:target_resource → 不同告警 (e.g. PostgreSQLDiskGrowth vs PodCrashLoop)
# 在 alert_type="custom" 時共用同一快取鍵 → 全部回傳相同 LLM 結果
context_hash = ""
if alert_context:
# alertname 優先;無 alertname 時 fallback 到 alert_type
_alertname = alert_context.get("alertname") or alert_context.get("alert_type", "")
_target = alert_context.get("target_resource", "")
context_hash = f"{_alertname}:{_target}"
context_hash = _build_alert_cache_context_hash(alert_context)
cache_key = self._generate_cache_key(prompt, context_hash)