mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
feat: telemetry alert rules with webhook delivery
core.alert_rules (migration 00012) — heartbeat_silence and error_spike rules evaluated by a 60s backend ticker. Transition-based firing: alert.fired on breach, alert.resolved on recovery, both first-class webhook event types riding the signed dispatcher + realtime stream. Dashboard manages rules on the Integrations page. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
345cf7918b
commit
2f4de543a9
28 files changed
+1693
-10
No files matched your search
@@ -0,0 +1,97 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"github.com/tdvorak/primora/apps/backend/internal/middleware"
|
||||
apperrors "github.com/tdvorak/primora/apps/backend/internal/response"
|
||||
"github.com/tdvorak/primora/apps/backend/internal/services"
|
||||
)
|
||||
|
||||
func (h *HTTPHandler) alertError(c *gin.Context, err error) {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
apperrors.Abort(c, http.StatusNotFound, "not_found", "alert rule not found")
|
||||
return
|
||||
}
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "invalid") || strings.Contains(msg, "must be") || strings.Contains(msg, "already exists") || strings.Contains(msg, "too long") {
|
||||
apperrors.Abort(c, http.StatusBadRequest, "bad_request", msg)
|
||||
return
|
||||
}
|
||||
h.handleError(c, err)
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) listAlertRules(c *gin.Context) {
|
||||
actor, projectID, ok := h.actorAndProject(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
result, err := h.Platform.ListAlertRules(c.Request.Context(), actor, projectID)
|
||||
if err != nil {
|
||||
h.alertError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": result})
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) createAlertRule(c *gin.Context) {
|
||||
actor, projectID, ok := h.actorAndProject(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body services.UpsertAlertRuleInput
|
||||
if !h.bindAndValidate(c, &body) {
|
||||
return
|
||||
}
|
||||
result, err := h.Platform.CreateAlertRule(c.Request.Context(), actor, projectID, body, middleware.RequestIDFromContext(c))
|
||||
if err != nil {
|
||||
h.alertError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, result)
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) updateAlertRule(c *gin.Context) {
|
||||
actor, projectID, ok := h.actorAndProject(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ruleID, err := uuid.Parse(c.Param("ruleID"))
|
||||
if err != nil {
|
||||
apperrors.Abort(c, http.StatusBadRequest, "bad_request", "invalid alert rule id")
|
||||
return
|
||||
}
|
||||
var body services.UpsertAlertRuleInput
|
||||
if !h.bindAndValidate(c, &body) {
|
||||
return
|
||||
}
|
||||
result, err := h.Platform.UpdateAlertRule(c.Request.Context(), actor, projectID, ruleID, body, middleware.RequestIDFromContext(c))
|
||||
if err != nil {
|
||||
h.alertError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) deleteAlertRule(c *gin.Context) {
|
||||
actor, projectID, ok := h.actorAndProject(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ruleID, err := uuid.Parse(c.Param("ruleID"))
|
||||
if err != nil {
|
||||
apperrors.Abort(c, http.StatusBadRequest, "bad_request", "invalid alert rule id")
|
||||
return
|
||||
}
|
||||
if err := h.Platform.DeleteAlertRule(c.Request.Context(), actor, projectID, ruleID, middleware.RequestIDFromContext(c)); err != nil {
|
||||
h.alertError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -121,6 +121,11 @@ func (h *HTTPHandler) Register(router *gin.Engine) {
|
||||
api.PUT("/projects/:projectID/secrets/:name", h.setProjectSecret)
|
||||
api.DELETE("/projects/:projectID/secrets/:name", h.deleteProjectSecret)
|
||||
api.POST("/projects/:projectID/secrets/:name/reveal", h.revealProjectSecret)
|
||||
|
||||
api.GET("/projects/:projectID/alerts", h.listAlertRules)
|
||||
api.POST("/projects/:projectID/alerts", h.createAlertRule)
|
||||
api.PUT("/projects/:projectID/alerts/:ruleID", h.updateAlertRule)
|
||||
api.DELETE("/projects/:projectID/alerts/:ruleID", h.deleteAlertRule)
|
||||
api.GET("/projects/:projectID/realtime/stream", h.realtimeStream)
|
||||
api.GET("/buckets/:bucketID/objects", h.listObjects)
|
||||
api.POST("/buckets/:bucketID/objects", h.uploadObject)
|
||||
|
||||
Reference in new issue
Block a user