stillbox/pkg/notify/notify.go

220 lines
4.5 KiB
Go
Raw Normal View History

2024-10-31 00:10:53 -04:00
package notify
import (
2024-11-07 23:17:16 -05:00
"bytes"
"context"
2024-10-31 00:10:53 -04:00
"fmt"
stdhttp "net/http"
2024-11-07 23:17:16 -05:00
"text/template"
2024-10-31 00:10:53 -04:00
"time"
2024-11-07 23:17:16 -05:00
"dynatron.me/x/stillbox/internal/common"
"dynatron.me/x/stillbox/pkg/alerting/alert"
2024-11-03 07:19:03 -05:00
"dynatron.me/x/stillbox/pkg/config"
2024-10-31 00:10:53 -04:00
2024-11-03 19:49:10 -05:00
"github.com/go-viper/mapstructure/v2"
2024-10-31 00:10:53 -04:00
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/http"
)
type Notifier interface {
2024-11-07 23:17:16 -05:00
Send(ctx context.Context, alerts []alert.Alert) error
2024-10-31 00:10:53 -04:00
}
2024-11-07 23:17:16 -05:00
type backend struct {
2024-10-31 00:10:53 -04:00
*notify.Notify
2024-11-07 23:17:16 -05:00
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,
2024-10-31 00:10:53 -04:00
}
2024-11-08 07:19:13 -05:00
const (
defaultBodyTemplStr = `{{ range . -}}
2024-11-07 23:17:16 -05:00
{{ .TGName }} is active with a score of {{ f .Score.Score 4 }}! ({{ f .Score.RecentCount 0 }}/{{ .Score.Count }} recent calls)
{{ end -}}`
2024-11-08 10:13:51 -05:00
defaultSubjectTemplStr = `Stillbox Alert ({{ highest . }})`
2024-11-08 07:19:13 -05:00
)
var (
defaultTemplate *template.Template
)
2024-11-07 23:17:16 -05:00
2024-11-08 07:19:13 -05:00
func init() {
defaultTemplate = template.New("notification")
defaultTemplate.Funcs(common.FuncMap).Funcs(alertFm)
_, err := defaultTemplate.New("body").Parse(defaultBodyTemplStr)
if err != nil {
panic(err)
}
2024-11-07 23:17:16 -05:00
2024-11-08 07:19:13 -05:00
_, err = defaultTemplate.New("subject").Parse(defaultSubjectTemplStr)
if err != nil {
panic(err)
}
}
2024-11-07 23:17:16 -05:00
// 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
}
2024-10-31 00:10:53 -04:00
2024-11-07 23:17:16 -05:00
func buildSlackWebhookPayload(cfg *slackWebhookConfig) func(string, string) any {
2024-10-31 00:10:53 -04:00
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,
2024-11-03 19:49:10 -05:00
TitleLink: cfg.MessageURL,
2024-10-31 00:10:53 -04:00
Timestamp: time.Now().Unix(),
},
},
2024-11-03 19:49:10 -05:00
IconEmoji: cfg.Icon,
2024-10-31 00:10:53 -04:00
}
return m
}
}
2024-11-03 19:49:10 -05:00
type slackWebhookConfig struct {
2024-11-07 23:17:16 -05:00
WebhookURL string `mapstructure:"webhookURL"`
Icon string `mapstructure:"icon"`
MessageURL string `mapstructure:"messageURL"`
SubjectTemplate string `mapstructure:"subjectTemplate"`
BodyTemplate string `mapstructure:"bodyTemplate"`
2024-11-03 19:49:10 -05:00
}
2024-11-07 23:17:16 -05:00
func (n *notifier) addService(cfg config.NotifyService) (err error) {
be := backend{}
switch cfg.SubjectTemplate {
case nil:
2024-11-08 07:19:13 -05:00
be.subject = defaultTemplate.Lookup("subject")
if be.subject == nil {
panic("subject template nil")
}
2024-11-07 23:17:16 -05:00
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:
2024-11-08 07:19:13 -05:00
be.body = defaultTemplate.Lookup("body")
if be.body == nil {
panic("body template nil")
}
2024-11-07 23:17:16 -05:00
default:
be.body, err = template.New("body").Funcs(common.FuncMap).Funcs(alertFm).Parse(*cfg.BodyTemplate)
if err != nil {
return err
}
}
be.Notify = notify.New()
2024-10-31 00:10:53 -04:00
switch cfg.Provider {
case "slackwebhook":
2024-11-03 19:49:10 -05:00
swc := &slackWebhookConfig{
Icon: "🚨",
}
err := mapstructure.Decode(cfg.Config, &swc)
if err != nil {
return err
}
2024-10-31 00:10:53 -04:00
hs := http.New()
hs.AddReceivers(&http.Webhook{
ContentType: "application/json",
Header: make(stdhttp.Header),
Method: stdhttp.MethodPost,
2024-11-03 19:49:10 -05:00
URL: swc.WebhookURL,
2024-11-07 23:17:16 -05:00
BuildPayload: buildSlackWebhookPayload(swc),
2024-10-31 00:10:53 -04:00
})
2024-11-07 23:17:16 -05:00
be.UseServices(hs)
2024-10-31 00:10:53 -04:00
default:
return fmt.Errorf("unknown provider '%s'", cfg.Provider)
}
2024-11-07 23:17:16 -05:00
n.backends = append(n.backends, be)
2024-10-31 00:10:53 -04:00
return nil
}
2024-11-07 23:17:16 -05:00
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
}
2024-10-31 00:10:53 -04:00
}
2024-11-07 23:17:16 -05:00
return nil
}
func New(cfg config.Notify) (Notifier, error) {
n := new(notifier)
2024-10-31 00:10:53 -04:00
for _, s := range cfg {
err := n.addService(s)
if err != nil {
return nil, err
}
}
return n, nil
}