notification_service.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. """Notification service for sending alerts through various channels."""
  2. import logging
  3. from typing import Any
  4. import httpx
  5. from extensions.ext_mail import mail
  6. from libs.email_i18n import get_email_i18n_service
  7. logger = logging.getLogger(__name__)
  8. class NotificationService:
  9. """Unified notification service for alerts."""
  10. @staticmethod
  11. def send_email(to_emails: list[str], subject: str, content: str) -> dict[str, Any]:
  12. """Send email notification.
  13. Args:
  14. to_emails: List of recipient email addresses
  15. subject: Email subject
  16. content: HTML email content
  17. Returns:
  18. dict with status: 'success' or 'failed', and optional 'error' message
  19. """
  20. if not mail.is_inited():
  21. return {"status": "failed", "error": "Email service not initialized"}
  22. try:
  23. email_service = get_email_i18n_service()
  24. for email in to_emails:
  25. email_service.send_raw_email(to=[email], subject=subject, html_content=content)
  26. logger.info(f"Alert email sent successfully to {to_emails}")
  27. return {"status": "success"}
  28. except Exception as e:
  29. logger.exception(f"Failed to send alert email to {to_emails}")
  30. return {"status": "failed", "error": str(e)}
  31. @staticmethod
  32. def send_dingtalk(webhook_urls: list[str], message: str) -> dict[str, Any]:
  33. """Send DingTalk notification.
  34. Args:
  35. webhook_urls: List of DingTalk webhook URLs
  36. message: Message text
  37. Returns:
  38. dict with status: 'success' or 'failed', and optional 'error' message
  39. """
  40. try:
  41. for webhook_url in webhook_urls:
  42. response = httpx.post(
  43. webhook_url,
  44. json={"msgtype": "text", "text": {"content": message}},
  45. timeout=5.0,
  46. )
  47. if response.status_code != 200:
  48. raise Exception(f"DingTalk notification failed: {response.text}")
  49. logger.info(f"DingTalk notification sent successfully to {len(webhook_urls)} webhooks")
  50. return {"status": "success"}
  51. except Exception as e:
  52. logger.exception("Failed to send DingTalk notification")
  53. return {"status": "failed", "error": str(e)}
  54. @staticmethod
  55. def send_wecom(webhook_urls: list[str], message: str) -> dict[str, Any]:
  56. """Send WeCom (Enterprise WeChat) notification.
  57. Args:
  58. webhook_urls: List of WeCom webhook URLs
  59. message: Message text
  60. Returns:
  61. dict with status: 'success' or 'failed', and optional 'error' message
  62. """
  63. try:
  64. for webhook_url in webhook_urls:
  65. response = httpx.post(
  66. webhook_url,
  67. json={"msgtype": "text", "text": {"content": message}},
  68. timeout=5.0,
  69. )
  70. if response.status_code != 200:
  71. raise Exception(f"WeCom notification failed: {response.text}")
  72. logger.info(f"WeCom notification sent successfully to {len(webhook_urls)} webhooks")
  73. return {"status": "success"}
  74. except Exception as e:
  75. logger.exception("Failed to send WeCom notification")
  76. return {"status": "failed", "error": str(e)}
  77. @classmethod
  78. def send_alert_notification(
  79. cls,
  80. channels: dict[str, list[str]],
  81. app_name: str,
  82. metric_name: str,
  83. metric_value: float,
  84. threshold_value: float,
  85. operator: str,
  86. ) -> dict[str, dict[str, Any]]:
  87. """Send alert notification through multiple channels.
  88. Args:
  89. channels: Dictionary of notification channels {'email': [...], 'dingtalk': [...], 'wecom': [...]}
  90. app_name: Name of the application
  91. metric_name: Name of the metric that triggered the alert
  92. metric_value: Current value of the metric
  93. threshold_value: Threshold that was exceeded
  94. operator: Comparison operator (>, <, etc.)
  95. Returns:
  96. Dictionary with status for each channel
  97. """
  98. results = {}
  99. # Format metric value based on type
  100. if "response_time" in metric_name:
  101. metric_display = f"{metric_value:.2f} ms"
  102. threshold_display = f"{threshold_value:.2f} ms"
  103. elif "error_rate" in metric_name:
  104. metric_display = f"{metric_value:.2f}%"
  105. threshold_display = f"{threshold_value:.2f}%"
  106. else:
  107. metric_display = f"{metric_value:.2f}"
  108. threshold_display = f"{threshold_value:.2f}"
  109. # Build message
  110. text_message = (
  111. f"🚨 告警通知\n\n"
  112. f"应用名称: {app_name}\n"
  113. f"告警指标: {metric_name}\n"
  114. f"当前值: {metric_display}\n"
  115. f"阈值: {operator} {threshold_display}\n"
  116. f"触发时间: {cls._get_current_time_str()}"
  117. )
  118. html_message = f"""
  119. <html>
  120. <body style="font-family: Arial, sans-serif; padding: 20px;">
  121. <h2 style="color: #ff4d4f;">🚨 告警通知</h2>
  122. <div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px;">
  123. <p><strong>应用名称:</strong> {app_name}</p>
  124. <p><strong>告警指标:</strong> {metric_name}</p>
  125. <p><strong>当前值:</strong> <span style="color: #ff4d4f;">{metric_display}</span></p>
  126. <p><strong>阈值:</strong> {operator} {threshold_display}</p>
  127. <p><strong>触发时间:</strong> {cls._get_current_time_str()}</p>
  128. </div>
  129. <p style="margin-top: 20px; color: #666;">
  130. 请及时检查应用运行状态并采取必要措施。
  131. </p>
  132. </body>
  133. </html>
  134. """
  135. # Send email notifications
  136. if "email" in channels and channels["email"]:
  137. results["email"] = cls.send_email(
  138. to_emails=channels["email"], subject=f"【告警】{app_name} - {metric_name}", content=html_message
  139. )
  140. # Send DingTalk notifications
  141. if "dingtalk" in channels and channels["dingtalk"]:
  142. results["dingtalk"] = cls.send_dingtalk(webhook_urls=channels["dingtalk"], message=text_message)
  143. # Send WeCom notifications
  144. if "wecom" in channels and channels["wecom"]:
  145. results["wecom"] = cls.send_wecom(webhook_urls=channels["wecom"], message=text_message)
  146. return results
  147. @staticmethod
  148. def _get_current_time_str() -> str:
  149. """Get current time as formatted string."""
  150. from datetime import datetime, timezone
  151. return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")