mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-16 18:26:16 +01:00
add ability to set custom name for extra filesystems (#379)
This commit is contained in:
@@ -152,7 +152,12 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
||||
data.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||
for name, stats := range a.fsStats {
|
||||
if !stats.Root && stats.DiskTotal > 0 {
|
||||
data.Stats.ExtraFs[name] = stats
|
||||
// Use custom name if available, otherwise use device name
|
||||
key := name
|
||||
if stats.Name != "" {
|
||||
key = stats.Name
|
||||
}
|
||||
data.Stats.ExtraFs[key] = stats
|
||||
}
|
||||
}
|
||||
slog.Debug("Extra FS", "data", data.Stats.ExtraFs)
|
||||
|
||||
@@ -13,6 +13,19 @@ import (
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
// parseFilesystemEntry parses a filesystem entry in the format "device__customname"
|
||||
// Returns the device/filesystem part and the custom name part
|
||||
func parseFilesystemEntry(entry string) (device, customName string) {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if parts := strings.SplitN(entry, "__", 2); len(parts) == 2 {
|
||||
device = strings.TrimSpace(parts[0])
|
||||
customName = strings.TrimSpace(parts[1])
|
||||
} else {
|
||||
device = entry
|
||||
}
|
||||
return device, customName
|
||||
}
|
||||
|
||||
// Sets up the filesystems to monitor for disk usage and I/O.
|
||||
func (a *Agent) initializeDiskInfo() {
|
||||
filesystem, _ := GetEnv("FILESYSTEM")
|
||||
@@ -37,7 +50,7 @@ func (a *Agent) initializeDiskInfo() {
|
||||
slog.Debug("Disk I/O", "diskstats", diskIoCounters)
|
||||
|
||||
// Helper function to add a filesystem to fsStats if it doesn't exist
|
||||
addFsStat := func(device, mountpoint string, root bool) {
|
||||
addFsStat := func(device, mountpoint string, root bool, customName ...string) {
|
||||
var key string
|
||||
if runtime.GOOS == "windows" {
|
||||
key = device
|
||||
@@ -66,7 +79,11 @@ func (a *Agent) initializeDiskInfo() {
|
||||
}
|
||||
}
|
||||
}
|
||||
a.fsStats[key] = &system.FsStats{Root: root, Mountpoint: mountpoint}
|
||||
fsStats := &system.FsStats{Root: root, Mountpoint: mountpoint}
|
||||
if len(customName) > 0 && customName[0] != "" {
|
||||
fsStats.Name = customName[0]
|
||||
}
|
||||
a.fsStats[key] = fsStats
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +103,14 @@ func (a *Agent) initializeDiskInfo() {
|
||||
|
||||
// Add EXTRA_FILESYSTEMS env var values to fsStats
|
||||
if extraFilesystems, exists := GetEnv("EXTRA_FILESYSTEMS"); exists {
|
||||
for _, fs := range strings.Split(extraFilesystems, ",") {
|
||||
for _, fsEntry := range strings.Split(extraFilesystems, ",") {
|
||||
// Parse custom name from format: device__customname
|
||||
fs, customName := parseFilesystemEntry(fsEntry)
|
||||
|
||||
found := false
|
||||
for _, p := range partitions {
|
||||
if strings.HasSuffix(p.Device, fs) || p.Mountpoint == fs {
|
||||
addFsStat(p.Device, p.Mountpoint, false)
|
||||
addFsStat(p.Device, p.Mountpoint, false, customName)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -98,7 +118,7 @@ func (a *Agent) initializeDiskInfo() {
|
||||
// if not in partitions, test if we can get disk usage
|
||||
if !found {
|
||||
if _, err := disk.Usage(fs); err == nil {
|
||||
addFsStat(filepath.Base(fs), fs, false)
|
||||
addFsStat(filepath.Base(fs), fs, false, customName)
|
||||
} else {
|
||||
slog.Error("Invalid filesystem", "name", fs, "err", err)
|
||||
}
|
||||
@@ -120,7 +140,8 @@ func (a *Agent) initializeDiskInfo() {
|
||||
|
||||
// Check if device is in /extra-filesystems
|
||||
if strings.HasPrefix(p.Mountpoint, efPath) {
|
||||
addFsStat(p.Device, p.Mountpoint, false)
|
||||
device, customName := parseFilesystemEntry(p.Mountpoint)
|
||||
addFsStat(device, p.Mountpoint, false, customName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +156,8 @@ func (a *Agent) initializeDiskInfo() {
|
||||
mountpoint := filepath.Join(efPath, folder.Name())
|
||||
slog.Debug("/extra-filesystems", "mountpoint", mountpoint)
|
||||
if !existingMountpoints[mountpoint] {
|
||||
addFsStat(folder.Name(), mountpoint, false)
|
||||
device, customName := parseFilesystemEntry(folder.Name())
|
||||
addFsStat(device, mountpoint, false, customName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
235
agent/disk_test.go
Normal file
235
agent/disk_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseFilesystemEntry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedFs string
|
||||
expectedName string
|
||||
}{
|
||||
{
|
||||
name: "simple device name",
|
||||
input: "sda1",
|
||||
expectedFs: "sda1",
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "device with custom name",
|
||||
input: "sda1__my-storage",
|
||||
expectedFs: "sda1",
|
||||
expectedName: "my-storage",
|
||||
},
|
||||
{
|
||||
name: "full device path with custom name",
|
||||
input: "/dev/sdb1__backup-drive",
|
||||
expectedFs: "/dev/sdb1",
|
||||
expectedName: "backup-drive",
|
||||
},
|
||||
{
|
||||
name: "NVMe device with custom name",
|
||||
input: "nvme0n1p2__fast-ssd",
|
||||
expectedFs: "nvme0n1p2",
|
||||
expectedName: "fast-ssd",
|
||||
},
|
||||
{
|
||||
name: "whitespace trimmed",
|
||||
input: " sda2__trimmed-name ",
|
||||
expectedFs: "sda2",
|
||||
expectedName: "trimmed-name",
|
||||
},
|
||||
{
|
||||
name: "empty custom name",
|
||||
input: "sda3__",
|
||||
expectedFs: "sda3",
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "empty device name",
|
||||
input: "__just-custom",
|
||||
expectedFs: "",
|
||||
expectedName: "just-custom",
|
||||
},
|
||||
{
|
||||
name: "multiple underscores in custom name",
|
||||
input: "sda1__my_custom_drive",
|
||||
expectedFs: "sda1",
|
||||
expectedName: "my_custom_drive",
|
||||
},
|
||||
{
|
||||
name: "custom name with spaces",
|
||||
input: "sda1__My Storage Drive",
|
||||
expectedFs: "sda1",
|
||||
expectedName: "My Storage Drive",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fsEntry := strings.TrimSpace(tt.input)
|
||||
var fs, customName string
|
||||
if parts := strings.SplitN(fsEntry, "__", 2); len(parts) == 2 {
|
||||
fs = strings.TrimSpace(parts[0])
|
||||
customName = strings.TrimSpace(parts[1])
|
||||
} else {
|
||||
fs = fsEntry
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedFs, fs)
|
||||
assert.Equal(t, tt.expectedName, customName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeDiskInfoWithCustomNames(t *testing.T) {
|
||||
// Set up environment variables
|
||||
oldEnv := os.Getenv("EXTRA_FILESYSTEMS")
|
||||
defer func() {
|
||||
if oldEnv != "" {
|
||||
os.Setenv("EXTRA_FILESYSTEMS", oldEnv)
|
||||
} else {
|
||||
os.Unsetenv("EXTRA_FILESYSTEMS")
|
||||
}
|
||||
}()
|
||||
|
||||
// Test with custom names
|
||||
os.Setenv("EXTRA_FILESYSTEMS", "sda1__my-storage,/dev/sdb1__backup-drive,nvme0n1p2")
|
||||
|
||||
// Mock disk partitions (we'll just test the parsing logic)
|
||||
// Since the actual disk operations are system-dependent, we'll focus on the parsing
|
||||
testCases := []struct {
|
||||
envValue string
|
||||
expectedFs []string
|
||||
expectedNames map[string]string
|
||||
}{
|
||||
{
|
||||
envValue: "sda1__my-storage,sdb1__backup-drive",
|
||||
expectedFs: []string{"sda1", "sdb1"},
|
||||
expectedNames: map[string]string{
|
||||
"sda1": "my-storage",
|
||||
"sdb1": "backup-drive",
|
||||
},
|
||||
},
|
||||
{
|
||||
envValue: "sda1,nvme0n1p2__fast-ssd",
|
||||
expectedFs: []string{"sda1", "nvme0n1p2"},
|
||||
expectedNames: map[string]string{
|
||||
"nvme0n1p2": "fast-ssd",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("env_"+tc.envValue, func(t *testing.T) {
|
||||
os.Setenv("EXTRA_FILESYSTEMS", tc.envValue)
|
||||
|
||||
// Create mock partitions that would match our test cases
|
||||
partitions := []disk.PartitionStat{}
|
||||
for _, fs := range tc.expectedFs {
|
||||
if strings.HasPrefix(fs, "/dev/") {
|
||||
partitions = append(partitions, disk.PartitionStat{
|
||||
Device: fs,
|
||||
Mountpoint: fs,
|
||||
})
|
||||
} else {
|
||||
partitions = append(partitions, disk.PartitionStat{
|
||||
Device: "/dev/" + fs,
|
||||
Mountpoint: "/" + fs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test the parsing logic by calling the relevant part
|
||||
// We'll create a simplified version to test just the parsing
|
||||
extraFilesystems := tc.envValue
|
||||
for _, fsEntry := range strings.Split(extraFilesystems, ",") {
|
||||
// Parse the entry
|
||||
fsEntry = strings.TrimSpace(fsEntry)
|
||||
var fs, customName string
|
||||
if parts := strings.SplitN(fsEntry, "__", 2); len(parts) == 2 {
|
||||
fs = strings.TrimSpace(parts[0])
|
||||
customName = strings.TrimSpace(parts[1])
|
||||
} else {
|
||||
fs = fsEntry
|
||||
}
|
||||
|
||||
// Verify the device is in our expected list
|
||||
assert.Contains(t, tc.expectedFs, fs, "parsed device should be in expected list")
|
||||
|
||||
// Check if custom name should exist
|
||||
if expectedName, exists := tc.expectedNames[fs]; exists {
|
||||
assert.Equal(t, expectedName, customName, "custom name should match expected")
|
||||
} else {
|
||||
assert.Empty(t, customName, "custom name should be empty when not expected")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsStatsWithCustomNames(t *testing.T) {
|
||||
// Test that FsStats properly stores custom names
|
||||
fsStats := &system.FsStats{
|
||||
Mountpoint: "/mnt/storage",
|
||||
Name: "my-custom-storage",
|
||||
DiskTotal: 100.0,
|
||||
DiskUsed: 50.0,
|
||||
}
|
||||
|
||||
assert.Equal(t, "my-custom-storage", fsStats.Name)
|
||||
assert.Equal(t, "/mnt/storage", fsStats.Mountpoint)
|
||||
assert.Equal(t, 100.0, fsStats.DiskTotal)
|
||||
assert.Equal(t, 50.0, fsStats.DiskUsed)
|
||||
}
|
||||
|
||||
func TestExtraFsKeyGeneration(t *testing.T) {
|
||||
// Test the logic for generating ExtraFs keys with custom names
|
||||
testCases := []struct {
|
||||
name string
|
||||
deviceName string
|
||||
customName string
|
||||
expectedKey string
|
||||
}{
|
||||
{
|
||||
name: "with custom name",
|
||||
deviceName: "sda1",
|
||||
customName: "my-storage",
|
||||
expectedKey: "my-storage",
|
||||
},
|
||||
{
|
||||
name: "without custom name",
|
||||
deviceName: "sda1",
|
||||
customName: "",
|
||||
expectedKey: "sda1",
|
||||
},
|
||||
{
|
||||
name: "empty custom name falls back to device",
|
||||
deviceName: "nvme0n1p2",
|
||||
customName: "",
|
||||
expectedKey: "nvme0n1p2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Simulate the key generation logic from agent.go
|
||||
key := tc.deviceName
|
||||
if tc.customName != "" {
|
||||
key = tc.customName
|
||||
}
|
||||
assert.Equal(t, tc.expectedKey, key)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ type FsStats struct {
|
||||
Time time.Time `json:"-"`
|
||||
Root bool `json:"-"`
|
||||
Mountpoint string `json:"-"`
|
||||
Name string `json:"-"`
|
||||
DiskTotal float64 `json:"d" cbor:"0,keyasint"`
|
||||
DiskUsed float64 `json:"du" cbor:"1,keyasint"`
|
||||
TotalRead uint64 `json:"-"`
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { Unit } from "@/lib/enums"
|
||||
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData } from "@/types"
|
||||
import type { ChartData, SystemStatsRecord } from "@/types"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function DiskChart({
|
||||
@@ -12,7 +12,7 @@ export default memo(function DiskChart({
|
||||
diskSize,
|
||||
chartData,
|
||||
}: {
|
||||
dataKey: string
|
||||
dataKey: string | ((data: SystemStatsRecord) => number | undefined)
|
||||
diskSize: number
|
||||
chartData: ChartData
|
||||
}) {
|
||||
|
||||
@@ -932,7 +932,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
>
|
||||
<DiskChart
|
||||
chartData={chartData}
|
||||
dataKey={`stats.efs.${extraFsName}.du`}
|
||||
dataKey={({ stats }: SystemStatsRecord) => stats?.efs?.[extraFsName]?.du}
|
||||
diskSize={systemStats.at(-1)?.stats.efs?.[extraFsName].d ?? NaN}
|
||||
/>
|
||||
</ChartCard>
|
||||
|
||||
Reference in New Issue
Block a user