New alert system and panel de control page
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user