New alert system and panel de control page

This commit is contained in:
Urtzi Alfaro
2025-11-27 15:52:40 +01:00
parent 1a2f4602f3
commit e902419b6e
178 changed files with 20982 additions and 6944 deletions

View File

@@ -9,7 +9,7 @@ from typing import List, Dict, Any, Optional
from uuid import UUID
import structlog
from app.models.alerts import Alert, AlertStatus, AlertSeverity
from app.models.events import Alert, AlertStatus
logger = structlog.get_logger()
@@ -23,7 +23,7 @@ class AlertsRepository:
async def get_alerts(
self,
tenant_id: UUID,
severity: Optional[str] = None,
priority_level: Optional[str] = None,
status: Optional[str] = None,
resolved: Optional[bool] = None,
limit: int = 100,
@@ -34,7 +34,7 @@ class AlertsRepository:
Args:
tenant_id: Tenant UUID
severity: Filter by severity (low, medium, high, urgent)
priority_level: Filter by priority level (critical, important, standard, info)
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
@@ -47,17 +47,24 @@ class AlertsRepository:
query = select(Alert).where(Alert.tenant_id == tenant_id)
# Apply filters
if severity:
query = query.where(Alert.severity == severity)
if priority_level:
query = query.where(Alert.priority_level == priority_level)
if status:
query = query.where(Alert.status == status)
# Convert string status to enum value
try:
status_enum = AlertStatus(status.lower())
query = query.where(Alert.status == status_enum)
except ValueError:
# Invalid status value, log and continue without filtering
logger.warning("Invalid status value provided", status=status)
pass
if resolved is not None:
if resolved:
query = query.where(Alert.status == AlertStatus.RESOLVED.value)
query = query.where(Alert.status == AlertStatus.RESOLVED)
else:
query = query.where(Alert.status != AlertStatus.RESOLVED.value)
query = query.where(Alert.status != AlertStatus.RESOLVED)
# Order by created_at descending (newest first)
query = query.order_by(Alert.created_at.desc())
@@ -72,7 +79,7 @@ class AlertsRepository:
"Retrieved alerts",
tenant_id=str(tenant_id),
count=len(alerts),
filters={"severity": severity, "status": status, "resolved": resolved}
filters={"priority_level": priority_level, "status": status, "resolved": resolved}
)
return list(alerts)
@@ -83,32 +90,32 @@ class AlertsRepository:
async def get_alerts_summary(self, tenant_id: UUID) -> Dict[str, Any]:
"""
Get summary of alerts by severity and status
Get summary of alerts by priority level and status
Args:
tenant_id: Tenant UUID
Returns:
Dict with counts by severity and status
Dict with counts by priority level and status
"""
try:
# Count by severity
severity_query = (
# Count by priority level
priority_query = (
select(
Alert.severity,
Alert.priority_level,
func.count(Alert.id).label("count")
)
.where(
and_(
Alert.tenant_id == tenant_id,
Alert.status != AlertStatus.RESOLVED.value
Alert.status != AlertStatus.RESOLVED
)
)
.group_by(Alert.severity)
.group_by(Alert.priority_level)
)
severity_result = await self.db.execute(severity_query)
severity_counts = {row[0]: row[1] for row in severity_result.all()}
priority_result = await self.db.execute(priority_query)
priority_counts = {row[0]: row[1] for row in priority_result.all()}
# Count by status
status_query = (
@@ -126,19 +133,23 @@ class AlertsRepository:
# Count active alerts (not resolved)
active_count = sum(
count for status, count in status_counts.items()
if status != AlertStatus.RESOLVED.value
if status != AlertStatus.RESOLVED
)
# Convert enum values to strings for dictionary lookups
status_counts_str = {status.value if hasattr(status, 'value') else status: count
for status, count in status_counts.items()}
# 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),
"critical_count": priority_counts.get('critical', 0),
"high_count": priority_counts.get('important', 0),
"medium_count": priority_counts.get('standard', 0),
"low_count": priority_counts.get('info', 0),
"resolved_count": status_counts_str.get('resolved', 0),
"acknowledged_count": status_counts_str.get('acknowledged', 0),
}
logger.info(

View File

@@ -10,7 +10,7 @@ from sqlalchemy import select, func, and_, extract, case
from sqlalchemy.ext.asyncio import AsyncSession
import structlog
from app.models.alerts import Alert, AlertInteraction, AlertSeverity, AlertStatus
from app.models.events import Alert, EventInteraction, AlertStatus
logger = structlog.get_logger()
@@ -28,7 +28,7 @@ class AlertAnalyticsRepository:
user_id: UUID,
interaction_type: str,
metadata: Optional[Dict[str, Any]] = None
) -> AlertInteraction:
) -> EventInteraction:
"""Create a new alert interaction"""
# Get alert to calculate response time
@@ -44,7 +44,7 @@ class AlertAnalyticsRepository:
response_time_seconds = int((now - alert.created_at).total_seconds())
# Create interaction
interaction = AlertInteraction(
interaction = EventInteraction(
tenant_id=tenant_id,
alert_id=alert_id,
user_id=user_id,
@@ -81,7 +81,7 @@ class AlertAnalyticsRepository:
self,
tenant_id: UUID,
interactions: List[Dict[str, Any]]
) -> List[AlertInteraction]:
) -> List[EventInteraction]:
"""Create multiple interactions in batch"""
created_interactions = []
@@ -113,22 +113,26 @@ class AlertAnalyticsRepository:
"""Get alert trends for the last N days"""
start_date = datetime.utcnow() - timedelta(days=days)
# Query alerts grouped by date and severity
# Query alerts grouped by date and priority_level (mapping to severity equivalents)
# Critical priority_level maps to urgent severity
# Important priority_level maps to high severity
# Standard priority_level maps to medium severity
# Info priority_level maps to low severity
query = (
select(
func.date(Alert.created_at).label('date'),
func.count(Alert.id).label('total_count'),
func.sum(
case((Alert.severity == AlertSeverity.URGENT, 1), else_=0)
case((Alert.priority_level == 'critical', 1), else_=0)
).label('urgent_count'),
func.sum(
case((Alert.severity == AlertSeverity.HIGH, 1), else_=0)
case((Alert.priority_level == 'important', 1), else_=0)
).label('high_count'),
func.sum(
case((Alert.severity == AlertSeverity.MEDIUM, 1), else_=0)
case((Alert.priority_level == 'standard', 1), else_=0)
).label('medium_count'),
func.sum(
case((Alert.severity == AlertSeverity.LOW, 1), else_=0)
case((Alert.priority_level == 'info', 1), else_=0)
).label('low_count')
)
.where(
@@ -178,13 +182,13 @@ class AlertAnalyticsRepository:
start_date = datetime.utcnow() - timedelta(days=days)
query = (
select(func.avg(AlertInteraction.response_time_seconds))
select(func.avg(EventInteraction.response_time_seconds))
.where(
and_(
AlertInteraction.tenant_id == tenant_id,
AlertInteraction.interaction_type == 'acknowledged',
AlertInteraction.interacted_at >= start_date,
AlertInteraction.response_time_seconds < 86400 # Less than 24 hours
EventInteraction.tenant_id == tenant_id,
EventInteraction.interaction_type == 'acknowledged',
EventInteraction.interacted_at >= start_date,
EventInteraction.response_time_seconds < 86400 # Less than 24 hours
)
)
)
@@ -380,3 +384,125 @@ class AlertAnalyticsRepository:
'predictedDailyAverage': predicted_avg,
'busiestDay': busiest_day
}
async def get_period_comparison(
self,
tenant_id: UUID,
current_days: int = 7,
previous_days: int = 7
) -> Dict[str, Any]:
"""
Compare current period metrics with previous period.
Used for week-over-week trend analysis in dashboard cards.
Args:
tenant_id: Tenant ID
current_days: Number of days in current period (default 7)
previous_days: Number of days in previous period (default 7)
Returns:
Dictionary with current/previous metrics and percentage changes
"""
from datetime import datetime, timedelta
now = datetime.utcnow()
current_start = now - timedelta(days=current_days)
previous_start = current_start - timedelta(days=previous_days)
previous_end = current_start
# Current period: AI handling rate (prevented issues / total)
current_total_query = select(func.count(Alert.id)).where(
and_(
Alert.tenant_id == tenant_id,
Alert.created_at >= current_start,
Alert.created_at <= now
)
)
current_total_result = await self.session.execute(current_total_query)
current_total = current_total_result.scalar() or 0
current_prevented_query = select(func.count(Alert.id)).where(
and_(
Alert.tenant_id == tenant_id,
Alert.type_class == 'prevented_issue',
Alert.created_at >= current_start,
Alert.created_at <= now
)
)
current_prevented_result = await self.session.execute(current_prevented_query)
current_prevented = current_prevented_result.scalar() or 0
current_handling_rate = (
(current_prevented / current_total * 100)
if current_total > 0 else 0
)
# Previous period: AI handling rate
previous_total_query = select(func.count(Alert.id)).where(
and_(
Alert.tenant_id == tenant_id,
Alert.created_at >= previous_start,
Alert.created_at < previous_end
)
)
previous_total_result = await self.session.execute(previous_total_query)
previous_total = previous_total_result.scalar() or 0
previous_prevented_query = select(func.count(Alert.id)).where(
and_(
Alert.tenant_id == tenant_id,
Alert.type_class == 'prevented_issue',
Alert.created_at >= previous_start,
Alert.created_at < previous_end
)
)
previous_prevented_result = await self.session.execute(previous_prevented_query)
previous_prevented = previous_prevented_result.scalar() or 0
previous_handling_rate = (
(previous_prevented / previous_total * 100)
if previous_total > 0 else 0
)
# Calculate percentage change
if previous_handling_rate > 0:
handling_rate_change = round(
((current_handling_rate - previous_handling_rate) / previous_handling_rate) * 100,
1
)
elif current_handling_rate > 0:
handling_rate_change = 100.0 # Went from 0% to something
else:
handling_rate_change = 0.0
# Alert count change
if previous_total > 0:
alert_count_change = round(
((current_total - previous_total) / previous_total) * 100,
1
)
elif current_total > 0:
alert_count_change = 100.0
else:
alert_count_change = 0.0
return {
'current_period': {
'days': current_days,
'total_alerts': current_total,
'prevented_issues': current_prevented,
'handling_rate_percentage': round(current_handling_rate, 1)
},
'previous_period': {
'days': previous_days,
'total_alerts': previous_total,
'prevented_issues': previous_prevented,
'handling_rate_percentage': round(previous_handling_rate, 1)
},
'changes': {
'handling_rate_change_percentage': handling_rate_change,
'alert_count_change_percentage': alert_count_change,
'trend_direction': 'up' if handling_rate_change > 0 else ('down' if handling_rate_change < 0 else 'stable')
}
}