Improve the frontend
This commit is contained in:
7
services/alert_processor/app/repositories/__init__.py
Normal file
7
services/alert_processor/app/repositories/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Alert Processor Repositories
|
||||
"""
|
||||
|
||||
from .analytics_repository import AlertAnalyticsRepository
|
||||
|
||||
__all__ = ['AlertAnalyticsRepository']
|
||||
@@ -0,0 +1,382 @@
|
||||
"""
|
||||
Alert Analytics Repository
|
||||
Handles all database operations for alert analytics
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import UUID
|
||||
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
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class AlertAnalyticsRepository:
|
||||
"""Repository for alert analytics operations"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create_interaction(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
alert_id: UUID,
|
||||
user_id: UUID,
|
||||
interaction_type: str,
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
) -> AlertInteraction:
|
||||
"""Create a new alert interaction"""
|
||||
|
||||
# Get alert to calculate response time
|
||||
alert_query = select(Alert).where(Alert.id == alert_id)
|
||||
result = await self.session.execute(alert_query)
|
||||
alert = result.scalar_one_or_none()
|
||||
|
||||
if not alert:
|
||||
raise ValueError(f"Alert {alert_id} not found")
|
||||
|
||||
# Calculate response time
|
||||
now = datetime.utcnow()
|
||||
response_time_seconds = int((now - alert.created_at).total_seconds())
|
||||
|
||||
# Create interaction
|
||||
interaction = AlertInteraction(
|
||||
tenant_id=tenant_id,
|
||||
alert_id=alert_id,
|
||||
user_id=user_id,
|
||||
interaction_type=interaction_type,
|
||||
interacted_at=now,
|
||||
response_time_seconds=response_time_seconds,
|
||||
interaction_metadata=metadata or {}
|
||||
)
|
||||
|
||||
self.session.add(interaction)
|
||||
|
||||
# Update alert status if applicable
|
||||
if interaction_type == 'acknowledged' and alert.status == AlertStatus.ACTIVE:
|
||||
alert.status = AlertStatus.ACKNOWLEDGED
|
||||
elif interaction_type == 'resolved':
|
||||
alert.status = AlertStatus.RESOLVED
|
||||
alert.resolved_at = now
|
||||
elif interaction_type == 'dismissed':
|
||||
alert.status = AlertStatus.IGNORED
|
||||
|
||||
await self.session.commit()
|
||||
await self.session.refresh(interaction)
|
||||
|
||||
logger.info(
|
||||
"Alert interaction created",
|
||||
alert_id=str(alert_id),
|
||||
interaction_type=interaction_type,
|
||||
response_time=response_time_seconds
|
||||
)
|
||||
|
||||
return interaction
|
||||
|
||||
async def create_interactions_batch(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
interactions: List[Dict[str, Any]]
|
||||
) -> List[AlertInteraction]:
|
||||
"""Create multiple interactions in batch"""
|
||||
created_interactions = []
|
||||
|
||||
for interaction_data in interactions:
|
||||
try:
|
||||
interaction = await self.create_interaction(
|
||||
tenant_id=tenant_id,
|
||||
alert_id=UUID(interaction_data['alert_id']),
|
||||
user_id=UUID(interaction_data['user_id']),
|
||||
interaction_type=interaction_data['interaction_type'],
|
||||
metadata=interaction_data.get('metadata')
|
||||
)
|
||||
created_interactions.append(interaction)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to create interaction in batch",
|
||||
error=str(e),
|
||||
alert_id=interaction_data.get('alert_id')
|
||||
)
|
||||
continue
|
||||
|
||||
return created_interactions
|
||||
|
||||
async def get_analytics_trends(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
days: int = 7
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get alert trends for the last N days"""
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
# Query alerts grouped by date and 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)
|
||||
).label('urgent_count'),
|
||||
func.sum(
|
||||
case((Alert.severity == AlertSeverity.HIGH, 1), else_=0)
|
||||
).label('high_count'),
|
||||
func.sum(
|
||||
case((Alert.severity == AlertSeverity.MEDIUM, 1), else_=0)
|
||||
).label('medium_count'),
|
||||
func.sum(
|
||||
case((Alert.severity == AlertSeverity.LOW, 1), else_=0)
|
||||
).label('low_count')
|
||||
)
|
||||
.where(
|
||||
and_(
|
||||
Alert.tenant_id == tenant_id,
|
||||
Alert.created_at >= start_date
|
||||
)
|
||||
)
|
||||
.group_by(func.date(Alert.created_at))
|
||||
.order_by(func.date(Alert.created_at))
|
||||
)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
# Fill in missing dates with zeros
|
||||
trends = []
|
||||
current_date = start_date.date()
|
||||
end_date = datetime.utcnow().date()
|
||||
|
||||
# Create a dict for quick lookup
|
||||
data_by_date = {row.date: row for row in rows}
|
||||
|
||||
while current_date <= end_date:
|
||||
date_str = current_date.isoformat()
|
||||
row = data_by_date.get(current_date)
|
||||
|
||||
trends.append({
|
||||
'date': date_str,
|
||||
'count': int(row.total_count) if row else 0,
|
||||
'urgentCount': int(row.urgent_count) if row else 0,
|
||||
'highCount': int(row.high_count) if row else 0,
|
||||
'mediumCount': int(row.medium_count) if row else 0,
|
||||
'lowCount': int(row.low_count) if row else 0,
|
||||
})
|
||||
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return trends
|
||||
|
||||
async def get_average_response_time(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
days: int = 7
|
||||
) -> int:
|
||||
"""Get average response time in minutes for acknowledged alerts"""
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
query = (
|
||||
select(func.avg(AlertInteraction.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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
avg_seconds = result.scalar_one_or_none()
|
||||
|
||||
if avg_seconds is None:
|
||||
return 0
|
||||
|
||||
# Convert to minutes
|
||||
return round(avg_seconds / 60)
|
||||
|
||||
async def get_top_categories(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
days: int = 7,
|
||||
limit: int = 3
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get top alert categories"""
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
query = (
|
||||
select(
|
||||
Alert.alert_type,
|
||||
func.count(Alert.id).label('count')
|
||||
)
|
||||
.where(
|
||||
and_(
|
||||
Alert.tenant_id == tenant_id,
|
||||
Alert.created_at >= start_date
|
||||
)
|
||||
)
|
||||
.group_by(Alert.alert_type)
|
||||
.order_by(func.count(Alert.id).desc())
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
# Calculate total for percentages
|
||||
total_query = (
|
||||
select(func.count(Alert.id))
|
||||
.where(
|
||||
and_(
|
||||
Alert.tenant_id == tenant_id,
|
||||
Alert.created_at >= start_date
|
||||
)
|
||||
)
|
||||
)
|
||||
total_result = await self.session.execute(total_query)
|
||||
total = total_result.scalar_one() or 1
|
||||
|
||||
categories = []
|
||||
for row in rows:
|
||||
percentage = round((row.count / total) * 100) if total > 0 else 0
|
||||
categories.append({
|
||||
'category': row.alert_type,
|
||||
'count': row.count,
|
||||
'percentage': percentage
|
||||
})
|
||||
|
||||
return categories
|
||||
|
||||
async def get_resolution_stats(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
days: int = 7
|
||||
) -> Dict[str, Any]:
|
||||
"""Get resolution statistics"""
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
# Total alerts
|
||||
total_query = (
|
||||
select(func.count(Alert.id))
|
||||
.where(
|
||||
and_(
|
||||
Alert.tenant_id == tenant_id,
|
||||
Alert.created_at >= start_date
|
||||
)
|
||||
)
|
||||
)
|
||||
total_result = await self.session.execute(total_query)
|
||||
total_alerts = total_result.scalar_one() or 0
|
||||
|
||||
# Resolved alerts
|
||||
resolved_query = (
|
||||
select(func.count(Alert.id))
|
||||
.where(
|
||||
and_(
|
||||
Alert.tenant_id == tenant_id,
|
||||
Alert.created_at >= start_date,
|
||||
Alert.status == AlertStatus.RESOLVED
|
||||
)
|
||||
)
|
||||
)
|
||||
resolved_result = await self.session.execute(resolved_query)
|
||||
resolved_alerts = resolved_result.scalar_one() or 0
|
||||
|
||||
# Active alerts
|
||||
active_query = (
|
||||
select(func.count(Alert.id))
|
||||
.where(
|
||||
and_(
|
||||
Alert.tenant_id == tenant_id,
|
||||
Alert.created_at >= start_date,
|
||||
Alert.status == AlertStatus.ACTIVE
|
||||
)
|
||||
)
|
||||
)
|
||||
active_result = await self.session.execute(active_query)
|
||||
active_alerts = active_result.scalar_one() or 0
|
||||
|
||||
resolution_rate = round((resolved_alerts / total_alerts) * 100) if total_alerts > 0 else 0
|
||||
|
||||
return {
|
||||
'totalAlerts': total_alerts,
|
||||
'resolvedAlerts': resolved_alerts,
|
||||
'activeAlerts': active_alerts,
|
||||
'resolutionRate': resolution_rate
|
||||
}
|
||||
|
||||
async def get_busiest_day(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
days: int = 7
|
||||
) -> str:
|
||||
"""Get busiest day of week"""
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
query = (
|
||||
select(
|
||||
extract('dow', Alert.created_at).label('day_of_week'),
|
||||
func.count(Alert.id).label('count')
|
||||
)
|
||||
.where(
|
||||
and_(
|
||||
Alert.tenant_id == tenant_id,
|
||||
Alert.created_at >= start_date
|
||||
)
|
||||
)
|
||||
.group_by(extract('dow', Alert.created_at))
|
||||
.order_by(func.count(Alert.id).desc())
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
row = result.first()
|
||||
|
||||
if not row:
|
||||
return 'N/A'
|
||||
|
||||
day_names = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
||||
return day_names[int(row.day_of_week)]
|
||||
|
||||
async def get_predicted_daily_average(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
days: int = 7
|
||||
) -> int:
|
||||
"""Calculate predicted daily average based on trends"""
|
||||
trends = await self.get_analytics_trends(tenant_id, days)
|
||||
|
||||
if not trends:
|
||||
return 0
|
||||
|
||||
total_count = sum(trend['count'] for trend in trends)
|
||||
return round(total_count / len(trends))
|
||||
|
||||
async def get_full_analytics(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
days: int = 7
|
||||
) -> Dict[str, Any]:
|
||||
"""Get complete analytics data"""
|
||||
|
||||
trends = await self.get_analytics_trends(tenant_id, days)
|
||||
avg_response_time = await self.get_average_response_time(tenant_id, days)
|
||||
top_categories = await self.get_top_categories(tenant_id, days)
|
||||
resolution_stats = await self.get_resolution_stats(tenant_id, days)
|
||||
busiest_day = await self.get_busiest_day(tenant_id, days)
|
||||
predicted_avg = await self.get_predicted_daily_average(tenant_id, days)
|
||||
|
||||
return {
|
||||
'trends': trends,
|
||||
'averageResponseTime': avg_response_time,
|
||||
'topCategories': top_categories,
|
||||
'totalAlerts': resolution_stats['totalAlerts'],
|
||||
'resolvedAlerts': resolution_stats['resolvedAlerts'],
|
||||
'activeAlerts': resolution_stats['activeAlerts'],
|
||||
'resolutionRate': resolution_stats['resolutionRate'],
|
||||
'predictedDailyAverage': predicted_avg,
|
||||
'busiestDay': busiest_day
|
||||
}
|
||||
Reference in New Issue
Block a user