Compare commits

...

7 Commits

Author SHA1 Message Date
henrygd
330d375997 change to atomic.bool for fetching details / smart 2025-12-18 15:02:59 -05:00
henrygd
8627e3ee97 updates 2025-12-18 12:34:11 -05:00
henrygd
5d04ee5a65 consolidate info bar data 2025-12-17 19:03:31 -05:00
henrygd
d93067ec34 updates 2025-12-17 17:32:59 -05:00
henrygd
82bd953941 add arch 2025-12-16 18:33:32 -05:00
henrygd
996444abeb update 2025-12-16 17:45:26 -05:00
henrygd
aef4baff5e rm index 2025-12-15 18:59:25 -05:00
6 changed files with 147 additions and 107 deletions

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"os"
"runtime"
"strconv"
"strings"
"time"
@@ -41,6 +42,11 @@ func (a *Agent) refreshStaticInfo() {
}
a.systemDetails.Hostname, _ = os.Hostname()
if arch, err := host.KernelArch(); err == nil {
a.systemDetails.Arch = arch
} else {
a.systemDetails.Arch = runtime.GOARCH
}
platform, _, version, _ := host.PlatformInformation()
@@ -49,24 +55,29 @@ func (a *Agent) refreshStaticInfo() {
a.systemDetails.OsName = fmt.Sprintf("macOS %s", version)
} else if strings.Contains(platform, "indows") {
a.systemDetails.Os = system.Windows
a.systemDetails.OsName = fmt.Sprintf("%s %s", strings.Replace(platform, "Microsoft ", "", 1), version)
a.systemDetails.OsName = strings.Replace(platform, "Microsoft ", "", 1)
a.systemDetails.Kernel = version
} else if platform == "freebsd" {
a.systemDetails.Os = system.Freebsd
a.systemDetails.Kernel = version
a.systemDetails.OsName = "FreeBSD"
a.systemDetails.Kernel, _ = host.KernelVersion()
if prettyName, err := getOsPrettyName(); err == nil {
a.systemDetails.OsName = prettyName
} else {
a.systemDetails.OsName = "FreeBSD"
}
} else {
a.systemDetails.Os = system.Linux
a.systemDetails.OsName = hostInfo.OperatingSystem
a.systemDetails.Kernel = hostInfo.KernelVersion
if a.systemDetails.OsName == "" {
if prettyName, err := getLinuxOsPrettyName(); err == nil {
if prettyName, err := getOsPrettyName(); err == nil {
a.systemDetails.OsName = prettyName
} else {
a.systemDetails.OsName = platform
}
}
a.systemDetails.Kernel = hostInfo.KernelVersion
if a.systemDetails.Kernel == "" {
a.systemDetails.Kernel = version
a.systemDetails.Kernel, _ = host.KernelVersion()
}
}
@@ -75,18 +86,17 @@ func (a *Agent) refreshStaticInfo() {
a.systemDetails.CpuModel = info[0].ModelName
}
// cores / threads
a.systemDetails.Cores, _ = cpu.Counts(false)
a.systemDetails.Threads = hostInfo.NCPU
if a.systemDetails.Threads == 0 {
if threads, err := cpu.Counts(true); err == nil {
if threads > 0 && threads < a.systemDetails.Cores {
// in lxc logical cores reflects container limits, so use that as cores if lower
a.systemDetails.Cores = threads
} else {
a.systemDetails.Threads = threads
}
}
cores, _ := cpu.Counts(false)
threads := hostInfo.NCPU
if threads == 0 {
threads, _ = cpu.Counts(true)
}
// in lxc, logical cores reflects container limits, so use that as cores if lower
if threads > 0 && threads < cores {
cores = threads
}
a.systemDetails.Cores = cores
a.systemDetails.Threads = threads
// total memory
a.systemDetails.MemoryTotal = hostInfo.MemTotal
@@ -267,8 +277,8 @@ func getARCSize() (uint64, error) {
return 0, fmt.Errorf("failed to parse size field")
}
// getLinuxOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems
func getLinuxOsPrettyName() (string, error) {
// getOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems
func getOsPrettyName() (string, error) {
file, err := os.Open("/etc/os-release")
if err != nil {
return "", err

View File

@@ -158,10 +158,11 @@ type Details struct {
Hostname string `cbor:"0,keyasint"`
Kernel string `cbor:"1,keyasint,omitempty"`
Cores int `cbor:"2,keyasint"`
Threads int `cbor:"3,keyasint,omitempty"`
Threads int `cbor:"3,keyasint"`
CpuModel string `cbor:"4,keyasint"`
Os Os `cbor:"5,keyasint"`
OsName string `cbor:"6,keyasint"`
Arch string `cbor:"7,keyasint"`
Podman bool `cbor:"8,keyasint,omitempty"`
MemoryTotal uint64 `cbor:"9,keyasint"`
}

View File

@@ -6,11 +6,10 @@ import (
"errors"
"fmt"
"hash/fnv"
"log/slog"
"math/rand"
"net"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/henrygd/beszel/internal/common"
@@ -30,20 +29,21 @@ import (
)
type System struct {
Id string `db:"id"`
Host string `db:"host"`
Port string `db:"port"`
Status string `db:"status"`
manager *SystemManager // Manager that this system belongs to
client *ssh.Client // SSH client for fetching data
data *system.CombinedData // system data from agent
ctx context.Context // Context for stopping the updater
cancel context.CancelFunc // Stops and removes system from updater
WsConn *ws.WsConn // Handler for agent WebSocket connection
agentVersion semver.Version // Agent version
updateTicker *time.Ticker // Ticker for updating the system
smartOnce sync.Once // Once for fetching and saving smart devices
detailsOnce sync.Once // Once for fetching and saving static system details
Id string `db:"id"`
Host string `db:"host"`
Port string `db:"port"`
Status string `db:"status"`
manager *SystemManager // Manager that this system belongs to
client *ssh.Client // SSH client for fetching data
data *system.CombinedData // system data from agent
ctx context.Context // Context for stopping the updater
cancel context.CancelFunc // Stops and removes system from updater
WsConn *ws.WsConn // Handler for agent WebSocket connection
agentVersion semver.Version // Agent version
updateTicker *time.Ticker // Ticker for updating the system
detailsFetched atomic.Bool // True if static system details have been fetched and saved
smartFetched atomic.Bool // True if SMART devices have been fetched and saved
smartFetching atomic.Bool // True if SMART devices are currently being fetched
}
func (sm *SystemManager) NewSystem(systemId string) *System {
@@ -119,10 +119,10 @@ func (sys *System) update() error {
options := common.DataRequestOptions{
CacheTimeMs: uint16(interval),
}
// fetch system details only on the first update
sys.detailsOnce.Do(func() {
// fetch system details if not already fetched
if !sys.detailsFetched.Load() {
options.IncludeDetails = true
})
}
data, err := sys.fetchDataFromAgent(options)
if err == nil {
_, err = sys.createRecords(data)
@@ -151,18 +151,11 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
}
hub := sys.manager.hub
err = hub.RunInTransaction(func(txApp core.App) error {
if data.Details != nil {
slog.Info("Static info", "data", data.Details)
if err := createStaticInfoRecord(txApp, data.Details, sys.Id); err != nil {
return err
}
}
// add system_stats and container_stats records
// add system_stats record
systemStatsCollection, err := txApp.FindCachedCollectionByNameOrId("system_stats")
if err != nil {
return err
}
systemStatsRecord := core.NewRecord(systemStatsCollection)
systemStatsRecord.Set("system", systemRecord.Id)
systemStatsRecord.Set("stats", data.Stats)
@@ -170,14 +163,14 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
if err := txApp.SaveNoValidate(systemStatsRecord); err != nil {
return err
}
// add containers and container_stats records
if len(data.Containers) > 0 {
// add / update containers records
if data.Containers[0].Id != "" {
if err := createContainerRecords(txApp, data.Containers, sys.Id); err != nil {
return err
}
}
// add new container_stats record
containerStatsCollection, err := txApp.FindCachedCollectionByNameOrId("container_stats")
if err != nil {
return err
@@ -198,9 +191,16 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
}
}
// add system details record
if data.Details != nil {
if err := createSystemDetailsRecord(txApp, data.Details, sys.Id); err != nil {
return err
}
sys.detailsFetched.Store(true)
}
// update system record (do this last because it triggers alerts and we need above records to be inserted first)
systemRecord.Set("status", up)
systemRecord.Set("info", data.Info)
if err := txApp.SaveNoValidate(systemRecord); err != nil {
return err
@@ -210,35 +210,42 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
// Fetch and save SMART devices when system first comes online
if err == nil {
sys.smartOnce.Do(func() {
go sys.FetchAndSaveSmartDevices()
})
if !sys.smartFetched.Load() && sys.smartFetching.CompareAndSwap(false, true) {
go func() {
defer sys.smartFetching.Store(false)
if err := sys.FetchAndSaveSmartDevices(); err == nil {
sys.smartFetched.Store(true)
}
}()
}
}
return systemRecord, err
}
func createStaticInfoRecord(app core.App, data *system.Details, systemId string) error {
record, err := app.FindRecordById("system_details", systemId)
if err != nil {
collection, err := app.FindCollectionByNameOrId("system_details")
if err != nil {
return err
}
record = core.NewRecord(collection)
record.Set("id", systemId)
func createSystemDetailsRecord(app core.App, data *system.Details, systemId string) error {
collectionName := "system_details"
params := dbx.Params{
"id": systemId,
"system": systemId,
"hostname": data.Hostname,
"kernel": data.Kernel,
"cores": data.Cores,
"threads": data.Threads,
"cpu": data.CpuModel,
"os": data.Os,
"os_name": data.OsName,
"arch": data.Arch,
"memory": data.MemoryTotal,
"podman": data.Podman,
"updated": time.Now().UTC(),
}
record.Set("system", systemId)
record.Set("hostname", data.Hostname)
record.Set("kernel", data.Kernel)
record.Set("cores", data.Cores)
record.Set("threads", data.Threads)
record.Set("cpu", data.CpuModel)
record.Set("os", data.Os)
record.Set("os_name", data.OsName)
record.Set("memory", data.MemoryTotal)
record.Set("podman", data.Podman)
return app.SaveNoValidate(record)
result, err := app.DB().Update(collectionName, params, dbx.HashExp{"id": systemId}).Execute()
rowsAffected, _ := result.RowsAffected()
if err != nil || rowsAffected == 0 {
_, err = app.DB().Insert(collectionName, params).Execute()
}
return err
}
func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {
@@ -388,7 +395,8 @@ func (sys *System) fetchStringFromAgentViaSSH(action common.WebSocketAction, req
if err := session.Shell(); err != nil {
return false, err
}
req := common.HubRequest[any]{Action: action, Data: requestData}
reqDataBytes, _ := cbor.Marshal(requestData)
req := common.HubRequest[cbor.RawMessage]{Action: action, Data: reqDataBytes}
_ = cbor.NewEncoder(stdin).Encode(req)
_ = stdin.Close()
var resp common.AgentResponse
@@ -452,7 +460,8 @@ func (sys *System) FetchSystemdInfoFromAgent(serviceName string) (systemd.Servic
return false, err
}
req := common.HubRequest[any]{Action: common.GetSystemdInfo, Data: common.SystemdInfoRequest{ServiceName: serviceName}}
reqDataBytes, _ := cbor.Marshal(common.SystemdInfoRequest{ServiceName: serviceName})
req := common.HubRequest[cbor.RawMessage]{Action: common.GetSystemdInfo, Data: reqDataBytes}
if err := cbor.NewEncoder(stdin).Encode(req); err != nil {
return false, err
}
@@ -500,7 +509,8 @@ func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) (*system.C
*sys.data = system.CombinedData{}
if sys.agentVersion.GTE(beszel.MinVersionAgentResponse) && stdinErr == nil {
req := common.HubRequest[any]{Action: common.GetData, Data: options}
reqDataBytes, _ := cbor.Marshal(options)
req := common.HubRequest[cbor.RawMessage]{Action: common.GetData, Data: reqDataBytes}
_ = cbor.NewEncoder(stdin).Encode(req)
_ = stdin.Close()

View File

@@ -1539,6 +1539,20 @@ func init() {
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text4161937994",
"max": 0,
"min": 0,
"name": "arch",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "number4245036687",
@@ -1596,9 +1610,7 @@ func init() {
}
],
"id": "pbc_3116237454",
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_5d1egp3jVU` + "`" + ` ON ` + "`" + `system_details` + "`" + ` (` + "`" + `system` + "`" + `)"
],
"indexes": [],
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
"name": "system_details",
"system": false,

View File

@@ -33,10 +33,7 @@
"noUnusedFunctionParameters": "error",
"noUnusedPrivateClassMembers": "error",
"useExhaustiveDependencies": {
"level": "warn",
"options": {
"reportUnnecessaryDependencies": false
}
"level": "off"
},
"useUniqueElementIds": "off",
"noUnusedVariables": "error"

View File

@@ -1,10 +1,5 @@
import ChartTimeSelect from "@/components/charts/chart-time-select"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons"
import { SystemStatus, ConnectionType, connectionTypeLabels, Os } from "@/lib/enums"
import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
import { plural } from "@lingui/core/macro"
import { useLingui } from "@lingui/react/macro"
import {
AppleIcon,
ChevronRightSquareIcon,
@@ -12,15 +7,21 @@ import {
CpuIcon,
GlobeIcon,
LayoutGridIcon,
MemoryStickIcon,
MonitorIcon,
Rows,
MemoryStickIcon,
} from "lucide-react"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types"
import { useEffect, useMemo, useState } from "react"
import { useLingui } from "@lingui/react/macro"
import ChartTimeSelect from "@/components/charts/chart-time-select"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons"
import { Separator } from "@/components/ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { pb } from "@/lib/api"
import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums"
import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types"
export default function InfoBar({
system,
@@ -40,22 +41,30 @@ export default function InfoBar({
// Fetch system_details on mount / when system changes
useEffect(() => {
let active = true
setDetails(null)
// skip fetching system details if agent is older version which includes details in Info struct
if (!system.id || system.info?.m) {
return setDetails(null)
return
}
pb.collection<SystemDetailsRecord>("system_details")
.getOne(system.id, {
fields: "hostname,kernel,cores,threads,cpu,os,os_name,memory,podman",
fields: "hostname,kernel,cores,threads,cpu,os,os_name,arch,memory,podman",
headers: {
"Cache-Control": "public, max-age=60",
},
})
.then((details) => {
setDetails(details)
setIsPodman(details.podman)
if (active) {
setDetails(details)
setIsPodman(details.podman)
}
})
.catch(() => setDetails(null))
.catch(() => {})
return () => {
active = false
}
}, [system.id])
// values for system info bar - use details with fallback to system.info
@@ -68,10 +77,11 @@ export default function InfoBar({
const hostname = details?.hostname ?? system.info.h
const kernel = details?.kernel ?? system.info.k
const cores = details?.cores ?? system.info.c
const threads = details?.threads ?? system.info.t
const threads = details?.threads ?? system.info.t ?? 0
const cpuModel = details?.cpu ?? system.info.m
const os = details?.os ?? system.info.os ?? Os.Linux
const osName = details?.os_name
const arch = details?.arch
const memory = details?.memory
const osInfo = {
@@ -80,7 +90,6 @@ export default function InfoBar({
// show kernel in tooltip if os name is available, otherwise show the kernel
value: osName || kernel,
label: osName ? kernel : undefined,
// label: t({ comment: "Linux kernel", message: "Kernel" }),
},
[Os.Darwin]: {
Icon: AppleIcon,
@@ -89,6 +98,7 @@ export default function InfoBar({
[Os.Windows]: {
Icon: WindowsIcon,
value: osName || kernel,
label: osName ? kernel : undefined,
},
[Os.FreeBSD]: {
Icon: FreeBsdIcon,
@@ -116,6 +126,12 @@ export default function InfoBar({
},
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
osInfo[os],
{
value: cpuModel,
Icon: CpuIcon,
hide: !cpuModel,
label: `${plural(cores, { one: "# core", other: "# cores" })} / ${plural(threads, { one: "# thread", other: "# threads" })}${arch ? ` / ${arch}` : ""}`,
},
] as {
value: string | number | undefined
label?: string
@@ -133,12 +149,6 @@ export default function InfoBar({
})
}
info.push({
value: `${cpuModel} (${cores}c${threads ? `/${threads}t` : ""})`,
Icon: CpuIcon,
hide: !cpuModel,
})
return info
}, [system, details, t])