109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
package alerts
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.wayfinderak.com/wayfinderak/log-guardian/internal/analysis"
|
|
"gitea.wayfinderak.com/wayfinderak/log-guardian/internal/store"
|
|
)
|
|
|
|
type Result struct {
|
|
ChannelID string
|
|
Detail string
|
|
}
|
|
|
|
type Dispatcher struct {
|
|
http *http.Client
|
|
}
|
|
|
|
func NewDispatcher() *Dispatcher { return &Dispatcher{http: &http.Client{Timeout: 15 * time.Second}} }
|
|
|
|
func (d *Dispatcher) Send(ctx context.Context, rule store.Rule, finding analysis.Finding, channels []store.AlertChannel) ([]Result, error) {
|
|
if len(channels) == 0 {
|
|
log.Printf("alert for rule %q: %s", rule.Name, finding.Summary)
|
|
return []Result{{Detail: "no channels configured; logged alert intent"}}, nil
|
|
}
|
|
var results []Result
|
|
var firstErr error
|
|
for _, channel := range channels {
|
|
if !channel.Enabled {
|
|
results = append(results, Result{ChannelID: channel.ID, Detail: "disabled"})
|
|
continue
|
|
}
|
|
var err error
|
|
switch channel.Type {
|
|
case "ntfy":
|
|
err = d.sendNtfy(ctx, rule, finding, channel)
|
|
case "gotify", "generic_webhook", "smtp":
|
|
err = fmt.Errorf("%s delivery is scaffolded but not implemented in phase 1", channel.Type)
|
|
default:
|
|
err = fmt.Errorf("unsupported channel type %q", channel.Type)
|
|
}
|
|
if err != nil {
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
results = append(results, Result{ChannelID: channel.ID, Detail: err.Error()})
|
|
continue
|
|
}
|
|
results = append(results, Result{ChannelID: channel.ID, Detail: "sent"})
|
|
}
|
|
return results, firstErr
|
|
}
|
|
|
|
func (d *Dispatcher) Test(ctx context.Context, channel store.AlertChannel) error {
|
|
finding := analysis.Finding{Summary: "Log Guardian test notification", Confidence: "test"}
|
|
rule := store.Rule{Name: "Test alert", Severity: "info"}
|
|
switch channel.Type {
|
|
case "ntfy":
|
|
return d.sendNtfy(ctx, rule, finding, channel)
|
|
case "gotify", "generic_webhook", "smtp":
|
|
return fmt.Errorf("%s delivery is scaffolded but not implemented in phase 1", channel.Type)
|
|
default:
|
|
return fmt.Errorf("unsupported channel type %q", channel.Type)
|
|
}
|
|
}
|
|
|
|
func (d *Dispatcher) sendNtfy(ctx context.Context, rule store.Rule, finding analysis.Finding, channel store.AlertChannel) error {
|
|
server := strings.TrimRight(channel.Params["server_url"], "/")
|
|
topic := strings.Trim(channel.Params["topic"], "/")
|
|
if server == "" || topic == "" {
|
|
return fmt.Errorf("ntfy requires server_url and topic")
|
|
}
|
|
target, err := url.JoinPath(server, topic)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := rule.Name + ": " + finding.Summary
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewBufferString(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Title", "Log Guardian: "+rule.Severity)
|
|
if priority := channel.Params["priority"]; priority != "" {
|
|
req.Header.Set("Priority", priority)
|
|
}
|
|
if tags := channel.Params["tags"]; tags != "" {
|
|
req.Header.Set("Tags", tags)
|
|
}
|
|
if token := channel.Params["token"]; token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
resp, err := d.http.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
|
return fmt.Errorf("ntfy delivery failed: status %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|