mirror of
https://github.com/fankes/beszel.git
synced 2025-10-18 17:29:28 +08:00
221 lines
5.9 KiB
Go
221 lines
5.9 KiB
Go
// Package alerts handles alert management and delivery.
|
|
package alerts
|
|
|
|
import (
|
|
"fmt"
|
|
"net/mail"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/nicholas-fedor/shoutrrr"
|
|
"github.com/pocketbase/dbx"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
|
)
|
|
|
|
type hubLike interface {
|
|
core.App
|
|
MakeLink(parts ...string) string
|
|
}
|
|
|
|
type AlertManager struct {
|
|
hub hubLike
|
|
alertQueue chan alertTask
|
|
stopChan chan struct{}
|
|
pendingAlerts sync.Map
|
|
}
|
|
|
|
type AlertMessageData struct {
|
|
UserID string
|
|
Title string
|
|
Message string
|
|
Link string
|
|
LinkText string
|
|
}
|
|
|
|
type UserNotificationSettings struct {
|
|
Emails []string `json:"emails"`
|
|
Webhooks []string `json:"webhooks"`
|
|
}
|
|
|
|
type SystemAlertStats struct {
|
|
Cpu float64 `json:"cpu"`
|
|
Mem float64 `json:"mp"`
|
|
Disk float64 `json:"dp"`
|
|
NetSent float64 `json:"ns"`
|
|
NetRecv float64 `json:"nr"`
|
|
Temperatures map[string]float32 `json:"t"`
|
|
LoadAvg [3]float64 `json:"la"`
|
|
}
|
|
|
|
type SystemAlertData struct {
|
|
systemRecord *core.Record
|
|
alertRecord *core.Record
|
|
name string
|
|
unit string
|
|
val float64
|
|
threshold float64
|
|
triggered bool
|
|
time time.Time
|
|
count uint8
|
|
min uint8
|
|
mapSums map[string]float32
|
|
descriptor string // override descriptor in notification body (for temp sensor, disk partition, etc)
|
|
}
|
|
|
|
// notification services that support title param
|
|
var supportsTitle = map[string]struct{}{
|
|
"bark": {},
|
|
"discord": {},
|
|
"gotify": {},
|
|
"ifttt": {},
|
|
"join": {},
|
|
"lark": {},
|
|
"matrix": {},
|
|
"ntfy": {},
|
|
"opsgenie": {},
|
|
"pushbullet": {},
|
|
"pushover": {},
|
|
"slack": {},
|
|
"teams": {},
|
|
"telegram": {},
|
|
"zulip": {},
|
|
}
|
|
|
|
// NewAlertManager creates a new AlertManager instance.
|
|
func NewAlertManager(app hubLike) *AlertManager {
|
|
am := &AlertManager{
|
|
hub: app,
|
|
alertQueue: make(chan alertTask, 5),
|
|
stopChan: make(chan struct{}),
|
|
}
|
|
am.bindEvents()
|
|
go am.startWorker()
|
|
return am
|
|
}
|
|
|
|
// Bind events to the alerts collection lifecycle
|
|
func (am *AlertManager) bindEvents() {
|
|
am.hub.OnRecordAfterUpdateSuccess("alerts").BindFunc(updateHistoryOnAlertUpdate)
|
|
am.hub.OnRecordAfterDeleteSuccess("alerts").BindFunc(resolveHistoryOnAlertDelete)
|
|
}
|
|
|
|
// SendAlert sends an alert to the user
|
|
func (am *AlertManager) SendAlert(data AlertMessageData) error {
|
|
// get user settings
|
|
record, err := am.hub.FindFirstRecordByFilter(
|
|
"user_settings", "user={:user}",
|
|
dbx.Params{"user": data.UserID},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// unmarshal user settings
|
|
userAlertSettings := UserNotificationSettings{
|
|
Emails: []string{},
|
|
Webhooks: []string{},
|
|
}
|
|
if err := record.UnmarshalJSONField("settings", &userAlertSettings); err != nil {
|
|
am.hub.Logger().Error("Failed to unmarshal user settings", "err", err)
|
|
}
|
|
// send alerts via webhooks
|
|
for _, webhook := range userAlertSettings.Webhooks {
|
|
if err := am.SendShoutrrrAlert(webhook, data.Title, data.Message, data.Link, data.LinkText); err != nil {
|
|
am.hub.Logger().Error("Failed to send shoutrrr alert", "err", err)
|
|
}
|
|
}
|
|
// send alerts via email
|
|
if len(userAlertSettings.Emails) == 0 {
|
|
return nil
|
|
}
|
|
addresses := []mail.Address{}
|
|
for _, email := range userAlertSettings.Emails {
|
|
addresses = append(addresses, mail.Address{Address: email})
|
|
}
|
|
message := mailer.Message{
|
|
To: addresses,
|
|
Subject: data.Title,
|
|
Text: data.Message + fmt.Sprintf("\n\n%s", data.Link),
|
|
From: mail.Address{
|
|
Address: am.hub.Settings().Meta.SenderAddress,
|
|
Name: am.hub.Settings().Meta.SenderName,
|
|
},
|
|
}
|
|
err = am.hub.NewMailClient().Send(&message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
am.hub.Logger().Info("Sent email alert", "to", message.To, "subj", message.Subject)
|
|
return nil
|
|
}
|
|
|
|
// SendShoutrrrAlert sends an alert via a Shoutrrr URL
|
|
func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link, linkText string) error {
|
|
// Parse the URL
|
|
parsedURL, err := url.Parse(notificationUrl)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing URL: %v", err)
|
|
}
|
|
scheme := parsedURL.Scheme
|
|
queryParams := parsedURL.Query()
|
|
|
|
// Add title
|
|
if _, ok := supportsTitle[scheme]; ok {
|
|
queryParams.Add("title", title)
|
|
} else if scheme == "mattermost" {
|
|
// use markdown title for mattermost
|
|
message = "##### " + title + "\n\n" + message
|
|
} else if scheme == "generic" && queryParams.Has("template") {
|
|
// add title as property if using generic with template json
|
|
titleKey := queryParams.Get("titlekey")
|
|
if titleKey == "" {
|
|
titleKey = "title"
|
|
}
|
|
queryParams.Add("$"+titleKey, title)
|
|
} else {
|
|
// otherwise just add title to message
|
|
message = title + "\n\n" + message
|
|
}
|
|
|
|
// Add link
|
|
if scheme == "ntfy" {
|
|
queryParams.Add("Actions", fmt.Sprintf("view, %s, %s", linkText, link))
|
|
} else if scheme == "lark" {
|
|
queryParams.Add("link", link)
|
|
} else if scheme == "bark" {
|
|
queryParams.Add("url", link)
|
|
} else {
|
|
message += "\n\n" + link
|
|
}
|
|
|
|
// Encode the modified query parameters back into the URL
|
|
parsedURL.RawQuery = queryParams.Encode()
|
|
// log.Println("URL after modification:", parsedURL.String())
|
|
|
|
err = shoutrrr.Send(parsedURL.String(), message)
|
|
|
|
if err == nil {
|
|
am.hub.Logger().Info("Sent shoutrrr alert", "title", title)
|
|
} else {
|
|
am.hub.Logger().Error("Error sending shoutrrr alert", "err", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (am *AlertManager) SendTestNotification(e *core.RequestEvent) error {
|
|
var data struct {
|
|
URL string `json:"url"`
|
|
}
|
|
err := e.BindBody(&data)
|
|
if err != nil || data.URL == "" {
|
|
return e.BadRequestError("URL is required", err)
|
|
}
|
|
err = am.SendShoutrrrAlert(data.URL, "Test Alert", "This is a notification from Beszel.", am.hub.Settings().Meta.AppURL, "View Beszel")
|
|
if err != nil {
|
|
return e.JSON(200, map[string]string{"err": err.Error()})
|
|
}
|
|
return e.JSON(200, map[string]bool{"err": false})
|
|
}
|