chore(go.d/ddsnmp): add basic SNMP table walking functionality (#20441)
Ilya Mashchenko committed
Jun 8, 2025 at 17:50 UTC
db12b6ae7f56be6cfc236145dfbd8bd800dc92d4
4 files changed
+193
-40
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_scalar.go
+1
-19
@@ -78,24 +78,6 @@ func (c *Collector) collectScalarMetric(cfg ddprofiledefinition.MetricsConfig, p
78
}
79
}
80
81
- var mappings map[int64]string
82
-
83
- if len(cfg.Symbol.Mapping) > 0 {
84
- mappings = make(map[int64]string)
85
- if isMappingKeysNumeric(cfg.Symbol.Mapping) {
86
- for k, v := range cfg.Symbol.Mapping {
87
- intKey, _ := strconv.ParseInt(k, 10, 64)
88
- mappings[intKey] = v
89
- }
90
- } else {
91
- for k, v := range cfg.Symbol.Mapping {
92
- if intVal, err := strconv.ParseInt(v, 10, 64); err == nil {
93
- mappings[intVal] = k
94
- }
95
- }
96
- }
97
- }
98
-
81
return &Metric{
82
Name: cfg.Symbol.Name,
83
Value: value,
@@ -103,7 +85,7 @@ func (c *Collector) collectScalarMetric(cfg ddprofiledefinition.MetricsConfig, p
85
Unit: cfg.Symbol.Unit,
86
Description: cfg.Symbol.Description,
87
Family: cfg.Symbol.Family,
106
- Mappings: mappings,
88
+ Mappings: convSymMappingToNumeric(cfg.Symbol),
89
MetricType: getMetricType(cfg.Symbol, pdu),
90
}, nil
91
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_table.go
new
+144
@@ -0,0 +1,144 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "strings"
9
+
10
+ "github.com/gosnmp/gosnmp"
11
+
12
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
13
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
14
+)
15
+
16
+func (c *Collector) collectTableMetrics(prof *ddsnmp.Profile) ([]Metric, error) {
17
+ var metrics []Metric
18
+ var errs []error
19
+
20
+ doneOids := make(map[string]bool)
21
+
22
+ for _, cfg := range prof.Definition.Metrics {
23
+ if cfg.IsScalar() || cfg.Table.OID == "" || doneOids[cfg.Table.OID] {
24
+ continue
25
+ }
26
+
27
+ doneOids[cfg.Table.OID] = true
28
+ tableMetrics, err := c.collectSingleTable(cfg)
29
+ if err != nil {
30
+ errs = append(errs, fmt.Errorf("table '%s': %w", cfg.Table.Name, err))
31
+ continue
32
+ }
33
+ metrics = append(metrics, tableMetrics...)
34
+ }
35
+
36
+ if len(metrics) == 0 && len(errs) > 0 {
37
+ return nil, errors.Join(errs...)
38
+ }
39
+
40
+ return metrics, nil
41
+}
42
+
43
+func (c *Collector) collectSingleTable(cfg ddprofiledefinition.MetricsConfig) ([]Metric, error) {
44
+ pdus, err := c.snmpWalk(cfg.Table.OID)
45
+ if err != nil {
46
+ return nil, fmt.Errorf("failed to walk table: %w", err)
47
+ }
48
+
49
+ if len(pdus) == 0 {
50
+ return nil, nil
51
+ }
52
+
53
+ // Build a set of column OIDs we're interested in
54
+ columnOIDs := make(map[string]ddprofiledefinition.SymbolConfig)
55
+ for _, sym := range cfg.Symbols {
56
+ columnOIDs[trimOID(sym.OID)] = sym
57
+ }
58
+
59
+ // Group PDUs by row index
60
+ rows := make(map[string]map[string]gosnmp.SnmpPDU) // index -> column OID -> PDU
61
+
62
+ for oid, pdu := range pdus {
63
+ // Check if this OID belongs to any of our columns
64
+ for columnOID := range columnOIDs {
65
+ if strings.HasPrefix(oid, columnOID+".") {
66
+ index := strings.TrimPrefix(oid, columnOID+".")
67
+
68
+ if rows[index] == nil {
69
+ rows[index] = make(map[string]gosnmp.SnmpPDU)
70
+ }
71
+ rows[index][columnOID] = pdu
72
+ break
73
+ }
74
+ }
75
+ }
76
+
77
+ var metrics []Metric
78
+ for index, rowPDUs := range rows {
79
+ rowMetrics, err := c.processTableRow(rowPDUs, columnOIDs)
80
+ if err != nil {
81
+ c.log.Debugf("Error processing row %s: %v", index, err)
82
+ continue
83
+ }
84
+ metrics = append(metrics, rowMetrics...)
85
+ }
86
+
87
+ return metrics, nil
88
+}
89
+
90
+func (c *Collector) processTableRow(rowPDUs map[string]gosnmp.SnmpPDU, columnOIDs map[string]ddprofiledefinition.SymbolConfig) ([]Metric, error) {
91
+ var metrics []Metric
92
+
93
+ for columnOID, sym := range columnOIDs {
94
+ pdu, ok := rowPDUs[columnOID]
95
+ if !ok {
96
+ continue
97
+ }
98
+
99
+ value, err := processSymbolValue(sym, pdu)
100
+ if err != nil {
101
+ c.log.Debugf("Error processing value for %s: %v", sym.Name, err)
102
+ continue
103
+ }
104
+
105
+ metric := Metric{
106
+ Name: sym.Name,
107
+ Value: value,
108
+ Tags: make(map[string]string),
109
+ Unit: sym.Unit,
110
+ Description: sym.Description,
111
+ MetricType: getMetricType(sym, pdu),
112
+ Family: sym.Family,
113
+ Mappings: convSymMappingToNumeric(sym),
114
+ }
115
+
116
+ metrics = append(metrics, metric)
117
+ }
118
+
119
+ return metrics, nil
120
+}
121
+
122
+func (c *Collector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error) {
123
+ pdus := make(map[string]gosnmp.SnmpPDU)
124
+
125
+ var resp []gosnmp.SnmpPDU
126
+ var err error
127
+
128
+ if c.snmpClient.Version() == gosnmp.Version1 {
129
+ resp, err = c.snmpClient.WalkAll(oid)
130
+ } else {
131
+ resp, err = c.snmpClient.BulkWalkAll(oid)
132
+ }
133
+ if err != nil {
134
+ return nil, err
135
+ }
136
+
137
+ for _, pdu := range resp {
138
+ if isPduWithData(pdu) {
139
+ pdus[trimOID(pdu.Name)] = pdu
140
+ }
141
+ }
142
+
143
+ return pdus, nil
144
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+25
-21
@@ -83,27 +83,7 @@ func (c *Collector) Collect() ([]*ProfileMetrics, error) {
83
c.log.Debugf("collecting metrics: %v", errors.Join(errs...))
84
}
85
86
- // Find device vendor and type from any profile that has them.
87
- // Multiple profiles can be loaded for a single device (e.g., base profiles, generic MIB profiles),
88
- // but only device-specific profiles contain vendor/type information.
89
- // We need to apply vendor/type to ALL metrics across ALL profiles to ensure consistent
90
- // metric family naming (e.g., "interface/stats" → "router/cisco/interface/stats").
91
- for _, ps := range c.profiles {
92
- if !ps.initialized {
93
- continue
94
- }
95
- if res, ok := ps.profile.Definition.Metadata["device"]; ok {
96
- if dt, dv := res.Fields["type"].Value, res.Fields["vendor"].Value; dt != "" && dv != "" {
97
- for _, pm := range metrics {
98
- for i := range pm.Metrics {
99
- m := &pm.Metrics[i]
100
- m.Family = processMetricFamily(m.Family, dt, dv)
101
- }
102
- }
103
- break
104
- }
105
- }
106
- }
86
+ c.updateMetricFamily(metrics)
87
88
return metrics, nil
89
}
@@ -141,6 +121,30 @@ func (c *Collector) collectProfile(ps *profileState) (*ProfileMetrics, error) {
121
}, nil
122
}
123
124
+func (c *Collector) updateMetricFamily(pms []*ProfileMetrics) {
125
+ // Find device vendor and type from any profile that has them.
126
+ // Multiple profiles can be loaded for a single device (e.g., base profiles, generic MIB profiles),
127
+ // but only device-specific profiles contain vendor/type information.
128
+ // We need to apply vendor/type to ALL metrics across ALL profiles to ensure consistent
129
+ // metric family naming (e.g., "interface/stats" → "router/cisco/interface/stats").
130
+ for _, ps := range c.profiles {
131
+ if !ps.initialized {
132
+ continue
133
+ }
134
+ if res, ok := ps.profile.Definition.Metadata["device"]; ok {
135
+ if dt, dv := res.Fields["type"].Value, res.Fields["vendor"].Value; dt != "" && dv != "" {
136
+ for _, pm := range pms {
137
+ for i := range pm.Metrics {
138
+ m := &pm.Metrics[i]
139
+ m.Family = processMetricFamily(m.Family, dt, dv)
140
+ }
141
+ }
142
+ return
143
+ }
144
+ }
145
+ }
146
+}
147
+
148
func (c *Collector) snmpGet(oids []string) (map[string]gosnmp.SnmpPDU, error) {
149
pdus := make(map[string]gosnmp.SnmpPDU)
150
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/utils.go
+23
@@ -14,6 +14,29 @@ import (
14
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
)
16
17
+func convSymMappingToNumeric(cfg ddprofiledefinition.SymbolConfig) map[int64]string {
18
+ if len(cfg.Mapping) == 0 {
19
+ return nil
20
+ }
21
+
22
+ mappings := make(map[int64]string)
23
+
24
+ if isMappingKeysNumeric(cfg.Mapping) {
25
+ for k, v := range cfg.Mapping {
26
+ intKey, _ := strconv.ParseInt(k, 10, 64)
27
+ mappings[intKey] = v
28
+ }
29
+ } else {
30
+ for k, v := range cfg.Mapping {
31
+ if intVal, err := strconv.ParseInt(v, 10, 64); err == nil {
32
+ mappings[intVal] = k
33
+ }
34
+ }
35
+ }
36
+
37
+ return mappings
38
+}
39
+
40
func getMetricType(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) ddprofiledefinition.ProfileMetricType {
41
if sym.MetricType != "" {
42
return sym.MetricType