stillbox/pkg/notify/notify.go
2024-11-08 10:13:51 -05:00

219 lines
4.5 KiB
Go

package notify
import (
"bytes"
"context"
"fmt"
stdhttp "net/http"
"text/template"
"time"
"dynatron.me/x/stillbox/internal/common"
"dynatron.me/x/stillbox/pkg/alerting/alert"
"dynatron.me/x/stillbox/pkg/config"
"github.com/go-viper/mapstructure/v2"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/http"
)
type Notifier interface {
Send(ctx context.Context, alerts []alert.Alert) error
}
type backend struct {
*notify.Notify
subject *template.Template
body *template.Template
}
type notifier struct {
backends []backend
}
func highest(a []alert.Alert) string {
if len(a) < 1 {
return "none"
}
top := a[0]
for _, a := range a {
if a.Score.Score > top.Score.Score {
top = a
}
}
return top.TGName
}
var alertFm = template.FuncMap{
"highest": highest,
}
const (
defaultBodyTemplStr = `{{ range . -}}
{{ .TGName }} is active with a score of {{ f .Score.Score 4 }}! ({{ f .Score.RecentCount 0 }}/{{ .Score.Count }} recent calls)
{{ end -}}`
defaultSubjectTemplStr = `Stillbox Alert ({{ highest . }})`
)
var (
defaultTemplate *template.Template
)
func init() {
defaultTemplate = template.New("notification")
defaultTemplate.Funcs(common.FuncMap).Funcs(alertFm)
_, err := defaultTemplate.New("body").Parse(defaultBodyTemplStr)
if err != nil {
panic(err)
}
_, err = defaultTemplate.New("subject").Parse(defaultSubjectTemplStr)
if err != nil {
panic(err)
}
}
// Send renders and sends the Alerts.
func (b *backend) Send(ctx context.Context, alerts []alert.Alert) (err error) {
var subject, body bytes.Buffer
err = b.subject.ExecuteTemplate(&subject, "subject", alerts)
if err != nil {
return err
}
err = b.body.ExecuteTemplate(&body, "body", alerts)
if err != nil {
return err
}
err = b.Notify.Send(ctx, subject.String(), body.String())
if err != nil {
return err
}
return nil
}
func buildSlackWebhookPayload(cfg *slackWebhookConfig) func(string, string) any {
type Attachment struct {
Title string `json:"title"`
Text string `json:"text"`
Fallback string `json:"fallback"`
Footer string `json:"footer"`
TitleLink string `json:"title_link"`
Timestamp int64 `json:"ts"`
}
return func(subject, message string) any {
m := struct {
Username string `json:"username"`
Attachments []Attachment `json:"attachments"`
IconEmoji string `json:"icon_emoji"`
}{
Username: "Stillbox",
Attachments: []Attachment{
{
Title: subject,
Text: message,
TitleLink: cfg.MessageURL,
Timestamp: time.Now().Unix(),
},
},
IconEmoji: cfg.Icon,
}
return m
}
}
type slackWebhookConfig struct {
WebhookURL string `mapstructure:"webhookURL"`
Icon string `mapstructure:"icon"`
MessageURL string `mapstructure:"messageURL"`
SubjectTemplate string `mapstructure:"subjectTemplate"`
BodyTemplate string `mapstructure:"bodyTemplate"`
}
func (n *notifier) addService(cfg config.NotifyService) (err error) {
be := backend{}
switch cfg.SubjectTemplate {
case nil:
be.subject = defaultTemplate.Lookup("subject")
if be.subject == nil {
panic("subject template nil")
}
default:
be.subject, err = template.New("subject").Funcs(common.FuncMap).Funcs(alertFm).Parse(*cfg.SubjectTemplate)
if err != nil {
return err
}
}
switch cfg.BodyTemplate {
case nil:
be.body = defaultTemplate.Lookup("body")
if be.body == nil {
panic("body template nil")
}
default:
be.body, err = template.New("body").Funcs(common.FuncMap).Funcs(alertFm).Parse(*cfg.BodyTemplate)
if err != nil {
return err
}
}
be.Notify = notify.New()
switch cfg.Provider {
case "slackwebhook":
swc := &slackWebhookConfig{
Icon: "🚨",
}
err := mapstructure.Decode(cfg.Config, &swc)
if err != nil {
return err
}
hs := http.New()
hs.AddReceivers(&http.Webhook{
ContentType: "application/json",
Header: make(stdhttp.Header),
Method: stdhttp.MethodPost,
URL: swc.WebhookURL,
BuildPayload: buildSlackWebhookPayload(swc),
})
be.UseServices(hs)
default:
return fmt.Errorf("unknown provider '%s'", cfg.Provider)
}
n.backends = append(n.backends, be)
return nil
}
func (n *notifier) Send(ctx context.Context, alerts []alert.Alert) error {
for _, be := range n.backends {
err := be.Send(ctx, alerts)
if err != nil {
return err
}
}
return nil
}
func New(cfg config.Notify) (Notifier, error) {
n := new(notifier)
for _, s := range cfg {
err := n.addService(s)
if err != nil {
return nil, err
}
}
return n, nil
}