Initial commit: LogHive centralized log management system
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""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,
|
||||
)
|
||||
Reference in New Issue
Block a user