@cryptotaxi247 / netdata / commits / 522d31c38

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"