alert.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. """Alert rule management API controllers."""
  2. from flask import request
  3. from flask_login import current_user
  4. from flask_restx import Resource, reqparse
  5. from werkzeug.exceptions import Forbidden, NotFound
  6. from controllers.console import api
  7. from controllers.console.app.wraps import get_app_model
  8. from controllers.console.wraps import account_initialization_required, setup_required
  9. from libs.login import login_required
  10. from services.alert_service import AlertService
  11. class AlertRuleListApi(Resource):
  12. """List and create alert rules for an app."""
  13. @setup_required
  14. @login_required
  15. @account_initialization_required
  16. @get_app_model
  17. def get(self, app_model):
  18. """Get all alert rules for an app."""
  19. # Check permission
  20. if app_model.tenant_id != current_user.current_tenant_id:
  21. raise Forbidden()
  22. rules = AlertService.get_alert_rules(str(app_model.id))
  23. return {"data": rules}
  24. @setup_required
  25. @login_required
  26. @account_initialization_required
  27. @get_app_model
  28. def post(self, app_model):
  29. """Create a new alert rule."""
  30. # Check permission
  31. if app_model.tenant_id != current_user.current_tenant_id:
  32. raise Forbidden()
  33. parser = reqparse.RequestParser()
  34. parser.add_argument("name", type=str, required=True, location="json")
  35. parser.add_argument("description", type=str, required=False, location="json", default="")
  36. parser.add_argument("is_enabled", type=bool, required=False, location="json", default=True)
  37. parser.add_argument("metric_type", type=str, required=True, location="json")
  38. parser.add_argument("operator", type=str, required=True, location="json")
  39. parser.add_argument("threshold_value", type=float, required=True, location="json")
  40. parser.add_argument("check_interval_minutes", type=int, required=False, location="json", default=5)
  41. parser.add_argument("consecutive_triggers", type=int, required=False, location="json", default=2)
  42. parser.add_argument("notification_channels", type=dict, required=True, location="json")
  43. args = parser.parse_args()
  44. # Validate metric_type
  45. valid_metric_types = ["error_rate", "avg_response_time", "response_time_p95", "response_time_p99"]
  46. if args["metric_type"] not in valid_metric_types:
  47. return {
  48. "code": "invalid_param",
  49. "message": f"metric_type must be one of: {', '.join(valid_metric_types)}",
  50. }, 400
  51. # Validate operator
  52. valid_operators = [">", "<", ">=", "<=", "="]
  53. if args["operator"] not in valid_operators:
  54. return {"code": "invalid_param", "message": f"operator must be one of: {', '.join(valid_operators)}"}, 400
  55. # Validate notification_channels
  56. if not args["notification_channels"]:
  57. return {"code": "invalid_param", "message": "notification_channels cannot be empty"}, 400
  58. # Validate check_interval_minutes
  59. if args["check_interval_minutes"] < 1 or args["check_interval_minutes"] > 1440:
  60. return {"code": "invalid_param", "message": "check_interval_minutes must be between 1 and 1440"}, 400
  61. # Validate consecutive_triggers
  62. if args["consecutive_triggers"] < 1 or args["consecutive_triggers"] > 10:
  63. return {"code": "invalid_param", "message": "consecutive_triggers must be between 1 and 10"}, 400
  64. try:
  65. rule = AlertService.create_alert_rule(
  66. app_id=str(app_model.id), data=args, created_by=str(current_user.id)
  67. )
  68. return rule, 201
  69. except ValueError as e:
  70. return {"code": "invalid_param", "message": str(e)}, 400
  71. class AlertRuleDetailApi(Resource):
  72. """Get, update, or delete a specific alert rule."""
  73. @setup_required
  74. @login_required
  75. @account_initialization_required
  76. @get_app_model
  77. def get(self, app_model, rule_id):
  78. """Get a specific alert rule."""
  79. # Check permission
  80. if app_model.tenant_id != current_user.current_tenant_id:
  81. raise Forbidden()
  82. rule = AlertService.get_alert_rule(rule_id)
  83. if not rule:
  84. raise NotFound("Alert rule not found")
  85. # Verify rule belongs to this app
  86. if rule.get("app_id") != str(app_model.id):
  87. raise Forbidden()
  88. return rule
  89. @setup_required
  90. @login_required
  91. @account_initialization_required
  92. @get_app_model
  93. def patch(self, app_model, rule_id):
  94. """Update an alert rule."""
  95. # Check permission
  96. if app_model.tenant_id != current_user.current_tenant_id:
  97. raise Forbidden()
  98. rule = AlertService.get_alert_rule(rule_id)
  99. if not rule:
  100. raise NotFound("Alert rule not found")
  101. # Verify rule belongs to this app
  102. if rule.get("app_id") != str(app_model.id):
  103. raise Forbidden()
  104. parser = reqparse.RequestParser()
  105. parser.add_argument("name", type=str, required=False, location="json")
  106. parser.add_argument("description", type=str, required=False, location="json")
  107. parser.add_argument("is_enabled", type=bool, required=False, location="json")
  108. parser.add_argument("metric_type", type=str, required=False, location="json")
  109. parser.add_argument("operator", type=str, required=False, location="json")
  110. parser.add_argument("threshold_value", type=float, required=False, location="json")
  111. parser.add_argument("check_interval_minutes", type=int, required=False, location="json")
  112. parser.add_argument("consecutive_triggers", type=int, required=False, location="json")
  113. parser.add_argument("notification_channels", type=dict, required=False, location="json")
  114. args = parser.parse_args()
  115. # Remove None values
  116. update_data = {k: v for k, v in args.items() if v is not None}
  117. # Validate metric_type if provided
  118. if "metric_type" in update_data:
  119. valid_metric_types = ["error_rate", "avg_response_time", "response_time_p95", "response_time_p99"]
  120. if update_data["metric_type"] not in valid_metric_types:
  121. return {
  122. "code": "invalid_param",
  123. "message": f"metric_type must be one of: {', '.join(valid_metric_types)}",
  124. }, 400
  125. # Validate operator if provided
  126. if "operator" in update_data:
  127. valid_operators = [">", "<", ">=", "<=", "="]
  128. if update_data["operator"] not in valid_operators:
  129. return {
  130. "code": "invalid_param",
  131. "message": f"operator must be one of: {', '.join(valid_operators)}",
  132. }, 400
  133. # Validate check_interval_minutes if provided
  134. if "check_interval_minutes" in update_data:
  135. if update_data["check_interval_minutes"] < 1 or update_data["check_interval_minutes"] > 1440:
  136. return {"code": "invalid_param", "message": "check_interval_minutes must be between 1 and 1440"}, 400
  137. # Validate consecutive_triggers if provided
  138. if "consecutive_triggers" in update_data:
  139. if update_data["consecutive_triggers"] < 1 or update_data["consecutive_triggers"] > 10:
  140. return {"code": "invalid_param", "message": "consecutive_triggers must be between 1 and 10"}, 400
  141. try:
  142. updated_rule = AlertService.update_alert_rule(rule_id, update_data)
  143. return updated_rule
  144. except ValueError as e:
  145. return {"code": "invalid_param", "message": str(e)}, 400
  146. @setup_required
  147. @login_required
  148. @account_initialization_required
  149. @get_app_model
  150. def delete(self, app_model, rule_id):
  151. """Delete an alert rule."""
  152. # Check permission
  153. if app_model.tenant_id != current_user.current_tenant_id:
  154. raise Forbidden()
  155. rule = AlertService.get_alert_rule(rule_id)
  156. if not rule:
  157. raise NotFound("Alert rule not found")
  158. # Verify rule belongs to this app
  159. if rule.get("app_id") != str(app_model.id):
  160. raise Forbidden()
  161. try:
  162. AlertService.delete_alert_rule(rule_id)
  163. return {"result": "success"}
  164. except ValueError as e:
  165. return {"code": "invalid_param", "message": str(e)}, 400
  166. class AlertRuleLogsApi(Resource):
  167. """Get alert logs for a specific rule."""
  168. @setup_required
  169. @login_required
  170. @account_initialization_required
  171. @get_app_model
  172. def get(self, app_model, rule_id):
  173. """Get alert logs for a rule."""
  174. # Check permission
  175. if app_model.tenant_id != current_user.current_tenant_id:
  176. raise Forbidden()
  177. rule = AlertService.get_alert_rule(rule_id)
  178. if not rule:
  179. raise NotFound("Alert rule not found")
  180. # Verify rule belongs to this app
  181. if rule.get("app_id") != str(app_model.id):
  182. raise Forbidden()
  183. parser = reqparse.RequestParser()
  184. parser.add_argument("limit", type=int, required=False, location="args", default=100)
  185. args = parser.parse_args()
  186. # Validate limit
  187. limit = min(args["limit"], 1000) # Cap at 1000
  188. logs = AlertService.get_alert_logs(rule_id, limit=limit)
  189. return {"data": logs}
  190. class AlertNotificationConfigApi(Resource):
  191. """Get and save notification channel configuration for an app."""
  192. @setup_required
  193. @login_required
  194. @account_initialization_required
  195. @get_app_model
  196. def get(self, app_model):
  197. """Get notification channel configuration."""
  198. # Check permission
  199. if app_model.tenant_id != current_user.current_tenant_id:
  200. raise Forbidden()
  201. config = AlertService.get_notification_config(str(app_model.id))
  202. return config
  203. @setup_required
  204. @login_required
  205. @account_initialization_required
  206. @get_app_model
  207. def post(self, app_model):
  208. """Save notification channel configuration."""
  209. # Check permission
  210. if app_model.tenant_id != current_user.current_tenant_id:
  211. raise Forbidden()
  212. parser = reqparse.RequestParser()
  213. parser.add_argument("email", type=dict, required=False, location="json", default={})
  214. parser.add_argument("dingtalk", type=dict, required=False, location="json", default={})
  215. parser.add_argument("wecom", type=dict, required=False, location="json", default={})
  216. args = parser.parse_args()
  217. try:
  218. config = AlertService.save_notification_config(str(app_model.id), args)
  219. return config
  220. except ValueError as e:
  221. return {"code": "invalid_param", "message": str(e)}, 400
  222. # Register API endpoints
  223. api.add_resource(AlertRuleListApi, "/apps/<uuid:app_id>/alert-rules")
  224. api.add_resource(AlertRuleDetailApi, "/apps/<uuid:app_id>/alert-rules/<string:rule_id>")
  225. api.add_resource(AlertRuleLogsApi, "/apps/<uuid:app_id>/alert-rules/<string:rule_id>/logs")
  226. api.add_resource(AlertNotificationConfigApi, "/apps/<uuid:app_id>/alert-notification-config")