feat(go.d/ddsnmp): make SNMP profile collection configurable (#20503)
Ilya Mashchenko committed
Jun 17, 2025 at 13:36 UTC
522d31c3894717809481c7ef7325f8d2cc7b3c33
7 files changed
+44
-207
src/go/plugin/go.d/collector/snmp/collect_profiles.go
+1
@@ -17,6 +17,7 @@ func (c *Collector) collectProfiles(mx map[string]int64) error {
17
}
18
if c.ddSnmpColl == nil {
19
c.ddSnmpColl = ddsnmpcollector.New(c.snmpClient, c.snmpProfiles, c.Logger)
20
+ c.ddSnmpColl.DoTableMetrics = c.EnableProfilesTableMetrics
21
}
22
23
pms, err := c.ddSnmpColl.Collect()
src/go/plugin/go.d/collector/snmp/config.go
+11
-10
@@ -6,16 +6,17 @@ import "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/vnodes"
6
7
type (
8
Config struct {
9
- UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
10
- Hostname string `yaml:"hostname" json:"hostname"`
11
- CreateVnode bool `yaml:"create_vnode,omitempty" json:"create_vnode"`
12
- Vnode vnodes.VirtualNode `yaml:"vnode,omitempty" json:"vnode"`
13
- Community string `yaml:"community,omitempty" json:"community"`
14
- User User `yaml:"user,omitempty" json:"user"`
15
- Options Options `yaml:"options,omitempty" json:"options"`
16
- ChartsInput []ChartConfig `yaml:"charts,omitempty" json:"charts"`
17
- NetworkInterfaceFilter NetworkInterfaceFilter `yaml:"network_interface_filter,omitempty" json:"network_interface_filter"`
18
- EnableProfiles bool `yaml:"enable_profiles,omitempty" json:"enable_profiles"`
9
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
10
+ Hostname string `yaml:"hostname" json:"hostname"`
11
+ CreateVnode bool `yaml:"create_vnode,omitempty" json:"create_vnode"`
12
+ Vnode vnodes.VirtualNode `yaml:"vnode,omitempty" json:"vnode"`
13
+ Community string `yaml:"community,omitempty" json:"community"`
14
+ User User `yaml:"user,omitempty" json:"user"`
15
+ Options Options `yaml:"options,omitempty" json:"options"`
16
+ ChartsInput []ChartConfig `yaml:"charts,omitempty" json:"charts"`
17
+ NetworkInterfaceFilter NetworkInterfaceFilter `yaml:"network_interface_filter,omitempty" json:"network_interface_filter"`
18
+ EnableProfiles bool `yaml:"enable_profiles,omitempty" json:"enable_profiles"`
19
+ EnableProfilesTableMetrics bool `yaml:"enable_profiles_table_metrics,omitempty" json:"enable_profiles_table_metrics"`
20
}
21
NetworkInterfaceFilter struct {
22
ByName string `yaml:"by_name,omitempty" json:"by_name"`
src/go/plugin/go.d/collector/snmp/config_schema.json
+25
@@ -342,6 +342,18 @@
342
"dimensions"
343
]
344
}
345
+ },
346
+ "enable_profiles": {
347
+ "title": "Enable SNMP Profile Collection",
348
+ "description": "Enable collection of metrics using SNMP profiles.",
349
+ "type": "boolean",
350
+ "default": true
351
+ },
352
+ "enable_profiles_table_metrics": {
353
+ "title": "Enable SNMP Table Metrics",
354
+ "description": "Enable collection of SNMP table metrics from profiles. Enabling this may **increase collection time and memory usage** for devices with many network interfaces*",
355
+ "type": "boolean",
356
+ "default": false
357
}
358
},
359
"required": [
@@ -357,6 +369,12 @@
369
"uiOptions": {
370
"fullPage": true
371
},
372
+ "enable_profiles": {
373
+ "ui:help": "Profiles provide pre-configured monitoring for specific device models and MIBs, including network interfaces, system information, and device-specific metrics."
374
+ },
375
+ "enable_profiles_table_metrics": {
376
+ "ui:help": "Table metrics include interface statistics, routing tables, and other tabular data."
377
+ },
378
"network_interface_filter": {
379
"ui:collapsible": true
380
},
@@ -458,6 +476,13 @@
476
"fields": [
477
"charts"
478
]
479
+ },
480
+ {
481
+ "title": "Profiles",
482
+ "fields": [
483
+ "enable_profiles",
484
+ "enable_profiles_table_metrics"
485
+ ]
486
}
487
]
488
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+3
-4
@@ -46,8 +46,7 @@ func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logg
46
snmpClient: snmpClient,
47
profiles: make(map[string]*profileState),
48
missingOIDs: make(map[string]bool),
49
- tableCache: newTableCache(10*time.Minute, 1), // 5 min TTL with 100% jitter
50
- //doTableMetrics: true,
49
+ tableCache: newTableCache(30*time.Minute, 1), // 100% jitter
50
}
51
52
for _, prof := range profiles {
@@ -66,7 +65,7 @@ type (
65
missingOIDs map[string]bool
66
tableCache *tableCache
67
69
- doTableMetrics bool
68
+ DoTableMetrics bool
69
}
70
profileState struct {
71
profile *ddsnmp.Profile
@@ -130,7 +129,7 @@ func (c *Collector) collectProfile(ps *profileState) (*ProfileMetrics, error) {
129
}
130
metrics = append(metrics, scalarMetrics...)
131
133
- if c.doTableMetrics {
132
+ if c.DoTableMetrics {
133
tableMetrics, err := c.collectTableMetrics(ps.profile)
134
if err != nil {
135
return nil, err
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
+1
-192
@@ -3343,7 +3343,7 @@ func TestCollector_Collect(t *testing.T) {
3343
tc.setupMock(mockHandler)
3344
3345
collector := New(mockHandler, tc.profiles, logger.New())
3346
- collector.doTableMetrics = true
3346
+ collector.DoTableMetrics = true
3347
collector.tableCache.setTTL(0, 0)
3348
3349
result, err := collector.Collect()
@@ -3379,197 +3379,6 @@ func TestCollector_Collect(t *testing.T) {
3379
}
3380
}
3381
3382
-//func TestCollector_TableCache(t *testing.T) {
3383
-// ctrl := gomock.NewController(t)
3384
-// defer ctrl.Finish()
3385
-//
3386
-// mockHandler := snmpmock.NewMockHandler(ctrl)
3387
-// mockHandler.EXPECT().MaxOids().Return(10).AnyTimes()
3388
-// mockHandler.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
3389
-//
3390
-// // First collection - expect table walk
3391
-// mockHandler.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3392
-// []gosnmp.SnmpPDU{
3393
-// // Row 1
3394
-// {Name: "1.3.6.1.2.1.2.2.1.2.1", Type: gosnmp.OctetString, Value: []byte("eth0")},
3395
-// {Name: "1.3.6.1.2.1.2.2.1.3.1", Type: gosnmp.Integer, Value: 6},
3396
-// {Name: "1.3.6.1.2.1.2.2.1.8.1", Type: gosnmp.Integer, Value: 1},
3397
-// {Name: "1.3.6.1.2.1.2.2.1.10.1", Type: gosnmp.Counter32, Value: uint(1000)},
3398
-// {Name: "1.3.6.1.2.1.2.2.1.16.1", Type: gosnmp.Counter32, Value: uint(2000)},
3399
-// // Row 2
3400
-// {Name: "1.3.6.1.2.1.2.2.1.2.2", Type: gosnmp.OctetString, Value: []byte("lo0")},
3401
-// {Name: "1.3.6.1.2.1.2.2.1.3.2", Type: gosnmp.Integer, Value: 1},
3402
-// {Name: "1.3.6.1.2.1.2.2.1.8.2", Type: gosnmp.Integer, Value: 1},
3403
-// {Name: "1.3.6.1.2.1.2.2.1.10.2", Type: gosnmp.Counter32, Value: uint(500)},
3404
-// {Name: "1.3.6.1.2.1.2.2.1.16.2", Type: gosnmp.Counter32, Value: uint(500)},
3405
-// }, nil,
3406
-// ).Times(1) // Walk only once
3407
-//
3408
-// // Second collection - expect GETs for each config
3409
-// // First config GETs its columns
3410
-// mockHandler.EXPECT().Get(gomock.InAnyOrder([]string{
3411
-// "1.3.6.1.2.1.2.2.1.10.1", // ifInOctets.1
3412
-// "1.3.6.1.2.1.2.2.1.2.1", // ifDescr.1 (tag)
3413
-// "1.3.6.1.2.1.2.2.1.10.2", // ifInOctets.2
3414
-// "1.3.6.1.2.1.2.2.1.2.2", // ifDescr.2 (tag)
3415
-// })).Return(
3416
-// &gosnmp.SnmpPacket{
3417
-// Variables: []gosnmp.SnmpPDU{
3418
-// {Name: "1.3.6.1.2.1.2.2.1.10.1", Type: gosnmp.Counter32, Value: uint(1500)},
3419
-// {Name: "1.3.6.1.2.1.2.2.1.2.1", Type: gosnmp.OctetString, Value: []byte("eth0")},
3420
-// {Name: "1.3.6.1.2.1.2.2.1.10.2", Type: gosnmp.Counter32, Value: uint(600)},
3421
-// {Name: "1.3.6.1.2.1.2.2.1.2.2", Type: gosnmp.OctetString, Value: []byte("lo0")},
3422
-// },
3423
-// }, nil,
3424
-// ).Times(1)
3425
-//
3426
-// // Second config GETs its columns
3427
-// mockHandler.EXPECT().Get(gomock.InAnyOrder([]string{
3428
-// "1.3.6.1.2.1.2.2.1.16.1", // ifOutOctets.1
3429
-// "1.3.6.1.2.1.2.2.1.3.1", // ifType.1 (tag)
3430
-// "1.3.6.1.2.1.2.2.1.16.2", // ifOutOctets.2
3431
-// "1.3.6.1.2.1.2.2.1.3.2", // ifType.2 (tag)
3432
-// })).Return(
3433
-// &gosnmp.SnmpPacket{
3434
-// Variables: []gosnmp.SnmpPDU{
3435
-// {Name: "1.3.6.1.2.1.2.2.1.16.1", Type: gosnmp.Counter32, Value: uint(2500)},
3436
-// {Name: "1.3.6.1.2.1.2.2.1.3.1", Type: gosnmp.Integer, Value: 6},
3437
-// {Name: "1.3.6.1.2.1.2.2.1.16.2", Type: gosnmp.Counter32, Value: uint(600)},
3438
-// {Name: "1.3.6.1.2.1.2.2.1.3.2", Type: gosnmp.Integer, Value: 1},
3439
-// },
3440
-// }, nil,
3441
-// ).Times(1)
3442
-//
3443
-// // Third config GETs its columns
3444
-// mockHandler.EXPECT().Get(gomock.InAnyOrder([]string{
3445
-// "1.3.6.1.2.1.2.2.1.8.1", // ifOperStatus.1
3446
-// "1.3.6.1.2.1.2.2.1.8.2", // ifOperStatus.2
3447
-// })).Return(
3448
-// &gosnmp.SnmpPacket{
3449
-// Variables: []gosnmp.SnmpPDU{
3450
-// {Name: "1.3.6.1.2.1.2.2.1.8.1", Type: gosnmp.Integer, Value: 1},
3451
-// {Name: "1.3.6.1.2.1.2.2.1.8.2", Type: gosnmp.Integer, Value: 1},
3452
-// },
3453
-// }, nil,
3454
-// ).Times(1)
3455
-//
3456
-// // Create profile with multiple configs for same table
3457
-// profile := &ddsnmp.Profile{
3458
-// SourceFile: "test-profile.yaml",
3459
-// Definition: &ddprofiledefinition.ProfileDefinition{
3460
-// Metrics: []ddprofiledefinition.MetricsConfig{
3461
-// {
3462
-// MIB: "IF-MIB",
3463
-// Table: ddprofiledefinition.SymbolConfig{
3464
-// OID: "1.3.6.1.2.1.2.2",
3465
-// Name: "ifTable",
3466
-// },
3467
-// Symbols: []ddprofiledefinition.SymbolConfig{
3468
-// {
3469
-// OID: "1.3.6.1.2.1.2.2.1.10",
3470
-// Name: "ifInOctets",
3471
-// },
3472
-// },
3473
-// MetricTags: []ddprofiledefinition.MetricTagConfig{
3474
-// {
3475
-// Tag: "interface",
3476
-// Symbol: ddprofiledefinition.SymbolConfigCompat{
3477
-// OID: "1.3.6.1.2.1.2.2.1.2",
3478
-// Name: "ifDescr",
3479
-// },
3480
-// },
3481
-// },
3482
-// },
3483
-// {
3484
-// MIB: "IF-MIB",
3485
-// Table: ddprofiledefinition.SymbolConfig{
3486
-// OID: "1.3.6.1.2.1.2.2",
3487
-// Name: "ifTable",
3488
-// },
3489
-// Symbols: []ddprofiledefinition.SymbolConfig{
3490
-// {
3491
-// OID: "1.3.6.1.2.1.2.2.1.16",
3492
-// Name: "ifOutOctets",
3493
-// },
3494
-// },
3495
-// MetricTags: []ddprofiledefinition.MetricTagConfig{
3496
-// {
3497
-// Tag: "if_type",
3498
-// Symbol: ddprofiledefinition.SymbolConfigCompat{
3499
-// OID: "1.3.6.1.2.1.2.2.1.3",
3500
-// Name: "ifType",
3501
-// },
3502
-// Mapping: map[string]string{
3503
-// "6": "ethernet",
3504
-// "1": "other",
3505
-// },
3506
-// },
3507
-// },
3508
-// },
3509
-// {
3510
-// MIB: "IF-MIB",
3511
-// Table: ddprofiledefinition.SymbolConfig{
3512
-// OID: "1.3.6.1.2.1.2.2",
3513
-// Name: "ifTable",
3514
-// },
3515
-// Symbols: []ddprofiledefinition.SymbolConfig{
3516
-// {
3517
-// OID: "1.3.6.1.2.1.2.2.1.8",
3518
-// Name: "ifOperStatus",
3519
-// Mapping: map[string]string{
3520
-// "1": "up",
3521
-// "2": "down",
3522
-// },
3523
-// },
3524
-// },
3525
-// StaticTags: []string{"source:cache_test"},
3526
-// },
3527
-// },
3528
-// },
3529
-// }
3530
-//
3531
-// // Create collector with cache enabled
3532
-// collector := New(mockHandler, []*ddsnmp.Profile{profile}, logger.New())
3533
-// collector.doTableMetrics = true
3534
-// collector.tableCache.setTTL(5*time.Minute, 0.1)
3535
-//
3536
-// // First collection - should walk tables
3537
-// result1, err := collector.Collect()
3538
-// require.NoError(t, err)
3539
-// require.Len(t, result1, 1)
3540
-// require.Len(t, result1[0].Metrics, 6) // 2 rows x 3 metrics
3541
-//
3542
-// // Verify first collection metrics
3543
-// metrics1 := result1[0].Metrics
3544
-// assert.Contains(t, metrics1, Metric{
3545
-// Name: "ifInOctets",
3546
-// Value: 1000,
3547
-// Tags: map[string]string{"interface": "eth0"},
3548
-// MetricType: "rate",
3549
-// IsTable: true,
3550
-// })
3551
-//
3552
-// // Second collection - should use cache
3553
-// result2, err := collector.Collect()
3554
-// require.NoError(t, err)
3555
-// require.Len(t, result2, 1)
3556
-// require.Len(t, result2[0].Metrics, 6) // 2 rows x 3 metrics
3557
-//
3558
-// // Verify second collection has updated values from cache
3559
-// metrics2 := result2[0].Metrics
3560
-// var foundUpdatedMetric bool
3561
-// for _, m := range metrics2 {
3562
-// if m.Name == "ifInOctets" && m.Tags["interface"] == "eth0" {
3563
-// assert.Equal(t, int64(1500), m.Value) // Updated value from cache
3564
-// foundUpdatedMetric = true
3565
-// }
3566
-// }
3567
-// assert.True(t, foundUpdatedMetric, "Should find updated metric from cache")
3568
-//
3569
-// // Verify all expectations were met
3570
-// ctrl.Finish()
3571
-//}
3572
-
3382
func mustCompileRegex(pattern string) *regexp.Regexp {
3383
re, err := regexp.Compile(pattern)
3384
if err != nil {
src/go/plugin/go.d/collector/snmp/testdata/config.json
+2
-1
@@ -53,5 +53,6 @@
53
]
54
}
55
],
56
- "enable_profiles": true
56
+ "enable_profiles": true,
57
+ "enable_profiles_table_metrics": true
58
}
src/go/plugin/go.d/collector/snmp/testdata/config.yaml
+1
@@ -2,6 +2,7 @@ update_every: 123
2
hostname: "ok"
3
create_vnode: yes
4
enable_profiles: yes
5
+enable_profiles_table_metrics: yes
6
vnode:
7
name: "ok"
8
guid: "ok"