change hub file structure

This commit is contained in:
TOomaAh
2024-08-07 23:59:02 +02:00
parent 8a04a9bed6
commit d840178cc0
107 changed files with 5025 additions and 550 deletions

155
internal/alerts/alerts.go Normal file
View File

@@ -0,0 +1,155 @@
package alerts
import (
"beszel/internal/entities/email"
"beszel/internal/entities/system"
"fmt"
"net/mail"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/mailer"
)
type AlertManager struct {
app *pocketbase.PocketBase
mailClient mailer.Mailer
}
func NewAlertManager(app *pocketbase.PocketBase) *AlertManager {
return &AlertManager{
app: app,
mailClient: app.NewMailClient(),
}
}
func (am *AlertManager) HandleSystemAlerts(newStatus string, newRecord *models.Record, oldRecord *models.Record) {
alertRecords, err := am.app.Dao().FindRecordsByExpr("alerts",
dbx.NewExp("system = {:system}", dbx.Params{"system": oldRecord.GetId()}),
)
if err != nil || len(alertRecords) == 0 {
// log.Println("no alerts found for system")
return
}
// log.Println("found alerts", len(alertRecords))
var systemInfo *system.SystemInfo
for _, alertRecord := range alertRecords {
name := alertRecord.GetString("name")
switch name {
case "Status":
am.handleStatusAlerts(newStatus, oldRecord, alertRecord)
case "CPU", "Memory", "Disk":
if newStatus != "up" {
continue
}
if systemInfo == nil {
systemInfo = getSystemInfo(newRecord)
}
if name == "CPU" {
am.handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.Cpu)
} else if name == "Memory" {
am.handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.MemPct)
} else if name == "Disk" {
am.handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.DiskPct)
}
}
}
}
func getSystemInfo(record *models.Record) *system.SystemInfo {
var SystemInfo system.SystemInfo
record.UnmarshalJSONField("info", &SystemInfo)
return &SystemInfo
}
func (am *AlertManager) handleSlidingValueAlert(newRecord *models.Record, alertRecord *models.Record, name string, curValue float64) {
triggered := alertRecord.GetBool("triggered")
threshold := alertRecord.GetFloat("value")
// fmt.Println(name, curValue, "threshold", threshold, "triggered", triggered)
var subject string
var body string
if !triggered && curValue > threshold {
alertRecord.Set("triggered", true)
systemName := newRecord.GetString("name")
subject = fmt.Sprintf("%s usage threshold exceeded on %s", name, systemName)
body = fmt.Sprintf("%s usage on %s is %.1f%%.\n\n%s\n\n- Beszel", name, systemName, curValue, am.app.Settings().Meta.AppUrl+"/system/"+systemName)
} else if triggered && curValue <= threshold {
alertRecord.Set("triggered", false)
systemName := newRecord.GetString("name")
subject = fmt.Sprintf("%s usage returned below threshold on %s", name, systemName)
body = fmt.Sprintf("%s usage on %s is below threshold at %.1f%%.\n\n%s\n\n- Beszel", name, systemName, curValue, am.app.Settings().Meta.AppUrl+"/system/"+systemName)
} else {
// fmt.Println(name, "not triggered")
return
}
if err := am.app.Dao().SaveRecord(alertRecord); err != nil {
// app.Logger().Error("failed to save alert record", "err", err.Error())
return
}
// expand the user relation and send the alert
if errs := am.app.Dao().ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
// app.Logger().Error("failed to expand user relation", "errs", errs)
return
}
if user := alertRecord.ExpandedOne("user"); user != nil {
am.sendAlert(email.NewEmailData(
user.GetString("email"),
subject,
body,
))
}
}
func (am *AlertManager) handleStatusAlerts(newStatus string, oldRecord *models.Record, alertRecord *models.Record) error {
var alertStatus string
switch newStatus {
case "up":
if oldRecord.GetString("status") == "down" {
alertStatus = "up"
}
case "down":
if oldRecord.GetString("status") == "up" {
alertStatus = "down"
}
}
if alertStatus == "" {
return nil
}
// expand the user relation
if errs := am.app.Dao().ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
return fmt.Errorf("failed to expand: %v", errs)
}
user := alertRecord.ExpandedOne("user")
if user == nil {
return nil
}
emoji := "\U0001F534"
if alertStatus == "up" {
emoji = "\u2705"
}
// send alert
systemName := oldRecord.GetString("name")
am.sendAlert(email.NewEmailData(
user.GetString("email"),
fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
fmt.Sprintf("Connection to %s is %s\n\n- Beszel", systemName, alertStatus),
))
return nil
}
func (am *AlertManager) sendAlert(data *email.EmailData) {
// fmt.Println("sending alert", "to", data.to, "subj", data.subj, "body", data.body)
message := &mailer.Message{
From: mail.Address{
Address: am.app.Settings().Meta.SenderAddress,
Name: am.app.Settings().Meta.SenderName,
},
To: []mail.Address{{Address: data.To()}},
Subject: data.Subject(),
Text: data.Body(),
}
if err := am.mailClient.Send(message); err != nil {
am.app.Logger().Error("Failed to send alert: ", "err", err.Error())
}
}

