- 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.
218 lines
6.3 KiB
Python
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
|