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
+
{