View File

@@ -0,0 +1,9 @@
package container
type ContainerStats struct {
Name string `json:"n"`
Cpu float64 `json:"c"`
Mem float64 `json:"m"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
}

View File

@@ -0,0 +1,27 @@
package email
type EmailData struct {
to string
subj string
body string
}
func NewEmailData(to, subj, body string) *EmailData {
return &EmailData{
to: to,
subj: subj,
body: body,
}
}
func (e *EmailData) To() string {
return e.to
}
func (e *EmailData) Subject() string {
return e.subj
}
func (e *EmailData) Body() string {
return e.body
}

View File

@@ -0,0 +1,17 @@
package server
import "golang.org/x/crypto/ssh"
type Server struct {
Host string
Port string
Status string
Client *ssh.Client
}
func NewServer(host, port string) *Server {
return &Server{
Host: host,
Port: port,
}
}

View File

@@ -0,0 +1,18 @@
package system
type SystemStats struct {
Cpu float64 `json:"cpu"`
Mem float64 `json:"m"`
MemUsed float64 `json:"mu"`
MemPct float64 `json:"mp"`
MemBuffCache float64 `json:"mb"`
Swap float64 `json:"s"`
SwapUsed float64 `json:"su"`
Disk float64 `json:"d"`
DiskUsed float64 `json:"du"`
DiskPct float64 `json:"dp"`
DiskRead float64 `json:"dr"`
DiskWrite float64 `json:"dw"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
}

View File

@@ -0,0 +1,20 @@
package system
import "beszel/internal/entities/container"
type SystemInfo struct {
Cores int `json:"c"`
Threads int `json:"t"`
CpuModel string `json:"m"`
// Os string `json:"o"`
Uptime uint64 `json:"u"`
Cpu float64 `json:"cpu"`
MemPct float64 `json:"mp"`
DiskPct float64 `json:"dp"`
}
type SystemData struct {
Stats SystemStats `json:"stats"`
Info SystemInfo `json:"info"`
Containers []container.ContainerStats `json:"container"`
}

466
internal/hub/hub.go Normal file
View File

