Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,13 +434,15 @@ host = "0.0.0.0"
port = 8200
interface = "" # Empty for all interfaces
api_key = "your-secret-key"
disk_includes = ["/mnt/storage"] # Additional mounts to monitor
disk_includes = ["/mnt/storage"] # Hard override: include these mounts even if small, tmpfs, or bind mounts
disk_excludes = ["/boot", "/tmp"] # Mounts to exclude

[monitor]
enabled = true
```

`disk_includes` is a hard override. Explicitly included mounts are reported even if they would normally be skipped for being special filesystems or smaller than 1 GiB. Disk reporting also dedupes bind mounts by default; explicitly included bind mounts are kept.

### Packet Loss Monitoring

Continuous network monitoring with MTR integration and performance tracking.
Expand Down Expand Up @@ -796,7 +798,7 @@ NETRONOME__AGENT_HOST=0.0.0.0 # Agent listen address
NETRONOME__AGENT_PORT=8200 # Agent port
NETRONOME__AGENT_INTERFACE= # Network interface to monitor (empty for all)
NETRONOME__AGENT_API_KEY= # Agent API key for authentication
NETRONOME__AGENT_DISK_INCLUDES= # Comma-separated paths to include
NETRONOME__AGENT_DISK_INCLUDES= # Comma-separated hard override include paths
NETRONOME__AGENT_DISK_EXCLUDES= # Comma-separated paths to exclude
```

