feat: Add alert endpoints to alert_processor service for dashboard

Implemented missing alert endpoints that the dashboard requires for
health status and action queue functionality.

Alert Processor Service Changes:
- Created alerts_repository.py:
  * get_alerts() - Filter alerts by severity/status/resolved with pagination
  * get_alerts_summary() - Count alerts by severity and status
  * get_alert_by_id() - Get specific alert

- Created alerts.py API endpoints:
  * GET /api/v1/tenants/{tenant_id}/alerts/summary - Alert counts
  * GET /api/v1/tenants/{tenant_id}/alerts - Filtered alert list
  * GET /api/v1/tenants/{tenant_id}/alerts/{alert_id} - Single alert

- Severity mapping: "critical" (dashboard) maps to "urgent" (alert_processor)
- Status enum: active, resolved, acknowledged, ignored
- Severity enum: low, medium, high, urgent

API Server Changes:
- Registered alerts_router in api_server.py
- Exported alerts_router in __init__.py

Procurement Client Changes:
- Updated get_critical_alerts() to use /alerts path
- Updated get_alerts_summary() to use /alerts/summary path
- Added severity mapping (critical → urgent)
- Added documentation about gateway routing

This fixes the 404 errors for alert endpoints in the dashboard.
This commit is contained in:
Claude
2025-11-07 22:16:16 +00:00
parent 6cd4ef0f56
commit 41d292913a
5 changed files with 413 additions and 6 deletions

View File

@@ -0,0 +1,178 @@
# services/alert_processor/app/repositories/alerts_repository.py
"""
Alerts Repository - Database access layer for alerts
"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_, or_
from typing import List, Dict, Any, Optional
from uuid import UUID
import structlog
from app.models.alerts import Alert, AlertStatus, AlertSeverity
logger = structlog.get_logger()
class AlertsRepository:
"""Repository for alert database operations"""
def __init__(self, db: AsyncSession):
self.db = db
async def get_alerts(
self,
tenant_id: UUID,
severity: Optional[str] = None,
status: Optional[str] = None,
resolved: Optional[bool] = None,
limit: int = 100,
offset: int = 0
) -> List[Alert]:
"""
Get alerts with optional filters
Args:
tenant_id: Tenant UUID
severity: Filter by severity (low, medium, high, urgent)
status: Filter by status (active, resolved, acknowledged, ignored)
resolved: Filter by resolved status (True = resolved, False = not resolved, None = all)
limit: Maximum number of results
offset: Pagination offset
Returns:
List of Alert objects
"""
try:
query = select(Alert).where(Alert.tenant_id == tenant_id)
# Apply filters
if severity:
query = query.where(Alert.severity == severity)
if status:
query = query.where(Alert.status == status)
if resolved is not None:
if resolved:
query = query.where(Alert.status == AlertStatus.RESOLVED.value)
else:
query = query.where(Alert.status != AlertStatus.RESOLVED.value)
# Order by created_at descending (newest first)
query = query.order_by(Alert.created_at.desc())
# Apply pagination
query = query.limit(limit).offset(offset)
result = await self.db.execute(query)
alerts = result.scalars().all()
logger.info(
"Retrieved alerts",
tenant_id=str(tenant_id),
count=len(alerts),
filters={"severity": severity, "status": status, "resolved": resolved}
)
return list(alerts)
except Exception as e:
logger.error("Error retrieving alerts", error=str(e), tenant_id=str(tenant_id))
raise
async def get_alerts_summary(self, tenant_id: UUID) -> Dict[str, Any]:
"""
Get summary of alerts by severity and status
Args:
tenant_id: Tenant UUID
Returns:
Dict with counts by severity and status
"""
try:
# Count by severity
severity_query = (
select(
Alert.severity,
func.count(Alert.id).label("count")
)
.where(
and_(
Alert.tenant_id == tenant_id,
Alert.status != AlertStatus.RESOLVED.value
)
)
.group_by(Alert.severity)
)
severity_result = await self.db.execute(severity_query)
severity_counts = {row[0]: row[1] for row in severity_result.all()}
# Count by status
status_query = (
select(
Alert.status,
func.count(Alert.id).label("count")
)
.where(Alert.tenant_id == tenant_id)
.group_by(Alert.status)
)
status_result = await self.db.execute(status_query)
status_counts = {row[0]: row[1] for row in status_result.all()}
# Count active alerts (not resolved)
active_count = sum(
count for status, count in status_counts.items()
if status != AlertStatus.RESOLVED.value
)
# Map to expected field names (dashboard expects "critical")
summary = {
"total_count": sum(status_counts.values()),
"active_count": active_count,
"critical_count": severity_counts.get(AlertSeverity.URGENT.value, 0), # Map URGENT to critical
"high_count": severity_counts.get(AlertSeverity.HIGH.value, 0),
"medium_count": severity_counts.get(AlertSeverity.MEDIUM.value, 0),
"low_count": severity_counts.get(AlertSeverity.LOW.value, 0),
"resolved_count": status_counts.get(AlertStatus.RESOLVED.value, 0),
"acknowledged_count": status_counts.get(AlertStatus.ACKNOWLEDGED.value, 0),
}
logger.info(
"Retrieved alerts summary",
tenant_id=str(tenant_id),
summary=summary
)
return summary
except Exception as e:
logger.error("Error retrieving alerts summary", error=str(e), tenant_id=str(tenant_id))
raise
async def get_alert_by_id(self, tenant_id: UUID, alert_id: UUID) -> Optional[Alert]:
"""Get a specific alert by ID"""
try:
query = select(Alert).where(
and_(
Alert.tenant_id == tenant_id,
Alert.id == alert_id
)
)
result = await self.db.execute(query)
alert = result.scalar_one_or_none()
if alert:
logger.info("Retrieved alert", alert_id=str(alert_id), tenant_id=str(tenant_id))
else:
logger.warning("Alert not found", alert_id=str(alert_id), tenant_id=str(tenant_id))
return alert
except Exception as e:
logger.error("Error retrieving alert", error=str(e), alert_id=str(alert_id))
raise