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