refactor(alert): extract shared HTTPProvider for webhook-based alerts

Discord, Slack, and Webhook providers now use a single HTTPProvider
struct with a PayloadFunc for the only part that differs. Centralizes
response body handling and adds HTTP status code checking (4xx/5xx
now return errors instead of being silently ignored).

Email and Ntfy keep separate implementations (different protocols).
Adding a new HTTP-based alert provider is now a one-line PayloadFunc.
This commit is contained in:
2026-05-15 00:46:05 -04:00
parent a6bb9a7aff
commit d6f33a4d1f
+41 -60
View File
@@ -17,15 +17,49 @@ type Provider interface {
Send(title, message string) error Send(title, message string) error
} }
type PayloadFunc func(title, message string) ([]byte, error)
type HTTPProvider struct {
URL string
Payload PayloadFunc
}
func (h *HTTPProvider) Send(title, message string) error {
body, err := h.Payload(title, message)
if err != nil {
return err
}
resp, err := alertClient.Post(h.URL, "application/json", bytes.NewBuffer(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("alert webhook returned HTTP %d", resp.StatusCode)
}
return nil
}
func discordPayload(title, message string) ([]byte, error) {
return json.Marshal(map[string]string{"content": fmt.Sprintf("**%s**\n%s", title, message)})
}
func slackPayload(title, message string) ([]byte, error) {
return json.Marshal(map[string]string{"text": fmt.Sprintf("*%s*\n%s", title, message)})
}
func webhookPayload(title, message string) ([]byte, error) {
return json.Marshal(map[string]string{"title": title, "message": message, "status": "alert"})
}
func GetProvider(cfg models.AlertConfig) Provider { func GetProvider(cfg models.AlertConfig) Provider {
switch cfg.Type { switch cfg.Type {
case "discord": case "discord":
return &DiscordProvider{URL: cfg.Settings["url"]} return &HTTPProvider{URL: cfg.Settings["url"], Payload: discordPayload}
case "slack": case "slack":
return &SlackProvider{URL: cfg.Settings["url"]} return &HTTPProvider{URL: cfg.Settings["url"], Payload: slackPayload}
case "webhook": case "webhook":
// Generic Webhook return &HTTPProvider{URL: cfg.Settings["url"], Payload: webhookPayload}
return &WebhookProvider{URL: cfg.Settings["url"]}
case "email": case "email":
port := "25" port := "25"
if p, ok := cfg.Settings["port"]; ok { if p, ok := cfg.Settings["port"]; ok {
@@ -56,62 +90,6 @@ func GetProvider(cfg models.AlertConfig) Provider {
} }
} }
// --- DISCORD ---
type DiscordProvider struct{ URL string }
func (d *DiscordProvider) Send(title, message string) error {
payload := map[string]string{"content": fmt.Sprintf("**%s**\n%s", title, message)}
jsonValue, err := json.Marshal(payload)
if err != nil {
return err
}
resp, err := alertClient.Post(d.URL, "application/json", bytes.NewBuffer(jsonValue))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// --- SLACK ---
type SlackProvider struct{ URL string }
func (s *SlackProvider) Send(title, message string) error {
payload := map[string]string{"text": fmt.Sprintf("*%s*\n%s", title, message)}
jsonValue, err := json.Marshal(payload)
if err != nil {
return err
}
resp, err := alertClient.Post(s.URL, "application/json", bytes.NewBuffer(jsonValue))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// --- GENERIC WEBHOOK ---
type WebhookProvider struct{ URL string }
func (w *WebhookProvider) Send(title, message string) error {
payload := map[string]string{
"title": title,
"message": message,
"status": "alert",
}
jsonValue, err := json.Marshal(payload)
if err != nil {
return err
}
resp, err := alertClient.Post(w.URL, "application/json", bytes.NewBuffer(jsonValue))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// --- EMAIL ---
type EmailProvider struct { type EmailProvider struct {
Host, Port, User, Pass, To, From string Host, Port, User, Pass, To, From string
} }
@@ -149,5 +127,8 @@ func (n *NtfyProvider) Send(title, message string) error {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("ntfy returned HTTP %d", resp.StatusCode)
}
return nil return nil
} }