# 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))