From 2e48aa55601964e49698c9d75a5391e77aa71273 Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Sun, 14 Jul 2024 22:26:13 -0400 Subject: [PATCH] add alert collection and status alerts --- main.go | 88 +++++++++++++++++-- migrations/1720568457_collections_snapshot.go | 64 ++++++++++++++ types.go | 6 ++ 3 files changed, 149 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 0645bdb..c5c8e87 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( _ "monitor-site/migrations" "net/http" "net/http/httputil" + "net/mail" "net/url" "os" "strings" @@ -23,6 +24,7 @@ import ( "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/plugins/migratecmd" "github.com/pocketbase/pocketbase/tools/cron" + "github.com/pocketbase/pocketbase/tools/mailer" "golang.org/x/crypto/ssh" ) @@ -134,25 +136,30 @@ func main() { }) // immediately create connection for new servers - app.OnRecordAfterCreateRequest("systems").Add(func(e *core.RecordCreateEvent) error { - go updateServer(e.Record) + app.OnModelAfterCreate("systems").Add(func(e *core.ModelEvent) error { + go updateServer(e.Model.(*models.Record)) return nil }) // do things after a systems record is updated - app.OnRecordAfterUpdateRequest("systems").Add(func(e *core.RecordUpdateEvent) error { - status := e.Record.Get("status") - // if server connection exists, close it - if status == "down" || status == "paused" { - deleteServerConnection(e.Record) + app.OnModelAfterUpdate("systems").Add(func(e *core.ModelEvent) error { + newRecord := e.Model.(*models.Record) + oldRecord := newRecord.OriginalCopy() + newStatus := newRecord.Get("status").(string) + + // if server is disconnected and connection exists, remove it + if newStatus == "down" || newStatus == "paused" { + deleteServerConnection(newRecord) } + // alerts + handleStatusAlerts(newStatus, oldRecord) return nil }) // do things after a systems record is deleted - app.OnRecordAfterDeleteRequest("systems").Add(func(e *core.RecordDeleteEvent) error { + app.OnModelAfterDelete("systems").Add(func(e *core.ModelEvent) error { // if server connection exists, close it - deleteServerConnection(e.Record) + deleteServerConnection(e.Model.(*models.Record)) return nil }) @@ -327,6 +334,69 @@ func requestJson(server *Server) (SystemData, error) { return systemData, nil } +func sendAlert(data EmailData) { + message := &mailer.Message{ + From: mail.Address{ + Address: app.Settings().Meta.SenderAddress, + Name: app.Settings().Meta.SenderName, + }, + To: []mail.Address{{Address: data.to}}, + Subject: data.subj, + Text: data.body, + } + if err := app.NewMailClient().Send(message); err != nil { + app.Logger().Error("Failed to send alert: ", "err", err.Error()) + } +} + +func handleStatusAlerts(newStatus string, oldRecord *models.Record) error { + var alertStatus string + switch newStatus { + case "up": + if oldRecord.Get("status") == "down" { + alertStatus = "up" + } + case "down": + if oldRecord.Get("status") == "up" { + alertStatus = "down" + } + } + if alertStatus == "" { + return nil + } + alerts, err := app.Dao().FindRecordsByFilter("alerts", "name = 'status' && system = {:system}", "-created", -1, 0, dbx.Params{ + "system": oldRecord.Get("id")}) + if err != nil { + fmt.Println("failed to get users", "err", err.Error()) + return nil + } + if len(alerts) == 0 { + return nil + } + // expand the user relation + if errs := app.Dao().ExpandRecords(alerts, []string{"user"}, nil); len(errs) > 0 { + return fmt.Errorf("failed to expand: %v", errs) + } + systemName := oldRecord.Get("name").(string) + emoji := "\U0001F534" + if alertStatus == "up" { + emoji = "\u2705" + } + for _, alert := range alerts { + user := alert.ExpandedOne("user") + if user == nil { + continue + } + // send alert + sendAlert(EmailData{ + to: user.Get("email").(string), + subj: fmt.Sprintf("Server %s is %s %v", systemName, alertStatus, emoji), + body: fmt.Sprintf("Server %s is %s %v", systemName, alertStatus, emoji), + }) + } + return nil +} + func getSSHKey() ([]byte, error) { // check if the key pair already exists existingKey, err := os.ReadFile("./pb_data/id_ed25519") diff --git a/migrations/1720568457_collections_snapshot.go b/migrations/1720568457_collections_snapshot.go index 3e8dbf6..508d5cd 100644 --- a/migrations/1720568457_collections_snapshot.go +++ b/migrations/1720568457_collections_snapshot.go @@ -251,6 +251,70 @@ func init() { "onlyVerified": false, "requireEmail": false } + }, + { + "id": "elngm8x1l60zi2v", + "created": "2024-07-15 01:16:04.044Z", + "updated": "2024-07-15 01:19:11.639Z", + "name": "alerts", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "hn5ly3vi", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "_pb_users_auth_", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "g5sl3jdg", + "name": "system", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "2hz5ncl8tizk5nx", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "zj3ingrv", + "name": "name", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "status" + ] + } + } + ], + "indexes": [], + "listRule": "@request.auth.id != \"\" && user.id = @request.auth.id", + "viewRule": "", + "createRule": "@request.auth.id != \"\" && user.id = @request.auth.id", + "updateRule": null, + "deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id", + "options": {} } ]` diff --git a/types.go b/types.go index 66e7e38..8c599b5 100644 --- a/types.go +++ b/types.go @@ -47,3 +47,9 @@ type ContainerStats struct { Mem float64 `json:"m"` // MemPct float64 `json:"mp"` } + +type EmailData struct { + to string + subj string + body string +}