200 lines
6.4 KiB
Python
200 lines
6.4 KiB
Python
"""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,
|
||
)
|