@@ -0,0 +1,466 @@
package hub
import (
"beszel/internal/alerts"
"beszel/internal/entities/server"
"beszel/internal/entities/system"
"beszel/internal/records"
"beszel/site"
"bytes"
"crypto/ed25519"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"github.com/pocketbase/pocketbase/tools/cron"
"golang.org/x/crypto/ssh"
)
type Hub struct {
app *pocketbase.PocketBase
serverConnectionsLock *sync.Mutex
serverConnections map[string]*server.Server
}
func NewHub(app *pocketbase.PocketBase) *Hub {
return &Hub{
app: app,
serverConnectionsLock: &sync.Mutex{},
serverConnections: make(map[string]*server.Server),
}
}
func (h *Hub) Run() {
rm := records.NewRecordManager(h.app)
am := alerts.NewAlertManager(h.app)
// loosely check if it was executed using "go run"
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
// // enable auto creation of migration files when making collection changes in the Admin UI
migratecmd.MustRegister(h.app, h.app.RootCmd, migratecmd.Config{
// (the isGoRun check is to enable it only during development)
Automigrate: isGoRun,
})
// set auth settings
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
usersCollection, err := h.app.Dao().FindCollectionByNameOrId("users")
if err != nil {
return err
}
usersAuthOptions := usersCollection.AuthOptions()
usersAuthOptions.AllowUsernameAuth = false
if os.Getenv("DISABLE_PASSWORD_AUTH") == "true" {
usersAuthOptions.AllowEmailAuth = false
} else {
usersAuthOptions.AllowEmailAuth = true
}
usersCollection.SetOptions(usersAuthOptions)
if err := h.app.Dao().SaveCollection(usersCollection); err != nil {
return err
}
return nil
})
// serve site
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
switch isGoRun {
case true:
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: "localhost:5173",
})
e.Router.GET("/static/*", apis.StaticDirectoryHandler(os.DirFS("./site/public/static"), false))
e.Router.Any("/*", echo.WrapHandler(proxy))
// e.Router.Any("/", echo.WrapHandler(proxy))
default:
e.Router.GET("/static/*", apis.StaticDirectoryHandler(site.Static, false))
e.Router.Any("/*", apis.StaticDirectoryHandler(site.Dist, true))
}
return nil
})
// set up cron jobs / ticker for system updates
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// 15 second ticker for system updates
go h.startSystemUpdateTicker()
// cron job to delete old records
scheduler := cron.New()
scheduler.MustAdd("delete old records", "8 * * * *", func() {
collections := []string{"system_stats", "container_stats"}
rm.DeleteOldRecords(collections, "1m", time.Hour)
rm.DeleteOldRecords(collections, "10m", 12*time.Hour)
rm.DeleteOldRecords(collections, "20m", 24*time.Hour)
rm.DeleteOldRecords(collections, "120m", 7*24*time.Hour)
rm.DeleteOldRecords(collections, "480m", 30*24*time.Hour)
})
scheduler.Start()
return nil
})
// ssh key setup
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// create ssh key if it doesn't exist
h.getSSHKey()
// api route to return public key
e.Router.GET("/api/beszel/getkey", func(c echo.Context) error {
requestData := apis.RequestInfo(c)
if requestData.AuthRecord == nil {
return apis.NewForbiddenError("Forbidden", nil)
}
key, err := os.ReadFile(h.app.DataDir() + "/id_ed25519.pub")
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{"key": strings.TrimSuffix(string(key), "\n")})
})
return nil
})
// other api routes
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// check if first time setup on login page
e.Router.GET("/api/beszel/first-run", func(c echo.Context) error {
adminNum, err := h.app.Dao().TotalAdmins()
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]bool{"firstRun": adminNum == 0})
})
return nil
})
// user creation - set default role to user if unset
h.app.OnModelBeforeCreate("users").Add(func(e *core.ModelEvent) error {
user := e.Model.(*models.Record)
if user.GetString("role") == "" {
user.Set("role", "user")
}
return nil
})
// system creation defaults
h.app.OnModelBeforeCreate("systems").Add(func(e *core.ModelEvent) error {
record := e.Model.(*models.Record)
record.Set("info", system.SystemInfo{})
record.Set("status", "pending")
return nil
})
// immediately create connection for new servers
h.app.OnModelAfterCreate("systems").Add(func(e *core.ModelEvent) error {
go h.updateSystem(e.Model.(*models.Record))
return nil
})
// do things after a systems record is updated
h.app.OnModelAfterUpdate("systems").Add(func(e *core.ModelEvent) error {
newRecord := e.Model.(*models.Record)
oldRecord := newRecord.OriginalCopy()
newStatus := newRecord.GetString("status")
// if server is disconnected and connection exists, remove it
if newStatus == "down" || newStatus == "paused" {
h.deleteServerConnection(newRecord)
}
// if server is set to pending (unpause), try to connect immediately
if newStatus == "pending" {
go h.updateSystem(newRecord)
}
// alerts
am.HandleSystemAlerts(newStatus, newRecord, oldRecord)
return nil
})
// do things after a systems record is deleted
h.app.OnModelAfterDelete("systems").Add(func(e *core.ModelEvent) error {
// if server connection exists, close it
h.deleteServerConnection(e.Model.(*models.Record))
return nil
})
h.app.OnModelAfterCreate("system_stats").Add(func(e *core.ModelEvent) error {
rm.CreateLongerRecords("system_stats", e.Model.(*models.Record))
return nil
})
h.app.OnModelAfterCreate("container_stats").Add(func(e *core.ModelEvent) error {
rm.CreateLongerRecords("container_stats", e.Model.(*models.Record))
return nil
})
if err := h.app.Start(); err != nil {
log.Fatal(err)
}
}
func (h *Hub) startSystemUpdateTicker() {
ticker := time.NewTicker(15 * time.Second)
for range ticker.C {
h.updateSystems()
}
}
func (h *Hub) updateSystems() {
records, err := h.app.Dao().FindRecordsByFilter(
"2hz5ncl8tizk5nx", // collection
"status != 'paused'", // filter
"updated", // sort
-1, // limit
0, // offset
)
// log.Println("records", len(records))
if err != nil || len(records) == 0 {
// h.app.Logger().Error("Failed to query systems")
return
}
fiftySecondsAgo := time.Now().UTC().Add(-50 * time.Second)
batchSize := len(records)/4 + 1
done := 0
for _, record := range records {
// break if batch size reached or if the system was updated less than 50 seconds ago
if done >= batchSize || record.GetDateTime("updated").Time().After(fiftySecondsAgo) {
break
}
// don't increment for down systems to avoid them jamming the queue
// because they're always first when sorted by least recently updated
if record.GetString("status") != "down" {
done++
}
go h.updateSystem(record)
}
}
func (h *Hub) updateSystem(record *models.Record) {
var s *server.Server
// check if server connection data exists
if _, ok := h.serverConnections[record.Id]; ok {
s = h.serverConnections[record.Id]
} else {
// create server connection struct
s = server.NewServer(
record.GetString("host"),
record.GetString("port"))
client, err := h.getServerConnection(s)
if err != nil {
h.app.Logger().Error("Failed to connect:", "err", err.Error(), "server", s.Host, "port", s.Port)
h.updateServerStatus(record, "down")
return
}
s.Client = client
h.serverConnectionsLock.Lock()
h.serverConnections[record.Id] = s
h.serverConnectionsLock.Unlock()
}
// get server stats from agent
systemData, err := requestJson(s)
if err != nil {
if err.Error() == "retry" {
// if previous connection was closed, try again
h.app.Logger().Error("Existing SSH connection closed. Retrying...", "host", s.Host, "port", s.Port)
h.deleteServerConnection(record)
h.updateSystem(record)
return
}
h.app.Logger().Error("Failed to get server stats: ", "err", err.Error())
h.updateServerStatus(record, "down")
return
}
// update system record
record.Set("status", "up")
record.Set("info", systemData.Info)
if err := h.app.Dao().SaveRecord(record); err != nil {
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
}
// add new system_stats record
system_stats, _ := h.app.Dao().FindCollectionByNameOrId("system_stats")
system_stats_record := models.NewRecord(system_stats)
system_stats_record.Set("system", record.Id)
system_stats_record.Set("stats", systemData.Stats)
system_stats_record.Set("type", "1m")
if err := h.app.Dao().SaveRecord(system_stats_record); err != nil {
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
}
// add new container_stats record
if len(systemData.Containers) > 0 {
container_stats, _ := h.app.Dao().FindCollectionByNameOrId("container_stats")
container_stats_record := models.NewRecord(container_stats)
container_stats_record.Set("system", record.Id)
container_stats_record.Set("stats", systemData.Containers)
container_stats_record.Set("type", "1m")
if err := h.app.Dao().SaveRecord(container_stats_record); err != nil {
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
}
}
}
// set server to status down and close connection
func (h *Hub) updateServerStatus(record *models.Record, status string) {
// if in map, close connection and remove from map
// this is now down automatically in an after update hook
// if status == "down" || status == "paused" {
// deleteServerConnection(record)
// }
if record.GetString("status") != status {
record.Set("status", status)
if err := h.app.Dao().SaveRecord(record); err != nil {
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
}
}
}
func (h *Hub) deleteServerConnection(record *models.Record) {
if _, ok := h.serverConnections[record.Id]; ok {
if h.serverConnections[record.Id].Client != nil {
h.serverConnections[record.Id].Client.Close()
}
h.serverConnectionsLock.Lock()
defer h.serverConnectionsLock.Unlock()
delete(h.serverConnections, record.Id)
}
}
func (h *Hub) getServerConnection(server *server.Server) (*ssh.Client, error) {
// h.app.Logger().Debug("new ssh connection", "server", server.Host)
key, err := h.getSSHKey()
if err != nil {
h.app.Logger().Error("Failed to get SSH key: ", "err", err.Error())
return nil, err
}
// Create the Signer for this private key.
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return nil, err
}
config := &ssh.ClientConfig{
User: "u",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", server.Host, server.Port), config)
if err != nil {
return nil, err
}
return client, nil
}
func requestJson(server *server.Server) (system.SystemData, error) {
session, err := server.Client.NewSession()
if err != nil {
return system.SystemData{}, errors.New("retry")
}
defer session.Close()
// Create a buffer to capture the output
var outputBuffer bytes.Buffer
session.Stdout = &outputBuffer
if err := session.Shell(); err != nil {
return system.SystemData{}, err
}
err = session.Wait()
if err != nil {
return system.SystemData{}, err
}
// Unmarshal the output into our struct
var systemData system.SystemData
err = json.Unmarshal(outputBuffer.Bytes(), &systemData)
if err != nil {
return system.SystemData{}, err
}
return systemData, nil
}
func (h *Hub) getSSHKey() ([]byte, error) {
dataDir := h.app.DataDir()
// check if the key pair already exists
existingKey, err := os.ReadFile(dataDir + "/id_ed25519")
if err == nil {
return existingKey, nil
}
// Generate the Ed25519 key pair
pubKey, privKey, err := ed25519.GenerateKey(nil)
if err != nil {
// h.app.Logger().Error("Error generating key pair:", "err", err.Error())
return nil, err
}
// Get the private key in OpenSSH format
privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "")
if err != nil {
// h.app.Logger().Error("Error marshaling private key:", "err", err.Error())
return nil, err
}
// Save the private key to a file
privateFile, err := os.Create(dataDir + "/id_ed25519")
if err != nil {
// h.app.Logger().Error("Error creating private key file:", "err", err.Error())
return nil, err
}
defer privateFile.Close()
if err := pem.Encode(privateFile, privKeyBytes); err != nil {
// h.app.Logger().Error("Error writing private key to file:", "err", err.Error())
return nil, err
}
// Generate the public key in OpenSSH format
publicKey, err := ssh.NewPublicKey(pubKey)
if err != nil {
return nil, err
}
pubKeyBytes := ssh.MarshalAuthorizedKey(publicKey)
// Save the public key to a file
publicFile, err := os.Create(dataDir + "/id_ed25519.pub")
if err != nil {
return nil, err
}
defer publicFile.Close()
if _, err := publicFile.Write(pubKeyBytes); err != nil {
return nil, err
}
h.app.Logger().Info("ed25519 SSH key pair generated successfully.")
h.app.Logger().Info("Private key saved to: " + dataDir + "/id_ed25519")
h.app.Logger().Info("Public key saved to: " + dataDir + "/id_ed25519.pub")
existingKey, err = os.ReadFile(dataDir + "/id_ed25519")
if err == nil {
return existingKey, nil
}
return nil, err
}

