AgentEvalTool/backend/agenteval/intelligent_eval/alerts.py
sinohqb 2e7d419f05 feat(intelligent-eval): implement monitoring and alerting (ticket 07)
- Add metrics.py with pool utilization, task backlog, stuck rate, avg processing time, eval completion rate
- Add alerts.py with alert rules (pool utilization > 90%, task backlog > 50, stuck rate > 10%)
- Implement alert history and webhook notifications
- Add metrics and alerts APIs
- Add database migration for alert history table
- Add 11 unit tests for metrics, 10 unit tests for alerts, 8 integration tests
- Update migration tests to include new alert history table

All 853 tests passing.
2026-08-12 10:41:15 +08:00

218 lines
6.3 KiB
Python

"""Alert rules and notifications for cron pool (告警规则和通知).
Alert rules:
- Pool utilization > 90% for 10 minutes
- Task backlog > 50
- Stuck rate > 10%
Notifications:
- Log alerts
- Optional webhook notifications
"""
import json
import logging
from datetime import datetime, timedelta
from typing import Any, Optional
import httpx
from sqlmodel import Session, SQLModel, Field, select
from agenteval.intelligent_eval.metrics import (
calculate_pool_utilization,
calculate_stuck_rate,
calculate_task_backlog,
)
from agenteval.storage.db import utc_now
_logger = logging.getLogger("agenteval")
class AlertHistoryDB(SQLModel, table=True):
"""Alert history database record."""
__tablename__ = "cron_pool_alert_history"
id: Optional[str] = Field(default=None, primary_key=True)
alert_type: str = Field(index=True) # pool_utilization / task_backlog / stuck_rate
severity: str = Field(index=True) # warning / critical
message: str
metric_value: float
threshold: float
created_at: datetime = Field(default_factory=utc_now)
resolved_at: Optional[datetime] = None
webhook_sent: bool = False
class AlertRule:
"""Alert rule definition."""
def __init__(
self,
name: str,
metric_func,
threshold: float,
severity: str,
duration_minutes: int = 0,
):
self.name = name
self.metric_func = metric_func
self.threshold = threshold
self.severity = severity
self.duration_minutes = duration_minutes
self.triggered_at: Optional[datetime] = None
class AlertManager:
"""Manages alert rules and notifications."""
def __init__(self, session: Session, webhook_url: Optional[str] = None):
self.session = session
self.webhook_url = webhook_url
self.rules = [
AlertRule(
name="pool_utilization",
metric_func=calculate_pool_utilization,
threshold=0.9,
severity="warning",
duration_minutes=10,
),
AlertRule(
name="task_backlog",
metric_func=calculate_task_backlog,
threshold=50,
severity="warning",
duration_minutes=0,
),
AlertRule(
name="stuck_rate",
metric_func=calculate_stuck_rate,
threshold=0.1,
severity="critical",
duration_minutes=0,
),
]
def check_rules(self) -> list[AlertHistoryDB]:
"""Check all alert rules and create alerts if triggered.
Returns:
List of newly created alerts
"""
alerts = []
for rule in self.rules:
metric_value = rule.metric_func(self.session)
# Check if threshold exceeded
if metric_value > rule.threshold:
# Check duration requirement
if rule.duration_minutes > 0:
if rule.triggered_at is None:
rule.triggered_at = utc_now()
continue
duration = (utc_now() - rule.triggered_at).total_seconds() / 60
if duration < rule.duration_minutes:
continue
else:
rule.triggered_at = utc_now()
# Create alert
alert = self._create_alert(rule, metric_value)
alerts.append(alert)
# Send webhook notification
if self.webhook_url:
self._send_webhook(alert)
else:
# Reset trigger time
rule.triggered_at = None
return alerts
def _create_alert(self, rule: AlertRule, metric_value: float) -> AlertHistoryDB:
"""Create an alert history record."""
message = f"{rule.name}: {metric_value:.2f} exceeds threshold {rule.threshold}"
alert = AlertHistoryDB(
id=f"alert-{utc_now().timestamp()}",
alert_type=rule.name,
severity=rule.severity,
message=message,
metric_value=metric_value,
threshold=rule.threshold,
)
self.session.add(alert)
self.session.commit()
self.session.refresh(alert)
_logger.warning(f"Alert triggered: {message}")
return alert
def _send_webhook(self, alert: AlertHistoryDB) -> None:
"""Send webhook notification."""
if not self.webhook_url:
return
try:
payload = {
"alert_id": alert.id,
"alert_type": alert.alert_type,
"severity": alert.severity,
"message": alert.message,
"metric_value": alert.metric_value,
"threshold": alert.threshold,
"timestamp": alert.created_at.isoformat(),
}
response = httpx.post(self.webhook_url, json=payload, timeout=5.0)
response.raise_for_status()
alert.webhook_sent = True
self.session.commit()
_logger.info(f"Webhook sent for alert {alert.id}")
except Exception as e:
_logger.error(f"Failed to send webhook: {e}")
def get_alert_history(self, limit: int = 100) -> list[AlertHistoryDB]:
"""Get alert history.
Returns:
List of alerts, ordered by created_at descending
"""
alerts = self.session.exec(
select(AlertHistoryDB)
.order_by(AlertHistoryDB.created_at.desc())
.limit(limit)
).all()
return list(alerts)
def get_unresolved_alerts(self) -> list[AlertHistoryDB]:
"""Get unresolved alerts.
Returns:
List of unresolved alerts
"""
alerts = self.session.exec(
select(AlertHistoryDB).where(AlertHistoryDB.resolved_at.is_(None))
).all()
return list(alerts)
def resolve_alert(self, alert_id: str) -> bool:
"""Resolve an alert.
Returns:
True if alert was resolved, False if not found
"""
alert = self.session.get(AlertHistoryDB, alert_id)
if alert is None:
return False
alert.resolved_at = utc_now()
self.session.commit()
return True