Compare commits
18 Commits
86ea23fe39
...
split-inte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb26877720 | ||
|
|
e149366451 | ||
|
|
8da1ded73e | ||
|
|
efa37b2312 | ||
|
|
bcdb4c92b5 | ||
|
|
a7d07310b6 | ||
|
|
8db87e5497 | ||
|
|
e601a0d564 | ||
|
|
07491108cd | ||
|
|
42ab17de1f | ||
|
|
2d14174f61 | ||
|
|
a19ccc9263 | ||
|
|
956880aa59 | ||
|
|
b2b54db409 | ||
|
|
32d5188eef | ||
|
|
46dab7f531 | ||
|
|
c898a9ebbc | ||
|
|
8a13b05c20 |
@@ -1,6 +1,6 @@
|
|||||||
# Node.js dependencies
|
# Node.js dependencies
|
||||||
node_modules
|
node_modules
|
||||||
src/site/node_modules
|
internalsite/node_modules
|
||||||
|
|
||||||
# Go build artifacts and binaries
|
# Go build artifacts and binaries
|
||||||
build
|
build
|
||||||
|
|||||||
16
.github/workflows/docker-images.yml
vendored
@@ -14,21 +14,21 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- image: henrygd/beszel
|
- image: henrygd/beszel
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: ./src/dockerfile_hub
|
dockerfile: ./internal/dockerfile_hub
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username_secret: DOCKERHUB_USERNAME
|
username_secret: DOCKERHUB_USERNAME
|
||||||
password_secret: DOCKERHUB_TOKEN
|
password_secret: DOCKERHUB_TOKEN
|
||||||
|
|
||||||
- image: henrygd/beszel-agent
|
- image: henrygd/beszel-agent
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: ./src/dockerfile_agent
|
dockerfile: ./internal/dockerfile_agent
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username_secret: DOCKERHUB_USERNAME
|
username_secret: DOCKERHUB_USERNAME
|
||||||
password_secret: DOCKERHUB_TOKEN
|
password_secret: DOCKERHUB_TOKEN
|
||||||
|
|
||||||
- image: henrygd/beszel-agent-nvidia
|
- image: henrygd/beszel-agent-nvidia
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: ./src/dockerfile_agent_nvidia
|
dockerfile: ./internal/dockerfile_agent_nvidia
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username_secret: DOCKERHUB_USERNAME
|
username_secret: DOCKERHUB_USERNAME
|
||||||
@@ -36,21 +36,21 @@ jobs:
|
|||||||
|
|
||||||
- image: ghcr.io/${{ github.repository }}/beszel
|
- image: ghcr.io/${{ github.repository }}/beszel
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: ./src/dockerfile_hub
|
dockerfile: ./internal/dockerfile_hub
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password_secret: GITHUB_TOKEN
|
password_secret: GITHUB_TOKEN
|
||||||
|
|
||||||
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: ./src/dockerfile_agent
|
dockerfile: ./internal/dockerfile_agent
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password_secret: GITHUB_TOKEN
|
password_secret: GITHUB_TOKEN
|
||||||
|
|
||||||
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
|
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: ./src/dockerfile_agent_nvidia
|
dockerfile: ./internal/dockerfile_agent_nvidia
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -68,10 +68,10 @@ jobs:
|
|||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --no-save --cwd ./src/site
|
run: bun install --no-save --cwd ./internal/site
|
||||||
|
|
||||||
- name: Build site
|
- name: Build site
|
||||||
run: bun run --cwd ./src/site build
|
run: bun run --cwd ./internal/site build
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
|||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --no-save --cwd ./src/site
|
run: bun install --no-save --cwd ./internal/site
|
||||||
|
|
||||||
- name: Build site
|
- name: Build site
|
||||||
run: bun run --cwd ./src/site build
|
run: bun run --cwd ./internal/site build
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
|
|||||||
7
.gitignore
vendored
@@ -8,15 +8,16 @@ beszel_data
|
|||||||
beszel_data*
|
beszel_data*
|
||||||
dist
|
dist
|
||||||
*.exe
|
*.exe
|
||||||
src/cmd/hub/hub
|
internal/cmd/hub/hub
|
||||||
src/cmd/agent/agent
|
internal/cmd/agent/agent
|
||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
*timestamp*
|
*timestamp*
|
||||||
.swc
|
.swc
|
||||||
src/site/src/locales/**/*.ts
|
internal/site/src/locales/**/*.ts
|
||||||
*.bak
|
*.bak
|
||||||
__debug_*
|
__debug_*
|
||||||
agent/lhm/obj
|
agent/lhm/obj
|
||||||
agent/lhm/bin
|
agent/lhm/bin
|
||||||
dockerfile_agent_dev
|
dockerfile_agent_dev
|
||||||
|
.vite
|
||||||
@@ -9,7 +9,7 @@ before:
|
|||||||
builds:
|
builds:
|
||||||
- id: beszel
|
- id: beszel
|
||||||
binary: beszel
|
binary: beszel
|
||||||
main: src/cmd/hub/hub.go
|
main: internal/cmd/hub/hub.go
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
@@ -22,7 +22,7 @@ builds:
|
|||||||
|
|
||||||
- id: beszel-agent
|
- id: beszel-agent
|
||||||
binary: beszel-agent
|
binary: beszel-agent
|
||||||
main: src/cmd/agent/agent.go
|
main: internal/cmd/agent/agent.go
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
|
|||||||
36
Makefile
@@ -26,11 +26,11 @@ tidy:
|
|||||||
|
|
||||||
build-web-ui:
|
build-web-ui:
|
||||||
@if command -v bun >/dev/null 2>&1; then \
|
@if command -v bun >/dev/null 2>&1; then \
|
||||||
bun install --cwd ./src/site && \
|
bun install --cwd ./internal/site && \
|
||||||
bun run --cwd ./src/site build; \
|
bun run --cwd ./internal/site build; \
|
||||||
else \
|
else \
|
||||||
npm install --prefix ./src/site && \
|
npm install --prefix ./internal/site && \
|
||||||
npm run --prefix ./src/site build; \
|
npm run --prefix ./internal/site build; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Conditional .NET build - only for Windows
|
# Conditional .NET build - only for Windows
|
||||||
@@ -48,45 +48,45 @@ build-dotnet-conditional:
|
|||||||
|
|
||||||
# Update build-agent to include conditional .NET build
|
# Update build-agent to include conditional .NET build
|
||||||
build-agent: tidy build-dotnet-conditional
|
build-agent: tidy build-dotnet-conditional
|
||||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./src/cmd/agent
|
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/agent
|
||||||
|
|
||||||
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
|
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
|
||||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./src/cmd/hub
|
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/hub
|
||||||
|
|
||||||
build-hub-dev: tidy
|
build-hub-dev: tidy
|
||||||
mkdir -p ./src/site/dist && touch ./src/site/dist/index.html
|
mkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html
|
||||||
GOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./src/cmd/hub
|
GOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/hub
|
||||||
|
|
||||||
build: build-agent build-hub
|
build: build-agent build-hub
|
||||||
|
|
||||||
generate-locales:
|
generate-locales:
|
||||||
@if [ ! -f ./src/site/src/locales/en/en.ts ]; then \
|
@if [ ! -f ./internal/site/src/locales/en/en.ts ]; then \
|
||||||
echo "Generating locales..."; \
|
echo "Generating locales..."; \
|
||||||
command -v bun >/dev/null 2>&1 && cd ./src/site && bun install && bun run sync || cd ./src/site && npm install && npm run sync; \
|
command -v bun >/dev/null 2>&1 && cd ./internal/site && bun install && bun run sync || cd ./internal/site && npm install && npm run sync; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dev-server: generate-locales
|
dev-server: generate-locales
|
||||||
cd ./src/site
|
cd ./internal/site
|
||||||
@if command -v bun >/dev/null 2>&1; then \
|
@if command -v bun >/dev/null 2>&1; then \
|
||||||
cd ./src/site && bun run dev --host 0.0.0.0; \
|
cd ./internal/site && bun run dev --host 0.0.0.0; \
|
||||||
else \
|
else \
|
||||||
cd ./src/site && npm run dev --host 0.0.0.0; \
|
cd ./internal/site && npm run dev --host 0.0.0.0; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dev-hub: export ENV=dev
|
dev-hub: export ENV=dev
|
||||||
dev-hub:
|
dev-hub:
|
||||||
mkdir -p ./src/site/dist && touch ./src/site/dist/index.html
|
mkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html
|
||||||
@if command -v entr >/dev/null 2>&1; then \
|
@if command -v entr >/dev/null 2>&1; then \
|
||||||
find ./src/cmd/hub/*.go ./src/{alerts,hub,records,users}/*.go | entr -r -s "cd ./src/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
|
find ./internal -type f -name '*.go' | entr -r -s "cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
|
||||||
else \
|
else \
|
||||||
cd ./src/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
|
cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dev-agent:
|
dev-agent:
|
||||||
@if command -v entr >/dev/null 2>&1; then \
|
@if command -v entr >/dev/null 2>&1; then \
|
||||||
find ./src/cmd/agent/*.go ./agent/*.go | entr -r go run github.com/henrygd/beszel/src/cmd/agent; \
|
find ./internal/cmd/agent/*.go ./agent/*.go | entr -r go run github.com/henrygd/beszel/internal/cmd/agent; \
|
||||||
else \
|
else \
|
||||||
go run github.com/henrygd/beszel/src/cmd/agent; \
|
go run github.com/henrygd/beszel/internal/cmd/agent; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build-dotnet:
|
build-dotnet:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
// Package agent handles the agent's SSH server and system stats collection.
|
// Package agent implements the Beszel monitoring agent that collects and serves system metrics.
|
||||||
|
//
|
||||||
|
// The agent runs on monitored systems and communicates collected data
|
||||||
|
// to the Beszel hub for centralized monitoring and alerting.
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -13,29 +16,29 @@ import (
|
|||||||
|
|
||||||
"github.com/gliderlabs/ssh"
|
"github.com/gliderlabs/ssh"
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/src/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"
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
zfs bool // true if system has arcstats
|
zfs bool // true if system has arcstats
|
||||||
memCalc string // Memory calculation formula
|
memCalc string // Memory calculation formula
|
||||||
fsNames []string // List of filesystem device names being monitored
|
fsNames []string // List of filesystem device names being monitored
|
||||||
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
|
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
|
||||||
netInterfaces map[string]struct{} // Stores all valid network interfaces
|
netInterfaces map[string]struct{} // Stores all valid network interfaces
|
||||||
netIoStats system.NetIoStats // Keeps track of bandwidth usage
|
netIoStats map[string]system.NetIoStats // Keeps track of per-interface bandwidth usage
|
||||||
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
|
systemInfo system.Info // Host system info
|
||||||
gpuManager *GPUManager // Manages GPU data
|
gpuManager *GPUManager // Manages GPU data
|
||||||
cache *SessionCache // Cache for system stats based on primary session ID
|
cache *SessionCache // Cache for system stats based on primary session ID
|
||||||
connectionManager *ConnectionManager // Channel to signal connection events
|
connectionManager *ConnectionManager // Channel to signal connection events
|
||||||
server *ssh.Server // SSH server
|
server *ssh.Server // SSH server
|
||||||
dataDir string // Directory for persisting data
|
dataDir string // Directory for persisting data
|
||||||
keys []gossh.PublicKey // SSH public keys
|
keys []gossh.PublicKey // SSH public keys
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAgent creates a new agent with the given data directory for persisting data.
|
// NewAgent creates a new agent with the given data directory for persisting data.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package agent
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Not thread safe since we only access from gatherStats which is already locked
|
// Not thread safe since we only access from gatherStats which is already locked
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"testing/synctest"
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/src/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
|
|
||||||
"github.com/fxamacker/cbor/v2"
|
"github.com/fxamacker/cbor/v2"
|
||||||
"github.com/lxzan/gws"
|
"github.com/lxzan/gws"
|
||||||
@@ -85,7 +85,7 @@ func getToken() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return string(tokenBytes), nil
|
return strings.TrimSpace(string(tokenBytes)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOptions returns the WebSocket client options, creating them if necessary.
|
// getOptions returns the WebSocket client options, creating them if necessary.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
|
|
||||||
"github.com/fxamacker/cbor/v2"
|
"github.com/fxamacker/cbor/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -537,4 +537,25 @@ func TestGetToken(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "", token, "Empty file should return empty string")
|
assert.Equal(t, "", token, "Empty file should return empty string")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("strips whitespace from TOKEN_FILE", func(t *testing.T) {
|
||||||
|
unsetEnvVars()
|
||||||
|
|
||||||
|
tokenWithWhitespace := " test-token-with-whitespace \n\t"
|
||||||
|
expectedToken := "test-token-with-whitespace"
|
||||||
|
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Remove(tokenFile.Name())
|
||||||
|
|
||||||
|
_, err = tokenFile.WriteString(tokenWithWhitespace)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tokenFile.Close()
|
||||||
|
|
||||||
|
os.Setenv("TOKEN_FILE", tokenFile.Name())
|
||||||
|
defer os.Unsetenv("TOKEN_FILE")
|
||||||
|
|
||||||
|
token, err := getToken()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedToken, token, "Whitespace should be stripped from token file content")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/container"
|
"github.com/henrygd/beszel/internal/entities/container"
|
||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
"golang.org/x/exp/slog"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *Agent) initializeNetIoStats() {
|
func (a *Agent) initializeNetIoStats() {
|
||||||
// reset valid network interfaces
|
// reset valid network interfaces
|
||||||
a.netInterfaces = make(map[string]struct{}, 0)
|
a.netInterfaces = make(map[string]struct{}, 0)
|
||||||
|
// reset network I/O stats per interface
|
||||||
|
a.netIoStats = make(map[string]system.NetIoStats, 0)
|
||||||
|
|
||||||
// map of network interface names passed in via NICS env var
|
// map of network interface names passed in via NICS env var
|
||||||
var nicsMap map[string]struct{}
|
var nicsMap map[string]struct{}
|
||||||
@@ -22,13 +26,10 @@ func (a *Agent) initializeNetIoStats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset network I/O stats
|
|
||||||
a.netIoStats.BytesSent = 0
|
|
||||||
a.netIoStats.BytesRecv = 0
|
|
||||||
|
|
||||||
// get intial network I/O stats
|
// get intial network I/O stats
|
||||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||||
a.netIoStats.Time = time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
for _, v := range netIO {
|
for _, v := range netIO {
|
||||||
switch {
|
switch {
|
||||||
// skip if nics exists and the interface is not in the list
|
// skip if nics exists and the interface is not in the list
|
||||||
@@ -43,10 +44,15 @@ func (a *Agent) initializeNetIoStats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
slog.Info("Detected network interface", "name", v.Name, "sent", v.BytesSent, "recv", v.BytesRecv)
|
slog.Info("Detected network interface", "name", v.Name, "sent", v.BytesSent, "recv", v.BytesRecv)
|
||||||
a.netIoStats.BytesSent += v.BytesSent
|
|
||||||
a.netIoStats.BytesRecv += v.BytesRecv
|
|
||||||
// store as a valid network interface
|
// store as a valid network interface
|
||||||
a.netInterfaces[v.Name] = struct{}{}
|
a.netInterfaces[v.Name] = struct{}{}
|
||||||
|
// initialize per-interface stats
|
||||||
|
a.netIoStats[v.Name] = system.NetIoStats{
|
||||||
|
BytesRecv: v.BytesRecv,
|
||||||
|
BytesSent: v.BytesSent,
|
||||||
|
Time: now,
|
||||||
|
Name: v.Name,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/common"
|
"github.com/shirou/gopsutil/v4/common"
|
||||||
"github.com/shirou/gopsutil/v4/sensors"
|
"github.com/shirou/gopsutil/v4/sensors"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/common"
|
"github.com/shirou/gopsutil/v4/common"
|
||||||
"github.com/shirou/gopsutil/v4/sensors"
|
"github.com/shirou/gopsutil/v4/sensors"
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/src/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
"github.com/fxamacker/cbor/v2"
|
"github.com/fxamacker/cbor/v2"
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/container"
|
"github.com/henrygd/beszel/internal/entities/container"
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
"github.com/fxamacker/cbor/v2"
|
"github.com/fxamacker/cbor/v2"
|
||||||
|
|||||||
197
agent/system.go
@@ -11,7 +11,7 @@ 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/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
@@ -176,53 +176,85 @@ func (a *Agent) getSystemStats() system.Stats {
|
|||||||
if len(a.netInterfaces) == 0 {
|
if len(a.netInterfaces) == 0 {
|
||||||
// if no network interfaces, initialize again
|
// if no network interfaces, initialize again
|
||||||
// this is a fix if agent started before network is online (#466)
|
// this is a fix if agent started before network is online (#466)
|
||||||
// maybe refactor this in the future to not cache interface names at all so we
|
|
||||||
// don't miss an interface that's been added after agent started in any circumstance
|
|
||||||
a.initializeNetIoStats()
|
a.initializeNetIoStats()
|
||||||
}
|
}
|
||||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||||
msElapsed := uint64(time.Since(a.netIoStats.Time).Milliseconds())
|
now := time.Now()
|
||||||
a.netIoStats.Time = time.Now()
|
|
||||||
totalBytesSent := uint64(0)
|
// pre-allocate maps with known capacity
|
||||||
totalBytesRecv := uint64(0)
|
interfaceCount := len(a.netInterfaces)
|
||||||
// sum all bytes sent and received
|
if systemStats.NetworkInterfaces == nil || len(systemStats.NetworkInterfaces) != interfaceCount {
|
||||||
|
systemStats.NetworkInterfaces = make(map[string]system.NetworkInterfaceStats, interfaceCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalSent, totalRecv float64
|
||||||
|
|
||||||
|
// single pass through interfaces
|
||||||
for _, v := range netIO {
|
for _, v := range netIO {
|
||||||
// skip if not in valid network interfaces list
|
// skip if not in valid network interfaces list
|
||||||
if _, exists := a.netInterfaces[v.Name]; !exists {
|
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
totalBytesSent += v.BytesSent
|
|
||||||
totalBytesRecv += v.BytesRecv
|
// get previous stats for this interface
|
||||||
}
|
prevStats, exists := a.netIoStats[v.Name]
|
||||||
// add to systemStats
|
var networkSentPs, networkRecvPs float64
|
||||||
var bytesSentPerSecond, bytesRecvPerSecond uint64
|
|
||||||
if msElapsed > 0 {
|
if exists {
|
||||||
bytesSentPerSecond = (totalBytesSent - a.netIoStats.BytesSent) * 1000 / msElapsed
|
secondsElapsed := time.Since(prevStats.Time).Seconds()
|
||||||
bytesRecvPerSecond = (totalBytesRecv - a.netIoStats.BytesRecv) * 1000 / msElapsed
|
if secondsElapsed > 0 {
|
||||||
}
|
// direct calculation to MB/s, avoiding intermediate bytes/sec
|
||||||
networkSentPs := bytesToMegabytes(float64(bytesSentPerSecond))
|
networkSentPs = bytesToMegabytes(float64(v.BytesSent-prevStats.BytesSent) / secondsElapsed)
|
||||||
networkRecvPs := bytesToMegabytes(float64(bytesRecvPerSecond))
|
networkRecvPs = bytesToMegabytes(float64(v.BytesRecv-prevStats.BytesRecv) / secondsElapsed)
|
||||||
// add check for issue (#150) where sent is a massive number
|
|
||||||
if networkSentPs > 10_000 || networkRecvPs > 10_000 {
|
|
||||||
slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs)
|
|
||||||
for _, v := range netIO {
|
|
||||||
if _, exists := a.netInterfaces[v.Name]; !exists {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
slog.Info(v.Name, "recv", v.BytesRecv, "sent", v.BytesSent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// accumulate totals
|
||||||
|
totalSent += networkSentPs
|
||||||
|
totalRecv += networkRecvPs
|
||||||
|
|
||||||
|
// store per-interface stats
|
||||||
|
systemStats.NetworkInterfaces[v.Name] = system.NetworkInterfaceStats{
|
||||||
|
NetworkSent: networkSentPs,
|
||||||
|
NetworkRecv: networkRecvPs,
|
||||||
|
TotalBytesSent: v.BytesSent,
|
||||||
|
TotalBytesRecv: v.BytesRecv,
|
||||||
|
}
|
||||||
|
|
||||||
|
// update previous stats (reuse existing struct if possible)
|
||||||
|
if prevStats.Name == v.Name {
|
||||||
|
prevStats.BytesRecv = v.BytesRecv
|
||||||
|
prevStats.BytesSent = v.BytesSent
|
||||||
|
prevStats.PacketsSent = v.PacketsSent
|
||||||
|
prevStats.PacketsRecv = v.PacketsRecv
|
||||||
|
prevStats.Time = now
|
||||||
|
a.netIoStats[v.Name] = prevStats
|
||||||
|
} else {
|
||||||
|
a.netIoStats[v.Name] = system.NetIoStats{
|
||||||
|
BytesRecv: v.BytesRecv,
|
||||||
|
BytesSent: v.BytesSent,
|
||||||
|
PacketsSent: v.PacketsSent,
|
||||||
|
PacketsRecv: v.PacketsRecv,
|
||||||
|
Time: now,
|
||||||
|
Name: v.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add check for issue (#150) where sent is a massive number
|
||||||
|
if totalSent > 10_000 || totalRecv > 10_000 {
|
||||||
|
slog.Warn("Invalid net stats. Resetting.", "sent", totalSent, "recv", totalRecv)
|
||||||
// reset network I/O stats
|
// reset network I/O stats
|
||||||
a.initializeNetIoStats()
|
a.initializeNetIoStats()
|
||||||
} else {
|
} else {
|
||||||
systemStats.NetworkSent = networkSentPs
|
systemStats.NetworkSent = totalSent
|
||||||
systemStats.NetworkRecv = networkRecvPs
|
systemStats.NetworkRecv = totalRecv
|
||||||
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
|
|
||||||
// update netIoStats
|
|
||||||
a.netIoStats.BytesSent = totalBytesSent
|
|
||||||
a.netIoStats.BytesRecv = totalBytesRecv
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// connection counts
|
||||||
|
a.updateConnectionCounts(&systemStats)
|
||||||
|
|
||||||
// temperatures
|
// temperatures
|
||||||
// TODO: maybe refactor to methods on systemStats
|
// TODO: maybe refactor to methods on systemStats
|
||||||
a.updateTemperatures(&systemStats)
|
a.updateTemperatures(&systemStats)
|
||||||
@@ -270,14 +302,109 @@ func (a *Agent) getSystemStats() system.Stats {
|
|||||||
a.systemInfo.MemPct = systemStats.MemPct
|
a.systemInfo.MemPct = systemStats.MemPct
|
||||||
a.systemInfo.DiskPct = systemStats.DiskPct
|
a.systemInfo.DiskPct = systemStats.DiskPct
|
||||||
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)
|
// Sum all per-interface network sent/recv and assign to systemInfo
|
||||||
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
|
var totalSent, totalRecv float64
|
||||||
|
for _, iface := range systemStats.NetworkInterfaces {
|
||||||
|
totalSent += iface.NetworkSent
|
||||||
|
totalRecv += iface.NetworkRecv
|
||||||
|
}
|
||||||
|
a.systemInfo.NetworkSent = twoDecimals(totalSent)
|
||||||
|
a.systemInfo.NetworkRecv = twoDecimals(totalRecv)
|
||||||
slog.Debug("sysinfo", "data", a.systemInfo)
|
slog.Debug("sysinfo", "data", a.systemInfo)
|
||||||
|
|
||||||
return systemStats
|
return systemStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Agent) updateConnectionCounts(systemStats *system.Stats) {
|
||||||
|
// Get IPv4 connections
|
||||||
|
connectionsIPv4, err := psutilNet.Connections("inet")
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("Failed to get IPv4 connection stats", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get IPv6 connections
|
||||||
|
connectionsIPv6, err := psutilNet.Connections("inet6")
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("Failed to get IPv6 connection stats", "err", err)
|
||||||
|
// Continue with IPv4 only if IPv6 fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Nets map if needed
|
||||||
|
if systemStats.Nets == nil {
|
||||||
|
systemStats.Nets = make(map[string]float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count IPv4 connection states
|
||||||
|
connStatsIPv4 := map[string]int{
|
||||||
|
"established": 0,
|
||||||
|
"listen": 0,
|
||||||
|
"time_wait": 0,
|
||||||
|
"close_wait": 0,
|
||||||
|
"syn_recv": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conn := range connectionsIPv4 {
|
||||||
|
// Only count TCP connections (Type 1 = SOCK_STREAM)
|
||||||
|
if conn.Type == 1 {
|
||||||
|
switch strings.ToUpper(conn.Status) {
|
||||||
|
case "ESTABLISHED":
|
||||||
|
connStatsIPv4["established"]++
|
||||||
|
case "LISTEN":
|
||||||
|
connStatsIPv4["listen"]++
|
||||||
|
case "TIME_WAIT":
|
||||||
|
connStatsIPv4["time_wait"]++
|
||||||
|
case "CLOSE_WAIT":
|
||||||
|
connStatsIPv4["close_wait"]++
|
||||||
|
case "SYN_RECV":
|
||||||
|
connStatsIPv4["syn_recv"]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count IPv6 connection states
|
||||||
|
connStatsIPv6 := map[string]int{
|
||||||
|
"established": 0,
|
||||||
|
"listen": 0,
|
||||||
|
"time_wait": 0,
|
||||||
|
"close_wait": 0,
|
||||||
|
"syn_recv": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conn := range connectionsIPv6 {
|
||||||
|
// Only count TCP connections (Type 1 = SOCK_STREAM)
|
||||||
|
if conn.Type == 1 {
|
||||||
|
switch strings.ToUpper(conn.Status) {
|
||||||
|
case "ESTABLISHED":
|
||||||
|
connStatsIPv6["established"]++
|
||||||
|
case "LISTEN":
|
||||||
|
connStatsIPv6["listen"]++
|
||||||
|
case "TIME_WAIT":
|
||||||
|
connStatsIPv6["time_wait"]++
|
||||||
|
case "CLOSE_WAIT":
|
||||||
|
connStatsIPv6["close_wait"]++
|
||||||
|
case "SYN_RECV":
|
||||||
|
connStatsIPv6["syn_recv"]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add IPv4 connection counts to Nets
|
||||||
|
systemStats.Nets["conn_established"] = float64(connStatsIPv4["established"])
|
||||||
|
systemStats.Nets["conn_listen"] = float64(connStatsIPv4["listen"])
|
||||||
|
systemStats.Nets["conn_timewait"] = float64(connStatsIPv4["time_wait"])
|
||||||
|
systemStats.Nets["conn_closewait"] = float64(connStatsIPv4["close_wait"])
|
||||||
|
systemStats.Nets["conn_synrecv"] = float64(connStatsIPv4["syn_recv"])
|
||||||
|
|
||||||
|
// Add IPv6 connection counts to Nets
|
||||||
|
systemStats.Nets["conn6_established"] = float64(connStatsIPv6["established"])
|
||||||
|
systemStats.Nets["conn6_listen"] = float64(connStatsIPv6["listen"])
|
||||||
|
systemStats.Nets["conn6_timewait"] = float64(connStatsIPv6["time_wait"])
|
||||||
|
systemStats.Nets["conn6_closewait"] = float64(connStatsIPv6["close_wait"])
|
||||||
|
systemStats.Nets["conn6_synrecv"] = float64(connStatsIPv6["syn_recv"])
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the size of the ZFS ARC memory cache in bytes
|
// Returns the size of the ZFS ARC memory cache in bytes
|
||||||
func getARCSize() (uint64, error) {
|
func getARCSize() (uint64, error) {
|
||||||
file, err := os.Open("/proc/spl/kstat/zfs/arcstats")
|
file, err := os.Open("/proc/spl/kstat/zfs/arcstats")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/ghupdate"
|
"github.com/henrygd/beszel/internal/ghupdate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// restarter knows how to restart the beszel-agent service.
|
// restarter knows how to restart the beszel-agent service.
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
// Package beszel provides core application constants and version information
|
||||||
|
// which are used throughout the application.
|
||||||
package beszel
|
package beszel
|
||||||
|
|
||||||
import "github.com/blang/semver"
|
import "github.com/blang/semver"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// Version is the current version of the application.
|
||||||
Version = "0.12.7"
|
Version = "0.12.7"
|
||||||
|
// AppName is the name of the application.
|
||||||
AppName = "beszel"
|
AppName = "beszel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MinVersionCbor is the minimum supported version for CBOR compatibility.
|
||||||
var MinVersionCbor = semver.MustParse("0.12.0")
|
var MinVersionCbor = semver.MustParse("0.12.0")
|
||||||
|
|||||||
4
i18n.yml
@@ -1,3 +1,3 @@
|
|||||||
files:
|
files:
|
||||||
- source: /beszel/site/src/locales/en/en.po
|
- source: /internal/site/src/locales/en/
|
||||||
translation: /beszel/site/src/locales/%two_letters_code%/%two_letters_code%.po
|
translation: /internal/site/src/locales/%two_letters_code%/%two_letters_code%.po
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
@@ -38,7 +38,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
case "Memory":
|
case "Memory":
|
||||||
val = data.Info.MemPct
|
val = data.Info.MemPct
|
||||||
case "Bandwidth":
|
case "Bandwidth":
|
||||||
val = data.Info.Bandwidth
|
val = data.Info.NetworkSent + data.Info.NetworkRecv
|
||||||
unit = " MB/s"
|
unit = " MB/s"
|
||||||
case "Disk":
|
case "Disk":
|
||||||
maxUsedPct := data.Info.DiskPct
|
maxUsedPct := data.Info.DiskPct
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"testing/synctest"
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
beszelTests "github.com/henrygd/beszel/src/tests"
|
beszelTests "github.com/henrygd/beszel/internal/tests"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/src/hub"
|
"github.com/henrygd/beszel/internal/hub"
|
||||||
_ "github.com/henrygd/beszel/src/migrations"
|
_ "github.com/henrygd/beszel/internal/migrations"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
"github.com/pocketbase/pocketbase"
|
||||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||||
@@ -10,7 +10,7 @@ COPY . ./
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
ARG TARGETOS TARGETARCH
|
ARG TARGETOS TARGETARCH
|
||||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./src/cmd/agent
|
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||||
|
|
||||||
RUN rm -rf /tmp/*
|
RUN rm -rf /tmp/*
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ RUN update-ca-certificates
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
ARG TARGETOS TARGETARCH
|
ARG TARGETOS TARGETARCH
|
||||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./src/cmd/hub
|
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./internal/cmd/hub
|
||||||
|
|
||||||
# ? -------------------------
|
# ? -------------------------
|
||||||
FROM scratch
|
FROM scratch
|
||||||
124
internal/entities/system/system.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
// TODO: this is confusing, make common package with common/types common/helpers etc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/henrygd/beszel/internal/entities/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NetworkInterfaceStats struct {
|
||||||
|
NetworkSent float64 `json:"ns"`
|
||||||
|
NetworkRecv float64 `json:"nr"`
|
||||||
|
MaxNetworkSent float64 `json:"nsm,omitempty"`
|
||||||
|
MaxNetworkRecv float64 `json:"nrm,omitempty"`
|
||||||
|
TotalBytesSent uint64 `json:"tbs,omitempty"` // Total bytes sent since boot
|
||||||
|
TotalBytesRecv uint64 `json:"tbr,omitempty"` // Total bytes received since boot
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stats struct {
|
||||||
|
Cpu float64 `json:"cpu" cbor:"0,keyasint"`
|
||||||
|
MaxCpu float64 `json:"cpum,omitempty" cbor:"1,keyasint,omitempty"`
|
||||||
|
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||||
|
MemUsed float64 `json:"mu" cbor:"3,keyasint"`
|
||||||
|
MemPct float64 `json:"mp" cbor:"4,keyasint"`
|
||||||
|
MemBuffCache float64 `json:"mb" cbor:"5,keyasint"`
|
||||||
|
MemZfsArc float64 `json:"mz,omitempty" cbor:"6,keyasint,omitempty"` // ZFS ARC memory
|
||||||
|
Swap float64 `json:"s,omitempty" cbor:"7,keyasint,omitempty"`
|
||||||
|
SwapUsed float64 `json:"su,omitempty" cbor:"8,keyasint,omitempty"`
|
||||||
|
DiskTotal float64 `json:"d" cbor:"9,keyasint"`
|
||||||
|
DiskUsed float64 `json:"du" cbor:"10,keyasint"`
|
||||||
|
DiskPct float64 `json:"dp" cbor:"11,keyasint"`
|
||||||
|
DiskReadPs float64 `json:"dr" cbor:"12,keyasint"`
|
||||||
|
DiskWritePs float64 `json:"dw" cbor:"13,keyasint"`
|
||||||
|
MaxDiskReadPs float64 `json:"drm,omitempty" cbor:"14,keyasint,omitempty"`
|
||||||
|
MaxDiskWritePs float64 `json:"dwm,omitempty" cbor:"15,keyasint,omitempty"`
|
||||||
|
NetworkInterfaces map[string]NetworkInterfaceStats `json:"ni" cbor:"16,omitempty"` // Per-interface network stats
|
||||||
|
NetworkSent float64 `json:"ns" cbor:"17,keyasint"` // Total network sent (MB/s)
|
||||||
|
NetworkRecv float64 `json:"nr" cbor:"18,keyasint"` // Total network recv (MB/s)
|
||||||
|
MaxNetworkSent float64 `json:"nsm,omitempty" cbor:"19,keyasint,omitempty"`
|
||||||
|
MaxNetworkRecv float64 `json:"nrm,omitempty" cbor:"20,keyasint,omitempty"`
|
||||||
|
Temperatures map[string]float64 `json:"t,omitempty" cbor:"21,keyasint,omitempty"`
|
||||||
|
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"22,keyasint,omitempty"`
|
||||||
|
GPUData map[string]GPUData `json:"g,omitempty" cbor:"23,keyasint,omitempty"`
|
||||||
|
LoadAvg1 float64 `json:"l1,omitempty" cbor:"24,keyasint,omitempty"`
|
||||||
|
LoadAvg5 float64 `json:"l5,omitempty" cbor:"25,keyasint,omitempty"`
|
||||||
|
LoadAvg15 float64 `json:"l15,omitempty" cbor:"26,keyasint,omitempty"`
|
||||||
|
LoadAvg [3]float64 `json:"la,omitempty" cbor:"27,keyasint"` // [1min, 5min, 15min]
|
||||||
|
Battery [2]uint8 `json:"bat,omitzero" cbor:"28,keyasint,omitzero"` // [percent, charge state]
|
||||||
|
MaxMem float64 `json:"mm,omitempty" cbor:"29,keyasint,omitempty"`
|
||||||
|
Nets map[string]float64 `json:"nets,omitempty" cbor:"30,keyasint,omitempty"` // Network connection statistics
|
||||||
|
}
|
||||||
|
|
||||||
|
type GPUData struct {
|
||||||
|
Name string `json:"n" cbor:"0,keyasint"`
|
||||||
|
Temperature float64 `json:"-"`
|
||||||
|
MemoryUsed float64 `json:"mu,omitempty" cbor:"1,keyasint,omitempty"`
|
||||||
|
MemoryTotal float64 `json:"mt,omitempty" cbor:"2,keyasint,omitempty"`
|
||||||
|
Usage float64 `json:"u" cbor:"3,keyasint"`
|
||||||
|
Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
|
||||||
|
Count float64 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FsStats struct {
|
||||||
|
Time time.Time `json:"-"`
|
||||||
|
Root bool `json:"-"`
|
||||||
|
Mountpoint string `json:"-"`
|
||||||
|
DiskTotal float64 `json:"d" cbor:"0,keyasint"`
|
||||||
|
DiskUsed float64 `json:"du" cbor:"1,keyasint"`
|
||||||
|
TotalRead uint64 `json:"-"`
|
||||||
|
TotalWrite uint64 `json:"-"`
|
||||||
|
DiskReadPs float64 `json:"r" cbor:"2,keyasint"`
|
||||||
|
DiskWritePs float64 `json:"w" cbor:"3,keyasint"`
|
||||||
|
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"4,keyasint,omitempty"`
|
||||||
|
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"5,keyasint,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetIoStats struct {
|
||||||
|
BytesRecv uint64
|
||||||
|
BytesSent uint64
|
||||||
|
PacketsSent uint64
|
||||||
|
PacketsRecv uint64
|
||||||
|
Time time.Time
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Os = uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
Linux Os = iota
|
||||||
|
Darwin
|
||||||
|
Windows
|
||||||
|
Freebsd
|
||||||
|
)
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
Hostname string `json:"h" cbor:"0,keyasint"`
|
||||||
|
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
|
||||||
|
Cores int `json:"c" cbor:"2,keyasint"`
|
||||||
|
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
|
||||||
|
CpuModel string `json:"m" cbor:"4,keyasint"`
|
||||||
|
Uptime uint64 `json:"u" cbor:"5,keyasint"`
|
||||||
|
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
|
||||||
|
MemPct float64 `json:"mp" cbor:"7,keyasint"`
|
||||||
|
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
|
||||||
|
NetworkSent float64 `json:"ns" cbor:"9,keyasint"` // Per-interface total (MB/s)
|
||||||
|
NetworkRecv float64 `json:"nr" cbor:"10,keyasint"` // Per-interface total (MB/s)
|
||||||
|
AgentVersion string `json:"v" cbor:"11,keyasint"`
|
||||||
|
Podman bool `json:"p,omitempty" cbor:"12,keyasint,omitempty"`
|
||||||
|
GpuPct float64 `json:"g,omitempty" cbor:"13,keyasint,omitempty"`
|
||||||
|
DashboardTemp float64 `json:"dt,omitempty" cbor:"14,keyasint,omitempty"`
|
||||||
|
Os Os `json:"os" cbor:"15,keyasint"`
|
||||||
|
LoadAvg1 float64 `json:"l1,omitempty" cbor:"16,keyasint,omitempty"`
|
||||||
|
LoadAvg5 float64 `json:"l5,omitempty" cbor:"17,keyasint,omitempty"`
|
||||||
|
LoadAvg15 float64 `json:"l15,omitempty" cbor:"18,keyasint,omitempty"`
|
||||||
|
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"` // [1min, 5min, 15min]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final data structure to return to the hub
|
||||||
|
type CombinedData struct {
|
||||||
|
Stats Stats `json:"stats" cbor:"0,keyasint"`
|
||||||
|
Info Info `json:"info" cbor:"1,keyasint"`
|
||||||
|
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
||||||
|
}
|
||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
"github.com/henrygd/beszel/src/hub/expirymap"
|
"github.com/henrygd/beszel/internal/hub/expirymap"
|
||||||
"github.com/henrygd/beszel/src/hub/ws"
|
"github.com/henrygd/beszel/internal/hub/ws"
|
||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
"github.com/lxzan/gws"
|
"github.com/lxzan/gws"
|
||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/agent"
|
"github.com/henrygd/beszel/agent"
|
||||||
"github.com/henrygd/beszel/src/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
"github.com/henrygd/beszel/src/hub/ws"
|
"github.com/henrygd/beszel/internal/hub/ws"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
pbtests "github.com/pocketbase/pocketbase/tests"
|
pbtests "github.com/pocketbase/pocketbase/tests"
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/tests"
|
"github.com/henrygd/beszel/internal/tests"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/hub/config"
|
"github.com/henrygd/beszel/internal/hub/config"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/src/alerts"
|
"github.com/henrygd/beszel/internal/alerts"
|
||||||
"github.com/henrygd/beszel/src/hub/config"
|
"github.com/henrygd/beszel/internal/hub/config"
|
||||||
"github.com/henrygd/beszel/src/hub/systems"
|
"github.com/henrygd/beszel/internal/hub/systems"
|
||||||
"github.com/henrygd/beszel/src/records"
|
"github.com/henrygd/beszel/internal/records"
|
||||||
"github.com/henrygd/beszel/src/users"
|
"github.com/henrygd/beszel/internal/users"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pocketbase/pocketbase"
|
"github.com/pocketbase/pocketbase"
|
||||||
@@ -69,6 +69,8 @@ func (h *Hub) StartHub() error {
|
|||||||
if err := config.SyncSystems(e); err != nil {
|
if err := config.SyncSystems(e); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// register middlewares
|
||||||
|
h.registerMiddlewares(e)
|
||||||
// register api routes
|
// register api routes
|
||||||
if err := h.registerApiRoutes(e); err != nil {
|
if err := h.registerApiRoutes(e); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -171,6 +173,37 @@ func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// custom middlewares
|
||||||
|
func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
|
||||||
|
// authorizes request with user matching the provided email
|
||||||
|
authorizeRequestWithEmail := func(e *core.RequestEvent, email string) (err error) {
|
||||||
|
if e.Auth != nil || email == "" {
|
||||||
|
return e.Next()
|
||||||
|
}
|
||||||
|
isAuthRefresh := e.Request.URL.Path == "/api/collections/users/auth-refresh" && e.Request.Method == http.MethodPost
|
||||||
|
e.Auth, err = e.App.FindFirstRecordByData("users", "email", email)
|
||||||
|
if err != nil || !isAuthRefresh {
|
||||||
|
return e.Next()
|
||||||
|
}
|
||||||
|
// auth refresh endpoint, make sure token is set in header
|
||||||
|
token, _ := e.Auth.NewAuthToken()
|
||||||
|
e.Request.Header.Set("Authorization", token)
|
||||||
|
return e.Next()
|
||||||
|
}
|
||||||
|
// authenticate with trusted header
|
||||||
|
if autoLogin, _ := GetEnv("AUTO_LOGIN"); autoLogin != "" {
|
||||||
|
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
||||||
|
return authorizeRequestWithEmail(e, autoLogin)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// authenticate with trusted header
|
||||||
|
if trustedHeader, _ := GetEnv("TRUSTED_AUTH_HEADER"); trustedHeader != "" {
|
||||||
|
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
||||||
|
return authorizeRequestWithEmail(e, e.Request.Header.Get(trustedHeader))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// custom api routes
|
// custom api routes
|
||||||
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||||
// auth protected routes
|
// auth protected routes
|
||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/migrations"
|
"github.com/henrygd/beszel/internal/migrations"
|
||||||
beszelTests "github.com/henrygd/beszel/src/tests"
|
beszelTests "github.com/henrygd/beszel/internal/tests"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
pbTests "github.com/pocketbase/pocketbase/tests"
|
pbTests "github.com/pocketbase/pocketbase/tests"
|
||||||
@@ -711,3 +711,117 @@ func TestCreateUserEndpointAvailability(t *testing.T) {
|
|||||||
scenario.Test(t)
|
scenario.Test(t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAutoLoginMiddleware(t *testing.T) {
|
||||||
|
var hubs []*beszelTests.TestHub
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
defer os.Unsetenv("AUTO_LOGIN")
|
||||||
|
for _, hub := range hubs {
|
||||||
|
hub.Cleanup()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
os.Setenv("AUTO_LOGIN", "user@test.com")
|
||||||
|
|
||||||
|
testAppFactory := func(t testing.TB) *pbTests.TestApp {
|
||||||
|
hub, _ := beszelTests.NewTestHub(t.TempDir())
|
||||||
|
hubs = append(hubs, hub)
|
||||||
|
hub.StartHub()
|
||||||
|
return hub.TestApp
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []beszelTests.ApiScenario{
|
||||||
|
{
|
||||||
|
Name: "GET /getkey - without auto login should fail",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: "/api/beszel/getkey",
|
||||||
|
ExpectedStatus: 401,
|
||||||
|
ExpectedContent: []string{"requires valid"},
|
||||||
|
TestAppFactory: testAppFactory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GET /getkey - with auto login should fail if no matching user",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: "/api/beszel/getkey",
|
||||||
|
ExpectedStatus: 401,
|
||||||
|
ExpectedContent: []string{"requires valid"},
|
||||||
|
TestAppFactory: testAppFactory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GET /getkey - with auto login should succeed",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: "/api/beszel/getkey",
|
||||||
|
ExpectedStatus: 200,
|
||||||
|
ExpectedContent: []string{"\"key\":", "\"v\":"},
|
||||||
|
TestAppFactory: testAppFactory,
|
||||||
|
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
|
||||||
|
beszelTests.CreateUser(app, "user@test.com", "password123")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
scenario.Test(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrustedHeaderMiddleware(t *testing.T) {
|
||||||
|
var hubs []*beszelTests.TestHub
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
defer os.Unsetenv("TRUSTED_AUTH_HEADER")
|
||||||
|
for _, hub := range hubs {
|
||||||
|
hub.Cleanup()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
os.Setenv("TRUSTED_AUTH_HEADER", "X-Beszel-Trusted")
|
||||||
|
|
||||||
|
testAppFactory := func(t testing.TB) *pbTests.TestApp {
|
||||||
|
hub, _ := beszelTests.NewTestHub(t.TempDir())
|
||||||
|
hubs = append(hubs, hub)
|
||||||
|
hub.StartHub()
|
||||||
|
return hub.TestApp
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []beszelTests.ApiScenario{
|
||||||
|
{
|
||||||
|
Name: "GET /getkey - without trusted header should fail",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: "/api/beszel/getkey",
|
||||||
|
ExpectedStatus: 401,
|
||||||
|
ExpectedContent: []string{"requires valid"},
|
||||||
|
TestAppFactory: testAppFactory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GET /getkey - with trusted header should fail if no matching user",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: "/api/beszel/getkey",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"X-Beszel-Trusted": "user@test.com",
|
||||||
|
},
|
||||||
|
ExpectedStatus: 401,
|
||||||
|
ExpectedContent: []string{"requires valid"},
|
||||||
|
TestAppFactory: testAppFactory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GET /getkey - with trusted header should succeed",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: "/api/beszel/getkey",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"X-Beszel-Trusted": "user@test.com",
|
||||||
|
},
|
||||||
|
ExpectedStatus: 200,
|
||||||
|
ExpectedContent: []string{"\"key\":", "\"v\":"},
|
||||||
|
TestAppFactory: testAppFactory,
|
||||||
|
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
|
||||||
|
beszelTests.CreateUser(app, "user@test.com", "password123")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
scenario.Test(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
package hub
|
package hub
|
||||||
|
|
||||||
import "github.com/henrygd/beszel/src/hub/systems"
|
import "github.com/henrygd/beszel/internal/hub/systems"
|
||||||
|
|
||||||
// TESTING ONLY: GetSystemManager returns the system manager
|
// TESTING ONLY: GetSystemManager returns the system manager
|
||||||
func (h *Hub) GetSystemManager() *systems.SystemManager {
|
func (h *Hub) GetSystemManager() *systems.SystemManager {
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/src/site"
|
"github.com/henrygd/beszel/internal/site"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/apis"
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/hub/ws"
|
"github.com/henrygd/beszel/internal/hub/ws"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
|
|
||||||
@@ -5,11 +5,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/hub/ws"
|
"github.com/henrygd/beszel/internal/hub/ws"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
|
|
||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"testing/synctest"
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/container"
|
"github.com/henrygd/beszel/internal/entities/container"
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
"github.com/henrygd/beszel/src/hub/systems"
|
"github.com/henrygd/beszel/internal/hub/systems"
|
||||||
"github.com/henrygd/beszel/src/tests"
|
"github.com/henrygd/beszel/internal/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
entities "github.com/henrygd/beszel/src/entities/system"
|
entities "github.com/henrygd/beszel/internal/entities/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TESTING ONLY: GetSystemCount returns the number of systems in the store
|
// TESTING ONLY: GetSystemCount returns the number of systems in the store
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/ghupdate"
|
"github.com/henrygd/beszel/internal/ghupdate"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"weak"
|
"weak"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
|
|
||||||
"github.com/fxamacker/cbor/v2"
|
"github.com/fxamacker/cbor/v2"
|
||||||
"github.com/lxzan/gws"
|
"github.com/lxzan/gws"
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
|
|
||||||
"github.com/fxamacker/cbor/v2"
|
"github.com/fxamacker/cbor/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/entities/container"
|
"github.com/henrygd/beszel/internal/entities/container"
|
||||||
"github.com/henrygd/beszel/src/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
@@ -206,15 +206,51 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
|||||||
sum.DiskPct += stats.DiskPct
|
sum.DiskPct += stats.DiskPct
|
||||||
sum.DiskReadPs += stats.DiskReadPs
|
sum.DiskReadPs += stats.DiskReadPs
|
||||||
sum.DiskWritePs += stats.DiskWritePs
|
sum.DiskWritePs += stats.DiskWritePs
|
||||||
|
sum.LoadAvg1 += stats.LoadAvg1
|
||||||
|
sum.LoadAvg5 += stats.LoadAvg5
|
||||||
|
sum.LoadAvg15 += stats.LoadAvg15
|
||||||
sum.NetworkSent += stats.NetworkSent
|
sum.NetworkSent += stats.NetworkSent
|
||||||
sum.NetworkRecv += stats.NetworkRecv
|
sum.NetworkRecv += stats.NetworkRecv
|
||||||
sum.LoadAvg[0] += stats.LoadAvg[0]
|
sum.LoadAvg[0] += stats.LoadAvg[0]
|
||||||
sum.LoadAvg[1] += stats.LoadAvg[1]
|
sum.LoadAvg[1] += stats.LoadAvg[1]
|
||||||
sum.LoadAvg[2] += stats.LoadAvg[2]
|
sum.LoadAvg[2] += stats.LoadAvg[2]
|
||||||
sum.Bandwidth[0] += stats.Bandwidth[0]
|
|
||||||
sum.Bandwidth[1] += stats.Bandwidth[1]
|
|
||||||
batterySum += int(stats.Battery[0])
|
batterySum += int(stats.Battery[0])
|
||||||
sum.Battery[1] = stats.Battery[1]
|
sum.Battery[1] = stats.Battery[1]
|
||||||
|
|
||||||
|
if stats.NetworkInterfaces != nil {
|
||||||
|
if sum.NetworkInterfaces == nil {
|
||||||
|
sum.NetworkInterfaces = make(map[string]system.NetworkInterfaceStats, len(stats.NetworkInterfaces))
|
||||||
|
}
|
||||||
|
for key, value := range stats.NetworkInterfaces {
|
||||||
|
if _, ok := sum.NetworkInterfaces[key]; !ok {
|
||||||
|
sum.NetworkInterfaces[key] = system.NetworkInterfaceStats{}
|
||||||
|
}
|
||||||
|
ni := sum.NetworkInterfaces[key]
|
||||||
|
ni.NetworkSent += value.NetworkSent
|
||||||
|
ni.NetworkRecv += value.NetworkRecv
|
||||||
|
ni.MaxNetworkSent += value.MaxNetworkSent
|
||||||
|
ni.MaxNetworkRecv += value.MaxNetworkRecv
|
||||||
|
// For cumulative totals, use the maximum value (most recent)
|
||||||
|
if value.TotalBytesSent > ni.TotalBytesSent {
|
||||||
|
ni.TotalBytesSent = value.TotalBytesSent
|
||||||
|
}
|
||||||
|
if value.TotalBytesRecv > ni.TotalBytesRecv {
|
||||||
|
ni.TotalBytesRecv = value.TotalBytesRecv
|
||||||
|
}
|
||||||
|
sum.NetworkInterfaces[key] = ni
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle network connection stats - use the latest values (most recent sample)
|
||||||
|
if stats.Nets != nil {
|
||||||
|
if sum.Nets == nil {
|
||||||
|
sum.Nets = make(map[string]float64)
|
||||||
|
}
|
||||||
|
for key, value := range stats.Nets {
|
||||||
|
sum.Nets[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set peak values
|
// Set peak values
|
||||||
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
|
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
|
||||||
sum.MaxMem = max(sum.MaxMem, stats.MaxMem, stats.MemUsed)
|
sum.MaxMem = max(sum.MaxMem, stats.MaxMem, stats.MemUsed)
|
||||||
@@ -222,8 +258,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
|||||||
sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)
|
sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)
|
||||||
sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)
|
sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)
|
||||||
sum.MaxDiskWritePs = max(sum.MaxDiskWritePs, stats.MaxDiskWritePs, stats.DiskWritePs)
|
sum.MaxDiskWritePs = max(sum.MaxDiskWritePs, stats.MaxDiskWritePs, stats.DiskWritePs)
|
||||||
sum.MaxBandwidth[0] = max(sum.MaxBandwidth[0], stats.MaxBandwidth[0], stats.Bandwidth[0])
|
|
||||||
sum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1])
|
|
||||||
|
|
||||||
// Accumulate temperatures
|
// Accumulate temperatures
|
||||||
if stats.Temperatures != nil {
|
if stats.Temperatures != nil {
|
||||||
@@ -291,14 +325,26 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
|||||||
sum.DiskPct = twoDecimals(sum.DiskPct / count)
|
sum.DiskPct = twoDecimals(sum.DiskPct / count)
|
||||||
sum.DiskReadPs = twoDecimals(sum.DiskReadPs / count)
|
sum.DiskReadPs = twoDecimals(sum.DiskReadPs / count)
|
||||||
sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
|
sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
|
||||||
|
sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count)
|
||||||
|
sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count)
|
||||||
|
sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count)
|
||||||
sum.NetworkSent = twoDecimals(sum.NetworkSent / count)
|
sum.NetworkSent = twoDecimals(sum.NetworkSent / count)
|
||||||
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
|
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
|
||||||
sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count)
|
sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count)
|
||||||
sum.LoadAvg[1] = twoDecimals(sum.LoadAvg[1] / count)
|
sum.LoadAvg[1] = twoDecimals(sum.LoadAvg[1] / count)
|
||||||
sum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count)
|
sum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count)
|
||||||
sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)
|
|
||||||
sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)
|
|
||||||
sum.Battery[0] = uint8(batterySum / int(count))
|
sum.Battery[0] = uint8(batterySum / int(count))
|
||||||
|
|
||||||
|
if sum.NetworkInterfaces != nil {
|
||||||
|
for key := range sum.NetworkInterfaces {
|
||||||
|
ni := sum.NetworkInterfaces[key]
|
||||||
|
ni.NetworkSent = twoDecimals(ni.NetworkSent / count)
|
||||||
|
ni.NetworkRecv = twoDecimals(ni.NetworkRecv / count)
|
||||||
|
ni.MaxNetworkSent = twoDecimals(max(ni.MaxNetworkSent, ni.NetworkSent))
|
||||||
|
ni.MaxNetworkRecv = twoDecimals(max(ni.MaxNetworkRecv, ni.NetworkRecv))
|
||||||
|
sum.NetworkInterfaces[key] = ni
|
||||||
|
}
|
||||||
|
}
|
||||||
// Average temperatures
|
// Average temperatures
|
||||||
if sum.Temperatures != nil && tempCount > 0 {
|
if sum.Temperatures != nil && tempCount > 0 {
|
||||||
for key := range sum.Temperatures {
|
for key := range sum.Temperatures {
|
||||||
@@ -363,19 +409,15 @@ func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds
|
|||||||
}
|
}
|
||||||
sums[stat.Name].Cpu += stat.Cpu
|
sums[stat.Name].Cpu += stat.Cpu
|
||||||
sums[stat.Name].Mem += stat.Mem
|
sums[stat.Name].Mem += stat.Mem
|
||||||
sums[stat.Name].NetworkSent += stat.NetworkSent
|
|
||||||
sums[stat.Name].NetworkRecv += stat.NetworkRecv
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]container.Stats, 0, len(sums))
|
result := make([]container.Stats, 0, len(sums))
|
||||||
for _, value := range sums {
|
for _, value := range sums {
|
||||||
result = append(result, container.Stats{
|
result = append(result, container.Stats{
|
||||||
Name: value.Name,
|
Name: value.Name,
|
||||||
Cpu: twoDecimals(value.Cpu / count),
|
Cpu: twoDecimals(value.Cpu / count),
|
||||||
Mem: twoDecimals(value.Mem / count),
|
Mem: twoDecimals(value.Mem / count),
|
||||||
NetworkSent: twoDecimals(value.NetworkSent / count),
|
|
||||||
NetworkRecv: twoDecimals(value.NetworkRecv / count),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/src/records"
|
"github.com/henrygd/beszel/internal/records"
|
||||||
"github.com/henrygd/beszel/src/tests"
|
"github.com/henrygd/beszel/internal/tests"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
@@ -17,7 +17,10 @@
|
|||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"correctness": {
|
||||||
|
"useUniqueElementIds": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"javascript": {
|
"javascript": {
|
||||||
@@ -35,4 +38,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -22,7 +22,7 @@ import { memo, useEffect, useRef, useState } from "react"
|
|||||||
import { $router, basePath, Link, navigate } from "./router"
|
import { $router, basePath, Link, navigate } from "./router"
|
||||||
import { SystemRecord } from "@/types"
|
import { SystemRecord } from "@/types"
|
||||||
import { SystemStatus } from "@/lib/enums"
|
import { SystemStatus } from "@/lib/enums"
|
||||||
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "./ui/icons"
|
import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/icons"
|
||||||
import { InputCopy } from "./ui/input-copy"
|
import { InputCopy } from "./ui/input-copy"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import {
|
import {
|
||||||
@@ -253,6 +253,12 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
|
|||||||
copyWindowsCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token),
|
copyWindowsCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token),
|
||||||
icons: [WindowsIcon],
|
icons: [WindowsIcon],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: t({ message: "FreeBSD command", context: "Button to copy install command" }),
|
||||||
|
onClick: async () =>
|
||||||
|
copyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token),
|
||||||
|
icons: [FreeBsdIcon],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: t`Manual setup instructions`,
|
text: t`Manual setup instructions`,
|
||||||
url: "https://beszel.dev/guide/agent-installation#binary",
|
url: "https://beszel.dev/guide/agent-installation#binary",
|
||||||
124
internal/site/src/components/charts/connection-chart.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { memo } from "react"
|
||||||
|
import { useLingui } from "@lingui/react/macro"
|
||||||
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
|
import {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
xAxis,
|
||||||
|
} from "@/components/ui/chart"
|
||||||
|
import { cn, formatShortDate, chartMargin } from "@/lib/utils"
|
||||||
|
import { ChartData } from "@/types"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
|
||||||
|
export default memo(function ConnectionChart({ chartData }: { chartData: ChartData }) {
|
||||||
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
const { t } = useLingui()
|
||||||
|
|
||||||
|
if (chartData.systemStats.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataKeys = [
|
||||||
|
{
|
||||||
|
name: t`IPv4 Established`,
|
||||||
|
dataKey: "stats.nets.conn_established",
|
||||||
|
color: "hsl(220, 70%, 50%)", // Blue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`IPv4 Listen`,
|
||||||
|
dataKey: "stats.nets.conn_listen",
|
||||||
|
color: "hsl(142, 70%, 45%)", // Green
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`IPv4 Time Wait`,
|
||||||
|
dataKey: "stats.nets.conn_timewait",
|
||||||
|
color: "hsl(48, 96%, 53%)", // Yellow
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`IPv4 Close Wait`,
|
||||||
|
dataKey: "stats.nets.conn_closewait",
|
||||||
|
color: "hsl(271, 81%, 56%)", // Purple
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`IPv4 Syn Recv`,
|
||||||
|
dataKey: "stats.nets.conn_synrecv",
|
||||||
|
color: "hsl(9, 78%, 56%)", // Red
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`IPv6 Established`,
|
||||||
|
dataKey: "stats.nets.conn6_established",
|
||||||
|
color: "hsl(220, 70%, 65%)", // Light Blue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`IPv6 Listen`,
|
||||||
|
dataKey: "stats.nets.conn6_listen",
|
||||||
|
color: "hsl(142, 70%, 60%)", // Light Green
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`IPv6 Time Wait`,
|
||||||
|
dataKey: "stats.nets.conn6_timewait",
|
||||||
|
color: "hsl(48, 96%, 68%)", // Light Yellow
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`IPv6 Close Wait`,
|
||||||
|
dataKey: "stats.nets.conn6_closewait",
|
||||||
|
color: "hsl(271, 81%, 71%)", // Light Purple
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`IPv6 Syn Recv`,
|
||||||
|
dataKey: "stats.nets.conn6_synrecv",
|
||||||
|
color: "hsl(9, 78%, 71%)", // Light Red
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ChartContainer
|
||||||
|
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||||
|
"opacity-100": yAxisWidth,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
direction="ltr"
|
||||||
|
orientation={chartData.orientation}
|
||||||
|
className="tracking-tighter"
|
||||||
|
width={yAxisWidth}
|
||||||
|
tickFormatter={(value) => updateYAxisWidth(value.toString())}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
{xAxis(chartData)}
|
||||||
|
<ChartTooltip
|
||||||
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={({ value }) => value.toString()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
{dataKeys.map((key, i) => (
|
||||||
|
<Area
|
||||||
|
key={i}
|
||||||
|
dataKey={key.dataKey}
|
||||||
|
name={key.name}
|
||||||
|
type="monotoneX"
|
||||||
|
fill={key.color}
|
||||||
|
fillOpacity={0.3}
|
||||||
|
stroke={key.color}
|
||||||
|
strokeOpacity={1}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import { memo, useMemo } from "react"
|
import { memo, useMemo } from "react"
|
||||||
import { cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
|
import { cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { $containerFilter, $userSettings } from "@/lib/stores"
|
import { $containerFilter, $userSettings } from "@/lib/stores"
|
||||||
import { ChartData } from "@/types"
|
import type { ChartData } from "@/types"
|
||||||
import { Separator } from "../ui/separator"
|
import { Separator } from "../ui/separator"
|
||||||
import { ChartType, Unit } from "@/lib/enums"
|
import { ChartType, Unit } from "@/lib/enums"
|
||||||
import { useYAxisWidth } from "./hooks"
|
import { useYAxisWidth } from "./hooks"
|
||||||
@@ -31,6 +31,7 @@ export default memo(function ContainerChart({
|
|||||||
|
|
||||||
const isNetChart = chartType === ChartType.Network
|
const isNetChart = chartType === ChartType.Network
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
|
||||||
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
|
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
|
||||||
const obj = {} as {
|
const obj = {} as {
|
||||||
toolTipFormatter: (item: any, key: string) => React.ReactNode | string
|
toolTipFormatter: (item: any, key: string) => React.ReactNode | string
|
||||||
@@ -47,7 +48,7 @@ export default memo(function ContainerChart({
|
|||||||
const chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes
|
const chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes
|
||||||
obj.tickFormatter = (val) => {
|
obj.tickFormatter = (val) => {
|
||||||
const { value, unit } = formatBytes(val, isNetChart, chartUnit, true)
|
const { value, unit } = formatBytes(val, isNetChart, chartUnit, true)
|
||||||
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
|
return updateYAxisWidth(`${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// tooltip formatter
|
// tooltip formatter
|
||||||
@@ -74,10 +75,10 @@ export default memo(function ContainerChart({
|
|||||||
} else if (chartType === ChartType.Memory) {
|
} else if (chartType === ChartType.Memory) {
|
||||||
obj.toolTipFormatter = (item: any) => {
|
obj.toolTipFormatter = (item: any) => {
|
||||||
const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
|
const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
|
||||||
return decimalString(value) + " " + unit
|
return `${decimalString(value)} ${unit}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit
|
obj.toolTipFormatter = (item: any) => `${decimalString(item.value)} ${unit}`
|
||||||
}
|
}
|
||||||
// data function
|
// data function
|
||||||
if (isNetChart) {
|
if (isNetChart) {
|
||||||
@@ -133,7 +134,7 @@ export default memo(function ContainerChart({
|
|||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
truncate={true}
|
truncate={true}
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />}
|
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />}
|
||||||
/>
|
/>
|
||||||
164
internal/site/src/components/charts/network-interface-chart.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { memo, useMemo } from "react"
|
||||||
|
import { useLingui } from "@lingui/react/macro"
|
||||||
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
|
import {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
xAxis,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
} from "@/components/ui/chart"
|
||||||
|
import { cn, formatShortDate, chartMargin, formatBytes, toFixedFloat, decimalString } from "@/lib/utils"
|
||||||
|
import { ChartData } from "@/types"
|
||||||
|
import { useStore } from "@nanostores/react"
|
||||||
|
import { $networkInterfaceFilter, $userSettings } from "@/lib/stores"
|
||||||
|
import { Unit } from "@/lib/enums"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
|
||||||
|
const getNestedValue = (path: string, max = false, data: any): number | null => {
|
||||||
|
// path format is like "eth0.ns" or "eth0.nr"
|
||||||
|
// need to access data.stats.ni[interface][property]
|
||||||
|
const parts = path.split(".")
|
||||||
|
if (parts.length !== 2) return null
|
||||||
|
|
||||||
|
const [interfaceName, property] = parts
|
||||||
|
const propertyKey = property + (max ? "m" : "")
|
||||||
|
|
||||||
|
return data?.stats?.ni?.[interfaceName]?.[propertyKey] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(function NetworkInterfaceChart({
|
||||||
|
chartData,
|
||||||
|
maxToggled = false,
|
||||||
|
max,
|
||||||
|
}: {
|
||||||
|
chartData: ChartData
|
||||||
|
maxToggled?: boolean
|
||||||
|
max?: number
|
||||||
|
}) {
|
||||||
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
const { i18n } = useLingui()
|
||||||
|
const networkInterfaceFilter = useStore($networkInterfaceFilter)
|
||||||
|
const userSettings = useStore($userSettings)
|
||||||
|
|
||||||
|
const { chartTime } = chartData
|
||||||
|
const showMax = chartTime !== "1h" && maxToggled
|
||||||
|
|
||||||
|
// Get network interface names from the latest stats
|
||||||
|
const networkInterfaces = useMemo(() => {
|
||||||
|
if (chartData.systemStats.length === 0) return []
|
||||||
|
const latestStats = chartData.systemStats[chartData.systemStats.length - 1]
|
||||||
|
const allInterfaces = Object.keys(latestStats.stats.ni || {})
|
||||||
|
|
||||||
|
// Filter interfaces based on filter value
|
||||||
|
if (networkInterfaceFilter) {
|
||||||
|
return allInterfaces.filter((iface) => iface.toLowerCase().includes(networkInterfaceFilter.toLowerCase()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return allInterfaces
|
||||||
|
}, [chartData.systemStats, networkInterfaceFilter])
|
||||||
|
|
||||||
|
const dataKeys = useMemo(() => {
|
||||||
|
// Generate colors for each interface - each interface gets a unique hue
|
||||||
|
// and sent/received use different shades of that hue
|
||||||
|
const interfaceColors = networkInterfaces.map((iface, index) => {
|
||||||
|
const hue = ((index * 360) / Math.max(networkInterfaces.length, 1)) % 360
|
||||||
|
return {
|
||||||
|
interface: iface,
|
||||||
|
sentColor: `hsl(${hue}, 70%, 45%)`, // Darker shade for sent
|
||||||
|
receivedColor: `hsl(${hue}, 70%, 65%)`, // Lighter shade for received
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return interfaceColors.flatMap(({ interface: iface, sentColor, receivedColor }) => [
|
||||||
|
{
|
||||||
|
name: `${iface} Sent`,
|
||||||
|
dataKey: `${iface}.ns`,
|
||||||
|
color: sentColor,
|
||||||
|
type: "sent" as const,
|
||||||
|
interface: iface,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${iface} Received`,
|
||||||
|
dataKey: `${iface}.nr`,
|
||||||
|
color: receivedColor,
|
||||||
|
type: "received" as const,
|
||||||
|
interface: iface,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}, [networkInterfaces, i18n.locale])
|
||||||
|
|
||||||
|
const colors = dataKeys.map((key) => key.name)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ChartContainer
|
||||||
|
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||||
|
"opacity-100": yAxisWidth,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
direction="ltr"
|
||||||
|
orientation={chartData.orientation}
|
||||||
|
className="tracking-tighter"
|
||||||
|
width={yAxisWidth}
|
||||||
|
tickFormatter={(value) => {
|
||||||
|
const { value: formattedValue, unit } = formatBytes(value, true, userSettings.unitNet ?? Unit.Bits, true)
|
||||||
|
const rounded = toFixedFloat(formattedValue, formattedValue >= 10 ? 1 : 2)
|
||||||
|
return updateYAxisWidth(`${rounded} ${unit}`)
|
||||||
|
}}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
{xAxis(chartData)}
|
||||||
|
<ChartTooltip
|
||||||
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
labelFormatter={(_: any, data: any) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={({ value }: any) => {
|
||||||
|
const { value: formattedValue, unit } = formatBytes(
|
||||||
|
value,
|
||||||
|
true,
|
||||||
|
userSettings.unitNet ?? Unit.Bits,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<span className="flex">
|
||||||
|
{decimalString(formattedValue, formattedValue >= 10 ? 1 : 2)} {unit}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{dataKeys.map((key, i) => {
|
||||||
|
const filtered =
|
||||||
|
networkInterfaceFilter && !key.interface.toLowerCase().includes(networkInterfaceFilter.toLowerCase())
|
||||||
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
|
return (
|
||||||
|
<Area
|
||||||
|
key={i}
|
||||||
|
dataKey={getNestedValue.bind(null, key.dataKey, showMax)}
|
||||||
|
name={key.name}
|
||||||
|
type="monotoneX"
|
||||||
|
fill={key.color}
|
||||||
|
fillOpacity={fillOpacity}
|
||||||
|
stroke={key.color}
|
||||||
|
strokeOpacity={strokeOpacity}
|
||||||
|
activeDot={{ opacity: filtered ? 0 : 1 }}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{colors.length < 12 && <ChartLegend content={<ChartLegendContent />} />}
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||