fix(agents): preserve explicit disk includes at ingestion

Carry the operator include override in disk reports so server filtering does not discard selected tmpfs mounts again. Keep automatic filtering for unmarked reports and agent exclusion precedence. Cover both ingest paths, collection, forwarding and wire compatibility for #1875.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-09 11:34:11 +01:00
parent 8dc2d26d94
commit c8cead7d5a
9 changed files with 140 additions and 24 deletions
@@ -15,6 +15,23 @@
## Purpose
### Explicit filesystem selections survive the report boundary
Disk reports carry optional `explicitlyIncluded` evidence when the collector
matches the operator's disk-include setting. Explicit exclusions still win
before usage collection; inclusion does not fabricate capacity when usage fails
or reports zero total. Host and Docker snapshot copies preserve this field.
It changes metric selection only, not enrollment, token scopes, identity or
command authority.
The flag is omitted when false. Older agents therefore retain default server
filtering; older servers ignore the additive field. Preserving selected tmpfs
through ingestion requires both agent and server support, not a server-only
upgrade. `TestDiskExplicitIncludeWireCompatibility` pins omission and round
trip; `TestIssue1875MountinfoToExplicitDiskCollection` pins include/exclude
selection and marker production; `TestBuildReportForwardsExplicitDiskIncludesAndExcludes`
pins report forwarding. These are synthetic proofs, not estate acceptance.
Historical incident reads preserve canonical event targets and treat resource
aliases as read selectors. They do not grant command authority to an alias or
infer present agent liveness from an old alert closure. A failed canonical read
@@ -17,6 +17,24 @@
## Purpose
### Preserve explicit agent filesystem selection at ingestion
Both `ApplyHostReport` and `ApplyDockerReport` honour a disk's optional
`explicitlyIncluded` marker when applying automatic filesystem filtering.
Selected tmpfs mounts must not be discarded a second time after the agent
has admitted them. Unmarked virtual/system filesystems remain filtered and
ordinary physical filesystem handling is unchanged. Selection evidence does
not confer command authority or imply successful observation beyond the
reported metrics.
`TestAgentReportsPreserveExplicitDiskIncludes` in
`internal/monitoring/monitor_host_agents_test.go` exercises JSON-decoded disks
through both ingestion paths: two selected equal-capacity tmpfs mounts survive,
unselected /run is rejected, and root remains. Agent-side exclusions still
precede marker production. Legacy reports without the marker retain the
existing default policy; matched agent and server support is required.
This proves ingestion behaviour, not reporter installation or release delivery.
### TrueNAS persistent-session liveness and successful poll cadence
Authenticated JSON-RPC WebSocket sessions send transport-only PING controls
+6 -2
View File
@@ -491,7 +491,7 @@ func TestBuildReportForwardsExplicitDiskIncludesAndExcludes(t *testing.T) {
metricsWithDiskFiltersFn: func(_ context.Context, exclude, include []string) (hostmetrics.Snapshot, error) {
gotExclude = append([]string(nil), exclude...)
gotInclude = append([]string(nil), include...)
return hostmetrics.Snapshot{}, nil
return hostmetrics.Snapshot{Disks: []agentshost.Disk{{Mountpoint: "/mnt/containers", Type: "tmpfs", TotalBytes: 1024, ExplicitlyIncluded: true}}}, nil
},
}
agent, err := New(Config{
@@ -506,9 +506,13 @@ func TestBuildReportForwardsExplicitDiskIncludesAndExcludes(t *testing.T) {
t.Fatalf("New() failed: %v", err)
}
if _, err := agent.buildReport(context.Background()); err != nil {
report, err := agent.buildReport(context.Background())
if err != nil {
t.Fatalf("buildReport() failed: %v", err)
}
if len(report.Disks) != 1 || !report.Disks[0].ExplicitlyIncluded || report.Disks[0].Mountpoint != "/mnt/containers" {
t.Fatalf("explicit disk lost in report: %+v", report.Disks)
}
if got := strings.Join(gotExclude, ","); got != "/mnt/private" {
t.Fatalf("disk excludes = %q, want /mnt/private", got)
}
+9 -8
View File
@@ -474,14 +474,15 @@ func collectDisksWithIncludes(ctx context.Context, diskExclude, diskInclude []st
}
disks = append(disks, agentshost.Disk{
Device: part.Device,
Mountpoint: part.Mountpoint,
Filesystem: part.Fstype,
Type: part.Fstype,
TotalBytes: int64(usage.Total),
UsedBytes: int64(usage.Used),
FreeBytes: int64(usage.Free),
Usage: usage.UsedPercent,
ExplicitlyIncluded: explicitlyIncluded,
Device: part.Device,
Mountpoint: part.Mountpoint,
Filesystem: part.Fstype,
Type: part.Fstype,
TotalBytes: int64(usage.Total),
UsedBytes: int64(usage.Used),
FreeBytes: int64(usage.Free),
Usage: usage.UsedPercent,
})
}
+1 -1
View File
@@ -405,7 +405,7 @@ func TestIssue1875MountinfoToExplicitDiskCollection(t *testing.T) {
var got []string
for _, disk := range disks {
got = append(got, disk.Mountpoint)
if disk.Filesystem != "tmpfs" || disk.TotalBytes != 1024 || disk.Usage != 75 {
if !disk.ExplicitlyIncluded || disk.Filesystem != "tmpfs" || disk.TotalBytes != 1024 || disk.Usage != 75 {
t.Fatalf("incorrect capacity/type: %+v", disk)
}
}
+5 -4
View File
@@ -2449,8 +2449,8 @@ func (m *Monitor) ApplyDockerReport(report agentsdocker.Report, tokenRecord *con
disks := make([]models.Disk, 0, len(report.Host.Disks))
for _, disk := range report.Host.Disks {
// Filter virtual/system filesystems (same as ApplyHostReport) to avoid
// inflated disk totals from tmpfs, overlayfs, etc.
if shouldSkip, _ := fsfilters.ShouldSkipFilesystem(disk.Type, disk.Mountpoint, uint64(disk.TotalBytes), uint64(disk.UsedBytes)); shouldSkip {
// inflated disk totals from tmpfs, overlayfs, etc., unless explicitly selected.
if shouldSkip, _ := fsfilters.ShouldSkipFilesystem(disk.Type, disk.Mountpoint, uint64(disk.TotalBytes), uint64(disk.UsedBytes)); shouldSkip && !disk.ExplicitlyIncluded {
continue
}
disks = append(disks, models.Disk{
@@ -3287,8 +3287,9 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
for _, disk := range report.Disks {
// Filter virtual/system filesystems and read-only filesystems to avoid cluttering
// the UI with tmpfs, devtmpfs, /dev, /run, /sys, docker overlay mounts, snap mounts,
// immutable OS images, etc. (issues #505, #690, #790).
if shouldSkip, _ := fsfilters.ShouldSkipFilesystem(disk.Type, disk.Mountpoint, uint64(disk.TotalBytes), uint64(disk.UsedBytes)); shouldSkip {
// immutable OS images, etc. (issues #505, #690, #790). Preserve the
// agent operator's explicit include override rather than filtering it again.
if shouldSkip, _ := fsfilters.ShouldSkipFilesystem(disk.Type, disk.Mountpoint, uint64(disk.TotalBytes), uint64(disk.UsedBytes)); shouldSkip && !disk.ExplicitlyIncluded {
continue
}
@@ -5836,3 +5836,51 @@ func TestApplyHostReportBridgeIdentity(t *testing.T) {
})
}
}
// Explicit includes must survive the wire and both server report paths.
func TestAgentReportsPreserveExplicitDiskIncludes(t *testing.T) {
var disks []agentshost.Disk
if err := json.Unmarshal([]byte(`[
{"device":"log2ram","mountpoint":"/var/log","type":"tmpfs","totalBytes":1024,"usedBytes":768,"explicitlyIncluded":true},
{"device":"tmpfs","mountpoint":"/mnt/ramdisk/plex-transcode","type":"tmpfs","totalBytes":1024,"usedBytes":256,"explicitlyIncluded":true},
{"device":"tmpfs","mountpoint":"/run","type":"tmpfs","totalBytes":1024},
{"device":"/dev/sda","mountpoint":"/","type":"ext4","totalBytes":4096,"usedBytes":1024}
]`), &disks); err != nil {
t.Fatal(err)
}
check := func(t *testing.T, got []models.Disk) {
t.Helper()
if len(got) != 3 {
t.Fatalf("accepted disks = %+v, want both selected tmpfs and root only", got)
}
for i, want := range []string{"/var/log", "/mnt/ramdisk/plex-transcode", "/"} {
if got[i].Mountpoint != want || got[i].Total != []int64{1024, 1024, 4096}[i] {
t.Fatalf("disk %d = %+v", i, got[i])
}
}
}
t.Run("host", func(t *testing.T) {
m := newTestMonitor(t)
host, err := m.ApplyHostReport(agentshost.Report{
Agent: agentshost.AgentInfo{ID: "include-host", IntervalSeconds: 30},
Host: agentshost.HostInfo{ID: "include-machine", Hostname: "include-host"},
Disks: disks, Timestamp: time.Now().UTC(),
}, nil)
if err != nil {
t.Fatal(err)
}
check(t, host.Disks)
})
t.Run("docker", func(t *testing.T) {
m := newTestMonitor(t)
host, err := m.ApplyDockerReport(agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "include-docker", IntervalSeconds: 30},
Host: agentsdocker.HostInfo{MachineID: "include-docker-machine", Hostname: "include-docker", Disks: disks},
Timestamp: time.Now().UTC(),
}, nil)
if err != nil {
t.Fatal(err)
}
check(t, host.Disks)
})
}
+12 -9
View File
@@ -311,15 +311,18 @@ type MemoryMetric struct {
// Disk represents disk utilisation metrics.
type Disk struct {
Device string `json:"device,omitempty"`
Mountpoint string `json:"mountpoint,omitempty"`
Filesystem string `json:"filesystem,omitempty"`
Type string `json:"type,omitempty"`
TotalBytes int64 `json:"totalBytes,omitempty"`
UsedBytes int64 `json:"usedBytes,omitempty"`
FreeBytes int64 `json:"freeBytes,omitempty"`
Usage float64 `json:"usage,omitempty"`
ZFSDatasets []ZFSDataset `json:"zfsDatasets,omitempty"`
// ExplicitlyIncluded carries the operator disk-include override through server filtering.
// Absent on older agents, which retain the default filesystem policy.
ExplicitlyIncluded bool `json:"explicitlyIncluded,omitempty"`
Device string `json:"device,omitempty"`
Mountpoint string `json:"mountpoint,omitempty"`
Filesystem string `json:"filesystem,omitempty"`
Type string `json:"type,omitempty"`
TotalBytes int64 `json:"totalBytes,omitempty"`
UsedBytes int64 `json:"usedBytes,omitempty"`
FreeBytes int64 `json:"freeBytes,omitempty"`
Usage float64 `json:"usage,omitempty"`
ZFSDatasets []ZFSDataset `json:"zfsDatasets,omitempty"`
}
// ZFSDataset is a bounded, read-only projection of `zfs list` for one pool.
+24
View File
@@ -850,3 +850,27 @@ func TestReportProxmoxLXCInventoryJSONRoundTrip(t *testing.T) {
t.Fatalf("nil Proxmox LXC inventory should be omitted: %s", bare)
}
}
func TestDiskExplicitIncludeWireCompatibility(t *testing.T) {
for _, included := range []bool{false, true} {
disk := Disk{Mountpoint: "/var/log", Type: "tmpfs", TotalBytes: 1024, ExplicitlyIncluded: included}
data, err := json.Marshal(disk)
if err != nil {
t.Fatal(err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
t.Fatal(err)
}
if _, present := fields["explicitlyIncluded"]; present != included {
t.Fatalf("include field presence = %v, want %v: %s", present, included, data)
}
var decoded Disk
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatal(err)
}
if decoded.ExplicitlyIncluded != included || decoded.Mountpoint != disk.Mountpoint || decoded.TotalBytes != disk.TotalBytes || decoded.Type != disk.Type {
t.Fatalf("round trip = %+v, want %+v", decoded, disk)
}
}
}