feat(go.d/snmp): add `ping_only` option (#22180)
Ilya Mashchenko committed
Apr 10, 2026 at 15:13 UTC
ca55ac041958378b8fe50a04fa82d77176681b70
8 files changed
+141
-8
src/go/plugin/go.d/collector/snmp/collect.go
+13
-5
@@ -30,15 +30,23 @@ func (c *Collector) collect() (map[string]int64, error) {
30
return nil, err
31
}
32
33
- mx, err := c.collectMetrics()
34
- if err != nil {
33
+ if c.PingOnly {
34
+ return c.collectPingOnly()
35
+ }
36
+ return c.collectDeviceMetrics()
37
+}
38
+
39
+func (c *Collector) collectPingOnly() (map[string]int64, error) {
40
+ mx := make(map[string]int64)
41
+
42
+ if err := c.collectPing(mx); err != nil {
43
return nil, err
44
}
45
46
return mx, nil
47
}
48
41
-func (c *Collector) collectMetrics() (map[string]int64, error) {
49
+func (c *Collector) collectDeviceMetrics() (map[string]int64, error) {
50
var (
51
snmpMx map[string]int64
52
pingMx map[string]int64
@@ -112,7 +120,7 @@ func (c *Collector) ensureInitialized() error {
120
})
121
}
122
115
- if c.ddSnmpColl == nil && !c.Ping.Enabled {
123
+ if c.ddSnmpColl == nil && !c.PingOnly && !c.Ping.Enabled {
124
return errors.New("no profiles found and ping disabled")
125
}
126
@@ -130,7 +138,7 @@ func (c *Collector) ensureInitialized() error {
138
139
c.sysInfo = si
140
133
- if c.Ping.Enabled {
141
+ if c.PingOnly || c.Ping.Enabled {
142
c.addPingCharts()
143
}
144
src/go/plugin/go.d/collector/snmp/collector.go
+7
-1
@@ -135,7 +135,7 @@ func (c *Collector) Init(context.Context) error {
135
return fmt.Errorf("failed to initialize SNMP client: %v", err)
136
}
137
138
- if c.Ping.Enabled {
138
+ if c.PingOnly || c.Ping.Enabled {
139
pr, err := c.initProber()
140
if err != nil {
141
return fmt.Errorf("failed to initialize ping prober: %v", err)
@@ -159,6 +159,12 @@ func (c *Collector) Check(context.Context) error {
159
return err
160
}
161
162
+ if c.PingOnly && c.prober != nil {
163
+ if _, err := c.prober.Ping(c.Hostname); err != nil && isPingUnrecoverableError(err) {
164
+ return fmt.Errorf("ping check failed: %v", err)
165
+ }
166
+ }
167
+
168
return nil
169
}
170
src/go/plugin/go.d/collector/snmp/collector_test.go
+106
@@ -7,8 +7,14 @@ import (
7
"errors"
8
"os"
9
"strings"
10
+ "syscall"
11
"testing"
12
+ "time"
13
14
+ probing "github.com/prometheus-community/pro-bing"
15
+
16
+ "github.com/netdata/netdata/go/plugins/logger"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/ping"
18
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
19
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
20
"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
@@ -185,6 +191,58 @@ func TestCollector_Check(t *testing.T) {
191
return c
192
},
193
},
194
+
195
+ "success: ping_only with successful ping": {
196
+ wantErr: false,
197
+ prepare: func(m *snmpmock.MockHandler) *Collector {
198
+ setMockClientInitExpect(m)
199
+ setMockClientSysInfoExpect(m)
200
+
201
+ c := New()
202
+ c.Config = prepareV2Config()
203
+ c.PingOnly = true
204
+ c.CreateVnode = false
205
+ c.newSnmpClient = func() gosnmp.Handler { return m }
206
+ c.newProber = func(cfg ping.ProberConfig, log *logger.Logger) ping.Prober { return &mockProber{} }
207
+ return c
208
+ },
209
+ },
210
+
211
+ "success: ping_only with recoverable ping error": {
212
+ wantErr: false,
213
+ prepare: func(m *snmpmock.MockHandler) *Collector {
214
+ setMockClientInitExpect(m)
215
+ setMockClientSysInfoExpect(m)
216
+
217
+ c := New()
218
+ c.Config = prepareV2Config()
219
+ c.PingOnly = true
220
+ c.CreateVnode = false
221
+ c.newSnmpClient = func() gosnmp.Handler { return m }
222
+ c.newProber = func(cfg ping.ProberConfig, log *logger.Logger) ping.Prober {
223
+ return &mockProber{pingErr: errors.New("host unreachable")}
224
+ }
225
+ return c
226
+ },
227
+ },
228
+
229
+ "failure: ping_only with unrecoverable ping error": {
230
+ wantErr: true,
231
+ prepare: func(m *snmpmock.MockHandler) *Collector {
232
+ setMockClientInitExpect(m)
233
+ setMockClientSysInfoExpect(m)
234
+
235
+ c := New()
236
+ c.Config = prepareV2Config()
237
+ c.PingOnly = true
238
+ c.CreateVnode = false
239
+ c.newSnmpClient = func() gosnmp.Handler { return m }
240
+ c.newProber = func(cfg ping.ProberConfig, log *logger.Logger) ping.Prober {
241
+ return &mockProber{pingErr: syscall.EPERM}
242
+ }
243
+ return c
244
+ },
245
+ },
246
}
247
248
for name, tc := range tests {
@@ -326,6 +384,30 @@ func TestCollector_Collect(t *testing.T) {
384
},
385
},
386
}
387
+ tests["collects ping only metrics"] = struct {
388
+ prepare func(m *snmpmock.MockHandler) *Collector
389
+ want map[string]int64
390
+ }{
391
+ prepare: func(m *snmpmock.MockHandler) *Collector {
392
+ setMockClientInitExpect(m)
393
+ setMockClientSysInfoExpect(m)
394
+
395
+ collr := New()
396
+ collr.Config = prepareV2Config()
397
+ collr.PingOnly = true
398
+ collr.CreateVnode = false
399
+ collr.newSnmpClient = func() gosnmp.Handler { return m }
400
+ collr.newProber = func(cfg ping.ProberConfig, log *logger.Logger) ping.Prober { return &mockProber{} }
401
+
402
+ return collr
403
+ },
404
+ want: map[string]int64{
405
+ "ping_rtt_min": (10 * time.Millisecond).Microseconds(),
406
+ "ping_rtt_max": (20 * time.Millisecond).Microseconds(),
407
+ "ping_rtt_avg": (15 * time.Millisecond).Microseconds(),
408
+ "ping_rtt_stddev": (5 * time.Millisecond).Microseconds(),
409
+ },
410
+ }
411
412
for name, tc := range tests {
413
t.Run(name, func(t *testing.T) {
@@ -345,6 +427,30 @@ func TestCollector_Collect(t *testing.T) {
427
}
428
}
429
430
+type mockProber struct {
431
+ pingErr error
432
+}
433
+
434
+func (m *mockProber) Ping(host string) (*probing.Statistics, error) {
435
+ if m.pingErr != nil {
436
+ return nil, m.pingErr
437
+ }
438
+
439
+ stats := probing.Statistics{
440
+ PacketsRecv: 5,
441
+ PacketsSent: 5,
442
+ PacketsRecvDuplicates: 0,
443
+ PacketLoss: 0,
444
+ Addr: host,
445
+ MinRtt: time.Millisecond * 10,
446
+ MaxRtt: time.Millisecond * 20,
447
+ AvgRtt: time.Millisecond * 15,
448
+ StdDevRtt: time.Millisecond * 5,
449
+ }
450
+
451
+ return &stats, nil
452
+}
453
+
454
type mockDdSnmpCollector struct {
455
pms []*ddsnmp.ProfileMetrics
456
meta map[string]ddsnmp.MetaTag
src/go/plugin/go.d/collector/snmp/config.go
+2
-1
@@ -22,7 +22,8 @@ type (
22
23
ManualProfiles []string `yaml:"manual_profiles,omitempty" json:"manual_profiles"`
24
25
- Ping PingConfig `yaml:"ping,omitempty" json:"ping"`
25
+ PingOnly bool `yaml:"ping_only,omitempty" json:"ping_only"`
26
+ Ping PingConfig `yaml:"ping,omitempty" json:"ping"`
27
}
28
29
PingConfig struct {
src/go/plugin/go.d/collector/snmp/config_schema.json
+10
@@ -208,6 +208,12 @@
208
},
209
"uniqueItems": true
210
},
211
+ "ping_only": {
212
+ "title": "Ping only",
213
+ "description": "Collect only ICMP round-trip time and skip SNMP profile metrics. Implies ping is enabled regardless of the ping.enabled setting.",
214
+ "type": "boolean",
215
+ "default": false
216
+ },
217
"ping": {
218
"title": "Ping",
219
"type": [
@@ -321,6 +327,9 @@
327
"ui:help": "Advanced. Usually left empty. Required when targeting a specific context on multi-context agents or when driving snmpsim virtual devices."
328
}
329
},
330
+ "ping_only": {
331
+ "ui:help": "A minimal SNMP sysInfo query still runs at startup for device identification, profile matching, and metadata."
332
+ },
333
"ping": {
334
"interface": {
335
"ui:widget": "hidden"
@@ -344,6 +353,7 @@
353
{
354
"title": "Ping",
355
"fields": [
356
+ "ping_only",
357
"ping"
358
]
359
},
src/go/plugin/go.d/collector/snmp/metadata.yaml
+1
-1
@@ -390,7 +390,7 @@ modules:
390
391
- name: ping_only
392
group: Ping
393
- description: Collect only ICMP round-trip metrics and skip periodic SNMP polling. A minimal SNMP sysInfo probe still runs at setup for naming/labels/metadata.
393
+ description: Collect only ICMP round-trip metrics and skip periodic SNMP polling. Implies ping is enabled regardless of the `ping.enabled` setting. A minimal SNMP sysInfo probe still runs at setup for naming/labels/metadata.
394
default_value: false
395
required: false
396
- name: ping.enabled
src/go/plugin/go.d/collector/snmp/testdata/config.json
+1
@@ -32,6 +32,7 @@
32
"manual_profiles": [
33
"ok"
34
],
35
+ "ping_only": true,
36
"ping": {
37
"enabled": true,
38
"network": "ip",
src/go/plugin/go.d/collector/snmp/testdata/config.yaml
+1
@@ -31,6 +31,7 @@ options:
31
max_request_size: 123
32
max_repetitions: 123
33
34
+ping_only: yes
35
ping:
36
enabled: yes
37
network: ip