Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions beszel/internal/alerts/alerts_history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package alerts

import (
"time"

"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)

func (am *AlertManager) RecordAlertHistory(alert SystemAlertData) {
// Get alert, user, system, name, value
alertId := alert.alertRecord.Id
userId := ""
if errs := am.hub.ExpandRecord(alert.alertRecord, []string{"user"}, nil); len(errs) == 0 {
if user := alert.alertRecord.ExpandedOne("user"); user != nil {
userId = user.Id
}
}
systemId := alert.systemRecord.Id
name := alert.name
value := alert.val
now := time.Now().UTC()

if alert.triggered {
// Create new alerts_history record
collection, err := am.hub.FindCollectionByNameOrId("alerts_history")
if err == nil {
history := core.NewRecord(collection)
history.Set("alert", alertId)
history.Set("user", userId)
history.Set("system", systemId)
history.Set("name", name)
history.Set("value", value)
history.Set("state", "active")
history.Set("created_date", now)
history.Set("solved_date", nil)
_ = am.hub.Save(history)
}
} else {
// Find latest active alerts_history record for this alert and set to solved
record, err := am.hub.FindFirstRecordByFilter(
"alerts_history",
"alert={:alert} && state='active'",
dbx.Params{"alert": alertId},
)
if err == nil && record != nil {
record.Set("state", "solved")
record.Set("solved_date", now)
_ = am.hub.Save(record)
}
}
}

// DeleteOldAlertHistory deletes alerts_history records older than the given retention duration
func (am *AlertManager) DeleteOldAlertHistory(retention time.Duration) {
now := time.Now().UTC()
cutoff := now.Add(-retention)
_, err := am.hub.DB().NewQuery(
"DELETE FROM alerts_history WHERE solved_date IS NOT NULL AND solved_date < {:cutoff}",
).Bind(dbx.Params{"cutoff": cutoff}).Execute()
if err != nil {
am.hub.Logger().Error("failed to delete old alerts_history records", "error", err)
}
}

// Helper to get retention duration from user settings
func getAlertHistoryRetention(settings map[string]interface{}) time.Duration {
retStr, _ := settings["alertHistoryRetention"].(string)
switch retStr {
case "1m":
return 30 * 24 * time.Hour
case "3m":
return 90 * 24 * time.Hour
case "6m":
return 180 * 24 * time.Hour
case "1y":
return 365 * 24 * time.Hour
default:
return 90 * 24 * time.Hour // default 3 months
}
}

// CleanUpAllAlertHistory deletes old alerts_history records for each user based on their retention setting
func (am *AlertManager) CleanUpAllAlertHistory() {
records, err := am.hub.FindAllRecords("user_settings")
if err != nil {
return
}
for _, record := range records {
var settings map[string]interface{}
if err := record.UnmarshalJSONField("settings", &settings); err != nil {
continue
}
am.DeleteOldAlertHistory(getAlertHistoryRetention(settings))
}
}
4 changes: 4 additions & 0 deletions beszel/internal/alerts/alerts_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
// app.Logger().Error("failed to save alert record", "err", err)
return
}

// Create Alert History
am.RecordAlertHistory(alert)

// expand the user relation and send the alert
if errs := am.hub.ExpandRecord(alert.alertRecord, []string{"user"}, nil); len(errs) > 0 {
// app.Logger().Error("failed to expand user relation", "errs", errs)
Expand Down
3 changes: 3 additions & 0 deletions beszel/internal/hub/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
h.Cron().MustAdd("delete old records", "8 * * * *", h.rm.DeleteOldRecords)
// create longer records every 10 minutes
h.Cron().MustAdd("create longer records", "*/10 * * * *", h.rm.CreateLongerRecords)
// delete old alert history records for each user based on their retention setting
h.Cron().MustAdd("delete old alerts_history", "5 */1 * * *", h.AlertManager.CleanUpAllAlertHistory)

return nil
}

Expand Down
3 changes: 3 additions & 0 deletions beszel/internal/users/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ type UserSettings struct {
// UnitTemp uint8 `json:"unitTemp"` // 0 for Celsius, 1 for Fahrenheit
// UnitNet uint8 `json:"unitNet"` // 0 for bytes, 1 for bits
// UnitDisk uint8 `json:"unitDisk"` // 0 for bytes, 1 for bits

// New field for alert history retention (e.g., "1m", "3m", "6m", "1y")
AlertHistoryRetention string `json:"alertHistoryRetention,omitempty"`
}

func NewUserManager(app core.App) *UserManager {
Expand Down
74 changes: 74 additions & 0 deletions beszel/migrations/1_create_alerts_history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package migrations

import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)

func init() {
m.Register(func(app core.App) error {
jsonData := `[
{
"name": "alerts_history",
"type": "base",
"system": false,
"listRule": "",
"deleteRule": "",
"viewRule": ""
"fields": [
{
"name": "alert",
"type": "relation",
"required": true,
"collectionId": "elngm8x1l60zi2v",
"cascadeDelete": true,
"maxSelect": 1
},
{
"name": "user",
"type": "relation",
"required": true,
"collectionId": "_pb_users_auth_",
"cascadeDelete": true,
"maxSelect": 1
},
{
"name": "system",
"type": "relation",
"required": true,
"collectionId": "2hz5ncl8tizk5nx",
"cascadeDelete": true,
"maxSelect": 1
},
{
"name": "name",
"type": "text",
"required": true
},
{
"name": "value",
"type": "number",
"required": true
},
{
"name": "state",
"type": "select",
"required": true,
"values": ["active", "solved"]
},
{
"name": "created_date",
"type": "date",
"required": true
},
{
"name": "solved_date",
"type": "date",
"required": false
}
]
}
]`
return app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false)
}, nil)
}
Loading
Loading