alert_service.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """
  2. Alert service for creating and dispatching system notifications.
  3. """
  4. from datetime import datetime, timezone
  5. from sqlalchemy import select
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from app.core.database import async_session_maker
  8. from app.models.alert import Alert
  9. from app.models.settings import Settings
  10. class AlertService:
  11. """Manage system alerts and notifications."""
  12. async def create_alert(
  13. self,
  14. alert_type: str,
  15. severity: str,
  16. title: str,
  17. message: str,
  18. alert_metadata: dict | None = None,
  19. ) -> Alert:
  20. """Create a new alert and dispatch to configured channels."""
  21. async with async_session_maker() as session:
  22. # Check if similar alert already exists (prevent spam)
  23. existing = await self._find_similar_alert(session, alert_type, title)
  24. if existing:
  25. print(f"[AlertService] Similar alert already exists, skipping: {title}")
  26. return existing
  27. # Create alert
  28. alert = Alert(
  29. timestamp=datetime.now(timezone.utc),
  30. alert_type=alert_type,
  31. severity=severity,
  32. title=title,
  33. message=message,
  34. alert_metadata=alert_metadata or {},
  35. sent_dashboard=True, # Always show in dashboard
  36. )
  37. session.add(alert)
  38. await session.commit()
  39. await session.refresh(alert)
  40. print(f"[AlertService] Created alert: [{severity}] {title}")
  41. # Dispatch to configured channels
  42. await self._dispatch_alert(session, alert)
  43. return alert
  44. async def _find_similar_alert(
  45. self, session: AsyncSession, alert_type: str, title: str
  46. ) -> Alert | None:
  47. """Find recent similar alert to prevent spam."""
  48. # Check if alert with same type and title was created in last 5 minutes
  49. from datetime import timedelta
  50. cutoff = datetime.now(timezone.utc) - timedelta(minutes=5)
  51. result = await session.execute(
  52. select(Alert)
  53. .where(Alert.alert_type == alert_type)
  54. .where(Alert.title == title)
  55. .where(Alert.timestamp > cutoff)
  56. .where(Alert.dismissed == False)
  57. )
  58. return result.scalar_one_or_none()
  59. async def _dispatch_alert(self, session: AsyncSession, alert: Alert):
  60. """Dispatch alert to configured channels (Telegram, Email, etc)."""
  61. # Get alert channels configuration
  62. result = await session.execute(
  63. select(Settings).where(Settings.key == "alert_channels")
  64. )
  65. settings = result.scalar_one_or_none()
  66. if not settings:
  67. return
  68. channels = settings.value
  69. # Telegram
  70. if channels.get("telegram", {}).get("enabled"):
  71. try:
  72. await self._send_telegram(alert, channels["telegram"])
  73. alert.sent_telegram = True
  74. except Exception as e:
  75. print(f"[AlertService] Failed to send Telegram: {e}")
  76. # Email
  77. if channels.get("email", {}).get("enabled"):
  78. try:
  79. await self._send_email(alert, channels["email"])
  80. alert.sent_email = True
  81. except Exception as e:
  82. print(f"[AlertService] Failed to send Email: {e}")
  83. await session.commit()
  84. async def _send_telegram(self, alert: Alert, config: dict):
  85. """Send alert via Telegram bot."""
  86. # TODO: Implement Telegram bot integration
  87. bot_token = config.get("bot_token")
  88. chat_ids = config.get("chat_ids", [])
  89. if not bot_token or not chat_ids:
  90. return
  91. # Format message
  92. severity_emoji = {
  93. "info": "ℹ️",
  94. "warning": "⚠️",
  95. "error": "❌",
  96. "critical": "🚨",
  97. }
  98. emoji = severity_emoji.get(alert.severity, "📢")
  99. text = f"{emoji} **{alert.title}**\n\n{alert.message}"
  100. print(f"[AlertService] Would send Telegram to {len(chat_ids)} chats: {text[:50]}...")
  101. # Import httpx and send message to Telegram API
  102. # await httpx.post(f"https://api.telegram.org/bot{bot_token}/sendMessage", ...)
  103. async def _send_email(self, alert: Alert, config: dict):
  104. """Send alert via Email."""
  105. # TODO: Implement Email SMTP integration
  106. smtp_server = config.get("smtp_server")
  107. recipients = config.get("recipients", [])
  108. if not smtp_server or not recipients:
  109. return
  110. print(f"[AlertService] Would send Email to {len(recipients)} recipients: {alert.title}")
  111. # Import smtplib and send email
  112. # ...
  113. async def get_active_alerts(self, session: AsyncSession) -> list[Alert]:
  114. """Get all active (non-dismissed) alerts."""
  115. result = await session.execute(
  116. select(Alert)
  117. .where(Alert.dismissed == False)
  118. .order_by(Alert.timestamp.desc())
  119. )
  120. return list(result.scalars().all())
  121. async def acknowledge_alert(
  122. self, session: AsyncSession, alert_id: int, user_id: int
  123. ):
  124. """Mark alert as acknowledged."""
  125. result = await session.execute(select(Alert).where(Alert.id == alert_id))
  126. alert = result.scalar_one_or_none()
  127. if alert:
  128. alert.acknowledged = True
  129. alert.acknowledged_at = datetime.now(timezone.utc)
  130. alert.acknowledged_by = user_id
  131. await session.commit()
  132. async def dismiss_alert(self, session: AsyncSession, alert_id: int):
  133. """Mark alert as dismissed."""
  134. result = await session.execute(select(Alert).where(Alert.id == alert_id))
  135. alert = result.scalar_one_or_none()
  136. if alert:
  137. alert.dismissed = True
  138. alert.dismissed_at = datetime.now(timezone.utc)
  139. await session.commit()
  140. # Global instance
  141. alert_service = AlertService()