Files
awoooi/apps/api/src/services/converged_alert_recurrence_notifier.py
Your Name ee2cc2bfc3
Some checks failed
CD Pipeline / tests (push) Failing after 1m23s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Code Review / ai-code-review (push) Successful in 15s
fix(alerts): 收斂 Telegram 告警到 SRE 戰情室
2026-06-12 11:06:16 +08:00

200 lines
6.4 KiB
Python
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.
"""Throttled Telegram notices for converged alert fingerprints."""
import hashlib
import html
from src.core.logging import get_logger
from src.core.redis_client import get_redis
from src.services.telegram_gateway import get_telegram_gateway
logger = get_logger("awoooi.converged_alert_recurrence")
CONVERGED_ALERT_RECURRENCE_REDIS_PREFIX = "awoooi:tg:converged_alert_recurrence:"
CONVERGED_ALERT_RECURRENCE_TTL_SECONDS = 30 * 60
CONVERGED_ALERT_RECURRENCE_FALLBACK_MILESTONES = frozenset({2, 3, 5, 10, 20, 50, 100})
def shorten_alert_text(value: str | None, *, limit: int = 80) -> str:
"""Keep Telegram recurrence notices readable and HTML-safe."""
text = " ".join(str(value or "-").split())
if len(text) <= limit:
return html.escape(text)
return html.escape(f"{text[: limit - 3]}...")
def is_converged_alert_recurrence_milestone(hit_count: int) -> bool:
"""Fallback dedupe when Redis is unavailable."""
if hit_count <= 1:
return False
return hit_count in CONVERGED_ALERT_RECURRENCE_FALLBACK_MILESTONES or hit_count % 100 == 0
def converged_alert_recurrence_key(fingerprint: str) -> str:
digest = hashlib.sha256(str(fingerprint or "missing").encode()).hexdigest()[:32]
return f"{CONVERGED_ALERT_RECURRENCE_REDIS_PREFIX}{digest}"
async def should_notify_converged_alert_recurrence(
*,
fingerprint: str,
hit_count: int,
) -> bool:
"""Throttle converged alert notices without making active incidents disappear."""
if hit_count <= 1:
return False
key = converged_alert_recurrence_key(fingerprint)
try:
redis = get_redis()
acquired = await redis.set(
key,
str(hit_count),
ex=CONVERGED_ALERT_RECURRENCE_TTL_SECONDS,
nx=True,
)
return bool(acquired)
except Exception as exc:
logger.warning(
"converged_alert_recurrence_dedup_unavailable",
fingerprint_hash=key.rsplit(":", 1)[-1],
hit_count=hit_count,
fallback="milestone",
error=str(exc),
)
return is_converged_alert_recurrence_milestone(hit_count)
def format_converged_alert_recurrence_message(
*,
source: str,
alertname: str,
severity: str,
namespace: str,
target_resource: str,
hit_count: int,
incident_id: str | None,
approval_id: str | None,
alert_category: str = "",
notification_type: str = "",
recurrence_stage: str = "converged",
) -> str:
"""Build a concise recurrence notice for an already-open alert fingerprint."""
is_llm_inflight = recurrence_stage == "llm_inflight"
title = "告警仍在發生AI 分析中" if is_llm_inflight else "告警仍在發生"
summary = (
"同一指紋已有背景 AI 分析鎖,重複告警已節流提醒,不再完全靜音。"
if is_llm_inflight
else "同一指紋已收斂,系統保留去重,但不再完全靜音。"
)
next_step = (
"下一步:等待背景分析完成;若持續超過分析鎖 TTL請查看 AwoooP 事件時間線與 API logs。"
if is_llm_inflight
else "下一步:請查看 AwoooP 事件時間線。"
)
return "\n".join(
[
f"<b>{title}</b>",
summary,
"",
f"來源:<code>{shorten_alert_text(source, limit=40)}</code>",
f"告警:<code>{shorten_alert_text(alertname, limit=80)}</code>",
f"嚴重度:<code>{shorten_alert_text(severity, limit=24)}</code>",
f"目標:<code>{shorten_alert_text(target_resource, limit=80)}</code>",
f"命名空間:<code>{shorten_alert_text(namespace, limit=48)}</code>",
f"累計次數:<b>{hit_count}</b>",
f"事件:<code>{shorten_alert_text(incident_id, limit=48)}</code>",
f"簽核:<code>{shorten_alert_text(approval_id, limit=48)}</code>",
f"分類:<code>{shorten_alert_text(alert_category or '-', limit=48)}</code>",
f"通知型別:<code>{shorten_alert_text(notification_type or '-', limit=48)}</code>",
"",
f"{next_step} 這不是新的自動修復授權。",
]
)
async def notify_converged_alert_recurrence(
*,
source: str,
fingerprint: str,
alertname: str,
severity: str,
namespace: str,
target_resource: str,
hit_count: int,
incident_id: str | None,
approval_id: str | None,
alert_category: str = "",
notification_type: str = "",
recurrence_stage: str = "converged",
) -> None:
"""Send a throttled recurrence notice for an already-open alert fingerprint."""
if not await should_notify_converged_alert_recurrence(
fingerprint=fingerprint,
hit_count=hit_count,
):
logger.info(
"converged_alert_recurrence_throttled",
source=source,
hit_count=hit_count,
approval_id=approval_id,
recurrence_stage=recurrence_stage,
)
return
text = format_converged_alert_recurrence_message(
source=source,
alertname=alertname,
severity=severity,
namespace=namespace,
target_resource=target_resource,
hit_count=hit_count,
incident_id=incident_id,
approval_id=approval_id,
alert_category=alert_category,
notification_type=notification_type,
recurrence_stage=recurrence_stage,
)
gateway = get_telegram_gateway()
sent_count = 0
failures: list[str] = []
try:
await gateway.send_alert_notification(text)
sent_count += 1
except Exception as exc:
failures.append(f"primary:{type(exc).__name__}")
logger.warning(
"converged_alert_recurrence_primary_failed",
source=source,
approval_id=approval_id,
recurrence_stage=recurrence_stage,
error=str(exc),
)
if sent_count:
logger.info(
"converged_alert_recurrence_sent",
source=source,
hit_count=hit_count,
approval_id=approval_id,
recurrence_stage=recurrence_stage,
mirrored_to_private=False,
sent_count=sent_count,
)
else:
logger.error(
"converged_alert_recurrence_failed",
source=source,
hit_count=hit_count,
approval_id=approval_id,
recurrence_stage=recurrence_stage,
failures=failures,
)