178
internal/records/records.go Normal file
View File

@@ -0,0 +1,178 @@
package records
import (
"beszel/internal/entities/container"
"beszel/internal/entities/system"
"fmt"
"math"
"reflect"
"time"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
)
type RecordManager struct {
app *pocketbase.PocketBase
}
func NewRecordManager(app *pocketbase.PocketBase) *RecordManager {
return &RecordManager{app: app}
}
func (rm *RecordManager) CreateLongerRecords(collectionName string, shorterRecord *models.Record) {
shorterRecordType := shorterRecord.GetString("type")
systemId := shorterRecord.GetString("system")
// 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 := rm.app.Dao().FindFirstRecordByFilter(
collectionName,
"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 := rm.app.Dao().FindRecordsByFilter(
collectionName,
"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 longer record
var stats interface{}
switch collectionName {
case "system_stats":
stats = rm.AverageSystemStats(allShorterRecords)
case "container_stats":
stats = rm.AverageContainerStats(allShorterRecords)
}
collection, _ := rm.app.Dao().FindCollectionByNameOrId(collectionName)
longerRecord := models.NewRecord(collection)
longerRecord.Set("system", systemId)
longerRecord.Set("stats", stats)
longerRecord.Set("type", longerRecordType)
if err := rm.app.Dao().SaveRecord(longerRecord); err != nil {
fmt.Println("failed to save longer record", "err", err.Error())
}
}
// calculate the average stats of a list of system_stats records
func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.SystemStats {
count := float64(len(records))
sum := reflect.New(reflect.TypeOf(system.SystemStats{})).Elem()
for _, record := range records {
var stats system.SystemStats
record.UnmarshalJSONField("stats", &stats)
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(system.SystemStats{})).Elem()
for i := 0; i < sum.NumField(); i++ {
average.Field(i).SetFloat(twoDecimals(sum.Field(i).Float() / count))
}
return average.Interface().(system.SystemStats)
}
// calculate the average stats of a list of container_stats records
func (rm *RecordManager) AverageContainerStats(records []*models.Record) (stats []container.ContainerStats) {
sums := make(map[string]*container.ContainerStats)
count := float64(len(records))
for _, record := range records {
var stats []container.ContainerStats
record.UnmarshalJSONField("stats", &stats)
for _, stat := range stats {
if _, ok := sums[stat.Name]; !ok {
sums[stat.Name] = &container.ContainerStats{Name: stat.Name, Cpu: 0, Mem: 0}
}
sums[stat.Name].Cpu += stat.Cpu
sums[stat.Name].Mem += stat.Mem
sums[stat.Name].NetworkSent += stat.NetworkSent
sums[stat.Name].NetworkRecv += stat.NetworkRecv
}
}
for _, value := range sums {
stats = append(stats, container.ContainerStats{
Name: value.Name,
Cpu: twoDecimals(value.Cpu / count),
Mem: twoDecimals(value.Mem / count),
NetworkSent: twoDecimals(value.NetworkSent / count),
NetworkRecv: twoDecimals(value.NetworkRecv / count),
})
}
return stats
}
/* Round float to two decimals */
func twoDecimals(value float64) float64 {
return math.Round(value*100) / 100
}
/* Delete records of specified collections and type that are older than timeLimit */
func (rm *RecordManager) DeleteOldRecords(collections []string, recordType string, timeLimit time.Duration) {
timeLimitStamp := time.Now().UTC().Add(-timeLimit).Format("2006-01-02 15:04:05")
// db query
expType := dbx.NewExp("type = {:type}", dbx.Params{"type": recordType})
expCreated := dbx.NewExp("created < {:created}", dbx.Params{"created": timeLimitStamp})
var records []*models.Record
for _, collection := range collections {
if collectionRecords, err := rm.app.Dao().FindRecordsByExpr(collection, expType, expCreated); err == nil {
records = append(records, collectionRecords...)
}
}
rm.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
for _, record := range records {
err := txDao.DeleteRecord(record)
if err != nil {
return err
}
}
return nil
})
}

56
internal/update/update.go Normal file
View File

@@ -0,0 +1,56 @@
package update
import (
"beszel"
"fmt"
"os"
"strings"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
"github.com/spf13/cobra"
)
func UpdateBeszel(cmd *cobra.Command, args []string) {
var latest *selfupdate.Release
var found bool
var err error
currentVersion := semver.MustParse(beszel.Version)
fmt.Println("beszel", currentVersion)
fmt.Println("Checking for updates...")
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
Filters: []string{"beszel_"},
})
latest, found, err = updater.DetectLatest("henrygd/beszel")
if err != nil {
fmt.Println("Error checking for updates:", err)
os.Exit(1)
}
if !found {
fmt.Println("No updates found")
os.Exit(0)
}
fmt.Println("Latest version:", latest.Version)
if latest.Version.LTE(currentVersion) {
fmt.Println("You are up to date")
return
}
var binaryPath string
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
binaryPath, err = os.Executable()
if err != nil {
fmt.Println("Error getting binary path:", err)
os.Exit(1)
}
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
if err != nil {
fmt.Println("Please try rerunning with sudo. Error:", err)
os.Exit(1)
}
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
}