mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-19 07:54:10 +00:00
Filter read-only filesystems from host agent disk metrics (related to #690)
Squashfs snap mounts on Ubuntu (and similar read-only filesystems like erofs on Home Assistant OS) always report near-full usage and trigger false disk alerts. The filter logic existed in Proxmox monitoring but wasn't applied to host agents. Changes: - Extract read-only filesystem filter to shared pkg/fsfilters package - Apply filter in hostmetrics.collectDisks() for host/docker agents - Apply filter in monitor.ApplyHostReport() for backward compatibility - Convert internal/monitoring/fs_filters.go to wrapper functions This prevents squashfs, erofs, iso9660, cdfs, udf, cramfs, romfs, and saturated overlay filesystems from generating alerts. Filtering happens at both collection time (agents) and ingestion time (server) to ensure older agents don't cause false alerts until they're updated.
This commit is contained in:
parent
d13a1e372a
commit
2e1ef44ecd
5 changed files with 221 additions and 47 deletions
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/fsfilters"
|
||||
gocpu "github.com/shirou/gopsutil/v4/cpu"
|
||||
godisk "github.com/shirou/gopsutil/v4/disk"
|
||||
goload "github.com/shirou/gopsutil/v4/load"
|
||||
|
|
@ -114,6 +115,13 @@ func collectDisks(ctx context.Context) []agentshost.Disk {
|
|||
continue
|
||||
}
|
||||
|
||||
// Skip read-only filesystems like squashfs (snap mounts), erofs, iso9660, etc.
|
||||
// These are immutable and always report near-full usage, which causes false alerts.
|
||||
// See issues #505 (Home Assistant OS) and #690 (Ubuntu snap mounts).
|
||||
if fsfilters.ShouldIgnoreReadOnlyFilesystem(part.Fstype, usage.Total, usage.Used) {
|
||||
continue
|
||||
}
|
||||
|
||||
disks = append(disks, agentshost.Disk{
|
||||
Device: part.Device,
|
||||
Mountpoint: part.Mountpoint,
|
||||
|
|
|
|||
|
|
@ -1,55 +1,15 @@
|
|||
package monitoring
|
||||
|
||||
import "strings"
|
||||
import "github.com/rcourtman/pulse-go-rewrite/pkg/fsfilters"
|
||||
|
||||
var readOnlyFilesystemPatterns = []struct {
|
||||
reason string
|
||||
substrings []string
|
||||
}{
|
||||
{reason: "erofs", substrings: []string{"erofs"}},
|
||||
{reason: "squashfs", substrings: []string{"squashfs", "squash-fs"}},
|
||||
{reason: "iso9660", substrings: []string{"iso9660"}},
|
||||
{reason: "cdfs", substrings: []string{"cdfs"}},
|
||||
{reason: "udf", substrings: []string{"udf"}},
|
||||
{reason: "cramfs", substrings: []string{"cramfs"}},
|
||||
{reason: "romfs", substrings: []string{"romfs"}},
|
||||
}
|
||||
|
||||
// readOnlyFilesystemReason returns a label explaining why a filesystem should be
|
||||
// ignored for usage calculations, along with a boolean indicating whether it is
|
||||
// a read-only filesystem that always reports full usage. This helps us avoid
|
||||
// false positives from immutable root images (overlay/squashfs/erofs) that ship
|
||||
// with Home Assistant OS and similar appliances (see issue #505).
|
||||
// readOnlyFilesystemReason is a compatibility wrapper around the shared filter.
|
||||
// Use fsfilters.ReadOnlyFilesystemReason directly in new code.
|
||||
func readOnlyFilesystemReason(fsType string, totalBytes, usedBytes uint64) (string, bool) {
|
||||
ft := strings.ToLower(strings.TrimSpace(fsType))
|
||||
if ft == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Common read-only filesystem types used for immutable system partitions.
|
||||
for _, pattern := range readOnlyFilesystemPatterns {
|
||||
for _, needle := range pattern.substrings {
|
||||
if strings.Contains(ft, needle) {
|
||||
return pattern.reason, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay-style filesystems can report 100% usage even though writes are
|
||||
// redirected elsewhere. Treat them as read-only when the reported usage is
|
||||
// saturated so we fall back to the writable layer metrics instead.
|
||||
if strings.Contains(ft, "overlay") || strings.Contains(ft, "overlayfs") {
|
||||
if totalBytes > 0 && usedBytes >= totalBytes {
|
||||
return "overlay", true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
return fsfilters.ReadOnlyFilesystemReason(fsType, totalBytes, usedBytes)
|
||||
}
|
||||
|
||||
// shouldIgnoreReadOnlyFilesystem reports whether the filesystem should be
|
||||
// skipped from usage aggregation.
|
||||
// shouldIgnoreReadOnlyFilesystem is a compatibility wrapper around the shared filter.
|
||||
// Use fsfilters.ShouldIgnoreReadOnlyFilesystem directly in new code.
|
||||
func shouldIgnoreReadOnlyFilesystem(fsType string, totalBytes, usedBytes uint64) bool {
|
||||
_, skip := readOnlyFilesystemReason(fsType, totalBytes, usedBytes)
|
||||
return skip
|
||||
return fsfilters.ShouldIgnoreReadOnlyFilesystem(fsType, totalBytes, usedBytes)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1975,6 +1975,13 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
|
|||
|
||||
disks := make([]models.Disk, 0, len(report.Disks))
|
||||
for _, disk := range report.Disks {
|
||||
// Filter read-only filesystems for backward compatibility with older host agents
|
||||
// that don't have the filter built in. Prevents false alerts for snap mounts,
|
||||
// immutable OS images, etc. (issues #505, #690).
|
||||
if shouldIgnoreReadOnlyFilesystem(disk.Type, uint64(disk.TotalBytes), uint64(disk.UsedBytes)) {
|
||||
continue
|
||||
}
|
||||
|
||||
usage := safeFloat(disk.Usage)
|
||||
if usage <= 0 && disk.TotalBytes > 0 {
|
||||
usage = safePercentage(float64(disk.UsedBytes), float64(disk.TotalBytes))
|
||||
|
|
|
|||
56
pkg/fsfilters/filters.go
Normal file
56
pkg/fsfilters/filters.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package fsfilters
|
||||
|
||||
import "strings"
|
||||
|
||||
var readOnlyFilesystemPatterns = []struct {
|
||||
reason string
|
||||
substrings []string
|
||||
}{
|
||||
{reason: "erofs", substrings: []string{"erofs"}},
|
||||
{reason: "squashfs", substrings: []string{"squashfs", "squash-fs"}},
|
||||
{reason: "iso9660", substrings: []string{"iso9660"}},
|
||||
{reason: "cdfs", substrings: []string{"cdfs"}},
|
||||
{reason: "udf", substrings: []string{"udf"}},
|
||||
{reason: "cramfs", substrings: []string{"cramfs"}},
|
||||
{reason: "romfs", substrings: []string{"romfs"}},
|
||||
}
|
||||
|
||||
// ReadOnlyFilesystemReason returns a label explaining why a filesystem should be
|
||||
// ignored for usage calculations, along with a boolean indicating whether it is
|
||||
// a read-only filesystem that always reports full usage. This helps us avoid
|
||||
// false positives from immutable root images (overlay/squashfs/erofs) that ship
|
||||
// with Home Assistant OS and similar appliances, as well as snap mounts on Ubuntu
|
||||
// systems (see issues #505, #690).
|
||||
func ReadOnlyFilesystemReason(fsType string, totalBytes, usedBytes uint64) (string, bool) {
|
||||
ft := strings.ToLower(strings.TrimSpace(fsType))
|
||||
if ft == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Common read-only filesystem types used for immutable system partitions.
|
||||
for _, pattern := range readOnlyFilesystemPatterns {
|
||||
for _, needle := range pattern.substrings {
|
||||
if strings.Contains(ft, needle) {
|
||||
return pattern.reason, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay-style filesystems can report 100% usage even though writes are
|
||||
// redirected elsewhere. Treat them as read-only when the reported usage is
|
||||
// saturated so we fall back to the writable layer metrics instead.
|
||||
if strings.Contains(ft, "overlay") || strings.Contains(ft, "overlayfs") {
|
||||
if totalBytes > 0 && usedBytes >= totalBytes {
|
||||
return "overlay", true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ShouldIgnoreReadOnlyFilesystem reports whether the filesystem should be
|
||||
// skipped from usage aggregation.
|
||||
func ShouldIgnoreReadOnlyFilesystem(fsType string, totalBytes, usedBytes uint64) bool {
|
||||
_, skip := ReadOnlyFilesystemReason(fsType, totalBytes, usedBytes)
|
||||
return skip
|
||||
}
|
||||
143
pkg/fsfilters/filters_test.go
Normal file
143
pkg/fsfilters/filters_test.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package fsfilters
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestReadOnlyFilesystemReason(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fsType string
|
||||
totalBytes uint64
|
||||
usedBytes uint64
|
||||
reason string
|
||||
skip bool
|
||||
}{
|
||||
{
|
||||
name: "erofs filesystem",
|
||||
fsType: "erofs",
|
||||
totalBytes: 10,
|
||||
usedBytes: 5,
|
||||
reason: "erofs",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "fuse.erofs filesystem",
|
||||
fsType: "fuse.erofs",
|
||||
totalBytes: 10,
|
||||
usedBytes: 8,
|
||||
reason: "erofs",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "overlay at capacity",
|
||||
fsType: "overlay",
|
||||
totalBytes: 100,
|
||||
usedBytes: 100,
|
||||
reason: "overlay",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "squashfs filesystem",
|
||||
fsType: "squashfs",
|
||||
totalBytes: 4096,
|
||||
usedBytes: 4096,
|
||||
reason: "squashfs",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "squash-fs alias",
|
||||
fsType: "Squash-FS",
|
||||
totalBytes: 2048,
|
||||
usedBytes: 2048,
|
||||
reason: "squashfs",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "iso9660 filesystem",
|
||||
fsType: "iso9660",
|
||||
totalBytes: 700 * 1024 * 1024,
|
||||
usedBytes: 700 * 1024 * 1024,
|
||||
reason: "iso9660",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "cdfs filesystem",
|
||||
fsType: "CDFS",
|
||||
totalBytes: 600 * 1024 * 1024,
|
||||
usedBytes: 600 * 1024 * 1024,
|
||||
reason: "cdfs",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "udf optical media",
|
||||
fsType: "udf",
|
||||
totalBytes: 4 * 1024 * 1024 * 1024,
|
||||
usedBytes: 4 * 1024 * 1024 * 1024,
|
||||
reason: "udf",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "cramfs image",
|
||||
fsType: "cramfs",
|
||||
totalBytes: 128 * 1024 * 1024,
|
||||
usedBytes: 128 * 1024 * 1024,
|
||||
reason: "cramfs",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "romfs firmware partition",
|
||||
fsType: "romfs",
|
||||
totalBytes: 16 * 1024 * 1024,
|
||||
usedBytes: 16 * 1024 * 1024,
|
||||
reason: "romfs",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "fuse iso image",
|
||||
fsType: "fuse.cdfs",
|
||||
totalBytes: 700 * 1024 * 1024,
|
||||
usedBytes: 700 * 1024 * 1024,
|
||||
reason: "cdfs",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "overlay below capacity",
|
||||
fsType: "overlay",
|
||||
totalBytes: 100,
|
||||
usedBytes: 50,
|
||||
reason: "",
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
name: "regular ext4",
|
||||
fsType: "ext4",
|
||||
totalBytes: 100,
|
||||
usedBytes: 50,
|
||||
reason: "",
|
||||
skip: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
reason, skip := ReadOnlyFilesystemReason(tc.fsType, tc.totalBytes, tc.usedBytes)
|
||||
if reason != tc.reason || skip != tc.skip {
|
||||
t.Errorf("expected (%q, %t), got (%q, %t)", tc.reason, tc.skip, reason, skip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldIgnoreReadOnlyFilesystem(t *testing.T) {
|
||||
if !ShouldIgnoreReadOnlyFilesystem("erofs", 100, 10) {
|
||||
t.Fatalf("expected erofs to be ignored")
|
||||
}
|
||||
|
||||
if !ShouldIgnoreReadOnlyFilesystem("squashfs", 100, 100) {
|
||||
t.Fatalf("expected squashfs to be ignored")
|
||||
}
|
||||
|
||||
if ShouldIgnoreReadOnlyFilesystem("ext4", 100, 10) {
|
||||
t.Fatalf("expected ext4 to be included")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue