Files
LogHive/backend/app/api/alerts.py
T
2026-05-09 14:55:14 +08:00

181 lines
6.4 KiB
Python

"""Alert management API routes."""
from datetime import datetime, timezone
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.alert import AlertRule, AlertHistory, AlertLevel, AlertOperator
from app.schemas.alert import (
AlertRuleCreate,
AlertRuleUpdate,
AlertRuleResponse,
AlertHistoryResponse,
)
router = APIRouter(prefix="/api/alerts", tags=["alerts"])
# ── Rule CRUD ──────────────────────────────────────────────────
@router.post("/rules", response_model=AlertRuleResponse, status_code=201)
async def create_alert_rule(
data: AlertRuleCreate,
db: AsyncSession = Depends(get_db),
):
"""Create a new alert rule."""
rule = AlertRule(
project_id=data.project_id,
name=data.name,
level=data.level,
field=data.field,
operator=data.operator,
threshold=data.threshold,
window_minutes=data.window_minutes,
notify_channels=",".join(data.notify_channels) if data.notify_channels else "",
)
db.add(rule)
await db.flush()
await db.refresh(rule)
return _rule_to_response(rule)
@router.get("/rules", response_model=List[AlertRuleResponse])
async def list_alert_rules(
project_id: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
):
"""List alert rules, optionally filtered by project."""
stmt = select(AlertRule)
if project_id:
stmt = stmt.where(AlertRule.project_id == project_id)
stmt = stmt.order_by(AlertRule.created_at.desc())
result = await db.execute(stmt)
return [_rule_to_response(r) for r in result.scalars().all()]
@router.get("/rules/{rule_id}", response_model=AlertRuleResponse)
async def get_alert_rule(rule_id: str, db: AsyncSession = Depends(get_db)):
"""Get a single alert rule."""
result = await db.execute(select(AlertRule).where(AlertRule.id == rule_id))
rule = result.scalar_one_or_none()
if not rule:
raise HTTPException(status_code=404, detail="Alert rule not found")
return _rule_to_response(rule)
@router.patch("/rules/{rule_id}", response_model=AlertRuleResponse)
async def update_alert_rule(
rule_id: str,
data: AlertRuleUpdate,
db: AsyncSession = Depends(get_db),
):
"""Update an alert rule."""
result = await db.execute(select(AlertRule).where(AlertRule.id == rule_id))
rule = result.scalar_one_or_none()
if not rule:
raise HTTPException(status_code=404, detail="Alert rule not found")
if data.name is not None:
rule.name = data.name
if data.level is not None:
rule.level = data.level
if data.field is not None:
rule.field = data.field
if data.operator is not None:
rule.operator = data.operator
if data.threshold is not None:
rule.threshold = data.threshold
if data.window_minutes is not None:
rule.window_minutes = data.window_minutes
if data.is_enabled is not None:
rule.is_enabled = data.is_enabled
if data.notify_channels is not None:
rule.notify_channels = ",".join(data.notify_channels)
await db.flush()
await db.refresh(rule)
return _rule_to_response(rule)
@router.delete("/rules/{rule_id}", status_code=204)
async def delete_alert_rule(rule_id: str, db: AsyncSession = Depends(get_db)):
"""Delete an alert rule."""
result = await db.execute(select(AlertRule).where(AlertRule.id == rule_id))
rule = result.scalar_one_or_none()
if not rule:
raise HTTPException(status_code=404, detail="Alert rule not found")
await db.delete(rule)
# ── Alert History ──────────────────────────────────────────────
@router.get("/history", response_model=List[AlertHistoryResponse])
async def list_alert_history(
project_id: Optional[str] = Query(None),
rule_id: Optional[str] = Query(None),
limit: int = Query(100, ge=1, le=1000),
db: AsyncSession = Depends(get_db),
):
"""List alert trigger history."""
stmt = select(AlertHistory).order_by(AlertHistory.triggered_at.desc()).limit(limit)
if project_id:
stmt = stmt.where(AlertHistory.project_id == project_id)
if rule_id:
stmt = stmt.where(AlertHistory.rule_id == rule_id)
result = await db.execute(stmt)
return [_history_to_response(h) for h in result.scalars().all()]
@router.post("/history/{history_id}/acknowledge", response_model=AlertHistoryResponse)
async def acknowledge_alert(
history_id: str,
db: AsyncSession = Depends(get_db),
):
"""Mark an alert as acknowledged."""
result = await db.execute(select(AlertHistory).where(AlertHistory.id == history_id))
history = result.scalar_one_or_none()
if not history:
raise HTTPException(status_code=404, detail="Alert history not found")
history.is_acknowledged = True
await db.flush()
await db.refresh(history)
return _history_to_response(history)
# ── Helpers ────────────────────────────────────────────────────
def _rule_to_response(rule: AlertRule) -> AlertRuleResponse:
return AlertRuleResponse(
id=rule.id,
project_id=rule.project_id,
name=rule.name,
level=rule.level.value if hasattr(rule.level, "value") else rule.level,
field=rule.field,
operator=rule.operator.value if hasattr(rule.operator, "value") else rule.operator,
threshold=rule.threshold,
window_minutes=rule.window_minutes,
is_enabled=rule.is_enabled,
notify_channels=rule.notify_channels.split(",") if rule.notify_channels else [],
created_at=rule.created_at,
)
def _history_to_response(history: AlertHistory) -> AlertHistoryResponse:
return AlertHistoryResponse(
id=history.id,
rule_id=history.rule_id,
project_id=history.project_id,
level=history.level.value if hasattr(history.level, "value") else history.level,
message=history.message,
triggered_value=history.triggered_value,
is_acknowledged=history.is_acknowledged,
triggered_at=history.triggered_at,
)