diff --git a/go.mod b/go.mod index 2c746d9..2dcaf3d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module monitor-site +module beszel go 1.22.4 diff --git a/main.go b/main.go index 6b11e1a..7d95374 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + _ "beszel/migrations" "bytes" "crypto/ed25519" "encoding/json" @@ -8,7 +9,6 @@ import ( "errors" "fmt" "log" - _ "monitor-site/migrations" "net/http" "net/http/httputil" "net/mail" @@ -82,39 +82,21 @@ func main() { return nil }) - // set up cron job to delete records older than 30 days + // set up cron jobs app.OnBeforeServe().Add(func(e *core.ServeEvent) error { scheduler := cron.New() - scheduler.MustAdd("delete old records", "* 2 * * *", func() { - // log.Println("Deleting old records...") - // Get the current time - now := time.Now().UTC() - // Subtract one month - oneMonthAgo := now.AddDate(0, 0, -30) - // Format the time as a string - timeString := oneMonthAgo.Format("2006-01-02 15:04:05") - // collections to be cleaned - collections := []string{"system_stats", "container_stats"} - - for _, collection := range collections { - records, err := app.Dao().FindRecordsByFilter( - collection, - fmt.Sprintf("created <= \"%s\"", timeString), // filter - "", // sort - -1, // limit - 0, // offset - ) - if err != nil { - log.Println(err) - return - } - // delete records - for _, record := range records { - if err := app.Dao().DeleteRecord(record); err != nil { - log.Fatal(err) - } - } - } + // delete records that are older than the display period + scheduler.MustAdd("delete old records", "0 */2 * * *", func() { + deleteOldRecords("system_stats", "1m", time.Hour) + deleteOldRecords("container_stats", "1m", time.Hour) + deleteOldRecords("system_stats", "10m", 12*time.Hour) + deleteOldRecords("container_stats", "10m", 12*time.Hour) + deleteOldRecords("system_stats", "20m", 24*time.Hour) + deleteOldRecords("container_stats", "20m", 24*time.Hour) + deleteOldRecords("system_stats", "120m", 7*24*time.Hour) + deleteOldRecords("container_stats", "120m", 7*24*time.Hour) + deleteOldRecords("system_stats", "480m", 30*24*time.Hour) + deleteOldRecords("container_stats", "480m", 30*24*time.Hour) }) scheduler.Start() return nil @@ -176,10 +158,9 @@ func main() { } // if server is set to pending (unpause), try to connect immediately - // commenting out because we don't want to get off of the one min schedule - // if newStatus == "pending" { - // go updateSystem(newRecord) - // } + if newStatus == "pending" { + go updateSystem(newRecord) + } // alerts handleStatusAlerts(newStatus, oldRecord) @@ -193,6 +174,12 @@ func main() { return nil }) + app.OnModelAfterCreate("system_stats").Add(func(e *core.ModelEvent) error { + createLongerRecords(e.Model.(*models.Record)) + // createLongerRecords(e.Model.(*models.Record).OriginalCopy(), e.Model.(*models.Record)) + return nil + }) + if err := app.Start(); err != nil { log.Fatal(err) } diff --git a/readme.md b/readme.md index 093b18c..eca7776 100644 --- a/readme.md +++ b/readme.md @@ -44,13 +44,13 @@ The hub and agent are distributed as single binary files, as well as docker imag ## Environment Variables -## Hub +### Hub | Name | Default | Description | | ----------------------- | ------- | -------------------------------- | | `DISABLE_PASSWORD_AUTH` | false | Disables password authentication | -## Agent +### Agent | Name | Default | Description | | ------------ | ------- | ------------------------------------------------ | diff --git a/records.go b/records.go new file mode 100644 index 0000000..80a53a4 --- /dev/null +++ b/records.go @@ -0,0 +1,158 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "math" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/types" +) + +func createLongerRecords(shorterRecord *models.Record) { + shorterRecordType := shorterRecord.Get("type").(string) + systemId := shorterRecord.Get("system").(string) + // fmt.Println("create longer records", "recordType", shorterRecordType, "systemId", systemId) + var longerRecordType string + var timeAgo time.Duration + var expectedShorterRecords int + switch shorterRecordType { + case "1m": + longerRecordType = "10m" + timeAgo = -10 * time.Minute + expectedShorterRecords = 10 + case "10m": + longerRecordType = "20m" + timeAgo = -20 * time.Minute + expectedShorterRecords = 2 + case "20m": + longerRecordType = "120m" + timeAgo = -120 * time.Minute + expectedShorterRecords = 6 + default: + longerRecordType = "480m" + timeAgo = -480 * time.Minute + expectedShorterRecords = 4 + } + + longerRecordPeriod := time.Now().UTC().Add(timeAgo + 10*time.Second).Format("2006-01-02 15:04:05") + // check creation time of last 10m record + lastLongerRecord, err := app.Dao().FindFirstRecordByFilter( + "system_stats", + "type = {:type} && system = {:system} && created > {:created}", + dbx.Params{"type": longerRecordType, "system": systemId, "created": longerRecordPeriod}, + ) + // return if longer record exists + if err == nil || lastLongerRecord != nil { + // log.Println("longer record found. returning") + return + } + // get shorter records from the past x minutes + // shorterRecordPeriod := time.Now().UTC().Add(timeAgo + time.Second).Format("2006-01-02 15:04:05") + allShorterRecords, err := app.Dao().FindRecordsByFilter( + "system_stats", + "type = {:type} && system = {:system} && created > {:created}", + "-created", + -1, + 0, + dbx.Params{"type": shorterRecordType, "system": systemId, "created": longerRecordPeriod}, + ) + // return if not enough shorter records + if err != nil || len(allShorterRecords) < expectedShorterRecords { + // log.Println("not enough shorter records. returning") + return + } + // average the shorter records and create 10m record + averagedStats := averageSystemStats(allShorterRecords) + collection, _ := app.Dao().FindCollectionByNameOrId("system_stats") + tenMinRecord := models.NewRecord(collection) + tenMinRecord.Set("system", systemId) + tenMinRecord.Set("stats", averagedStats) + tenMinRecord.Set("type", longerRecordType) + if err := app.Dao().SaveRecord(tenMinRecord); err != nil { + fmt.Println("failed to save longer record", "err", err.Error()) + } + +} + +// func averageSystemStats(records []*models.Record) SystemStats { +// numStats := len(records) +// firstStats := records[0].Get("stats").(SystemStats) +// sum := reflect.New(reflect.TypeOf(firstStats)).Elem() + +// for _, record := range records { +// stats := record.Get("stats").(SystemStats) +// statValue := reflect.ValueOf(stats) +// for i := 0; i < statValue.NumField(); i++ { +// field := sum.Field(i) +// field.SetFloat(field.Float() + statValue.Field(i).Float()) +// } +// } + +// average := reflect.New(reflect.TypeOf(firstStats)).Elem() +// for i := 0; i < sum.NumField(); i++ { +// average.Field(i).SetFloat(twoDecimals(sum.Field(i).Float() / float64(numStats))) +// } + +// return average.Interface().(SystemStats) +// } + +func averageSystemStats(records []*models.Record) SystemStats { + var sum SystemStats + count := float64(len(records)) + + for _, record := range records { + var stats SystemStats + json.Unmarshal([]byte(record.Get("stats").(types.JsonRaw)), &stats) + sum.Cpu += stats.Cpu + sum.Mem += stats.Mem + sum.MemUsed += stats.MemUsed + sum.MemPct += stats.MemPct + sum.MemBuffCache += stats.MemBuffCache + sum.Disk += stats.Disk + sum.DiskUsed += stats.DiskUsed + sum.DiskPct += stats.DiskPct + sum.DiskRead += stats.DiskRead + sum.DiskWrite += stats.DiskWrite + sum.NetworkSent += stats.NetworkSent + sum.NetworkRecv += stats.NetworkRecv + } + + return SystemStats{ + Cpu: twoDecimals(sum.Cpu / count), + Mem: twoDecimals(sum.Mem / count), + MemUsed: twoDecimals(sum.MemUsed / count), + MemPct: twoDecimals(sum.MemPct / count), + MemBuffCache: twoDecimals(sum.MemBuffCache / count), + Disk: twoDecimals(sum.Disk / count), + DiskUsed: twoDecimals(sum.DiskUsed / count), + DiskPct: twoDecimals(sum.DiskPct / count), + DiskRead: twoDecimals(sum.DiskRead / count), + DiskWrite: twoDecimals(sum.DiskWrite / count), + NetworkSent: twoDecimals(sum.NetworkSent / count), + NetworkRecv: twoDecimals(sum.NetworkRecv / count), + } +} + +/* Round float to two decimals */ +func twoDecimals(value float64) float64 { + return math.Round(value*100) / 100 +} + +/* Delete records of specified collection and type that are older than timeLimit */ +func deleteOldRecords(collection string, recordType string, timeLimit time.Duration) { + log.Println("Deleting old", recordType, "records...") + timeLimitStamp := time.Now().UTC().Add(timeLimit).Format("2006-01-02 15:04:05") + records, _ := app.Dao().FindRecordsByExpr(collection, + dbx.NewExp("type = {:type}", dbx.Params{"type": recordType}), + dbx.NewExp("created < {:created}", dbx.Params{"created": timeLimitStamp}), + ) + for _, record := range records { + if err := app.Dao().DeleteRecord(record); err != nil { + log.Fatal(err) + } + } +} diff --git a/site/src/components/command-palette.tsx b/site/src/components/command-palette.tsx index 16eba21..bb91d28 100644 --- a/site/src/components/command-palette.tsx +++ b/site/src/components/command-palette.tsx @@ -5,6 +5,8 @@ import { DatabaseBackupIcon, Github, LayoutDashboard, + LockKeyholeIcon, + LogsIcon, MailIcon, Server, } from 'lucide-react' @@ -97,6 +99,15 @@ export default function CommandPalette() { PocketBase Admin + { + window.location.href = '/_/#/logs' + }} + > + + Logs + Admin + { window.location.href = '/_/#/settings/backups' @@ -106,6 +117,16 @@ export default function CommandPalette() { Database backups Admin + { + window.location.href = '/_/#/settings/auth-providers' + }} + > + + Auth Providers + Admin + {