223 lines
7.5 KiB
Python
223 lines
7.5 KiB
Python
|
|
# 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))
|