From 8b3adcafe6dfe460beb231bbeac2fc478a69830b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C4=8Ctvrtka?= Date: Thu, 27 Aug 2026 12:32:40 +0200 Subject: [PATCH 1/7] PMM-15362 Disable default-enabled node_exporter collectors explicitly. --- managed/services/agents/node.go | 69 ++++++++++++++++++++++++++++ managed/services/agents/node_test.go | 52 +++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/managed/services/agents/node.go b/managed/services/agents/node.go index afee9b6a71b..6df49ef3efa 100644 --- a/managed/services/agents/node.go +++ b/managed/services/agents/node.go @@ -16,6 +16,7 @@ package agents import ( + "slices" "sort" agentv1 "github.com/percona/pmm/api/agent/v1" @@ -32,6 +33,58 @@ var ( v2_28_00 = version.MustParse("2.28.0-0") ) +// defaultEnabledNodeExporterCollectors lists node_exporter collectors that are enabled by default on Linux. +// Dropping the "--collector." flag does not stop them, so "--no-collector." has to be passed +// explicitly for the disabled ones. +var defaultEnabledNodeExporterCollectors = []string{ + "arp", + "bcache", + "bonding", + "btrfs", + "conntrack", + "cpu", + "cpufreq", + "diskstats", + "dmi", + "edac", + "entropy", + "fibrechannel", + "filefd", + "filesystem", + "hwmon", + "infiniband", + "ipvs", + "loadavg", + "mdadm", + "meminfo", + "netclass", + "netdev", + "netstat", + "nfs", + "nfsd", + "nvme", + "os", + "powersupplyclass", + "pressure", + "rapl", + "schedstat", + "selinux", + "sockstat", + "softnet", + "stat", + "tapestats", + "textfile", + "thermal_zone", + "time", + "timex", + "udp_queues", + "uname", + "vmstat", + "watchdog", + "xfs", + "zfs", +} + func nodeExporterConfig(node *models.Node, exporter *models.Agent, agentVersion *version.Parsed) (*agentv1.SetStateRequest_AgentProcess, error) { listenAddress := getExporterListenAddress(node, exporter) tdp := models.TemplateDelimsPair(exporter.ExporterOptions.MetricsPath) @@ -124,6 +177,22 @@ func nodeExporterConfig(node *models.Node, exporter *models.Agent, agentVersion args = collectors.FilterOutCollectors("--collector.", args, exporter.ExporterOptions.DisabledCollectors) + // Collectors enabled by node_exporter itself keep running after their "--collector." flag is + // filtered out above, so disable them explicitly. Collectors are not tweaked on macOS, where the + // default enabled ones differ from Linux. + if node.Distro != "darwin" { + disableArgs := collectors.DisableDefaultEnabledCollectors( + "--no-collector.", + defaultEnabledNodeExporterCollectors, + exporter.ExporterOptions.DisabledCollectors, + ) + for _, arg := range disableArgs { + if !slices.Contains(args, arg) { // some collectors are already disabled above + args = append(args, arg) + } + } + } + if exporter.ExporterOptions.MetricsPath != "" { args = append(args, "--web.telemetry-path="+exporter.ExporterOptions.MetricsPath) } diff --git a/managed/services/agents/node_test.go b/managed/services/agents/node_test.go index 0998ac12f76..d0f6a4fb737 100644 --- a/managed/services/agents/node_test.go +++ b/managed/services/agents/node_test.go @@ -255,6 +255,7 @@ func TestNodeExporterConfig(t *testing.T) { "--no-collector.arp", "--no-collector.bcache", "--no-collector.conntrack", + "--no-collector.cpu", "--no-collector.drbd", "--no-collector.edac", "--no-collector.infiniband", @@ -263,8 +264,10 @@ func TestNodeExporterConfig(t *testing.T) { "--no-collector.ksmd", "--no-collector.logind", "--no-collector.mdadm", + "--no-collector.meminfo", "--no-collector.mountstats", "--no-collector.netclass", + "--no-collector.netstat", "--no-collector.nfs", "--no-collector.nfsd", "--no-collector.ntp", @@ -275,6 +278,7 @@ func TestNodeExporterConfig(t *testing.T) { "--no-collector.systemd", "--no-collector.tcpstat", "--no-collector.timex", + "--no-collector.vmstat", "--no-collector.wifi", "--no-collector.xfs", "--no-collector.zfs", @@ -291,6 +295,54 @@ func TestNodeExporterConfig(t *testing.T) { require.Equal(t, expected, actual) }) + t.Run("LinuxDisabledDefaultEnabledCollectors", func(t *testing.T) { + t.Parallel() + node := &models.Node{} + exporter := &models.Agent{ + AgentID: "agent-id", + AgentType: models.NodeExporterType, + ExporterOptions: models.ExporterOptions{ + DisabledCollectors: []string{"arp", "dmi", "processes"}, + }, + } + agentVersion := version.MustParse("2.15.1") + + actual, err := nodeExporterConfig(node, exporter, agentVersion) + require.NoError(t, err, "Unable to build node exporter config") + + requireNoDuplicateFlags(t, actual.Args) + // enabled by default by node_exporter, so it has to be disabled explicitly + require.Contains(t, actual.Args, "--no-collector.dmi") + // already disabled by us, so it is not passed twice + require.Contains(t, actual.Args, "--no-collector.arp") + // disabled by default by node_exporter, so dropping the flag is enough + require.NotContains(t, actual.Args, "--collector.processes") + require.NotContains(t, actual.Args, "--no-collector.processes") + }) + + t.Run("MacOSDisabledCollectors", func(t *testing.T) { + t.Parallel() + node := &models.Node{ + Distro: "darwin", + } + exporter := &models.Agent{ + AgentID: "agent-id", + AgentType: models.NodeExporterType, + ExporterOptions: models.ExporterOptions{ + DisabledCollectors: []string{"cpu", "diskstats"}, + }, + } + agentVersion := version.MustParse("2.15.1") + + actual, err := nodeExporterConfig(node, exporter, agentVersion) + require.NoError(t, err, "Unable to build node exporter config") + + // collectors are not tweaked on macOS, where the default enabled ones differ from Linux + for _, arg := range actual.Args { + require.NotContains(t, arg, "--no-collector.") + } + }) + t.Run("MacOS", func(t *testing.T) { t.Parallel() node := &models.Node{ From 9531dbed781a0d3dae35ea495a534317ba8a9f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C4=8Ctvrtka?= Date: Thu, 27 Aug 2026 12:49:37 +0200 Subject: [PATCH 2/7] PMM-15362 Improvements. --- managed/services/agents/node.go | 14 ++-- managed/services/agents/node_test.go | 101 ++++++++++++++++++++++----- version/features.go | 4 ++ 3 files changed, 95 insertions(+), 24 deletions(-) diff --git a/managed/services/agents/node.go b/managed/services/agents/node.go index 6df49ef3efa..9e33a0123e5 100644 --- a/managed/services/agents/node.go +++ b/managed/services/agents/node.go @@ -33,9 +33,10 @@ var ( v2_28_00 = version.MustParse("2.28.0-0") ) -// defaultEnabledNodeExporterCollectors lists node_exporter collectors that are enabled by default on Linux. -// Dropping the "--collector." flag does not stop them, so "--no-collector." has to be passed -// explicitly for the disabled ones. +// defaultEnabledNodeExporterCollectors lists collectors that node_exporter enables on Linux on its own, +// as of node_exporter 1.8.2. Dropping the "--collector." flag does not stop those, so disabling one +// means passing "--no-collector." explicitly. 14 of them are in the "disabled" block below already, +// which is why they are appended only when missing. var defaultEnabledNodeExporterCollectors = []string{ "arp", "bcache", @@ -177,10 +178,9 @@ func nodeExporterConfig(node *models.Node, exporter *models.Agent, agentVersion args = collectors.FilterOutCollectors("--collector.", args, exporter.ExporterOptions.DisabledCollectors) - // Collectors enabled by node_exporter itself keep running after their "--collector." flag is - // filtered out above, so disable them explicitly. Collectors are not tweaked on macOS, where the - // default enabled ones differ from Linux. - if node.Distro != "darwin" { + // Collectors are not tweaked on macOS, where node_exporter enables a different set by default. + // Older pmm-agents ship node_exporter builds that do not know all of the flags below and would exit. + if node.Distro != "darwin" && agentVersion.IsFeatureSupported(version.NodeExporterV1_8) { disableArgs := collectors.DisableDefaultEnabledCollectors( "--no-collector.", defaultEnabledNodeExporterCollectors, diff --git a/managed/services/agents/node_test.go b/managed/services/agents/node_test.go index d0f6a4fb737..ea0da881619 100644 --- a/managed/services/agents/node_test.go +++ b/managed/services/agents/node_test.go @@ -206,6 +206,8 @@ func TestNodeExporterConfig(t *testing.T) { require.Equal(t, expected, actual) }) + // pmm-agent 2.x ships a node_exporter that does not know all "--no-collector." flags, + // so disabled collectors only get their enable flag dropped there t.Run("LinuxDisabledCollectors", func(t *testing.T) { t.Parallel() node := &models.Node{} @@ -255,7 +257,6 @@ func TestNodeExporterConfig(t *testing.T) { "--no-collector.arp", "--no-collector.bcache", "--no-collector.conntrack", - "--no-collector.cpu", "--no-collector.drbd", "--no-collector.edac", "--no-collector.infiniband", @@ -264,10 +265,8 @@ func TestNodeExporterConfig(t *testing.T) { "--no-collector.ksmd", "--no-collector.logind", "--no-collector.mdadm", - "--no-collector.meminfo", "--no-collector.mountstats", "--no-collector.netclass", - "--no-collector.netstat", "--no-collector.nfs", "--no-collector.nfsd", "--no-collector.ntp", @@ -278,7 +277,6 @@ func TestNodeExporterConfig(t *testing.T) { "--no-collector.systemd", "--no-collector.tcpstat", "--no-collector.timex", - "--no-collector.vmstat", "--no-collector.wifi", "--no-collector.xfs", "--no-collector.zfs", @@ -295,6 +293,9 @@ func TestNodeExporterConfig(t *testing.T) { require.Equal(t, expected, actual) }) + // Disabling a collector that node_exporter enables on its own takes "--no-collector.", + // dropping "--collector." is not enough. pmm-agent 3.x is the oldest one shipping a + // node_exporter that knows all of those flags. t.Run("LinuxDisabledDefaultEnabledCollectors", func(t *testing.T) { t.Parallel() node := &models.Node{} @@ -302,22 +303,81 @@ func TestNodeExporterConfig(t *testing.T) { AgentID: "agent-id", AgentType: models.NodeExporterType, ExporterOptions: models.ExporterOptions{ - DisabledCollectors: []string{"arp", "dmi", "processes"}, + // arp is disabled by us already, dmi is not passed by us at all, + // netstat.fields is a flag of the netstat collector, not a collector + DisabledCollectors: []string{"cpu", "netstat", "netstat.fields", "vmstat", "meminfo", "arp", "dmi"}, }, } - agentVersion := version.MustParse("2.15.1") + agentVersion := version.MustParse("3.0.0") actual, err := nodeExporterConfig(node, exporter, agentVersion) require.NoError(t, err, "Unable to build node exporter config") + expected := []string{ + "--collector.bonding", + "--collector.buddyinfo", + "--collector.diskstats", + "--collector.entropy", + "--collector.filefd", + "--collector.filesystem", + "--collector.hwmon", + "--collector.loadavg", + "--collector.meminfo_numa", + "--collector.netdev", + "--collector.processes", + "--collector.standard.go", + "--collector.standard.process", + "--collector.stat", + "--collector.textfile.directory.hr=" + pathsBase(agentVersion, "{{", "}}") + "/collectors/textfile-collector/high-resolution", + "--collector.textfile.directory.lr=" + pathsBase(agentVersion, "{{", "}}") + "/collectors/textfile-collector/low-resolution", + "--collector.textfile.directory.mr=" + pathsBase(agentVersion, "{{", "}}") + "/collectors/textfile-collector/medium-resolution", + "--collector.textfile.hr", + "--collector.textfile.lr", + "--collector.textfile.mr", + "--collector.time", + "--collector.uname", + "--collector.vmstat.fields=^(pg(steal_(kswapd|direct)|refill|alloc)_(movable|normal|dma3?2?)" + + "|nr_(dirty.*|slab.*|vmscan.*|isolated.*|free.*|shmem.*|i?n?active.*|anon_transparent_.*|writeback.*|unstable" + + "|unevictable|mlock|mapped|bounce|page_table_pages|kernel_stack)|drop_slab|slabs_scanned|pgd?e?activate" + + "|pgpg(in|out)|pswp(in|out)|pgm?a?j?fault)$", + "--no-collector.arp", + "--no-collector.bcache", + "--no-collector.conntrack", + "--no-collector.cpu", + "--no-collector.dmi", + "--no-collector.drbd", + "--no-collector.edac", + "--no-collector.infiniband", + "--no-collector.interrupts", + "--no-collector.ipvs", + "--no-collector.ksmd", + "--no-collector.logind", + "--no-collector.mdadm", + "--no-collector.meminfo", + "--no-collector.mountstats", + "--no-collector.netclass", + "--no-collector.netstat", + "--no-collector.nfs", + "--no-collector.nfsd", + "--no-collector.ntp", + "--no-collector.qdisc", + "--no-collector.runit", + "--no-collector.sockstat", + "--no-collector.supervisord", + "--no-collector.systemd", + "--no-collector.tcpstat", + "--no-collector.timex", + "--no-collector.vmstat", + "--no-collector.wifi", + "--no-collector.xfs", + "--no-collector.zfs", + "--web.disable-exporter-metrics", + "--web.listen-address=0.0.0.0:{{ .listen_port }}", + "--web.config.file={{ .TextFiles.webConfig }}", + } + requireNoDuplicateFlags(t, actual.Args) - // enabled by default by node_exporter, so it has to be disabled explicitly - require.Contains(t, actual.Args, "--no-collector.dmi") - // already disabled by us, so it is not passed twice - require.Contains(t, actual.Args, "--no-collector.arp") - // disabled by default by node_exporter, so dropping the flag is enough - require.NotContains(t, actual.Args, "--collector.processes") - require.NotContains(t, actual.Args, "--no-collector.processes") + require.Equal(t, expected, actual.Args) }) t.Run("MacOSDisabledCollectors", func(t *testing.T) { @@ -332,15 +392,22 @@ func TestNodeExporterConfig(t *testing.T) { DisabledCollectors: []string{"cpu", "diskstats"}, }, } - agentVersion := version.MustParse("2.15.1") + agentVersion := version.MustParse("3.0.0") actual, err := nodeExporterConfig(node, exporter, agentVersion) require.NoError(t, err, "Unable to build node exporter config") - // collectors are not tweaked on macOS, where the default enabled ones differ from Linux - for _, arg := range actual.Args { - require.NotContains(t, arg, "--no-collector.") + // collectors are not tweaked on macOS, where node_exporter enables a different set by default + expected := []string{ + "--collector.textfile.directory.hr=" + pathsBase(agentVersion, "{{", "}}") + "/collectors/textfile-collector/high-resolution", + "--collector.textfile.directory.lr=" + pathsBase(agentVersion, "{{", "}}") + "/collectors/textfile-collector/low-resolution", + "--collector.textfile.directory.mr=" + pathsBase(agentVersion, "{{", "}}") + "/collectors/textfile-collector/medium-resolution", + "--web.disable-exporter-metrics", + "--web.listen-address=0.0.0.0:{{ .listen_port }}", + "--web.config.file={{ .TextFiles.webConfig }}", } + + require.Equal(t, expected, actual.Args) }) t.Run("MacOS", func(t *testing.T) { diff --git a/version/features.go b/version/features.go index d3c256fa49e..7036997d81d 100644 --- a/version/features.go +++ b/version/features.go @@ -25,6 +25,10 @@ var ( MysqlExporterPluginCollector FeatureVersion = MustParse("2.36.0-0") NomadAgentSupportVersion FeatureVersion = MustParse("3.2.0-0") MongoDBRtaAgentSupportVersion FeatureVersion = MustParse("3.7.0-0") + // NodeExporterV1_8 is the first pmm-agent shipping node_exporter 1.8, the oldest build that knows + // every collector we may have to disable explicitly. pmm-agent 2.x ships 1.4.0, where flags such as + // "--no-collector.watchdog" do not exist and would make the exporter exit. + NodeExporterV1_8 FeatureVersion = MustParse("3.0.0-0") ) // IsFeatureSupported checks if the feature is supported by the version. From ff7f7e6ea312db64fefb3c505794a06ad3319cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C4=8Ctvrtka?= Date: Thu, 27 Aug 2026 13:58:08 +0200 Subject: [PATCH 3/7] PMM-15362 Lint. --- version/features.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version/features.go b/version/features.go index 7036997d81d..4dd7aca3094 100644 --- a/version/features.go +++ b/version/features.go @@ -26,8 +26,8 @@ var ( NomadAgentSupportVersion FeatureVersion = MustParse("3.2.0-0") MongoDBRtaAgentSupportVersion FeatureVersion = MustParse("3.7.0-0") // NodeExporterV1_8 is the first pmm-agent shipping node_exporter 1.8, the oldest build that knows - // every collector we may have to disable explicitly. pmm-agent 2.x ships 1.4.0, where flags such as - // "--no-collector.watchdog" do not exist and would make the exporter exit. + // every collector we may have to disable explicitly. In pmm-agent 2.x, which ships 1.4.0, flags + // such as "--no-collector.watchdog" do not exist and would make the exporter exit. NodeExporterV1_8 FeatureVersion = MustParse("3.0.0-0") ) From ef085ba0a09f680a87d4533b9b9e11e021dc1fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C4=8Ctvrtka?= Date: Thu, 27 Aug 2026 14:29:34 +0200 Subject: [PATCH 4/7] PMM-15362 Improvements. --- managed/services/agents/node.go | 10 ++- managed/services/agents/node_test.go | 96 ++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/managed/services/agents/node.go b/managed/services/agents/node.go index 9e33a0123e5..aaae679cbaa 100644 --- a/managed/services/agents/node.go +++ b/managed/services/agents/node.go @@ -37,6 +37,14 @@ var ( // as of node_exporter 1.8.2. Dropping the "--collector." flag does not stop those, so disabling one // means passing "--no-collector." explicitly. 14 of them are in the "disabled" block below already, // which is why they are appended only when missing. +// +// Entries are exact node_exporter collector names, the same way DisabledCollectors is matched everywhere +// else, so a name here never stands for a family of collectors. In particular "textfile" is the upstream +// base collector alone: the textfile metrics PMM actually collects come from the separate, default-off +// "textfile.hr"/"textfile.mr"/"textfile.lr" collectors, and silencing those means listing them by name so +// that FilterOutCollectors drops their "--collector." flag. Disabling a collector must stay in sync with +// scrapeConfigsForNodeExporter, which filters the same names out of "collect[]" - naming a disabled +// collector there makes node_exporter answer the whole resolution endpoint with HTTP 400. var defaultEnabledNodeExporterCollectors = []string{ "arp", "bcache", @@ -74,7 +82,7 @@ var defaultEnabledNodeExporterCollectors = []string{ "softnet", "stat", "tapestats", - "textfile", + "textfile", // the upstream base collector only, not PMM's textfile.hr/textfile.mr/textfile.lr "thermal_zone", "time", "timex", diff --git a/managed/services/agents/node_test.go b/managed/services/agents/node_test.go index ea0da881619..47cde2b2270 100644 --- a/managed/services/agents/node_test.go +++ b/managed/services/agents/node_test.go @@ -380,6 +380,102 @@ func TestNodeExporterConfig(t *testing.T) { require.Equal(t, expected, actual.Args) }) + // "textfile" is the upstream base collector, not an umbrella over PMM's textfile.hr/mr/lr ones. + // Those three are default-off, so dropping their "--collector." flag is what disables them, and they + // have to be named individually - both here and in the "collect[]" filter built by + // scrapeConfigsForNodeExporter, which must never name a collector we disabled. + t.Run("LinuxDisabledTextfileCollectors", func(t *testing.T) { + t.Parallel() + node := &models.Node{} + exporter := &models.Agent{ + AgentID: "agent-id", + AgentType: models.NodeExporterType, + ExporterOptions: models.ExporterOptions{ + DisabledCollectors: []string{"textfile", "textfile.hr"}, + }, + } + agentVersion := version.MustParse("3.0.0") + + actual, err := nodeExporterConfig(node, exporter, agentVersion) + require.NoError(t, err, "Unable to build node exporter config") + + expected := []string{ + "--collector.bonding", + "--collector.buddyinfo", + "--collector.cpu", + "--collector.diskstats", + "--collector.entropy", + "--collector.filefd", + "--collector.filesystem", + "--collector.hwmon", + "--collector.loadavg", + "--collector.meminfo", + "--collector.meminfo_numa", + "--collector.netdev", + "--collector.netstat", + "--collector.netstat.fields=^(.*_(InErrors|InErrs|InCsumErrors)" + + "|Tcp_(ActiveOpens|PassiveOpens|RetransSegs|CurrEstab|AttemptFails|OutSegs|InSegs|EstabResets|OutRsts|OutSegs)|Tcp_Rto(Algorithm|Min|Max)" + + "|Udp_(RcvbufErrors|SndbufErrors)|Udp(6?|Lite6?)_(InDatagrams|OutDatagrams|RcvbufErrors|SndbufErrors|NoPorts)" + + "|Icmp6?_(OutEchoReps|OutEchos|InEchos|InEchoReps|InAddrMaskReps|InAddrMasks|OutAddrMaskReps|OutAddrMasks|InTimestampReps|InTimestamps" + + "|OutTimestampReps|OutTimestamps|OutErrors|InDestUnreachs|OutDestUnreachs|InTimeExcds|InRedirects|OutRedirects|InMsgs|OutMsgs)" + + "|IcmpMsg_(InType3|OutType3)|Ip(6|Ext)_(InOctets|OutOctets)|Ip_Forwarding|TcpExt_(Listen.*|Syncookies.*|TCPTimeouts))$", + "--collector.processes", + "--collector.standard.go", + "--collector.standard.process", + "--collector.stat", + // the directory flags are inert once their collector is off, so they are left alone + "--collector.textfile.directory.hr=" + pathsBase(agentVersion, "{{", "}}") + "/collectors/textfile-collector/high-resolution", + "--collector.textfile.directory.lr=" + pathsBase(agentVersion, "{{", "}}") + "/collectors/textfile-collector/low-resolution", + "--collector.textfile.directory.mr=" + pathsBase(agentVersion, "{{", "}}") + "/collectors/textfile-collector/medium-resolution", + // "--collector.textfile.hr" is gone, while mr and lr keep collecting + "--collector.textfile.lr", + "--collector.textfile.mr", + "--collector.time", + "--collector.uname", + "--collector.vmstat", + "--collector.vmstat.fields=^(pg(steal_(kswapd|direct)|refill|alloc)_(movable|normal|dma3?2?)" + + "|nr_(dirty.*|slab.*|vmscan.*|isolated.*|free.*|shmem.*|i?n?active.*|anon_transparent_.*|writeback.*|unstable" + + "|unevictable|mlock|mapped|bounce|page_table_pages|kernel_stack)|drop_slab|slabs_scanned|pgd?e?activate" + + "|pgpg(in|out)|pswp(in|out)|pgm?a?j?fault)$", + "--no-collector.arp", + "--no-collector.bcache", + "--no-collector.conntrack", + "--no-collector.drbd", + "--no-collector.edac", + "--no-collector.infiniband", + "--no-collector.interrupts", + "--no-collector.ipvs", + "--no-collector.ksmd", + "--no-collector.logind", + "--no-collector.mdadm", + "--no-collector.mountstats", + "--no-collector.netclass", + "--no-collector.nfs", + "--no-collector.nfsd", + "--no-collector.ntp", + "--no-collector.qdisc", + "--no-collector.runit", + "--no-collector.sockstat", + "--no-collector.supervisord", + "--no-collector.systemd", + "--no-collector.tcpstat", + // the base collector is default-on, so it takes an explicit "--no-" flag ... + "--no-collector.textfile", + "--no-collector.timex", + "--no-collector.wifi", + "--no-collector.xfs", + "--no-collector.zfs", + "--web.disable-exporter-metrics", + "--web.listen-address=0.0.0.0:{{ .listen_port }}", + "--web.config.file={{ .TextFiles.webConfig }}", + } + + requireNoDuplicateFlags(t, actual.Args) + require.Equal(t, expected, actual.Args) + // ... while textfile.hr is default-off, so it must not get one + require.NotContains(t, actual.Args, "--no-collector.textfile.hr") + }) + t.Run("MacOSDisabledCollectors", func(t *testing.T) { t.Parallel() node := &models.Node{ From 5e317fbcfc4cfe2c76783bde2fbd7921cedb376c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C4=8Ctvrtka?= Date: Thu, 27 Aug 2026 14:57:14 +0200 Subject: [PATCH 5/7] PMM-15362 Move inline comments to their own lines. --- managed/services/agents/node.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/managed/services/agents/node.go b/managed/services/agents/node.go index aaae679cbaa..89b4b5e14b9 100644 --- a/managed/services/agents/node.go +++ b/managed/services/agents/node.go @@ -82,7 +82,8 @@ var defaultEnabledNodeExporterCollectors = []string{ "softnet", "stat", "tapestats", - "textfile", // the upstream base collector only, not PMM's textfile.hr/textfile.mr/textfile.lr + // the upstream base collector only, not PMM's textfile.hr/textfile.mr/textfile.lr + "textfile", "thermal_zone", "time", "timex", @@ -195,7 +196,8 @@ func nodeExporterConfig(node *models.Node, exporter *models.Agent, agentVersion exporter.ExporterOptions.DisabledCollectors, ) for _, arg := range disableArgs { - if !slices.Contains(args, arg) { // some collectors are already disabled above + // some collectors are already disabled above + if !slices.Contains(args, arg) { args = append(args, arg) } } From 2c752ea8fa91d5fd44979e23ab3d236ee258f701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C4=8Ctvrtka?= Date: Mon, 31 Aug 2026 12:59:33 +0200 Subject: [PATCH 6/7] PMM-15362 Force VM config update when node_exporter collectors change. --- managed/services/inventory/agents.go | 11 +++++++++++ managed/services/inventory/agents_test.go | 2 ++ 2 files changed, 13 insertions(+) diff --git a/managed/services/inventory/agents.go b/managed/services/inventory/agents.go index 69c8b9acefe..b47411e9d6f 100644 --- a/managed/services/inventory/agents.go +++ b/managed/services/inventory/agents.go @@ -18,6 +18,7 @@ package inventory import ( "context" + "fmt" "os" "strings" @@ -243,6 +244,16 @@ func (as *AgentsService) ChangeNodeExporter(ctx context.Context, agentID string, if !ok { return nil, unexpectedAgentTypeError(agent) } + + // Changing the disabled collectors changes the set of collectors node_exporter accepts in + // collect[] parameters, and it rejects the whole scrape with 400 for an unknown one. Force an + // immediate synchronous config update so that VictoriaMetrics never scrapes the restarted + // exporter with the stale collector list, the same way port changes are handled in + // agents.Handler.stateChanged (PMM-14267). + if err := as.vmdb.ForceConfigurationUpdate(ctx); err != nil { + return nil, fmt.Errorf("failed to force configuration update: %w", err) + } + as.state.RequestStateUpdate(ctx, nodeExporter.PmmAgentId) res := &inventoryv1.ChangeAgentResponse{ diff --git a/managed/services/inventory/agents_test.go b/managed/services/inventory/agents_test.go index 9dd8430d355..61517ab7639 100644 --- a/managed/services/inventory/agents_test.go +++ b/managed/services/inventory/agents_test.go @@ -106,6 +106,8 @@ func TestAgents(t *testing.T) { }) t.Run("ChangeNodeExporterAndRemoveCustomLabels", func(t *testing.T) { + as.vmdb.(*mockPrometheusService).On("ForceConfigurationUpdate", ctx).Return(nil) + actualNodeExporter, err := as.ChangeNodeExporter( ctx, "00000000-0000-4000-8000-000000000006", From 4cb1355581b05350a904e2c58aa7289599e39d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C4=8Ctvrtka?= Date: Mon, 31 Aug 2026 14:33:25 +0200 Subject: [PATCH 7/7] PMM-15362 Lint. --- managed/services/inventory/agents.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/managed/services/inventory/agents.go b/managed/services/inventory/agents.go index b47411e9d6f..1e28b19ede2 100644 --- a/managed/services/inventory/agents.go +++ b/managed/services/inventory/agents.go @@ -250,7 +250,8 @@ func (as *AgentsService) ChangeNodeExporter(ctx context.Context, agentID string, // immediate synchronous config update so that VictoriaMetrics never scrapes the restarted // exporter with the stale collector list, the same way port changes are handled in // agents.Handler.stateChanged (PMM-14267). - if err := as.vmdb.ForceConfigurationUpdate(ctx); err != nil { + err = as.vmdb.ForceConfigurationUpdate(ctx) + if err != nil { return nil, fmt.Errorf("failed to force configuration update: %w", err) }