Files
bakery-ia/services/alert_processor/app/api/alerts.py

223 lines
7.5 KiB
Python
Raw Normal View History

# services/alert_processor/app/api/alerts.py
"""
Alerts API endpoints for dashboard and alert management
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from pydantic import BaseModel, Field
from uuid import UUID
from datetime import datetime
import structlog
from shared.database.base import get_db
from app.repositories.alerts_repository import AlertsRepository
from app.models.alerts import AlertSeverity, AlertStatus
logger = structlog.get_logger()
router = APIRouter()
# ============================================================
# Response Models
# ============================================================
class AlertResponse(BaseModel):
"""Individual alert response"""
id: str
tenant_id: str
item_type: str
alert_type: str
severity: str
status: str
service: str
title: str
message: str
actions: Optional[dict] = None
alert_metadata: Optional[dict] = None
created_at: datetime
updated_at: datetime
resolved_at: Optional[datetime] = None
class Config:
from_attributes = True
class AlertsSummaryResponse(BaseModel):
"""Alerts summary for dashboard"""
total_count: int = Field(..., description="Total number of alerts")
active_count: int = Field(..., description="Number of active (unresolved) alerts")
critical_count: int = Field(..., description="Number of critical/urgent alerts")
high_count: int = Field(..., description="Number of high severity alerts")
medium_count: int = Field(..., description="Number of medium severity alerts")
low_count: int = Field(..., description="Number of low severity alerts")
resolved_count: int = Field(..., description="Number of resolved alerts")
acknowledged_count: int = Field(..., description="Number of acknowledged alerts")
class AlertsListResponse(BaseModel):
"""List of alerts with pagination"""
alerts: List[AlertResponse]
total: int
limit: int
offset: int
# ============================================================
# API Endpoints
# ============================================================
@router.get(
"/api/v1/tenants/{tenant_id}/alerts/summary",
response_model=AlertsSummaryResponse,
summary="Get alerts summary",
description="Get summary of alerts by severity and status for dashboard health indicator"
)
async def get_alerts_summary(
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
) -> AlertsSummaryResponse:
"""
Get alerts summary for dashboard
Returns counts of alerts grouped by severity and status.
Critical count maps to URGENT severity for dashboard compatibility.
"""
try:
repo = AlertsRepository(db)
summary = await repo.get_alerts_summary(tenant_id)
return AlertsSummaryResponse(**summary)
except Exception as e:
logger.error("Error getting alerts summary", error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail=str(e))
@router.get(
"/api/v1/tenants/{tenant_id}/alerts",
response_model=AlertsListResponse,
summary="Get alerts list",
description="Get filtered list of alerts with pagination"
)
async def get_alerts(
tenant_id: UUID = Path(..., description="Tenant ID"),
severity: Optional[str] = Query(None, description="Filter by severity: low, medium, high, urgent"),
status: Optional[str] = Query(None, description="Filter by status: active, resolved, acknowledged, ignored"),
resolved: Optional[bool] = Query(None, description="Filter by resolved status: true=resolved only, false=unresolved only"),
limit: int = Query(100, ge=1, le=1000, description="Maximum number of results"),
offset: int = Query(0, ge=0, description="Pagination offset"),
db: AsyncSession = Depends(get_db)
) -> AlertsListResponse:
"""
Get filtered list of alerts
Supports filtering by:
- severity: low, medium, high, urgent (maps to "critical" in dashboard)
- status: active, resolved, acknowledged, ignored
- resolved: boolean filter for resolved status
- pagination: limit and offset
"""
try:
# Validate severity enum
if severity and severity not in [s.value for s in AlertSeverity]:
raise HTTPException(
status_code=400,
detail=f"Invalid severity. Must be one of: {[s.value for s in AlertSeverity]}"
)
# Validate status enum
if status and status not in [s.value for s in AlertStatus]:
raise HTTPException(
status_code=400,
detail=f"Invalid status. Must be one of: {[s.value for s in AlertStatus]}"
)
repo = AlertsRepository(db)
alerts = await repo.get_alerts(
tenant_id=tenant_id,
severity=severity,
status=status,
resolved=resolved,
limit=limit,
offset=offset
)
# Convert to response models
alert_responses = [
AlertResponse(
id=str(alert.id),
tenant_id=str(alert.tenant_id),
item_type=alert.item_type,
alert_type=alert.alert_type,
severity=alert.severity,
status=alert.status,
service=alert.service,
title=alert.title,
message=alert.message,
actions=alert.actions,
alert_metadata=alert.alert_metadata,
created_at=alert.created_at,
updated_at=alert.updated_at,
resolved_at=alert.resolved_at
)
for alert in alerts
]
return AlertsListResponse(
alerts=alert_responses,
total=len(alert_responses), # In a real implementation, you'd query the total count separately
limit=limit,
offset=offset
)
except HTTPException:
raise
except Exception as e:
logger.error("Error getting alerts", error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail=str(e))
@router.get(
"/api/v1/tenants/{tenant_id}/alerts/{alert_id}",
response_model=AlertResponse,
summary="Get alert by ID",
description="Get a specific alert by its ID"
)
async def get_alert(
tenant_id: UUID = Path(..., description="Tenant ID"),
alert_id: UUID = Path(..., description="Alert ID"),
db: AsyncSession = Depends(get_db)
) -> AlertResponse:
"""Get a specific alert by ID"""
try:
repo = AlertsRepository(db)
alert = await repo.get_alert_by_id(tenant_id, alert_id)
if not alert:
raise HTTPException(status_code=404, detail="Alert not found")
return AlertResponse(
id=str(alert.id),
tenant_id=str(alert.tenant_id),
item_type=alert.item_type,
alert_type=alert.alert_type,
severity=alert.severity,
status=alert.status,
service=alert.service,
title=alert.title,
message=alert.message,
actions=alert.actions,
alert_metadata=alert.alert_metadata,
created_at=alert.created_at,
updated_at=alert.updated_at,
resolved_at=alert.resolved_at
)
except HTTPException:
raise
except Exception as e:
logger.error("Error getting alert", error=str(e), alert_id=str(alert_id))
raise HTTPException(status_code=500, detail=str(e))