Expand Down
4 changes: 2 additions & 2 deletions cmd/netronome/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ var (
rootCmd = &cobra.Command{
Use: "netronome",
Short: "Netronome is a network performance testing and monitoring tool",
Long: `Netronome is a network performance testing and monitoring tool that helps you
Long: `Netronome is a network performance testing and monitoring tool that helps you
track and analyze your network performance over time.`,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
Expand Down Expand Up @@ -118,7 +118,7 @@ func init() {
agentCmd.Flags().StringP("interface", "i", "", "network interface to monitor (empty for all)")
agentCmd.Flags().StringP("api-key", "k", "", "API key for authentication")
agentCmd.Flags().StringP("log-level", "l", "", "log level (trace, debug, info, warn, error)")
agentCmd.Flags().StringSlice("disk-include", []string{}, "additional disk mount points to monitor (e.g., /mnt/storage)")
agentCmd.Flags().StringSlice("disk-include", []string{}, "disk mount points to force into monitoring, even if small or normally filtered (e.g., /mnt/storage)")
agentCmd.Flags().StringSlice("disk-exclude", []string{}, "disk mount points to exclude from monitoring (e.g., /boot)")
agentCmd.Flags().Bool("disable-system-metrics", false, "disable system metrics collection (CPU, memory, disk, temperature)")
agentCmd.Flags().Bool("tailscale", false, "enable Tailscale for secure connectivity")
Expand Down
141 changes: 135 additions & 6 deletions internal/agent/disk_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@
package agent

import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/rs/zerolog/log"
"github.com/shirou/gopsutil/v4/disk"
)

const minDiskReportSize = 1024 * 1024 * 1024

type diskReportEntry struct {
Partition disk.PartitionStat
Usage *disk.UsageStat
ExplicitInclude bool
}

// matchPath checks if a path matches a pattern, supporting both glob and prefix patterns
func matchPath(pattern, path string) bool {
// Check if pattern ends with * for prefix matching
Expand All @@ -36,21 +46,20 @@ func matchPath(pattern, path string) bool {
return pattern == path
}

// shouldIncludeDisk determines if a disk should be included in monitoring based on filter rules
func (a *Agent) shouldIncludeDisk(mountpoint, device, fstype string) bool {
func (a *Agent) diskFilterDecision(mountpoint, device, fstype string) (include bool, explicit bool) {
// First check if it's explicitly included - this takes precedence
for _, pattern := range a.config.DiskIncludes {
if matchPath(pattern, mountpoint) {
log.Debug().Str("mount", mountpoint).Str("pattern", pattern).Msg("Disk included by pattern match")
return true
return true, true
}
}

// Then check if it's explicitly excluded
for _, pattern := range a.config.DiskExcludes {
if matchPath(pattern, mountpoint) {
log.Debug().Str("mount", mountpoint).Str("pattern", pattern).Msg("Disk excluded by pattern match")
return false
return false, false
}
}

Expand All @@ -73,11 +82,131 @@ func (a *Agent) shouldIncludeDisk(mountpoint, device, fstype string) bool {
fstype == "sysfs" ||
fstype == "cgroup" ||
fstype == "cgroup2" {
return false
return false, false
}

// Include everything else by default
return true
return true, false
}

// shouldIncludeDisk determines if a disk should be included in monitoring based on filter rules
func (a *Agent) shouldIncludeDisk(mountpoint, device, fstype string) bool {
include, _ := a.diskFilterDecision(mountpoint, device, fstype)
return include
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

func (a *Agent) buildDiskReportEntries(partitions []disk.PartitionStat, usageFn func(string) (*disk.UsageStat, error)) []diskReportEntry {
entries := make([]diskReportEntry, 0, len(partitions))

for _, partition := range partitions {
include, explicit := a.diskFilterDecision(partition.Mountpoint, partition.Device, partition.Fstype)
if !include {
log.Debug().
Str("mount", partition.Mountpoint).
Str("device", partition.Device).
Str("fstype", partition.Fstype).
Msg("Skipping disk based on filter rules")
continue
}

usage, err := usageFn(partition.Mountpoint)
if err != nil {
log.Debug().Err(err).Str("mount", partition.Mountpoint).Msg("Failed to get disk usage")
continue
}

if !explicit && usage.Total < minDiskReportSize {
log.Debug().
Str("mount", partition.Mountpoint).
Uint64("total", usage.Total).
Msg("Skipping small disk")
continue
}

entries = append(entries, diskReportEntry{
Partition: partition,
Usage: usage,
ExplicitInclude: explicit,
})
}

return dedupeDiskReportEntries(entries)
}

func dedupeDiskReportEntries(entries []diskReportEntry) []diskReportEntry {
if len(entries) < 2 {
return entries
}

grouped := make(map[string][]int, len(entries))
for i, entry := range entries {
grouped[diskReportSignature(entry)] = append(grouped[diskReportSignature(entry)], i)
}

keep := make([]bool, len(entries))

for _, indexes := range grouped {
if len(indexes) == 1 {
keep[indexes[0]] = true
continue
}

preferred := indexes[0]
for _, idx := range indexes[1:] {
if prefersDiskEntry(entries[idx], entries[preferred]) {
preferred = idx
}
}

keep[preferred] = true

for _, idx := range indexes {
if idx == preferred {
continue
}
if entries[idx].ExplicitInclude || !isBindMount(entries[idx].Partition.Opts) {
keep[idx] = true
}
}
}

deduped := make([]diskReportEntry, 0, len(entries))
for i, entry := range entries {
if keep[i] {
deduped = append(deduped, entry)
}
}

return deduped
}

func diskReportSignature(entry diskReportEntry) string {
return fmt.Sprintf("%s|%s|%d|%d|%d", entry.Partition.Device, entry.Partition.Fstype, entry.Usage.Total, entry.Usage.Used, entry.Usage.Free)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func prefersDiskEntry(left, right diskReportEntry) bool {
leftBind := isBindMount(left.Partition.Opts)
rightBind := isBindMount(right.Partition.Opts)

if leftBind != rightBind {
return !leftBind
}

if len(left.Partition.Mountpoint) != len(right.Partition.Mountpoint) {
return len(left.Partition.Mountpoint) < len(right.Partition.Mountpoint)
}

return left.Partition.Mountpoint < right.Partition.Mountpoint
}

func isBindMount(opts []string) bool {
for _, opt := range opts {
if opt == "bind" {
return true
}
}

return false
}

// getDevicePaths returns a list of device paths to check for SMART data
Expand Down
109 changes: 109 additions & 0 deletions internal/agent/disk_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2024-2026, s0up and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later

package agent

import (
"testing"

"github.com/shirou/gopsutil/v4/disk"
"github.com/stretchr/testify/assert"

"github.com/autobrr/netronome/internal/config"
)

func TestBuildDiskReportEntries_ExplicitIncludeBypassesSizeFilter(t *testing.T) {
agent := New(&config.AgentConfig{
DiskIncludes: []string{"/var/log"},
})

partitions := []disk.PartitionStat{
{
Device: "tmpfs",
Mountpoint: "/var/log",
Fstype: "tmpfs",
},
}

entries := agent.buildDiskReportEntries(partitions, func(mountpoint string) (*disk.UsageStat, error) {
return &disk.UsageStat{
Path: mountpoint,
Total: 128 * 1024 * 1024,
Used: 64 * 1024 * 1024,
Free: 64 * 1024 * 1024,
UsedPercent: 50,
}, nil
})

assert.Len(t, entries, 1)
assert.Equal(t, "/var/log", entries[0].Partition.Mountpoint)
assert.True(t, entries[0].ExplicitInclude)
}

func TestBuildDiskReportEntries_DedupesNonExplicitBindMounts(t *testing.T) {
agent := New(&config.AgentConfig{})

partitions := []disk.PartitionStat{
{
Device: "/dev/mmcblk0p2",
Mountpoint: "/",
Fstype: "ext4",
Opts: []string{"rw", "relatime"},
},
{
Device: "/dev/mmcblk0p2",
Mountpoint: "/var/hdd.log",
Fstype: "ext4",
Opts: []string{"rw", "relatime", "bind"},
},
}

entries := agent.buildDiskReportEntries(partitions, func(mountpoint string) (*disk.UsageStat, error) {
return &disk.UsageStat{
Path: mountpoint,
Total: 32 * 1024 * 1024 * 1024,
Used: 8 * 1024 * 1024 * 1024,
Free: 24 * 1024 * 1024 * 1024,
UsedPercent: 25,
}, nil
})

assert.Len(t, entries, 1)
assert.Equal(t, "/", entries[0].Partition.Mountpoint)
}

func TestBuildDiskReportEntries_KeepsExplicitlyIncludedBindMounts(t *testing.T) {
agent := New(&config.AgentConfig{
DiskIncludes: []string{"/var/hdd.log"},
})

partitions := []disk.PartitionStat{
{
Device: "/dev/mmcblk0p2",
Mountpoint: "/",
Fstype: "ext4",
Opts: []string{"rw", "relatime"},
},
{
Device: "/dev/mmcblk0p2",
Mountpoint: "/var/hdd.log",
Fstype: "ext4",
Opts: []string{"rw", "relatime", "bind"},
},
}

entries := agent.buildDiskReportEntries(partitions, func(mountpoint string) (*disk.UsageStat, error) {
return &disk.UsageStat{
Path: mountpoint,
Total: 32 * 1024 * 1024 * 1024,
Used: 8 * 1024 * 1024 * 1024,
Free: 24 * 1024 * 1024 * 1024,
UsedPercent: 25,
}, nil
})

assert.Len(t, entries, 2)
assert.Equal(t, "/", entries[0].Partition.Mountpoint)
assert.Equal(t, "/var/hdd.log", entries[1].Partition.Mountpoint)
assert.True(t, entries[1].ExplicitInclude)
}
31 changes: 7 additions & 24 deletions internal/agent/hardware.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,32 +197,15 @@ func (a *Agent) getHardwareStats() (*HardwareStats, error) {
}
}

for _, partition := range partitions {
// Check if this disk should be included based on filters
if !a.shouldIncludeDisk(partition.Mountpoint, partition.Device, partition.Fstype) {
log.Debug().
Str("mount", partition.Mountpoint).
Str("device", partition.Device).
Str("fstype", partition.Fstype).
Msg("Skipping disk based on filter rules")
continue
}

usage, err := disk.Usage(partition.Mountpoint)
for _, entry := range a.buildDiskReportEntries(partitions, func(mountpoint string) (*disk.UsageStat, error) {
usage, err := disk.Usage(mountpoint)
if err != nil {
log.Debug().Err(err).Str("mount", partition.Mountpoint).Msg("Failed to get disk usage")
continue
return nil, err
}

// Skip if disk is too small (less than 1GB)
if usage.Total < 1024*1024*1024 {
log.Debug().
Str("mount", partition.Mountpoint).
Uint64("total", usage.Total).
Msg("Skipping small disk")
continue
}

return usage, nil
}) {
partition := entry.Partition
usage := entry.Usage
diskStat := DiskStats{
Path: partition.Mountpoint,
Device: partition.Device,
Expand Down
Loading