mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 14:06:18 +01:00
Compare commits
8 Commits
952-collec
...
split-syst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
330d375997 | ||
|
|
8627e3ee97 | ||
|
|
5d04ee5a65 | ||
|
|
d93067ec34 | ||
|
|
82bd953941 | ||
|
|
996444abeb | ||
|
|
aef4baff5e | ||
|
|
3dea061e93 |
@@ -17,19 +17,12 @@ import (
|
|||||||
"github.com/gliderlabs/ssh"
|
"github.com/gliderlabs/ssh"
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/agent/deltatracker"
|
"github.com/henrygd/beszel/agent/deltatracker"
|
||||||
|
"github.com/henrygd/beszel/internal/common"
|
||||||
"github.com/henrygd/beszel/internal/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
"github.com/shirou/gopsutil/v4/host"
|
"github.com/shirou/gopsutil/v4/host"
|
||||||
gossh "golang.org/x/crypto/ssh"
|
gossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// StaticInfoIntervalMs defines the cache time threshold for including static system info
|
|
||||||
// Requests with cache time >= this value will include static info (reduces bandwidth)
|
|
||||||
// Note: uint16 max is 65535, so we can't use 15 minutes directly. The hub will make
|
|
||||||
// periodic requests at this interval.
|
|
||||||
StaticInfoIntervalMs uint16 = 60_001 // Just above the standard 60s interval
|
|
||||||
)
|
|
||||||
|
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
sync.Mutex // Used to lock agent while collecting data
|
sync.Mutex // Used to lock agent while collecting data
|
||||||
debug bool // true if LOG_LEVEL is set to debug
|
debug bool // true if LOG_LEVEL is set to debug
|
||||||
@@ -45,8 +38,8 @@ type Agent struct {
|
|||||||
netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers
|
netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers
|
||||||
dockerManager *dockerManager // Manages Docker API requests
|
dockerManager *dockerManager // Manages Docker API requests
|
||||||
sensorConfig *SensorConfig // Sensors config
|
sensorConfig *SensorConfig // Sensors config
|
||||||
systemInfo system.Info // Host system info (dynamic dashboard data)
|
systemInfo system.Info // Host system info (dynamic)
|
||||||
staticSystemInfo system.StaticInfo // Static system info (collected at longer intervals)
|
systemDetails system.Details // Host system details (static, once-per-connection)
|
||||||
gpuManager *GPUManager // Manages GPU data
|
gpuManager *GPUManager // Manages GPU data
|
||||||
cache *systemDataCache // Cache for system stats based on cache time
|
cache *systemDataCache // Cache for system stats based on cache time
|
||||||
connectionManager *ConnectionManager // Channel to signal connection events
|
connectionManager *ConnectionManager // Channel to signal connection events
|
||||||
@@ -106,8 +99,11 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
|
|||||||
|
|
||||||
slog.Debug(beszel.Version)
|
slog.Debug(beszel.Version)
|
||||||
|
|
||||||
|
// initialize docker manager
|
||||||
|
agent.dockerManager = newDockerManager()
|
||||||
|
|
||||||
// initialize system info
|
// initialize system info
|
||||||
agent.initializeSystemInfo()
|
agent.refreshStaticInfo()
|
||||||
|
|
||||||
// initialize connection manager
|
// initialize connection manager
|
||||||
agent.connectionManager = newConnectionManager(agent)
|
agent.connectionManager = newConnectionManager(agent)
|
||||||
@@ -121,9 +117,6 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
|
|||||||
// initialize net io stats
|
// initialize net io stats
|
||||||
agent.initializeNetIoStats()
|
agent.initializeNetIoStats()
|
||||||
|
|
||||||
// initialize docker manager
|
|
||||||
agent.dockerManager = newDockerManager(agent)
|
|
||||||
|
|
||||||
agent.systemdManager, err = newSystemdManager()
|
agent.systemdManager, err = newSystemdManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug("Systemd", "err", err)
|
slog.Debug("Systemd", "err", err)
|
||||||
@@ -142,7 +135,7 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
|
|||||||
|
|
||||||
// if debugging, print stats
|
// if debugging, print stats
|
||||||
if agent.debug {
|
if agent.debug {
|
||||||
slog.Debug("Stats", "data", agent.gatherStats(0))
|
slog.Debug("Stats", "data", agent.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000, IncludeDetails: true}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return agent, nil
|
return agent, nil
|
||||||
@@ -157,10 +150,11 @@ func GetEnv(key string) (value string, exists bool) {
|
|||||||
return os.LookupEnv(key)
|
return os.LookupEnv(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedData {
|
||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
|
|
||||||
|
cacheTimeMs := options.CacheTimeMs
|
||||||
data, isCached := a.cache.Get(cacheTimeMs)
|
data, isCached := a.cache.Get(cacheTimeMs)
|
||||||
if isCached {
|
if isCached {
|
||||||
slog.Debug("Cached data", "cacheTimeMs", cacheTimeMs)
|
slog.Debug("Cached data", "cacheTimeMs", cacheTimeMs)
|
||||||
@@ -171,16 +165,14 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
|||||||
Stats: a.getSystemStats(cacheTimeMs),
|
Stats: a.getSystemStats(cacheTimeMs),
|
||||||
Info: a.systemInfo,
|
Info: a.systemInfo,
|
||||||
}
|
}
|
||||||
// slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs)
|
|
||||||
|
|
||||||
// Include static info for requests with longer intervals (e.g., 15 min)
|
// Include static info only when requested
|
||||||
// This reduces bandwidth by only sending static data occasionally
|
if options.IncludeDetails {
|
||||||
if cacheTimeMs >= StaticInfoIntervalMs {
|
data.Details = &a.systemDetails
|
||||||
staticInfoCopy := a.staticSystemInfo
|
|
||||||
data.StaticInfo = &staticInfoCopy
|
|
||||||
slog.Debug("Including static info", "cacheTimeMs", cacheTimeMs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs)
|
||||||
|
|
||||||
if a.dockerManager != nil {
|
if a.dockerManager != nil {
|
||||||
if containerStats, err := a.dockerManager.getDockerStats(cacheTimeMs); err == nil {
|
if containerStats, err := a.dockerManager.getDockerStats(cacheTimeMs); err == nil {
|
||||||
data.Containers = containerStats
|
data.Containers = containerStats
|
||||||
@@ -242,11 +234,7 @@ func (a *Agent) getFingerprint() string {
|
|||||||
// if no fingerprint is found, generate one
|
// if no fingerprint is found, generate one
|
||||||
fingerprint, err := host.HostID()
|
fingerprint, err := host.HostID()
|
||||||
if err != nil || fingerprint == "" {
|
if err != nil || fingerprint == "" {
|
||||||
cpuModel := ""
|
fingerprint = a.systemDetails.Hostname + a.systemDetails.CpuModel
|
||||||
if len(a.staticSystemInfo.Cpus) > 0 {
|
|
||||||
cpuModel = a.staticSystemInfo.Cpus[0].Model
|
|
||||||
}
|
|
||||||
fingerprint = a.staticSystemInfo.Hostname + cpuModel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// hash fingerprint
|
// hash fingerprint
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func createTestCacheData() *system.CombinedData {
|
|||||||
DiskTotal: 100000,
|
DiskTotal: 100000,
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
},
|
},
|
||||||
Containers: []*container.Stats{
|
Containers: []*container.Stats{
|
||||||
{
|
{
|
||||||
@@ -128,7 +128,7 @@ func TestCacheMultipleIntervals(t *testing.T) {
|
|||||||
Mem: 16384,
|
Mem: 16384,
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host-2",
|
AgentVersion: "0.12.0",
|
||||||
},
|
},
|
||||||
Containers: []*container.Stats{},
|
Containers: []*container.Stats{},
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ func TestCacheOverwrite(t *testing.T) {
|
|||||||
Mem: 32768,
|
Mem: 32768,
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "updated-host",
|
AgentVersion: "0.12.0",
|
||||||
},
|
},
|
||||||
Containers: []*container.Stats{},
|
Containers: []*container.Stats{},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ func (client *WebSocketClient) handleAuthChallenge(msg *common.HubRequest[cbor.R
|
|||||||
|
|
||||||
if authRequest.NeedSysInfo {
|
if authRequest.NeedSysInfo {
|
||||||
response.Name, _ = GetEnv("SYSTEM_NAME")
|
response.Name, _ = GetEnv("SYSTEM_NAME")
|
||||||
response.Hostname = client.agent.staticSystemInfo.Hostname
|
response.Hostname = client.agent.systemDetails.Hostname
|
||||||
serverAddr := client.agent.connectionManager.serverOptions.Addr
|
serverAddr := client.agent.connectionManager.serverOptions.Addr
|
||||||
_, response.Port, _ = net.SplitHostPort(serverAddr)
|
_, response.Port, _ = net.SplitHostPort(serverAddr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ type dockerManager struct {
|
|||||||
decoder *json.Decoder // Reusable JSON decoder that reads from buf
|
decoder *json.Decoder // Reusable JSON decoder that reads from buf
|
||||||
apiStats *container.ApiStats // Reusable API stats object
|
apiStats *container.ApiStats // Reusable API stats object
|
||||||
excludeContainers []string // Patterns to exclude containers by name
|
excludeContainers []string // Patterns to exclude containers by name
|
||||||
|
usingPodman bool // Whether the Docker Engine API is running on Podman
|
||||||
|
|
||||||
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
|
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
|
||||||
// Maps cache time intervals to container-specific CPU usage tracking
|
// Maps cache time intervals to container-specific CPU usage tracking
|
||||||
@@ -478,7 +479,7 @@ func (dm *dockerManager) deleteContainerStatsSync(id string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new http client for Docker or Podman API
|
// Creates a new http client for Docker or Podman API
|
||||||
func newDockerManager(a *Agent) *dockerManager {
|
func newDockerManager() *dockerManager {
|
||||||
dockerHost, exists := GetEnv("DOCKER_HOST")
|
dockerHost, exists := GetEnv("DOCKER_HOST")
|
||||||
if exists {
|
if exists {
|
||||||
// return nil if set to empty string
|
// return nil if set to empty string
|
||||||
@@ -564,7 +565,7 @@ func newDockerManager(a *Agent) *dockerManager {
|
|||||||
|
|
||||||
// If using podman, return client
|
// If using podman, return client
|
||||||
if strings.Contains(dockerHost, "podman") {
|
if strings.Contains(dockerHost, "podman") {
|
||||||
a.staticSystemInfo.Podman = true
|
manager.usingPodman = true
|
||||||
manager.goodDockerVersion = true
|
manager.goodDockerVersion = true
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
@@ -746,3 +747,23 @@ func decodeDockerLogStream(reader io.Reader, builder *strings.Builder) error {
|
|||||||
totalBytesRead += int(n)
|
totalBytesRead += int(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHostInfo fetches the system info from Docker
|
||||||
|
func (dm *dockerManager) GetHostInfo() (info container.HostInfo, err error) {
|
||||||
|
resp, err := dm.client.Get("http://localhost/info")
|
||||||
|
if err != nil {
|
||||||
|
return info, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||||
|
slog.Error("Failed to decode Docker version response", "error", err)
|
||||||
|
return info, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *dockerManager) IsPodman() bool {
|
||||||
|
return dm.usingPodman
|
||||||
|
}
|
||||||
|
|||||||
@@ -802,6 +802,24 @@ func TestNetworkRateCalculationFormula(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetHostInfo(t *testing.T) {
|
||||||
|
data, err := os.ReadFile("test-data/system_info.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var info container.HostInfo
|
||||||
|
err = json.Unmarshal(data, &info)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "6.8.0-31-generic", info.KernelVersion)
|
||||||
|
assert.Equal(t, "Ubuntu 24.04 LTS", info.OperatingSystem)
|
||||||
|
// assert.Equal(t, "24.04", info.OSVersion)
|
||||||
|
// assert.Equal(t, "linux", info.OSType)
|
||||||
|
// assert.Equal(t, "x86_64", info.Architecture)
|
||||||
|
assert.EqualValues(t, 4, info.NCPU)
|
||||||
|
assert.EqualValues(t, 2095882240, info.MemTotal)
|
||||||
|
// assert.Equal(t, "27.0.1", info.ServerVersion)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeltaTrackerCacheTimeIsolation(t *testing.T) {
|
func TestDeltaTrackerCacheTimeIsolation(t *testing.T) {
|
||||||
// Test that different cache times have separate DeltaTracker instances
|
// Test that different cache times have separate DeltaTracker instances
|
||||||
dm := &dockerManager{
|
dm := &dockerManager{
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func (h *GetDataHandler) Handle(hctx *HandlerContext) error {
|
|||||||
var options common.DataRequestOptions
|
var options common.DataRequestOptions
|
||||||
_ = cbor.Unmarshal(hctx.Request.Data, &options)
|
_ = cbor.Unmarshal(hctx.Request.Data, &options)
|
||||||
|
|
||||||
sysStats := hctx.Agent.gatherStats(options.CacheTimeMs)
|
sysStats := hctx.Agent.gatherStats(options)
|
||||||
return hctx.SendResponse(sysStats, hctx.RequestID)
|
return hctx.SendResponse(sysStats, hctx.RequestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ func (a *Agent) handleSSHRequest(w io.Writer, req *common.HubRequest[cbor.RawMes
|
|||||||
|
|
||||||
// handleLegacyStats serves the legacy one-shot stats payload for older hubs
|
// handleLegacyStats serves the legacy one-shot stats payload for older hubs
|
||||||
func (a *Agent) handleLegacyStats(w io.Writer, hubVersion semver.Version) error {
|
func (a *Agent) handleLegacyStats(w io.Writer, hubVersion semver.Version) error {
|
||||||
stats := a.gatherStats(60_000)
|
stats := a.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000})
|
||||||
return a.writeToSession(w, stats, hubVersion)
|
return a.writeToSession(w, stats, hubVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ func TestWriteToSessionEncoding(t *testing.T) {
|
|||||||
err = json.Unmarshal([]byte(encodedData), &decodedJson)
|
err = json.Unmarshal([]byte(encodedData), &decodedJson)
|
||||||
assert.Error(t, err, "Should not be valid JSON data")
|
assert.Error(t, err, "Should not be valid JSON data")
|
||||||
|
|
||||||
assert.Equal(t, testData.Info.Hostname, decodedCbor.Info.Hostname)
|
assert.Equal(t, testData.Details.Hostname, decodedCbor.Details.Hostname)
|
||||||
assert.Equal(t, testData.Stats.Cpu, decodedCbor.Stats.Cpu)
|
assert.Equal(t, testData.Stats.Cpu, decodedCbor.Stats.Cpu)
|
||||||
} else {
|
} else {
|
||||||
// Should be JSON - try to decode as JSON
|
// Should be JSON - try to decode as JSON
|
||||||
@@ -526,7 +526,7 @@ func TestWriteToSessionEncoding(t *testing.T) {
|
|||||||
assert.Error(t, err, "Should not be valid CBOR data")
|
assert.Error(t, err, "Should not be valid CBOR data")
|
||||||
|
|
||||||
// Verify the decoded JSON data matches our test data
|
// Verify the decoded JSON data matches our test data
|
||||||
assert.Equal(t, testData.Info.Hostname, decodedJson.Info.Hostname)
|
assert.Equal(t, testData.Details.Hostname, decodedJson.Details.Hostname)
|
||||||
assert.Equal(t, testData.Stats.Cpu, decodedJson.Stats.Cpu)
|
assert.Equal(t, testData.Stats.Cpu, decodedJson.Stats.Cpu)
|
||||||
|
|
||||||
// Verify it looks like JSON (starts with '{' and contains readable field names)
|
// Verify it looks like JSON (starts with '{' and contains readable field names)
|
||||||
@@ -551,7 +551,6 @@ func createTestCombinedData() *system.CombinedData {
|
|||||||
DiskPct: 50.0,
|
DiskPct: 50.0,
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
|
||||||
Uptime: 3600,
|
Uptime: 3600,
|
||||||
AgentVersion: "0.12.0",
|
AgentVersion: "0.12.0",
|
||||||
},
|
},
|
||||||
|
|||||||
301
agent/system.go
301
agent/system.go
@@ -2,6 +2,7 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,10 +13,9 @@ import (
|
|||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/agent/battery"
|
"github.com/henrygd/beszel/agent/battery"
|
||||||
|
"github.com/henrygd/beszel/internal/entities/container"
|
||||||
"github.com/henrygd/beszel/internal/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
"github.com/jaypipes/ghw/pkg/block"
|
|
||||||
ghwnet "github.com/jaypipes/ghw/pkg/net"
|
|
||||||
ghwpci "github.com/jaypipes/ghw/pkg/pci"
|
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
"github.com/shirou/gopsutil/v4/host"
|
"github.com/shirou/gopsutil/v4/host"
|
||||||
"github.com/shirou/gopsutil/v4/load"
|
"github.com/shirou/gopsutil/v4/load"
|
||||||
@@ -30,77 +30,80 @@ type prevDisk struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sets initial / non-changing values about the host system
|
// Sets initial / non-changing values about the host system
|
||||||
func (a *Agent) initializeSystemInfo() {
|
func (a *Agent) refreshStaticInfo() {
|
||||||
hostname, _ := os.Hostname()
|
a.systemInfo.AgentVersion = beszel.Version
|
||||||
a.staticSystemInfo.Hostname = hostname
|
|
||||||
a.staticSystemInfo.AgentVersion = beszel.Version
|
|
||||||
|
|
||||||
platform, family, version, _ := host.PlatformInformation()
|
// get host info from Docker if available
|
||||||
|
var hostInfo container.HostInfo
|
||||||
|
|
||||||
var osFamily, osVersion, osKernel string
|
if a.dockerManager != nil {
|
||||||
var osType system.Os
|
a.systemDetails.Podman = a.dockerManager.IsPodman()
|
||||||
if platform == "darwin" {
|
hostInfo, _ = a.dockerManager.GetHostInfo()
|
||||||
osKernel = version
|
}
|
||||||
osFamily = "macOS" // macOS is the family name for Darwin
|
|
||||||
osVersion = version
|
a.systemDetails.Hostname, _ = os.Hostname()
|
||||||
} else if strings.Contains(platform, "indows") {
|
if arch, err := host.KernelArch(); err == nil {
|
||||||
osKernel = strings.Replace(platform, "Microsoft ", "", 1) + " " + version
|
a.systemDetails.Arch = arch
|
||||||
osFamily = family
|
|
||||||
osVersion = version
|
|
||||||
osType = system.Windows
|
|
||||||
} else if platform == "freebsd" {
|
|
||||||
osKernel = version
|
|
||||||
osFamily = family
|
|
||||||
osVersion = version
|
|
||||||
} else {
|
} else {
|
||||||
osFamily = family
|
a.systemDetails.Arch = runtime.GOARCH
|
||||||
osVersion = version
|
}
|
||||||
osKernel = ""
|
|
||||||
osRelease := readOsRelease()
|
platform, _, version, _ := host.PlatformInformation()
|
||||||
if pretty, ok := osRelease["PRETTY_NAME"]; ok {
|
|
||||||
osFamily = pretty
|
if platform == "darwin" {
|
||||||
|
a.systemDetails.Os = system.Darwin
|
||||||
|
a.systemDetails.OsName = fmt.Sprintf("macOS %s", version)
|
||||||
|
} else if strings.Contains(platform, "indows") {
|
||||||
|
a.systemDetails.Os = system.Windows
|
||||||
|
a.systemDetails.OsName = strings.Replace(platform, "Microsoft ", "", 1)
|
||||||
|
a.systemDetails.Kernel = version
|
||||||
|
} else if platform == "freebsd" {
|
||||||
|
a.systemDetails.Os = system.Freebsd
|
||||||
|
a.systemDetails.Kernel, _ = host.KernelVersion()
|
||||||
|
if prettyName, err := getOsPrettyName(); err == nil {
|
||||||
|
a.systemDetails.OsName = prettyName
|
||||||
|
} else {
|
||||||
|
a.systemDetails.OsName = "FreeBSD"
|
||||||
}
|
}
|
||||||
if name, ok := osRelease["NAME"]; ok {
|
} else {
|
||||||
osFamily = name
|
a.systemDetails.Os = system.Linux
|
||||||
|
a.systemDetails.OsName = hostInfo.OperatingSystem
|
||||||
|
if a.systemDetails.OsName == "" {
|
||||||
|
if prettyName, err := getOsPrettyName(); err == nil {
|
||||||
|
a.systemDetails.OsName = prettyName
|
||||||
|
} else {
|
||||||
|
a.systemDetails.OsName = platform
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if versionId, ok := osRelease["VERSION_ID"]; ok {
|
a.systemDetails.Kernel = hostInfo.KernelVersion
|
||||||
osVersion = versionId
|
if a.systemDetails.Kernel == "" {
|
||||||
|
a.systemDetails.Kernel, _ = host.KernelVersion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if osKernel == "" {
|
|
||||||
osKernel, _ = host.KernelVersion()
|
|
||||||
}
|
|
||||||
a.staticSystemInfo.KernelVersion = osKernel
|
|
||||||
a.staticSystemInfo.Os = osType
|
|
||||||
a.staticSystemInfo.Oses = []system.OsInfo{{
|
|
||||||
Family: osFamily,
|
|
||||||
Version: osVersion,
|
|
||||||
Kernel: osKernel,
|
|
||||||
}}
|
|
||||||
|
|
||||||
// cpu model
|
// cpu model
|
||||||
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
||||||
arch := runtime.GOARCH
|
a.systemDetails.CpuModel = info[0].ModelName
|
||||||
totalCores := 0
|
}
|
||||||
totalThreads := 0
|
// cores / threads
|
||||||
for _, cpuInfo := range info {
|
cores, _ := cpu.Counts(false)
|
||||||
totalCores += int(cpuInfo.Cores)
|
threads := hostInfo.NCPU
|
||||||
totalThreads++
|
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
|
||||||
|
if a.systemDetails.MemoryTotal == 0 {
|
||||||
|
if v, err := mem.VirtualMemory(); err == nil {
|
||||||
|
a.systemDetails.MemoryTotal = v.Total
|
||||||
}
|
}
|
||||||
modelName := info[0].ModelName
|
|
||||||
if idx := strings.Index(modelName, "@"); idx > 0 {
|
|
||||||
modelName = strings.TrimSpace(modelName[:idx])
|
|
||||||
}
|
|
||||||
cpu := system.CpuInfo{
|
|
||||||
Model: modelName,
|
|
||||||
SpeedGHz: fmt.Sprintf("%.2f GHz", info[0].Mhz/1000),
|
|
||||||
Arch: arch,
|
|
||||||
Cores: totalCores,
|
|
||||||
Threads: totalThreads,
|
|
||||||
}
|
|
||||||
a.staticSystemInfo.Cpus = []system.CpuInfo{cpu}
|
|
||||||
a.staticSystemInfo.Threads = totalThreads
|
|
||||||
slog.Debug("CPU info populated", "cpus", a.staticSystemInfo.Cpus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// zfs
|
// zfs
|
||||||
@@ -109,41 +112,6 @@ func (a *Agent) initializeSystemInfo() {
|
|||||||
} else {
|
} else {
|
||||||
a.zfs = true
|
a.zfs = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect disk info (model/vendor)
|
|
||||||
a.staticSystemInfo.Disks = getDiskInfo()
|
|
||||||
|
|
||||||
// Collect network interface info
|
|
||||||
a.staticSystemInfo.Networks = getNetworkInfo()
|
|
||||||
|
|
||||||
// Collect total memory and store in staticSystemInfo.Memory
|
|
||||||
if v, err := mem.VirtualMemory(); err == nil {
|
|
||||||
total := fmt.Sprintf("%d GB", int((float64(v.Total)/(1024*1024*1024))+0.5))
|
|
||||||
a.staticSystemInfo.Memory = []system.MemoryInfo{{Total: total}}
|
|
||||||
slog.Debug("Memory info populated", "memory", a.staticSystemInfo.Memory)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// readPrettyName reads the PRETTY_NAME from /etc/os-release
|
|
||||||
func readPrettyName() string {
|
|
||||||
file, err := os.Open("/etc/os-release")
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if strings.HasPrefix(line, "PRETTY_NAME=") {
|
|
||||||
// Remove the prefix and any surrounding quotes
|
|
||||||
prettyName := strings.TrimPrefix(line, "PRETTY_NAME=")
|
|
||||||
prettyName = strings.Trim(prettyName, `"`)
|
|
||||||
return prettyName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns current info, stats about the host system
|
// Returns current info, stats about the host system
|
||||||
@@ -268,21 +236,16 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update base system info
|
// update system info
|
||||||
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
|
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
|
||||||
a.systemInfo.Cpu = systemStats.Cpu
|
a.systemInfo.Cpu = systemStats.Cpu
|
||||||
a.systemInfo.LoadAvg = systemStats.LoadAvg
|
a.systemInfo.LoadAvg = systemStats.LoadAvg
|
||||||
// TODO: remove these in future release in favor of load avg array
|
|
||||||
a.systemInfo.LoadAvg1 = systemStats.LoadAvg[0]
|
|
||||||
a.systemInfo.LoadAvg5 = systemStats.LoadAvg[1]
|
|
||||||
a.systemInfo.LoadAvg15 = systemStats.LoadAvg[2]
|
|
||||||
a.systemInfo.MemPct = systemStats.MemPct
|
a.systemInfo.MemPct = systemStats.MemPct
|
||||||
a.systemInfo.DiskPct = systemStats.DiskPct
|
a.systemInfo.DiskPct = systemStats.DiskPct
|
||||||
a.systemInfo.Battery = systemStats.Battery
|
a.systemInfo.Battery = systemStats.Battery
|
||||||
a.systemInfo.Uptime, _ = host.Uptime()
|
a.systemInfo.Uptime, _ = host.Uptime()
|
||||||
// TODO: in future release, remove MB bandwidth values in favor of bytes
|
|
||||||
a.systemInfo.Bandwidth = twoDecimals(systemStats.NetworkSent + systemStats.NetworkRecv)
|
|
||||||
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
|
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
|
||||||
|
a.systemInfo.Threads = a.systemDetails.Threads
|
||||||
slog.Debug("sysinfo", "data", a.systemInfo)
|
slog.Debug("sysinfo", "data", a.systemInfo)
|
||||||
|
|
||||||
return systemStats
|
return systemStats
|
||||||
@@ -314,135 +277,23 @@ func getARCSize() (uint64, error) {
|
|||||||
return 0, fmt.Errorf("failed to parse size field")
|
return 0, fmt.Errorf("failed to parse size field")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDiskInfo() []system.DiskInfo {
|
// getOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems
|
||||||
blockInfo, err := block.New()
|
func getOsPrettyName() (string, error) {
|
||||||
if err != nil {
|
|
||||||
slog.Debug("Failed to get block info with ghw", "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var disks []system.DiskInfo
|
|
||||||
for _, disk := range blockInfo.Disks {
|
|
||||||
disks = append(disks, system.DiskInfo{
|
|
||||||
Name: disk.Name,
|
|
||||||
Model: disk.Model,
|
|
||||||
Vendor: disk.Vendor,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return disks
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNetworkInfo() []system.NetworkInfo {
|
|
||||||
netInfo, err := ghwnet.New()
|
|
||||||
if err != nil {
|
|
||||||
slog.Debug("Failed to get network info with ghw", "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pciInfo, err := ghwpci.New()
|
|
||||||
if err != nil {
|
|
||||||
slog.Debug("Failed to get PCI info with ghw", "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var networks []system.NetworkInfo
|
|
||||||
for _, nic := range netInfo.NICs {
|
|
||||||
if nic.IsVirtual {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var vendor, model string
|
|
||||||
if nic.PCIAddress != nil && pciInfo != nil {
|
|
||||||
for _, dev := range pciInfo.Devices {
|
|
||||||
if dev.Address == *nic.PCIAddress {
|
|
||||||
if dev.Vendor != nil {
|
|
||||||
vendor = dev.Vendor.Name
|
|
||||||
}
|
|
||||||
if dev.Product != nil {
|
|
||||||
model = dev.Product.Name
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
networks = append(networks, system.NetworkInfo{
|
|
||||||
Name: nic.Name,
|
|
||||||
Vendor: vendor,
|
|
||||||
Model: model,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return networks
|
|
||||||
}
|
|
||||||
|
|
||||||
// getInterfaceCapabilitiesFromGhw uses ghw library to get interface capabilities
|
|
||||||
func getInterfaceCapabilitiesFromGhw(nic *ghwnet.NIC) string {
|
|
||||||
// Use the speed information from ghw if available
|
|
||||||
if nic.Speed != "" {
|
|
||||||
return nic.Speed
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no speed info from ghw, try to get interface type from name
|
|
||||||
return getInterfaceTypeFromName(nic.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getInterfaceTypeFromName tries to determine interface type from name
|
|
||||||
func getInterfaceTypeFromName(ifaceName string) string {
|
|
||||||
// Common interface naming patterns
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(ifaceName, "eth"):
|
|
||||||
return "Ethernet"
|
|
||||||
case strings.HasPrefix(ifaceName, "en"):
|
|
||||||
return "Ethernet"
|
|
||||||
case strings.HasPrefix(ifaceName, "wlan"):
|
|
||||||
return "WiFi"
|
|
||||||
case strings.HasPrefix(ifaceName, "wl"):
|
|
||||||
return "WiFi"
|
|
||||||
case strings.HasPrefix(ifaceName, "usb"):
|
|
||||||
return "USB"
|
|
||||||
case strings.HasPrefix(ifaceName, "tun"):
|
|
||||||
return "Tunnel"
|
|
||||||
case strings.HasPrefix(ifaceName, "tap"):
|
|
||||||
return "TAP"
|
|
||||||
case strings.HasPrefix(ifaceName, "br"):
|
|
||||||
return "Bridge"
|
|
||||||
case strings.HasPrefix(ifaceName, "bond"):
|
|
||||||
return "Bond"
|
|
||||||
case strings.HasPrefix(ifaceName, "veth"):
|
|
||||||
return "Virtual Ethernet"
|
|
||||||
case strings.HasPrefix(ifaceName, "docker"):
|
|
||||||
return "Docker"
|
|
||||||
case strings.HasPrefix(ifaceName, "lo"):
|
|
||||||
return "Loopback"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readOsRelease() map[string]string {
|
|
||||||
file, err := os.Open("/etc/os-release")
|
file, err := os.Open("/etc/os-release")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string]string{}
|
return "", err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
release := make(map[string]string)
|
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if i := strings.Index(line, "="); i > 0 {
|
if after, ok := strings.CutPrefix(line, "PRETTY_NAME="); ok {
|
||||||
key := line[:i]
|
value := after
|
||||||
val := strings.Trim(line[i+1:], `"`)
|
value = strings.Trim(value, `"`)
|
||||||
release[key] = val
|
return value, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return release
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMemoryInfo() []system.MemoryInfo {
|
return "", errors.New("pretty name not found")
|
||||||
var total string
|
|
||||||
if v, err := mem.VirtualMemory(); err == nil {
|
|
||||||
total = fmt.Sprintf("%d GB", int((float64(v.Total)/(1024*1024*1024))+0.5))
|
|
||||||
}
|
|
||||||
return []system.MemoryInfo{{
|
|
||||||
Total: total,
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
agent/test-data/system_info.json
Normal file
17
agent/test-data/system_info.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS",
|
||||||
|
"Containers": 14,
|
||||||
|
"ContainersRunning": 3,
|
||||||
|
"ContainersPaused": 1,
|
||||||
|
"ContainersStopped": 10,
|
||||||
|
"Images": 508,
|
||||||
|
"Driver": "overlay2",
|
||||||
|
"KernelVersion": "6.8.0-31-generic",
|
||||||
|
"OperatingSystem": "Ubuntu 24.04 LTS",
|
||||||
|
"OSVersion": "24.04",
|
||||||
|
"OSType": "linux",
|
||||||
|
"Architecture": "x86_64",
|
||||||
|
"NCPU": 4,
|
||||||
|
"MemTotal": 2095882240,
|
||||||
|
"ServerVersion": "27.0.1"
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import "github.com/blang/semver"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Version is the current version of the application.
|
// Version is the current version of the application.
|
||||||
Version = "0.17.0"
|
Version = "0.18.0-beta.1"
|
||||||
// AppName is the name of the application.
|
// AppName is the name of the application.
|
||||||
AppName = "beszel"
|
AppName = "beszel"
|
||||||
)
|
)
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -9,7 +9,6 @@ require (
|
|||||||
github.com/fxamacker/cbor/v2 v2.9.0
|
github.com/fxamacker/cbor/v2 v2.9.0
|
||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jaypipes/ghw v0.17.0
|
|
||||||
github.com/lxzan/gws v1.8.9
|
github.com/lxzan/gws v1.8.9
|
||||||
github.com/nicholas-fedor/shoutrrr v0.12.1
|
github.com/nicholas-fedor/shoutrrr v0.12.1
|
||||||
github.com/pocketbase/dbx v1.11.0
|
github.com/pocketbase/dbx v1.11.0
|
||||||
@@ -25,7 +24,6 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
@@ -43,14 +41,11 @@ require (
|
|||||||
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jaypipes/pcidb v1.0.1 // indirect
|
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.1 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
|||||||
11
go.sum
11
go.sum
@@ -2,8 +2,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
|
||||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||||
@@ -43,7 +41,6 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
@@ -71,10 +68,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
|
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
|
||||||
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||||
github.com/jaypipes/ghw v0.17.0 h1:EVLJeNcy5z6GK/Lqby0EhBpynZo+ayl8iJWY0kbEUJA=
|
|
||||||
github.com/jaypipes/ghw v0.17.0/go.mod h1:In8SsaDqlb1oTyrbmTC14uy+fbBMvp+xdqX51MidlD8=
|
|
||||||
github.com/jaypipes/pcidb v1.0.1 h1:WB2zh27T3nwg8AE8ei81sNRb9yWBii3JGNJtT7K9Oic=
|
|
||||||
github.com/jaypipes/pcidb v1.0.1/go.mod h1:6xYUz/yYEyOkIkUt2t2J2folIuZ4Yg6uByCGFXMCeE4=
|
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
@@ -90,8 +83,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
|||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/nicholas-fedor/shoutrrr v0.12.1 h1:8NjY+I3K7cGHy89ncnaPGUA0ex44XbYK3SAFJX9YMI8=
|
github.com/nicholas-fedor/shoutrrr v0.12.1 h1:8NjY+I3K7cGHy89ncnaPGUA0ex44XbYK3SAFJX9YMI8=
|
||||||
@@ -100,8 +91,6 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns
|
|||||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ func TestBatteryAlertLogic(t *testing.T) {
|
|||||||
combinedDataHigh := &system.CombinedData{
|
combinedDataHigh := &system.CombinedData{
|
||||||
Stats: statsHigh,
|
Stats: statsHigh,
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +100,10 @@ func TestBatteryAlertLogic(t *testing.T) {
|
|||||||
combinedDataLow := &system.CombinedData{
|
combinedDataLow := &system.CombinedData{
|
||||||
Stats: statsLow,
|
Stats: statsLow,
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +142,10 @@ func TestBatteryAlertLogic(t *testing.T) {
|
|||||||
combinedDataRecovered := &system.CombinedData{
|
combinedDataRecovered := &system.CombinedData{
|
||||||
Stats: statsRecovered,
|
Stats: statsRecovered,
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,10 +198,10 @@ func TestBatteryAlertNoBattery(t *testing.T) {
|
|||||||
combinedData := &system.CombinedData{
|
combinedData := &system.CombinedData{
|
||||||
Stats: statsNoBattery,
|
Stats: statsNoBattery,
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,10 +294,10 @@ func TestBatteryAlertAveragedSamples(t *testing.T) {
|
|||||||
Battery: [2]uint8{15, 1},
|
Battery: [2]uint8{15, 1},
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,10 +360,10 @@ func TestBatteryAlertAveragedSamples(t *testing.T) {
|
|||||||
Battery: [2]uint8{50, 1},
|
Battery: [2]uint8{50, 1},
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ type FingerprintResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DataRequestOptions struct {
|
type DataRequestOptions struct {
|
||||||
CacheTimeMs uint16 `cbor:"0,keyasint"`
|
CacheTimeMs uint16 `cbor:"0,keyasint"`
|
||||||
// ResourceType uint8 `cbor:"1,keyasint,omitempty,omitzero"`
|
IncludeDetails bool `cbor:"1,keyasint"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerLogsRequest struct {
|
type ContainerLogsRequest struct {
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ type ApiStats struct {
|
|||||||
MemoryStats MemoryStats `json:"memory_stats"`
|
MemoryStats MemoryStats `json:"memory_stats"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Docker system info from /info
|
||||||
|
type HostInfo struct {
|
||||||
|
OperatingSystem string `json:"OperatingSystem"`
|
||||||
|
KernelVersion string `json:"KernelVersion"`
|
||||||
|
NCPU int `json:"NCPU"`
|
||||||
|
MemTotal uint64 `json:"MemTotal"`
|
||||||
|
// OSVersion string `json:"OSVersion"`
|
||||||
|
// OSType string `json:"OSType"`
|
||||||
|
// Architecture string `json:"Architecture"`
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {
|
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {
|
||||||
cpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuContainer
|
cpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuContainer
|
||||||
systemDelta := s.CPUStats.SystemUsage - prevCpuSystem
|
systemDelta := s.CPUStats.SystemUsage - prevCpuSystem
|
||||||
|
|||||||
@@ -115,37 +115,6 @@ const (
|
|||||||
Freebsd
|
Freebsd
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiskInfo struct {
|
|
||||||
Name string `json:"n"`
|
|
||||||
Model string `json:"m,omitempty"`
|
|
||||||
Vendor string `json:"v,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NetworkInfo struct {
|
|
||||||
Name string `json:"n"`
|
|
||||||
Vendor string `json:"v,omitempty"`
|
|
||||||
Model string `json:"m,omitempty"`
|
|
||||||
Speed string `json:"s,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MemoryInfo struct {
|
|
||||||
Total string `json:"t,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CpuInfo struct {
|
|
||||||
Model string `json:"m"`
|
|
||||||
SpeedGHz string `json:"s"`
|
|
||||||
Arch string `json:"a"`
|
|
||||||
Cores int `json:"c"`
|
|
||||||
Threads int `json:"t"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OsInfo struct {
|
|
||||||
Family string `json:"f"`
|
|
||||||
Version string `json:"v"`
|
|
||||||
Kernel string `json:"k"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectionType = uint8
|
type ConnectionType = uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -154,36 +123,29 @@ const (
|
|||||||
ConnectionTypeWebSocket
|
ConnectionTypeWebSocket
|
||||||
)
|
)
|
||||||
|
|
||||||
// StaticInfo contains system information that rarely or never changes
|
// Core system data that is needed in All Systems table
|
||||||
// This is collected at a longer interval (e.g., 10-15 minutes) to reduce bandwidth
|
|
||||||
type StaticInfo struct {
|
|
||||||
Hostname string `json:"h" cbor:"0,keyasint"`
|
|
||||||
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
|
|
||||||
Threads int `json:"t,omitempty" cbor:"2,keyasint,omitempty"`
|
|
||||||
AgentVersion string `json:"v" cbor:"3,keyasint"`
|
|
||||||
Podman bool `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
|
|
||||||
Os Os `json:"os" cbor:"5,keyasint"`
|
|
||||||
Disks []DiskInfo `json:"d,omitempty" cbor:"6,omitempty"`
|
|
||||||
Networks []NetworkInfo `json:"n,omitempty" cbor:"7,omitempty"`
|
|
||||||
Memory []MemoryInfo `json:"m" cbor:"8"`
|
|
||||||
Cpus []CpuInfo `json:"c" cbor:"9"`
|
|
||||||
Oses []OsInfo `json:"o,omitempty" cbor:"10,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info contains frequently-changing system snapshot data for the dashboard
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Uptime uint64 `json:"u" cbor:"0,keyasint"`
|
Hostname string `json:"h,omitempty" cbor:"0,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
Cpu float64 `json:"cpu" cbor:"1,keyasint"`
|
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
MemPct float64 `json:"mp" cbor:"2,keyasint"`
|
Cores int `json:"c,omitzero" cbor:"2,keyasint,omitzero"` // deprecated - moved to Details struct
|
||||||
DiskPct float64 `json:"dp" cbor:"3,keyasint"`
|
CpuModel string `json:"m,omitempty" cbor:"4,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
Bandwidth float64 `json:"b" cbor:"4,keyasint"`
|
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
GpuPct float64 `json:"g,omitempty" cbor:"5,keyasint,omitempty"`
|
Os Os `json:"os,omitempty" cbor:"14,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
DashboardTemp float64 `json:"dt,omitempty" cbor:"6,keyasint,omitempty"`
|
// Threads is needed in Info struct to calculate load average thresholds
|
||||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"7,keyasint,omitempty"`
|
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
|
||||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"8,keyasint,omitempty"`
|
Uptime uint64 `json:"u" cbor:"5,keyasint"`
|
||||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"9,keyasint,omitempty"`
|
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
|
||||||
BandwidthBytes uint64 `json:"bb" cbor:"10,keyasint"`
|
MemPct float64 `json:"mp" cbor:"7,keyasint"`
|
||||||
// TODO: remove load fields in future release in favor of load avg array
|
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
|
||||||
|
Bandwidth float64 `json:"b" cbor:"9,keyasint"`
|
||||||
|
AgentVersion string `json:"v" cbor:"10,keyasint"`
|
||||||
|
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
||||||
|
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
||||||
|
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||||
|
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||||
|
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||||
|
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
||||||
|
|
||||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||||
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
||||||
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||||
@@ -191,11 +153,25 @@ type Info struct {
|
|||||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"23,keyasint,omitzero"` // [percent, charge state]
|
Battery [2]uint8 `json:"bat,omitzero" cbor:"23,keyasint,omitzero"` // [percent, charge state]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data that does not change during process lifetime and is not needed in All Systems table
|
||||||
|
type Details struct {
|
||||||
|
Hostname string `cbor:"0,keyasint"`
|
||||||
|
Kernel string `cbor:"1,keyasint,omitempty"`
|
||||||
|
Cores int `cbor:"2,keyasint"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
// Final data structure to return to the hub
|
// Final data structure to return to the hub
|
||||||
type CombinedData struct {
|
type CombinedData struct {
|
||||||
Stats Stats `json:"stats" cbor:"0,keyasint"`
|
Stats Stats `json:"stats" cbor:"0,keyasint"`
|
||||||
Info Info `json:"info" cbor:"1,keyasint"`
|
Info Info `json:"info" cbor:"1,keyasint"`
|
||||||
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
||||||
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
|
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
|
||||||
StaticInfo *StaticInfo `json:"static_info,omitempty" cbor:"4,keyasint,omitempty"` // Collected at longer intervals
|
Details *Details `cbor:"4,keyasint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
@@ -29,20 +29,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type System struct {
|
type System struct {
|
||||||
Id string `db:"id"`
|
Id string `db:"id"`
|
||||||
Host string `db:"host"`
|
Host string `db:"host"`
|
||||||
Port string `db:"port"`
|
Port string `db:"port"`
|
||||||
Status string `db:"status"`
|
Status string `db:"status"`
|
||||||
manager *SystemManager // Manager that this system belongs to
|
manager *SystemManager // Manager that this system belongs to
|
||||||
client *ssh.Client // SSH client for fetching data
|
client *ssh.Client // SSH client for fetching data
|
||||||
data *system.CombinedData // system data from agent
|
data *system.CombinedData // system data from agent
|
||||||
staticInfo *system.StaticInfo // cached static system info, fetched once per connection
|
ctx context.Context // Context for stopping the updater
|
||||||
ctx context.Context // Context for stopping the updater
|
cancel context.CancelFunc // Stops and removes system from updater
|
||||||
cancel context.CancelFunc // Stops and removes system from updater
|
WsConn *ws.WsConn // Handler for agent WebSocket connection
|
||||||
WsConn *ws.WsConn // Handler for agent WebSocket connection
|
agentVersion semver.Version // Agent version
|
||||||
agentVersion semver.Version // Agent version
|
updateTicker *time.Ticker // Ticker for updating the system
|
||||||
updateTicker *time.Ticker // Ticker for updating the system
|
detailsFetched atomic.Bool // True if static system details have been fetched and saved
|
||||||
smartOnce sync.Once // Once for fetching and saving smart devices
|
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 {
|
func (sm *SystemManager) NewSystem(systemId string) *System {
|
||||||
@@ -115,22 +116,15 @@ func (sys *System) update() error {
|
|||||||
sys.handlePaused()
|
sys.handlePaused()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
options := common.DataRequestOptions{
|
||||||
// Determine which cache time to use based on whether we need static info
|
CacheTimeMs: uint16(interval),
|
||||||
cacheTimeMs := uint16(interval)
|
|
||||||
if sys.staticInfo == nil {
|
|
||||||
// Request with a cache time that signals the agent to include static info
|
|
||||||
// We use 60001ms (just above the standard interval) since uint16 max is 65535
|
|
||||||
cacheTimeMs = 60_001
|
|
||||||
}
|
}
|
||||||
|
// fetch system details if not already fetched
|
||||||
data, err := sys.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: cacheTimeMs})
|
if !sys.detailsFetched.Load() {
|
||||||
|
options.IncludeDetails = true
|
||||||
|
}
|
||||||
|
data, err := sys.fetchDataFromAgent(options)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// If we received static info, cache it
|
|
||||||
if data.StaticInfo != nil {
|
|
||||||
sys.staticInfo = data.StaticInfo
|
|
||||||
sys.manager.hub.Logger().Debug("Cached static system info", "system", sys.Id)
|
|
||||||
}
|
|
||||||
_, err = sys.createRecords(data)
|
_, err = sys.createRecords(data)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -151,23 +145,17 @@ func (sys *System) handlePaused() {
|
|||||||
|
|
||||||
// createRecords updates the system record and adds system_stats and container_stats records
|
// createRecords updates the system record and adds system_stats and container_stats records
|
||||||
func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) {
|
func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) {
|
||||||
// Build complete info combining dynamic and static data
|
|
||||||
completeInfo := sys.buildCompleteInfo(data)
|
|
||||||
|
|
||||||
sys.manager.hub.Logger().Debug("Creating records - complete info", "info", completeInfo)
|
|
||||||
|
|
||||||
systemRecord, err := sys.getRecord()
|
systemRecord, err := sys.getRecord()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
hub := sys.manager.hub
|
hub := sys.manager.hub
|
||||||
err = hub.RunInTransaction(func(txApp core.App) error {
|
err = hub.RunInTransaction(func(txApp core.App) error {
|
||||||
// add system_stats and container_stats records
|
// add system_stats record
|
||||||
systemStatsCollection, err := txApp.FindCachedCollectionByNameOrId("system_stats")
|
systemStatsCollection, err := txApp.FindCachedCollectionByNameOrId("system_stats")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
systemStatsRecord := core.NewRecord(systemStatsCollection)
|
systemStatsRecord := core.NewRecord(systemStatsCollection)
|
||||||
systemStatsRecord.Set("system", systemRecord.Id)
|
systemStatsRecord.Set("system", systemRecord.Id)
|
||||||
systemStatsRecord.Set("stats", data.Stats)
|
systemStatsRecord.Set("stats", data.Stats)
|
||||||
@@ -175,14 +163,14 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
|
|||||||
if err := txApp.SaveNoValidate(systemStatsRecord); err != nil {
|
if err := txApp.SaveNoValidate(systemStatsRecord); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add containers and container_stats records
|
||||||
if len(data.Containers) > 0 {
|
if len(data.Containers) > 0 {
|
||||||
// add / update containers records
|
|
||||||
if data.Containers[0].Id != "" {
|
if data.Containers[0].Id != "" {
|
||||||
if err := createContainerRecords(txApp, data.Containers, sys.Id); err != nil {
|
if err := createContainerRecords(txApp, data.Containers, sys.Id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// add new container_stats record
|
|
||||||
containerStatsCollection, err := txApp.FindCachedCollectionByNameOrId("container_stats")
|
containerStatsCollection, err := txApp.FindCachedCollectionByNameOrId("container_stats")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -203,10 +191,17 @@ 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)
|
// 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("status", up)
|
||||||
|
systemRecord.Set("info", data.Info)
|
||||||
systemRecord.Set("info", completeInfo)
|
|
||||||
if err := txApp.SaveNoValidate(systemRecord); err != nil {
|
if err := txApp.SaveNoValidate(systemRecord); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -215,76 +210,42 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
|
|||||||
|
|
||||||
// Fetch and save SMART devices when system first comes online
|
// Fetch and save SMART devices when system first comes online
|
||||||
if err == nil {
|
if err == nil {
|
||||||
sys.smartOnce.Do(func() {
|
if !sys.smartFetched.Load() && sys.smartFetching.CompareAndSwap(false, true) {
|
||||||
go sys.FetchAndSaveSmartDevices()
|
go func() {
|
||||||
})
|
defer sys.smartFetching.Store(false)
|
||||||
|
if err := sys.FetchAndSaveSmartDevices(); err == nil {
|
||||||
|
sys.smartFetched.Store(true)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemRecord, err
|
return systemRecord, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildCompleteInfo combines the dynamic Info with cached StaticInfo to create a complete system info structure
|
func createSystemDetailsRecord(app core.App, data *system.Details, systemId string) error {
|
||||||
// This is needed because we've split the original Info structure for bandwidth optimization
|
collectionName := "system_details"
|
||||||
func (sys *System) buildCompleteInfo(data *system.CombinedData) map[string]interface{} {
|
params := dbx.Params{
|
||||||
info := make(map[string]interface{})
|
"id": systemId,
|
||||||
|
"system": systemId,
|
||||||
// Add dynamic fields from data.Info
|
"hostname": data.Hostname,
|
||||||
if data.Info.Uptime > 0 {
|
"kernel": data.Kernel,
|
||||||
info["u"] = data.Info.Uptime
|
"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(),
|
||||||
}
|
}
|
||||||
info["cpu"] = data.Info.Cpu
|
result, err := app.DB().Update(collectionName, params, dbx.HashExp{"id": systemId}).Execute()
|
||||||
info["mp"] = data.Info.MemPct
|
rowsAffected, _ := result.RowsAffected()
|
||||||
info["dp"] = data.Info.DiskPct
|
if err != nil || rowsAffected == 0 {
|
||||||
info["b"] = data.Info.Bandwidth
|
_, err = app.DB().Insert(collectionName, params).Execute()
|
||||||
info["bb"] = data.Info.BandwidthBytes
|
|
||||||
if data.Info.GpuPct > 0 {
|
|
||||||
info["g"] = data.Info.GpuPct
|
|
||||||
}
|
}
|
||||||
if data.Info.DashboardTemp > 0 {
|
return err
|
||||||
info["dt"] = data.Info.DashboardTemp
|
|
||||||
}
|
|
||||||
if data.Info.LoadAvg1 > 0 || data.Info.LoadAvg5 > 0 || data.Info.LoadAvg15 > 0 {
|
|
||||||
info["l1"] = data.Info.LoadAvg1
|
|
||||||
info["l5"] = data.Info.LoadAvg5
|
|
||||||
info["l15"] = data.Info.LoadAvg15
|
|
||||||
info["la"] = data.Info.LoadAvg
|
|
||||||
}
|
|
||||||
if data.Info.ConnectionType > 0 {
|
|
||||||
info["ct"] = data.Info.ConnectionType
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add static fields from cached staticInfo
|
|
||||||
if sys.staticInfo != nil {
|
|
||||||
info["h"] = sys.staticInfo.Hostname
|
|
||||||
if sys.staticInfo.KernelVersion != "" {
|
|
||||||
info["k"] = sys.staticInfo.KernelVersion
|
|
||||||
}
|
|
||||||
if sys.staticInfo.Threads > 0 {
|
|
||||||
info["t"] = sys.staticInfo.Threads
|
|
||||||
}
|
|
||||||
info["v"] = sys.staticInfo.AgentVersion
|
|
||||||
if sys.staticInfo.Podman {
|
|
||||||
info["p"] = true
|
|
||||||
}
|
|
||||||
info["os"] = sys.staticInfo.Os
|
|
||||||
if len(sys.staticInfo.Cpus) > 0 {
|
|
||||||
info["c"] = sys.staticInfo.Cpus
|
|
||||||
}
|
|
||||||
if len(sys.staticInfo.Memory) > 0 {
|
|
||||||
info["m"] = sys.staticInfo.Memory
|
|
||||||
}
|
|
||||||
if len(sys.staticInfo.Disks) > 0 {
|
|
||||||
info["d"] = sys.staticInfo.Disks
|
|
||||||
}
|
|
||||||
if len(sys.staticInfo.Networks) > 0 {
|
|
||||||
info["n"] = sys.staticInfo.Networks
|
|
||||||
}
|
|
||||||
if len(sys.staticInfo.Oses) > 0 {
|
|
||||||
info["o"] = sys.staticInfo.Oses
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return info
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {
|
func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {
|
||||||
@@ -434,7 +395,8 @@ func (sys *System) fetchStringFromAgentViaSSH(action common.WebSocketAction, req
|
|||||||
if err := session.Shell(); err != nil {
|
if err := session.Shell(); err != nil {
|
||||||
return false, err
|
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)
|
_ = cbor.NewEncoder(stdin).Encode(req)
|
||||||
_ = stdin.Close()
|
_ = stdin.Close()
|
||||||
var resp common.AgentResponse
|
var resp common.AgentResponse
|
||||||
@@ -498,7 +460,8 @@ func (sys *System) FetchSystemdInfoFromAgent(serviceName string) (systemd.Servic
|
|||||||
return false, err
|
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 {
|
if err := cbor.NewEncoder(stdin).Encode(req); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -546,7 +509,8 @@ func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) (*system.C
|
|||||||
*sys.data = system.CombinedData{}
|
*sys.data = system.CombinedData{}
|
||||||
|
|
||||||
if sys.agentVersion.GTE(beszel.MinVersionAgentResponse) && stdinErr == nil {
|
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)
|
_ = cbor.NewEncoder(stdin).Encode(req)
|
||||||
_ = stdin.Close()
|
_ = stdin.Close()
|
||||||
|
|
||||||
@@ -686,7 +650,6 @@ func (sys *System) closeSSHConnection() {
|
|||||||
sys.client.Close()
|
sys.client.Close()
|
||||||
sys.client = nil
|
sys.client = nil
|
||||||
}
|
}
|
||||||
sys.staticInfo = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeWebSocketConnection closes the WebSocket connection but keeps the system in the manager
|
// closeWebSocketConnection closes the WebSocket connection but keeps the system in the manager
|
||||||
@@ -696,7 +659,6 @@ func (sys *System) closeWebSocketConnection() {
|
|||||||
if sys.WsConn != nil {
|
if sys.WsConn != nil {
|
||||||
sys.WsConn.Close(nil)
|
sys.WsConn.Close(nil)
|
||||||
}
|
}
|
||||||
sys.staticInfo = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractAgentVersion extracts the beszel version from SSH server version string
|
// extractAgentVersion extracts the beszel version from SSH server version string
|
||||||
|
|||||||
@@ -266,18 +266,20 @@ func testOld(t *testing.T, hub *tests.TestHub) {
|
|||||||
|
|
||||||
// Create test system data
|
// Create test system data
|
||||||
testData := &system.CombinedData{
|
testData := &system.CombinedData{
|
||||||
|
Details: &system.Details{
|
||||||
|
Hostname: "data-test.example.com",
|
||||||
|
Kernel: "5.15.0-generic",
|
||||||
|
Cores: 4,
|
||||||
|
Threads: 8,
|
||||||
|
CpuModel: "Test CPU",
|
||||||
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "data-test.example.com",
|
Uptime: 3600,
|
||||||
KernelVersion: "5.15.0-generic",
|
Cpu: 25.5,
|
||||||
Cores: 4,
|
MemPct: 40.2,
|
||||||
Threads: 8,
|
DiskPct: 60.0,
|
||||||
CpuModel: "Test CPU",
|
Bandwidth: 100.0,
|
||||||
Uptime: 3600,
|
AgentVersion: "1.0.0",
|
||||||
Cpu: 25.5,
|
|
||||||
MemPct: 40.2,
|
|
||||||
DiskPct: 60.0,
|
|
||||||
Bandwidth: 100.0,
|
|
||||||
AgentVersion: "1.0.0",
|
|
||||||
},
|
},
|
||||||
Stats: system.Stats{
|
Stats: system.Stats{
|
||||||
Cpu: 25.5,
|
Cpu: 25.5,
|
||||||
|
|||||||
@@ -1439,6 +1439,184 @@ func init() {
|
|||||||
"type": "base",
|
"type": "base",
|
||||||
"updateRule": null,
|
"updateRule": null,
|
||||||
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id"
|
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"createRule": "",
|
||||||
|
"deleteRule": "",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "[a-z0-9]{15}",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3208210256",
|
||||||
|
"max": 15,
|
||||||
|
"min": 15,
|
||||||
|
"name": "id",
|
||||||
|
"pattern": "^[a-z0-9]+$",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": true,
|
||||||
|
"required": true,
|
||||||
|
"system": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"collectionId": "2hz5ncl8tizk5nx",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation3377271179",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "system",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3847340049",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "hostname",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1789936913",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "os",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text2818598173",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "os_name",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1574083243",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "kernel",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3128971310",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "cpu",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"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",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "cores",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1871592925",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "threads",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number3933025333",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "memory",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool2200265312",
|
||||||
|
"name": "podman",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "autodate3332085495",
|
||||||
|
"name": "updated",
|
||||||
|
"onCreate": true,
|
||||||
|
"onUpdate": true,
|
||||||
|
"presentable": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "autodate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "pbc_3116237454",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
|
||||||
|
"name": "system_details",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": "",
|
||||||
|
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id"
|
||||||
}
|
}
|
||||||
]`
|
]`
|
||||||
|
|
||||||
@@ -33,10 +33,7 @@
|
|||||||
"noUnusedFunctionParameters": "error",
|
"noUnusedFunctionParameters": "error",
|
||||||
"noUnusedPrivateClassMembers": "error",
|
"noUnusedPrivateClassMembers": "error",
|
||||||
"useExhaustiveDependencies": {
|
"useExhaustiveDependencies": {
|
||||||
"level": "warn",
|
"level": "off"
|
||||||
"options": {
|
|
||||||
"reportUnnecessaryDependencies": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"useUniqueElementIds": "off",
|
"useUniqueElementIds": "off",
|
||||||
"noUnusedVariables": "error"
|
"noUnusedVariables": "error"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.17.0",
|
"version": "0.18.0-beta.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
|
|||||||
@@ -3,17 +3,7 @@ import { Trans, useLingui } from "@lingui/react/macro"
|
|||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import { timeTicks } from "d3-time"
|
import { timeTicks } from "d3-time"
|
||||||
import {
|
import { XIcon } from "lucide-react"
|
||||||
ChevronRightSquareIcon,
|
|
||||||
ClockArrowUp,
|
|
||||||
CpuIcon,
|
|
||||||
GlobeIcon,
|
|
||||||
HardDriveIcon,
|
|
||||||
LayoutGridIcon,
|
|
||||||
MonitorIcon,
|
|
||||||
ServerIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { subscribeKeys } from "nanostores"
|
import { subscribeKeys } from "nanostores"
|
||||||
import React, { type JSX, lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import React, { type JSX, lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import AreaChartDefault, { type DataPoint } from "@/components/charts/area-chart"
|
import AreaChartDefault, { type DataPoint } from "@/components/charts/area-chart"
|
||||||
@@ -26,7 +16,7 @@ import MemChart from "@/components/charts/mem-chart"
|
|||||||
import SwapChart from "@/components/charts/swap-chart"
|
import SwapChart from "@/components/charts/swap-chart"
|
||||||
import TemperatureChart from "@/components/charts/temperature-chart"
|
import TemperatureChart from "@/components/charts/temperature-chart"
|
||||||
import { getPbTimestamp, pb } from "@/lib/api"
|
import { getPbTimestamp, pb } from "@/lib/api"
|
||||||
import { ChartType, ConnectionType, connectionTypeLabels, Os, SystemStatus, Unit } from "@/lib/enums"
|
import { ChartType, Os, SystemStatus, Unit } from "@/lib/enums"
|
||||||
import { batteryStateTranslations } from "@/lib/i18n"
|
import { batteryStateTranslations } from "@/lib/i18n"
|
||||||
import {
|
import {
|
||||||
$allSystemsById,
|
$allSystemsById,
|
||||||
@@ -46,8 +36,6 @@ import {
|
|||||||
compareSemVer,
|
compareSemVer,
|
||||||
decimalString,
|
decimalString,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
secondsToString,
|
|
||||||
getHostDisplayValue,
|
|
||||||
listen,
|
listen,
|
||||||
parseSemVer,
|
parseSemVer,
|
||||||
toFixedFloat,
|
toFixedFloat,
|
||||||
@@ -63,20 +51,18 @@ import type {
|
|||||||
SystemStats,
|
SystemStats,
|
||||||
SystemStatsRecord,
|
SystemStatsRecord,
|
||||||
} from "@/types"
|
} from "@/types"
|
||||||
import ChartTimeSelect from "../charts/chart-time-select"
|
|
||||||
import { $router, navigate } from "../router"
|
import { $router, navigate } from "../router"
|
||||||
import Spinner from "../spinner"
|
import Spinner from "../spinner"
|
||||||
import { Button } from "../ui/button"
|
import { Button } from "../ui/button"
|
||||||
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||||
import { AppleIcon, ChartAverage, ChartMax, EthernetIcon, FreeBsdIcon, Rows, TuxIcon, WebSocketIcon, WindowsIcon } from "../ui/icons"
|
import { ChartAverage, ChartMax } from "../ui/icons"
|
||||||
import { Input } from "../ui/input"
|
import { Input } from "../ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
||||||
import { Separator } from "../ui/separator"
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
|
||||||
import NetworkSheet from "./system/network-sheet"
|
import NetworkSheet from "./system/network-sheet"
|
||||||
import CpuCoresSheet from "./system/cpu-sheet"
|
import CpuCoresSheet from "./system/cpu-sheet"
|
||||||
import LineChartDefault from "../charts/line-chart"
|
import LineChartDefault from "../charts/line-chart"
|
||||||
import { pinnedAxisDomain } from "../ui/chart"
|
import { pinnedAxisDomain } from "../ui/chart"
|
||||||
|
import InfoBar from "./system/info-bar"
|
||||||
|
|
||||||
type ChartTimeData = {
|
type ChartTimeData = {
|
||||||
time: number
|
time: number
|
||||||
@@ -156,8 +142,8 @@ async function getStats<T extends SystemStatsRecord | ContainerStatsRecord>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function dockerOrPodman(str: string, system: SystemRecord): string {
|
function dockerOrPodman(str: string, isPodman: boolean): string {
|
||||||
if (system.info.p) {
|
if (isPodman) {
|
||||||
return str.replace("docker", "podman").replace("Docker", "Podman")
|
return str.replace("docker", "podman").replace("Docker", "Podman")
|
||||||
}
|
}
|
||||||
return str
|
return str
|
||||||
@@ -180,6 +166,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
const isLongerChart = !["1m", "1h"].includes(chartTime) // true if chart time is not 1m or 1h
|
const isLongerChart = !["1m", "1h"].includes(chartTime) // true if chart time is not 1m or 1h
|
||||||
const userSettings = $userSettings.get()
|
const userSettings = $userSettings.get()
|
||||||
const chartWrapRef = useRef<HTMLDivElement>(null)
|
const chartWrapRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [isPodman, setIsPodman] = useState(system.info?.p ?? false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -219,7 +206,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
// subscribe to realtime metrics if chart time is 1m
|
// subscribe to realtime metrics if chart time is 1m
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
|
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unsub = () => { }
|
let unsub = () => {}
|
||||||
if (!system.id || chartTime !== "1m") {
|
if (!system.id || chartTime !== "1m") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -335,105 +322,9 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
})
|
})
|
||||||
}, [system, chartTime])
|
}, [system, chartTime])
|
||||||
|
|
||||||
// Helper to format hardware info (disk/nic) with vendor and model
|
useEffect(() => {
|
||||||
const formatHardwareInfo = useCallback((item: { n: string; v?: string; m?: string }) => {
|
setIsPodman(system.info?.p ?? false)
|
||||||
const vendor = item.v && item.v.toLowerCase() !== 'unknown' ? item.v : null
|
}, [system.info?.p])
|
||||||
const model = item.m && item.m.toLowerCase() !== 'unknown' ? item.m : null
|
|
||||||
if (vendor && model) {
|
|
||||||
return `${item.n}: ${vendor} ${model}`
|
|
||||||
} else if (model) {
|
|
||||||
return `${item.n}: ${model}`
|
|
||||||
} else if (vendor) {
|
|
||||||
return `${item.n}: ${vendor}`
|
|
||||||
}
|
|
||||||
return item.n
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// values for system info bar
|
|
||||||
const systemInfo = useMemo(() => {
|
|
||||||
if (!system.info) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const osInfo = {
|
|
||||||
[Os.Linux]: {
|
|
||||||
Icon: TuxIcon,
|
|
||||||
value: system.info.k,
|
|
||||||
label: t({ comment: "Linux kernel", message: "Kernel" }),
|
|
||||||
},
|
|
||||||
[Os.Darwin]: {
|
|
||||||
Icon: AppleIcon,
|
|
||||||
value: `macOS ${system.info.k}`,
|
|
||||||
},
|
|
||||||
[Os.Windows]: {
|
|
||||||
Icon: WindowsIcon,
|
|
||||||
value: system.info.k,
|
|
||||||
},
|
|
||||||
[Os.FreeBSD]: {
|
|
||||||
Icon: FreeBsdIcon,
|
|
||||||
value: system.info.k,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
let uptime: string
|
|
||||||
if (system.info.u < 3600) {
|
|
||||||
uptime = secondsToString(system.info.u, "minute")
|
|
||||||
} else if (system.info.u < 360000) {
|
|
||||||
uptime = secondsToString(system.info.u, "hour")
|
|
||||||
} else {
|
|
||||||
uptime = secondsToString(system.info.u, "day")
|
|
||||||
}
|
|
||||||
// Extract CPU and Memory info from arrays
|
|
||||||
const cpuInfo = system.info.c && system.info.c.length > 0 ? system.info.c[0] : undefined
|
|
||||||
const memoryInfo = system.info.m && system.info.m.length > 0 ? system.info.m[0] : undefined
|
|
||||||
const osData = system.info.o && system.info.o.length > 0 ? system.info.o[0] : undefined
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
|
|
||||||
{
|
|
||||||
value: system.info.h,
|
|
||||||
Icon: MonitorIcon,
|
|
||||||
label: "Hostname",
|
|
||||||
// hide if hostname is same as host or name
|
|
||||||
hide: system.info.h === system.host || system.info.h === system.name,
|
|
||||||
},
|
|
||||||
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
|
|
||||||
osData ? {
|
|
||||||
value: `${osData.f} ${osData.v}`.trim(),
|
|
||||||
Icon: osInfo[system.info.os ?? Os.Linux]?.Icon ?? TuxIcon,
|
|
||||||
label: osData.k ? `Kernel: ${osData.k}` : undefined,
|
|
||||||
} : osInfo[system.info.os ?? Os.Linux],
|
|
||||||
cpuInfo ? {
|
|
||||||
value: cpuInfo.m,
|
|
||||||
Icon: CpuIcon,
|
|
||||||
hide: !cpuInfo.m,
|
|
||||||
label: [
|
|
||||||
(cpuInfo.c || cpuInfo.t) ? `Cores / Threads: ${cpuInfo.c || '?'} / ${cpuInfo.t || cpuInfo.c || '?'}` : null,
|
|
||||||
cpuInfo.a ? `Arch: ${cpuInfo.a}` : null,
|
|
||||||
cpuInfo.s ? `Speed: ${cpuInfo.s}` : null,
|
|
||||||
].filter(Boolean).join('\n'),
|
|
||||||
} : undefined,
|
|
||||||
memoryInfo ? {
|
|
||||||
value: memoryInfo.t,
|
|
||||||
Icon: ServerIcon,
|
|
||||||
label: "Total Memory",
|
|
||||||
} : undefined,
|
|
||||||
system.info.d && system.info.d.length > 0 ? {
|
|
||||||
value: `${system.info.d.length} ${system.info.d.length === 1 ? t`Disk` : t`Disks`}`,
|
|
||||||
Icon: HardDriveIcon,
|
|
||||||
label: system.info.d.map(formatHardwareInfo).join('\n'),
|
|
||||||
} : undefined,
|
|
||||||
system.info.n && system.info.n.length > 0 ? {
|
|
||||||
value: `${system.info.n.length} ${system.info.n.length === 1 ? t`NIC` : t`NICs`}`,
|
|
||||||
Icon: EthernetIcon,
|
|
||||||
label: system.info.n.map(formatHardwareInfo).join('\n'),
|
|
||||||
} : undefined,
|
|
||||||
].filter(Boolean) as {
|
|
||||||
value: string | number | undefined
|
|
||||||
label?: string
|
|
||||||
Icon: React.ElementType
|
|
||||||
hide?: boolean
|
|
||||||
}[]
|
|
||||||
}, [system, t, formatHardwareInfo])
|
|
||||||
|
|
||||||
/** Space for tooltip if more than 10 sensors and no containers table */
|
/** Space for tooltip if more than 10 sensors and no containers table */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -503,113 +394,11 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined || gpu.pp !== undefined)
|
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined || gpu.pp !== undefined)
|
||||||
const hasGpuEnginesData = lastGpuVals.some((gpu) => gpu.e !== undefined)
|
const hasGpuEnginesData = lastGpuVals.some((gpu) => gpu.e !== undefined)
|
||||||
|
|
||||||
let translatedStatus: string = system.status
|
|
||||||
if (system.status === SystemStatus.Up) {
|
|
||||||
translatedStatus = t({ message: "Up", comment: "Context: System is up" })
|
|
||||||
} else if (system.status === SystemStatus.Down) {
|
|
||||||
translatedStatus = t({ message: "Down", comment: "Context: System is down" })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={chartWrapRef} className="grid gap-4 mb-14 overflow-x-clip">
|
<div ref={chartWrapRef} className="grid gap-4 mb-14 overflow-x-clip">
|
||||||
{/* system info */}
|
{/* system info */}
|
||||||
<Card>
|
<InfoBar system={system} chartData={chartData} grid={grid} setGrid={setGrid} setIsPodman={setIsPodman} />
|
||||||
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
|
||||||
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="capitalize flex gap-2 items-center">
|
|
||||||
<span className={cn("relative flex h-3 w-3")}>
|
|
||||||
{system.status === SystemStatus.Up && (
|
|
||||||
<span
|
|
||||||
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
|
||||||
style={{ animationDuration: "1.5s" }}
|
|
||||||
></span>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
|
||||||
"bg-green-500": system.status === SystemStatus.Up,
|
|
||||||
"bg-red-500": system.status === SystemStatus.Down,
|
|
||||||
"bg-primary/40": system.status === SystemStatus.Paused,
|
|
||||||
"bg-yellow-500": system.status === SystemStatus.Pending,
|
|
||||||
})}
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
{translatedStatus}
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
{system.info.ct && (
|
|
||||||
<TooltipContent>
|
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
{system.info.ct === ConnectionType.WebSocket ? (
|
|
||||||
<WebSocketIcon className="size-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
|
|
||||||
)}
|
|
||||||
{connectionTypeLabels[system.info.ct as ConnectionType]}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
{systemInfo.map(({ value, label, Icon, hide }) => {
|
|
||||||
if (hide || !value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const content = (
|
|
||||||
<div className="flex gap-1.5 items-center">
|
|
||||||
<Icon className="h-4 w-4" /> {value}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<div key={value} className="contents">
|
|
||||||
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
|
||||||
{label ? (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={150}>
|
|
||||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
|
||||||
<TooltipContent>{label}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
|
||||||
content
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
|
|
||||||
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
|
|
||||||
<TooltipProvider delayDuration={100}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
aria-label={t`Toggle grid`}
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="hidden xl:flex p-0 text-primary"
|
|
||||||
onClick={() => setGrid(!grid)}
|
|
||||||
>
|
|
||||||
{grid ? (
|
|
||||||
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
|
|
||||||
) : (
|
|
||||||
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>{t`Toggle grid`}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
|
|
||||||
{/* <Tabs defaultValue="overview" className="w-full">
|
{/* <Tabs defaultValue="overview" className="w-full">
|
||||||
<TabsList className="w-full h-11">
|
<TabsList className="w-full h-11">
|
||||||
@@ -621,7 +410,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs> */}
|
</Tabs> */}
|
||||||
|
|
||||||
|
|
||||||
{/* main charts */}
|
{/* main charts */}
|
||||||
<div className="grid xl:grid-cols-2 gap-4">
|
<div className="grid xl:grid-cols-2 gap-4">
|
||||||
<ChartCard
|
<ChartCard
|
||||||
@@ -657,7 +445,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title={dockerOrPodman(t`Docker CPU Usage`, system)}
|
title={dockerOrPodman(t`Docker CPU Usage`, isPodman)}
|
||||||
description={t`Average CPU utilization of containers`}
|
description={t`Average CPU utilization of containers`}
|
||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
@@ -684,8 +472,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title={dockerOrPodman(t`Docker Memory Usage`, system)}
|
title={dockerOrPodman(t`Docker Memory Usage`, isPodman)}
|
||||||
description={dockerOrPodman(t`Memory usage of docker containers`, system)}
|
description={dockerOrPodman(t`Memory usage of docker containers`, isPodman)}
|
||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerChart
|
<ContainerChart
|
||||||
@@ -805,8 +593,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title={dockerOrPodman(t`Docker Network I/O`, system)}
|
title={dockerOrPodman(t`Docker Network I/O`, isPodman)}
|
||||||
description={dockerOrPodman(t`Network traffic of docker containers`, system)}
|
description={dockerOrPodman(t`Network traffic of docker containers`, isPodman)}
|
||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerChart
|
<ContainerChart
|
||||||
@@ -845,10 +633,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
|
|
||||||
{/* Temperature chart */}
|
{/* Temperature chart */}
|
||||||
{systemStats.at(-1)?.stats.t && (
|
{systemStats.at(-1)?.stats.t && (
|
||||||
<div
|
<div ref={temperatureChartRef} className={cn("odd:last-of-type:col-span-full", { "col-span-full": !grid })}>
|
||||||
ref={temperatureChartRef}
|
|
||||||
className={cn("odd:last-of-type:col-span-full", { "col-span-full": !grid })}
|
|
||||||
>
|
|
||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
@@ -1010,7 +795,9 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
label: t`Write`,
|
label: t`Write`,
|
||||||
dataKey: ({ stats }) => {
|
dataKey: ({ stats }) => {
|
||||||
if (showMax) {
|
if (showMax) {
|
||||||
return stats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
|
return (
|
||||||
|
stats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return stats?.efs?.[extraFsName]?.wb || (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024
|
return stats?.efs?.[extraFsName]?.wb || (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024
|
||||||
},
|
},
|
||||||
@@ -1048,9 +835,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{compareSemVer(chartData.agentVersion, parseSemVer("0.15.0")) >= 0 && (
|
{compareSemVer(chartData.agentVersion, parseSemVer("0.15.0")) >= 0 && <LazySmartTable systemId={system.id} />}
|
||||||
<LazySmartTable systemId={system.id} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{containerData.length > 0 && compareSemVer(chartData.agentVersion, parseSemVer("0.14.0")) >= 0 && (
|
{containerData.length > 0 && compareSemVer(chartData.agentVersion, parseSemVer("0.14.0")) >= 0 && (
|
||||||
<LazyContainersTable systemId={system.id} />
|
<LazyContainersTable systemId={system.id} />
|
||||||
@@ -1106,13 +891,10 @@ function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilt
|
|||||||
return () => clearTimeout(handle)
|
return () => clearTimeout(handle)
|
||||||
}, [inputValue, storeValue, store])
|
}, [inputValue, storeValue, store])
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
const value = e.target.value
|
||||||
const value = e.target.value
|
setInputValue(value)
|
||||||
setInputValue(value)
|
}, [])
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleClear = useCallback(() => {
|
const handleClear = useCallback(() => {
|
||||||
setInputValue("")
|
setInputValue("")
|
||||||
@@ -1239,4 +1021,4 @@ function LazySystemdTable({ systemId }: { systemId: string }) {
|
|||||||
{isIntersecting && <SystemdTable systemId={systemId} />}
|
{isIntersecting && <SystemdTable systemId={systemId} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
259
internal/site/src/components/routes/system/info-bar.tsx
Normal file
259
internal/site/src/components/routes/system/info-bar.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import { plural } from "@lingui/core/macro"
|
||||||
|
import { useLingui } from "@lingui/react/macro"
|
||||||
|
import {
|
||||||
|
AppleIcon,
|
||||||
|
ChevronRightSquareIcon,
|
||||||
|
ClockArrowUp,
|
||||||
|
CpuIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
LayoutGridIcon,
|
||||||
|
MemoryStickIcon,
|
||||||
|
MonitorIcon,
|
||||||
|
Rows,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
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,
|
||||||
|
chartData,
|
||||||
|
grid,
|
||||||
|
setGrid,
|
||||||
|
setIsPodman,
|
||||||
|
}: {
|
||||||
|
system: SystemRecord
|
||||||
|
chartData: ChartData
|
||||||
|
grid: boolean
|
||||||
|
setGrid: (grid: boolean) => void
|
||||||
|
setIsPodman: (isPodman: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { t } = useLingui()
|
||||||
|
const [details, setDetails] = useState<SystemDetailsRecord | null>(null)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
pb.collection<SystemDetailsRecord>("system_details")
|
||||||
|
.getOne(system.id, {
|
||||||
|
fields: "hostname,kernel,cores,threads,cpu,os,os_name,arch,memory,podman",
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "public, max-age=60",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((details) => {
|
||||||
|
if (active) {
|
||||||
|
setDetails(details)
|
||||||
|
setIsPodman(details.podman)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false
|
||||||
|
}
|
||||||
|
}, [system.id])
|
||||||
|
|
||||||
|
// values for system info bar - use details with fallback to system.info
|
||||||
|
const systemInfo = useMemo(() => {
|
||||||
|
if (!system.info) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use details if available, otherwise fall back to system.info
|
||||||
|
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 ?? 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 = {
|
||||||
|
[Os.Linux]: {
|
||||||
|
Icon: TuxIcon,
|
||||||
|
// show kernel in tooltip if os name is available, otherwise show the kernel
|
||||||
|
value: osName || kernel,
|
||||||
|
label: osName ? kernel : undefined,
|
||||||
|
},
|
||||||
|
[Os.Darwin]: {
|
||||||
|
Icon: AppleIcon,
|
||||||
|
value: osName || `macOS ${kernel}`,
|
||||||
|
},
|
||||||
|
[Os.Windows]: {
|
||||||
|
Icon: WindowsIcon,
|
||||||
|
value: osName || kernel,
|
||||||
|
label: osName ? kernel : undefined,
|
||||||
|
},
|
||||||
|
[Os.FreeBSD]: {
|
||||||
|
Icon: FreeBsdIcon,
|
||||||
|
value: osName || kernel,
|
||||||
|
label: osName ? kernel : undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let uptime: string
|
||||||
|
if (system.info.u < 3600) {
|
||||||
|
uptime = secondsToString(system.info.u, "minute")
|
||||||
|
} else if (system.info.u < 360000) {
|
||||||
|
uptime = secondsToString(system.info.u, "hour")
|
||||||
|
} else {
|
||||||
|
uptime = secondsToString(system.info.u, "day")
|
||||||
|
}
|
||||||
|
const info = [
|
||||||
|
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
|
||||||
|
{
|
||||||
|
value: hostname,
|
||||||
|
Icon: MonitorIcon,
|
||||||
|
label: "Hostname",
|
||||||
|
// hide if hostname is same as host or name
|
||||||
|
hide: hostname === system.host || hostname === system.name,
|
||||||
|
},
|
||||||
|
{ 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
|
||||||
|
Icon: React.ElementType
|
||||||
|
hide?: boolean
|
||||||
|
}[]
|
||||||
|
|
||||||
|
if (memory) {
|
||||||
|
const memValue = formatBytes(memory, false, undefined, false)
|
||||||
|
info.push({
|
||||||
|
value: `${toFixedFloat(memValue.value, memValue.value >= 10 ? 1 : 2)} ${memValue.unit}`,
|
||||||
|
Icon: MemoryStickIcon,
|
||||||
|
hide: !memory,
|
||||||
|
label: t`Memory`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}, [system, details, t])
|
||||||
|
|
||||||
|
let translatedStatus: string = system.status
|
||||||
|
if (system.status === SystemStatus.Up) {
|
||||||
|
translatedStatus = t({ message: "Up", comment: "Context: System is up" })
|
||||||
|
} else if (system.status === SystemStatus.Down) {
|
||||||
|
translatedStatus = t({ message: "Down", comment: "Context: System is down" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
||||||
|
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="capitalize flex gap-2 items-center">
|
||||||
|
<span className={cn("relative flex h-3 w-3")}>
|
||||||
|
{system.status === SystemStatus.Up && (
|
||||||
|
<span
|
||||||
|
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||||
|
style={{ animationDuration: "1.5s" }}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
||||||
|
"bg-green-500": system.status === SystemStatus.Up,
|
||||||
|
"bg-red-500": system.status === SystemStatus.Down,
|
||||||
|
"bg-primary/40": system.status === SystemStatus.Paused,
|
||||||
|
"bg-yellow-500": system.status === SystemStatus.Pending,
|
||||||
|
})}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
{translatedStatus}
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{system.info.ct && (
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
{system.info.ct === ConnectionType.WebSocket ? (
|
||||||
|
<WebSocketIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
|
||||||
|
)}
|
||||||
|
{connectionTypeLabels[system.info.ct as ConnectionType]}
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
{systemInfo.map(({ value, label, Icon, hide }) => {
|
||||||
|
if (hide || !value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const content = (
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
<Icon className="h-4 w-4" /> {value}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div key={value} className="contents">
|
||||||
|
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||||
|
{label ? (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={150}>
|
||||||
|
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||||
|
<TooltipContent>{label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
|
||||||
|
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
|
||||||
|
<TooltipProvider delayDuration={100}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
aria-label={t`Toggle grid`}
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="hidden xl:flex p-0 text-primary"
|
||||||
|
onClick={() => setGrid(!grid)}
|
||||||
|
>
|
||||||
|
{grid ? (
|
||||||
|
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
|
||||||
|
) : (
|
||||||
|
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t`Toggle grid`}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
internal/site/src/types.d.ts
vendored
47
internal/site/src/types.d.ts
vendored
@@ -27,32 +27,12 @@ export interface SystemRecord extends RecordModel {
|
|||||||
host: string
|
host: string
|
||||||
status: "up" | "down" | "paused" | "pending"
|
status: "up" | "down" | "paused" | "pending"
|
||||||
port: string
|
port: string
|
||||||
info: systemInfo
|
info: SystemInfo
|
||||||
v: string
|
v: string
|
||||||
updated: string
|
updated: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CpuInfo {
|
export interface SystemInfo {
|
||||||
m: string
|
|
||||||
s: string
|
|
||||||
a: string
|
|
||||||
c: number
|
|
||||||
t: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OsInfo {
|
|
||||||
f: string
|
|
||||||
v: string
|
|
||||||
k: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NetworkLocationInfo {
|
|
||||||
ip?: string
|
|
||||||
isp?: string
|
|
||||||
asn?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface systemInfo {
|
|
||||||
/** hostname */
|
/** hostname */
|
||||||
h: string
|
h: string
|
||||||
/** kernel **/
|
/** kernel **/
|
||||||
@@ -95,16 +75,6 @@ export interface systemInfo {
|
|||||||
g?: number
|
g?: number
|
||||||
/** dashboard display temperature */
|
/** dashboard display temperature */
|
||||||
dt?: number
|
dt?: number
|
||||||
/** disks info (array of block devices with model/vendor/serial) */
|
|
||||||
d?: { n: string; m?: string; v?: string; serial?: string }[]
|
|
||||||
/** networks info (array of network interfaces with vendor/model/capabilities) */
|
|
||||||
n?: { n: string; v?: string; m?: string; s?: string }[]
|
|
||||||
/** memory info (array with total property) */
|
|
||||||
m?: { t: string }[]
|
|
||||||
/** cpu info (array of cpu objects) */
|
|
||||||
c?: CpuInfo[]
|
|
||||||
/** os info (array of os objects) */
|
|
||||||
o?: OsInfo[]
|
|
||||||
/** operating system */
|
/** operating system */
|
||||||
os?: Os
|
os?: Os
|
||||||
/** connection type */
|
/** connection type */
|
||||||
@@ -410,6 +380,19 @@ export interface SmartAttribute {
|
|||||||
wf?: string
|
wf?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SystemDetailsRecord extends RecordModel {
|
||||||
|
system: string
|
||||||
|
hostname: string
|
||||||
|
kernel: string
|
||||||
|
cores: number
|
||||||
|
threads: number
|
||||||
|
cpu: string
|
||||||
|
os: Os
|
||||||
|
os_name: string
|
||||||
|
memory: number
|
||||||
|
podman: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface SmartDeviceRecord extends RecordModel {
|
export interface SmartDeviceRecord extends RecordModel {
|
||||||
id: string
|
id: string
|
||||||
system: string
|
system: string
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
## 0.18.0
|
||||||
|
|
||||||
|
- Remove `la1`, `la5`, `la15` fields from `Info` struct in favor of `la` array.
|
||||||
|
|
||||||
|
- Remove `MB` bandwidth values in favor of bytes.
|
||||||
|
|
||||||
## 0.17.0
|
## 0.17.0
|
||||||
|
|
||||||
- Add quiet hours to silence alerts during specific time periods. (#265)
|
- Add quiet hours to silence alerts during specific time periods. (#265)
|
||||||
|
|||||||
Reference in New Issue
Block a user