@cryptotaxi247 / netdata-1 / commits / db12b6ae7

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