Files
Tomas DvorakandDevin 2f4de543a9 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>
2026-09-24 19:09:19 +02:00

98 lines
2.6 KiB
Go

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)
}