fix(awooop): record grouped alert events
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m6s
CD Pipeline / build-and-deploy (push) Successful in 3m48s
CD Pipeline / post-deploy-checks (push) Successful in 1m25s

This commit is contained in:
Your Name
2026-05-07 01:35:09 +08:00
parent 1a1dea00eb
commit 251554c044
8 changed files with 373 additions and 0 deletions

View File

@@ -128,6 +128,116 @@ async def mirror_inbound_event(
return event_id
def build_grouped_alert_provider_event_id(alert_id: str, fingerprint: str) -> str:
"""建立 grouped child alert 的冪等 provider_event_id。"""
safe_alert_id = str(alert_id).strip() or "unknown"
safe_fingerprint = str(fingerprint).strip()[:32] or "no-fingerprint"
return f"alert-group:{safe_alert_id}:{safe_fingerprint}"
def format_grouped_alert_event_content(
*,
alert_id: str,
alertname: str,
severity: str,
namespace: str,
target_resource: str,
group_key: str,
count: int,
parent_fingerprint: str | None,
fingerprint: str,
) -> str:
"""格式化只落 AwoooP、不發 Telegram 的告警收斂事件摘要。"""
parent = parent_fingerprint or "-"
target = target_resource or "-"
ns = namespace or "default"
return "\n".join(
[
"告警已收斂,不發 Telegram",
f"Alert ID: {alert_id}",
f"Alert: {alertname}",
f"Severity: {severity}",
f"Namespace: {ns}",
f"Target: {target}",
f"Group: {group_key}",
f"Group Count: {count}",
f"Parent Fingerprint: {parent}",
f"Child Fingerprint: {fingerprint}",
]
)
async def record_grouped_alert_event(
*,
project_id: str,
alert_id: str,
alertname: str,
severity: str,
namespace: str,
target_resource: str,
group_key: str,
count: int,
parent_fingerprint: str | None,
fingerprint: str,
) -> UUID | None:
"""
將被 AlertGroupingService 收斂的子告警落到 AwoooP conversation_event。
這條路徑刻意不發 Telegram只保留 operator-facing 脈絡:
- 群組不洗版
- Console 仍能看到同組告警正在持續發生
- DB 失敗 fail-open不影響 Alertmanager webhook ACK
"""
try:
from src.db.base import get_db_context
provider_event_id = build_grouped_alert_provider_event_id(alert_id, fingerprint)
content = format_grouped_alert_event_content(
alert_id=alert_id,
alertname=alertname,
severity=severity,
namespace=namespace,
target_resource=target_resource,
group_key=group_key,
count=count,
parent_fingerprint=parent_fingerprint,
fingerprint=fingerprint,
)
async with get_db_context(project_id) as db:
event_id = await mirror_inbound_event(
db,
project_id=project_id,
channel_type="internal",
provider_event_id=provider_event_id,
platform_subject_id="alertmanager",
channel_user_id="alertmanager",
channel_chat_id=f"alert-group:{group_key}",
content_type="text",
raw_content=content,
provider_ts=datetime.now(timezone.utc),
)
logger.info(
"grouped_alert_event_recorded",
project_id=project_id,
alert_id=alert_id,
event_id=str(event_id),
group_key=group_key,
count=count,
)
return event_id
except Exception as exc:
logger.warning(
"grouped_alert_event_record_failed",
project_id=project_id,
alert_id=alert_id,
group_key=group_key,
error=str(exc),
)
return None
# ─────────────────────────────────────────────────────────────────────────────
# 出站訊息記錄
# ─────────────────────────────────────────────────────────────────────────────

View File

@@ -18,6 +18,7 @@ from sqlalchemy import func, select
from src.db.awooop_models import (
AwoooPContractRevision,
AwoooPConversationEvent,
AwoooPProject,
AwoooPRunState,
)
@@ -31,6 +32,7 @@ logger = structlog.get_logger(__name__)
_MAX_CONTRACTS = 200
_DEFAULT_PER_PAGE = 50
_MAX_PER_PAGE = 200
_MAX_EVENTS = 100
# =============================================================================
# Tenants
@@ -147,6 +149,54 @@ async def list_runs(
return {"runs": runs, "total": total, "page": page, "per_page": per_page}
# =============================================================================
# Channel Events
# =============================================================================
async def list_recent_channel_events(
*,
project_id: str | None,
channel_type: str | None,
provider_prefix: str | None,
limit: int,
) -> dict[str, Any]:
"""列出最近 channel events供 Operator Console 顯示收斂/鏡像脈絡。"""
safe_limit = max(1, min(limit, _MAX_EVENTS))
async with get_db_context("awoooi") as db:
stmt = select(AwoooPConversationEvent).order_by(
AwoooPConversationEvent.received_at.desc()
)
if project_id is not None:
stmt = stmt.where(AwoooPConversationEvent.project_id == project_id)
if channel_type is not None:
stmt = stmt.where(AwoooPConversationEvent.channel_type == channel_type)
if provider_prefix is not None:
stmt = stmt.where(
AwoooPConversationEvent.provider_event_id.like(
f"{provider_prefix}%"
)
)
result = await db.execute(stmt.limit(safe_limit))
rows = list(result.scalars().all())
events = [
{
"event_id": r.event_id,
"project_id": r.project_id,
"channel_type": r.channel_type,
"provider_event_id": r.provider_event_id,
"channel_chat_id": r.channel_chat_id,
"content_preview": r.content_preview,
"is_duplicate": r.is_duplicate,
"received_at": r.received_at,
}
for r in rows
]
return {"events": events, "total": len(events), "limit": safe_limit}
# =============================================================================
# Approvals
# =============================================================================