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:
178
services/alert_processor/app/repositories/alerts_repository.py
Normal file
178
services/alert_processor/app/repositories/alerts_repository.py
Normal 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
|
||||
Reference in New Issue
Block a user