improvement(go.d): add ddsnmp profile collector (scalar only) (#20415)
Ilya Mashchenko committed
Jun 4, 2025 at 18:05 UTC
f0eecb8c58080b967ff1114245d0ea18b1bf53b8
17 files changed
+1530
-669
src/go/plugin/go.d/collector/snmp/charts.go
-54
@@ -19,8 +19,6 @@ const (
19
prioNetIfaceAdminStatus
20
prioNetIfaceOperStatus
21
prioSysUptime
22
-
23
- priosnmp
22
)
23
24
var netIfaceChartsTmpl = module.Charts{
@@ -34,20 +32,6 @@ var netIfaceChartsTmpl = module.Charts{
32
netIfaceOperStatusChartTmpl.Copy(),
33
}
34
37
-var (
38
- snmpChartTemplate = module.Chart{
39
- ID: "%s",
40
- Title: "%s",
41
- Units: "%s",
42
- Fam: "%s",
43
- Ctx: "snmp.%s",
44
- Priority: priosnmp,
45
- Dims: module.Dims{
46
- {ID: "%s", Name: "%s"},
47
- },
48
- }
49
-)
50
-
35
var (
36
netIfaceTrafficChartTmpl = module.Chart{
37
ID: "snmp_device_net_iface_%s_traffic",
@@ -194,44 +178,6 @@ func (c *Collector) addNetIfaceCharts(iface *netInterface) {
178
}
179
}
180
197
-func (c *Collector) addSNMPChart(processedMetric processedMetric) {
198
- if processedMetric.tableName == "" {
199
- chart := snmpChartTemplate.Copy()
200
-
201
- chart.ID = fmt.Sprintf(chart.ID, processedMetric.name)
202
- if processedMetric.description == "" {
203
- chart.Title = fmt.Sprintf(chart.Title, "TBD Description")
204
- } else {
205
- chart.Title = fmt.Sprintf(chart.Title, processedMetric.description)
206
- }
207
- if processedMetric.unit == "" {
208
- chart.Units = fmt.Sprintf(chart.Units, "TBD unit")
209
- } else {
210
- chart.Units = fmt.Sprintf(chart.Units, processedMetric.unit)
211
- }
212
- chart.Ctx = fmt.Sprintf(chart.Ctx, processedMetric.name)
213
- chart.Fam = fmt.Sprintf(chart.Fam, processedMetric.name)
214
-
215
- for _, dim := range chart.Dims {
216
- dim.ID = fmt.Sprintf(dim.ID, processedMetric.name)
217
- dim.Name = fmt.Sprintf(dim.Name, processedMetric.name)
218
- }
219
-
220
- if err := c.Charts().Add(chart); err != nil {
221
- c.Warning(err)
222
- }
223
- }
224
-}
225
-
226
-func (c *Collector) removeSNMPChart(name string) {
227
- for _, chart := range *c.Charts() {
228
- if chart.ID == name {
229
- chart.MarkRemove()
230
- chart.MarkNotCreated()
231
- }
232
- }
233
-}
234
-
181
func (c *Collector) removeNetIfaceCharts(iface *netInterface) {
182
px := fmt.Sprintf("snmp_device_net_iface_%s_", cleanIfaceName(iface.ifName))
183
for _, chart := range *c.Charts() {
src/go/plugin/go.d/collector/snmp/collect.go
+1
-1
@@ -30,7 +30,7 @@ func (c *Collector) collect() (map[string]int64, error) {
30
}
31
32
if c.EnableProfiles {
33
- c.snmpProfiles = ddsnmp.Find(c.sysInfo.SysObjectID)
33
+ c.snmpProfiles = ddsnmp.FindProfiles(c.sysInfo.SysObjectID)
34
}
35
}
36
src/go/plugin/go.d/collector/snmp/collect_profiles.go
+4
-151
@@ -3,163 +3,16 @@
3
package snmp
4
5
import (
6
- "fmt"
7
- "log"
8
- "strings"
9
-
10
- "github.com/gosnmp/gosnmp"
6
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
7
)
8
13
-type processedMetric struct {
14
- oid string
15
- name string
16
- value interface{}
17
- metricType gosnmp.Asn1BER
18
- tableName string
19
- unit string
20
- description string
21
-}
22
-
23
-func (c *Collector) collectProfiles(mx map[string]int64) error {
9
+func (c *Collector) collectProfiles(_ map[string]int64) error {
10
if len(c.snmpProfiles) == 0 {
11
return nil
12
}
27
-
28
- metricMap := map[string]processedMetric{}
29
-
30
- for _, prof := range c.snmpProfiles {
31
- results, err := parseMetrics(prof.Definition.Metrics)
32
- if err != nil {
33
- return err
34
- }
35
-
36
- for _, oid := range results.OIDs {
37
- response, err := c.snmpClient.Get([]string{oid})
38
- if err != nil {
39
- return err
40
- }
41
- for _, metric := range results.parsedMetrics {
42
- switch s := metric.(type) {
43
- case parsedSymbolMetric:
44
- if s.baseoid == oid {
45
- metricMap[oid] = processedMetric{
46
- oid: oid,
47
- name: s.name,
48
- value: response.Variables[0].Value,
49
- metricType: response.Variables[0].Type,
50
- unit: s.unit,
51
- description: s.description,
52
- }
53
- }
54
- }
55
- }
56
- }
57
-
58
- for _, oid := range results.nextOIDs {
59
- if len(oid) == 0 {
60
- continue
61
- }
62
-
63
- tableRows, err := c.walkOIDTree(oid)
64
- if err != nil {
65
- return fmt.Errorf("error walking OID tree: %v, oid %s", err, oid)
66
- }
67
-
68
- for _, metric := range results.parsedMetrics {
69
- switch s := metric.(type) {
70
- case parsedTableMetric:
71
- if s.rowOID == oid {
72
- for key, value := range tableRows {
73
- value.name = s.name
74
- value.tableName = s.tableName
75
- tableRows[key] = value
76
- }
77
- metricMap = mergeProcessedMetricMaps(metricMap, tableRows)
78
- }
79
- }
80
- }
81
- }
13
+ if c.ddSnmpColl == nil {
14
+ c.ddSnmpColl = ddsnmpcollector.New(c.snmpClient, c.snmpProfiles, c.Logger)
15
}
16
84
- c.makeChartsFromMetricMap(mx, metricMap)
85
-
17
return nil
18
}
88
-
89
-func (c *Collector) walkOIDTree(baseOID string) (map[string]processedMetric, error) {
90
- tableRows := make(map[string]processedMetric)
91
-
92
- currentOID := baseOID
93
- for {
94
- result, err := c.snmpClient.GetNext([]string{currentOID})
95
- if err != nil {
96
- return tableRows, fmt.Errorf("snmpgetnext failed: %v", err)
97
- }
98
- if len(result.Variables) == 0 {
99
- log.Println("No OID returned, ending walk.")
100
- return tableRows, nil
101
- }
102
- pdu := result.Variables[0]
103
-
104
- nextOID := strings.Replace(pdu.Name, ".", "", 1) //remove dot at the start of the OID
105
-
106
- // If the next OID does not start with the base OID, we've reached the end of the subtree.
107
- if !strings.HasPrefix(nextOID, baseOID) {
108
- return tableRows, nil
109
- }
110
-
111
- metricType := pdu.Type
112
- value := fmt.Sprintf("%v", pdu.Value)
113
-
114
- tableRows[nextOID] = processedMetric{
115
- oid: nextOID,
116
- value: value,
117
- metricType: metricType,
118
- }
119
-
120
- currentOID = nextOID
121
- }
122
-}
123
-
124
-func (c *Collector) makeChartsFromMetricMap(mx map[string]int64, metricMap map[string]processedMetric) {
125
- seen := make(map[string]bool)
126
-
127
- for _, metric := range metricMap {
128
- if metric.tableName == "" {
129
- switch s := metric.value.(type) {
130
- case int:
131
- name := metric.name
132
- if name == "" {
133
- continue
134
- }
135
-
136
- seen[name] = true
137
-
138
- if !c.seenMetrics[name] {
139
- c.seenMetrics[name] = true
140
- c.addSNMPChart(metric)
141
- }
142
-
143
- mx[metric.name] = int64(s)
144
- }
145
- }
146
-
147
- }
148
- for name := range c.seenMetrics {
149
- if !seen[name] {
150
- delete(c.seenMetrics, name)
151
- c.removeSNMPChart(name)
152
- }
153
- }
154
-}
155
-
156
-func mergeProcessedMetricMaps(m1 map[string]processedMetric, m2 map[string]processedMetric) map[string]processedMetric {
157
- merged := make(map[string]processedMetric)
158
- for k, v := range m1 {
159
- merged[k] = v
160
- }
161
- for key, value := range m2 {
162
- merged[key] = value
163
- }
164
- return merged
165
-}
src/go/plugin/go.d/collector/snmp/collector.go
+6
-4
@@ -8,13 +8,14 @@ import (
8
"errors"
9
"fmt"
10
11
+ "github.com/gosnmp/gosnmp"
12
+
13
"github.com/netdata/netdata/go/plugins/pkg/matcher"
14
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/discovery/sd/discoverer/snmpsd"
15
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
16
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/vnodes"
17
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
16
-
17
- "github.com/gosnmp/gosnmp"
18
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
19
)
20
21
//go:embed "config_schema.json"
@@ -34,8 +35,8 @@ func init() {
35
func New() *Collector {
36
return &Collector{
37
Config: Config{
37
- CreateVnode: true,
38
- Community: "public",
38
+ CreateVnode: true,
39
+ EnableProfiles: true,
40
Options: Options{
41
Port: 161,
42
Retries: 1,
@@ -74,6 +75,7 @@ type Collector struct {
75
76
newSnmpClient func() gosnmp.Handler
77
snmpClient gosnmp.Handler
78
+ ddSnmpColl *ddsnmpcollector.Collector
79
80
netIfaceFilterByName matcher.Matcher
81
netIfaceFilterByType matcher.Matcher
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/metrics.go
+6
-4
@@ -81,14 +81,14 @@ type SymbolConfig struct {
81
MetricType ProfileMetricType `yaml:"metric_type,omitempty" json:"metric_type,omitempty"`
82
Unit string `yaml:"unit,omitempty" json:"unit,omitempty"`
83
Description string `yaml:"description,omitempty" json:"description,omitempty"`
84
+ Mapping map[string]string `yaml:"mapping,omitempty" json:"mapping,omitempty"`
85
}
86
87
// Clone creates a duplicate of this SymbolConfig
88
func (s SymbolConfig) Clone() SymbolConfig {
88
- // SymbolConfig has no mutable members, so simple assignment copies it.
89
- // (technically this is false - regexes in go are mutable SOLELY through the
90
- // .Longest() method. But we never use that, so we ignore it here)
91
- return s
89
+ ss := s
90
+ ss.Mapping = maps.Clone(ss.Mapping)
91
+ return ss
92
}
93
94
// MetricTagConfig holds metric tag info
@@ -98,6 +98,8 @@ type MetricTagConfig struct {
98
// Table config
99
Index uint `yaml:"index,omitempty" json:"index,omitempty"`
100
101
+ Table string `yaml:"table,omitempty" json:"table,omitempty"`
102
+
103
// DEPRECATED: Use .Symbol instead
104
Column SymbolConfig `yaml:"column,omitempty" json:"-"`
105
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_device_meta.go
new
+113
@@ -0,0 +1,113 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "slices"
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) collectDeviceMetadata(prof *ddsnmp.Profile) (map[string]string, error) {
17
+ if len(prof.Definition.Metadata) == 0 {
18
+ return nil, nil
19
+ }
20
+
21
+ var tagOIDs []string
22
+ tags := make(map[string]string)
23
+
24
+ for resName, metaCfg := range prof.Definition.Metadata {
25
+ if !ddprofiledefinition.IsMetadataResourceWithScalarOids(resName) {
26
+ continue
27
+ }
28
+ for name, field := range metaCfg.Fields {
29
+ switch {
30
+ case field.Value != "":
31
+ tags[name] = field.Value
32
+ case field.Symbol.OID != "":
33
+ tagOIDs = append(tagOIDs, field.Symbol.OID)
34
+ case len(field.Symbols) > 0:
35
+ for _, sym := range field.Symbols {
36
+ if sym.OID != "" {
37
+ tagOIDs = append(tagOIDs, sym.OID)
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ slices.Sort(tagOIDs)
45
+ tagOIDs = slices.Compact(tagOIDs)
46
+
47
+ if len(tagOIDs) == 0 {
48
+ return ternary(len(tags) > 0, tags, nil), nil
49
+ }
50
+
51
+ pdus, err := c.snmpGet(tagOIDs)
52
+ if err != nil {
53
+ return nil, err
54
+ }
55
+
56
+ var errs []error
57
+
58
+ for resName, metaCfg := range prof.Definition.Metadata {
59
+ if !ddprofiledefinition.IsMetadataResourceWithScalarOids(resName) {
60
+ continue
61
+ }
62
+ for name, field := range metaCfg.Fields {
63
+ switch {
64
+ case field.Symbol.OID != "":
65
+ v, err := processSymbolTagValue(field.Symbol, pdus)
66
+ if err != nil {
67
+ errs = append(errs, fmt.Errorf("failed to process meta device tag value for '%s': %v", name, err))
68
+ continue
69
+ }
70
+ tags[name] = v
71
+ case len(field.Symbols) > 0:
72
+ for _, sym := range field.Symbols {
73
+ v, err := processSymbolTagValue(sym, pdus)
74
+ if err != nil {
75
+ errs = append(errs, fmt.Errorf("failed to process meta device tag value for '%s': %v", name, err))
76
+ continue
77
+ }
78
+ tags[name] = v
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ if len(errs) > 0 && len(tags) == 0 {
85
+ return nil, fmt.Errorf("failed to process any meta device tags: %v", errors.Join(errs...))
86
+ }
87
+
88
+ return tags, nil
89
+}
90
+
91
+func processSymbolTagValue(symCfg ddprofiledefinition.SymbolConfig, result map[string]gosnmp.SnmpPDU) (string, error) {
92
+ pdu, ok := result[trimOID(symCfg.OID)]
93
+ if !ok {
94
+ return "", nil
95
+ }
96
+
97
+ val, err := convPduToStringf(pdu, symCfg.Format)
98
+ if err != nil {
99
+ return "", err
100
+ }
101
+
102
+ switch {
103
+ case symCfg.ExtractValueCompiled != nil:
104
+ if sm := symCfg.ExtractValueCompiled.FindStringSubmatch(val); len(sm) > 1 {
105
+ return sm[1], nil
106
+ }
107
+ case symCfg.MatchPatternCompiled != nil:
108
+ if sm := symCfg.MatchPatternCompiled.FindStringSubmatch(val); len(sm) > 0 {
109
+ return replaceSubmatches(symCfg.MatchValue, sm), nil
110
+ }
111
+ }
112
+ return val, nil
113
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_global_tags.go
new
+112
@@ -0,0 +1,112 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "maps"
9
+ "slices"
10
+ "strings"
11
+
12
+ "github.com/gosnmp/gosnmp"
13
+
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
15
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
16
+)
17
+
18
+func (c *Collector) collectGlobalTags(prof *ddsnmp.Profile) (map[string]string, error) {
19
+ if len(prof.Definition.MetricTags) == 0 {
20
+ return nil, nil
21
+ }
22
+
23
+ var oids []string
24
+
25
+ for _, tagCfg := range prof.Definition.MetricTags {
26
+ if tagCfg.Symbol.OID != "" {
27
+ oids = append(oids, tagCfg.Symbol.OID)
28
+ }
29
+ }
30
+
31
+ slices.Sort(oids)
32
+ oids = slices.Compact(oids)
33
+
34
+ if len(oids) == 0 {
35
+ return nil, nil
36
+ }
37
+
38
+ pdus, err := c.snmpGet(oids)
39
+ if err != nil {
40
+ return nil, err
41
+ }
42
+
43
+ tags := make(map[string]string)
44
+ var errs []error
45
+
46
+ for _, tag := range prof.Definition.StaticTags {
47
+ parts := strings.SplitN(tag, ":", 2)
48
+ if len(parts) == 2 {
49
+ tags[parts[0]] = parts[1]
50
+ }
51
+ }
52
+
53
+ for _, cfg := range prof.Definition.MetricTags {
54
+ v, err := processMetricTagValue(cfg, pdus)
55
+ if err != nil {
56
+ errs = append(errs, fmt.Errorf("failed to process tag value for '%s/%s': %v", cfg.Tag, cfg.Symbol.Name, err))
57
+ continue
58
+ }
59
+ maps.Copy(tags, v)
60
+ }
61
+
62
+ if len(errs) > 0 && len(tags) == 0 {
63
+ return nil, fmt.Errorf("failed to process any global tags: %v", errors.Join(errs...))
64
+ }
65
+
66
+ return tags, nil
67
+}
68
+
69
+func processMetricTagValue(cfg ddprofiledefinition.MetricTagConfig, pdus map[string]gosnmp.SnmpPDU) (map[string]string, error) {
70
+ tagName := ternary(cfg.Tag != "", cfg.Tag, cfg.Symbol.Name)
71
+ if tagName == "" {
72
+ return nil, nil
73
+ }
74
+
75
+ pdu, ok := pdus[trimOID(cfg.Symbol.OID)]
76
+ if !ok {
77
+ return nil, nil
78
+ }
79
+
80
+ val, err := convPduToStringf(pdu, cfg.Symbol.Format)
81
+ if err != nil {
82
+ return nil, err
83
+ }
84
+
85
+ tags := make(map[string]string)
86
+
87
+ switch {
88
+ case len(cfg.Mapping) > 0:
89
+ if v, ok := cfg.Mapping[val]; ok {
90
+ val = v
91
+ }
92
+ tags[tagName] = val
93
+ case cfg.Pattern != nil:
94
+ if sm := cfg.Pattern.FindStringSubmatch(val); len(sm) > 0 {
95
+ for name, tmpl := range cfg.Tags {
96
+ tags[name] = replaceSubmatches(tmpl, sm)
97
+ }
98
+ }
99
+ case cfg.Symbol.ExtractValueCompiled != nil:
100
+ if sm := cfg.Symbol.ExtractValueCompiled.FindStringSubmatch(val); len(sm) > 1 {
101
+ tags[tagName] = sm[1]
102
+ }
103
+ case cfg.Symbol.MatchPatternCompiled != nil:
104
+ if sm := cfg.Symbol.MatchPatternCompiled.FindStringSubmatch(val); len(sm) > 0 {
105
+ tags[tagName] = replaceSubmatches(cfg.Symbol.MatchValue, sm)
106
+ }
107
+ default:
108
+ tags[tagName] = val
109
+ }
110
+
111
+ return tags, nil
112
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_scalar.go
new
+126
@@ -0,0 +1,126 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "slices"
9
+ "strconv"
10
+ "strings"
11
+
12
+ "github.com/gosnmp/gosnmp"
13
+
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
15
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
16
+)
17
+
18
+func (c *Collector) collectScalarMetrics(prof *ddsnmp.Profile) ([]Metric, error) {
19
+ var oids []string
20
+
21
+ for _, m := range prof.Definition.Metrics {
22
+ if m.IsScalar() {
23
+ oids = append(oids, m.Symbol.OID)
24
+ }
25
+ }
26
+
27
+ slices.Sort(oids)
28
+ oids = slices.Compact(oids)
29
+
30
+ pdus, err := c.snmpGet(oids)
31
+ if err != nil {
32
+ return nil, err
33
+ }
34
+
35
+ metrics := make([]Metric, 0, len(prof.Definition.Metrics))
36
+ var errs []error
37
+
38
+ for _, cfg := range prof.Definition.Metrics {
39
+ if !cfg.IsScalar() {
40
+ continue
41
+ }
42
+
43
+ metric, err := c.collectScalarMetric(cfg, pdus)
44
+ if err != nil {
45
+ errs = append(errs, fmt.Errorf("metric '%s': %w", cfg.Symbol.Name, err))
46
+ c.log.Debugf("Error processing scalar metric '%s': %v", cfg.Symbol.Name, err)
47
+ continue
48
+ }
49
+
50
+ if metric != nil {
51
+ metrics = append(metrics, *metric)
52
+ }
53
+ }
54
+
55
+ if len(metrics) == 0 && len(errs) > 0 {
56
+ return nil, errors.Join(errs...)
57
+ }
58
+
59
+ return metrics, nil
60
+}
61
+
62
+func (c *Collector) collectScalarMetric(cfg ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU) (*Metric, error) {
63
+ pdu, ok := pdus[trimOID(cfg.Symbol.OID)]
64
+ if !ok {
65
+ return nil, nil
66
+ }
67
+
68
+ value, err := processSymbolValue(cfg.Symbol, pdu)
69
+ if err != nil {
70
+ return nil, fmt.Errorf("error processing value: %w", err)
71
+ }
72
+
73
+ tags := make(map[string]string)
74
+
75
+ for _, tag := range cfg.StaticTags {
76
+ if n, v, _ := strings.Cut(tag, ":"); n != "" && v != "" {
77
+ tags[n] = v
78
+ }
79
+ }
80
+
81
+ return &Metric{
82
+ Name: cfg.Symbol.Name,
83
+ Value: value,
84
+ Tags: tags,
85
+ Unit: cfg.Symbol.Unit,
86
+ Description: cfg.Symbol.Description,
87
+ MetricType: string(getMetricType(cfg.Symbol, pdu)),
88
+ }, nil
89
+}
90
+
91
+func processSymbolValue(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) (int64, error) {
92
+ var value int64
93
+
94
+ if isPduNumericType(pdu) {
95
+ value = gosnmp.ToBigInt(pdu.Value).Int64()
96
+ } else {
97
+ s, err := convPduToStringf(pdu, sym.Format)
98
+ if err != nil {
99
+ return 0, err
100
+ }
101
+ switch {
102
+ case sym.ExtractValueCompiled != nil:
103
+ if sm := sym.ExtractValueCompiled.FindStringSubmatch(s); len(sm) > 1 {
104
+ s = sm[1]
105
+ }
106
+ case sym.MatchPatternCompiled != nil:
107
+ if sm := sym.MatchPatternCompiled.FindStringSubmatch(s); len(sm) > 0 {
108
+ s = replaceSubmatches(sym.MatchValue, sm)
109
+ }
110
+ }
111
+ if v, ok := sym.Mapping[s]; ok {
112
+ s = v
113
+ }
114
+
115
+ value, err = strconv.ParseInt(s, 10, 64)
116
+ if err != nil {
117
+ return 0, err
118
+ }
119
+ }
120
+
121
+ if sym.ScaleFactor != 0 {
122
+ value = int64(float64(value) * sym.ScaleFactor)
123
+ }
124
+
125
+ return value, nil
126
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
new
+137
@@ -0,0 +1,137 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "log/slog"
9
+ "maps"
10
+ "slices"
11
+
12
+ "github.com/gosnmp/gosnmp"
13
+
14
+ "github.com/netdata/netdata/go/plugins/logger"
15
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
16
+)
17
+
18
+type ProfileMetrics struct {
19
+ Source string
20
+ DeviceMetadata map[string]string
21
+ Metrics []Metric
22
+}
23
+
24
+type Metric struct {
25
+ Name string
26
+ Description string
27
+ Unit string
28
+ MetricType string
29
+ Tags map[string]string
30
+ Mappings map[string]string
31
+ Value int64
32
+}
33
+
34
+func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logger) *Collector {
35
+ coll := &Collector{
36
+ log: log.With(slog.String("ddsnmp", "collector")),
37
+ snmpClient: snmpClient,
38
+ profiles: make(map[string]*profileState),
39
+ }
40
+
41
+ for _, prof := range profiles {
42
+ prof := prof
43
+ coll.profiles[prof.SourceFile] = &profileState{profile: prof}
44
+ }
45
+
46
+ return coll
47
+}
48
+
49
+type (
50
+ Collector struct {
51
+ log *logger.Logger
52
+ snmpClient gosnmp.Handler
53
+ profiles map[string]*profileState
54
+ }
55
+ profileState struct {
56
+ profile *ddsnmp.Profile
57
+ initialized bool
58
+ globalTags map[string]string
59
+ deviceMetadata map[string]string
60
+ }
61
+)
62
+
63
+func (c *Collector) Collect() ([]*ProfileMetrics, error) {
64
+ var metrics []*ProfileMetrics
65
+ var errs []error
66
+
67
+ for _, prof := range c.profiles {
68
+ ms, err := c.collectProfile(prof)
69
+ if err != nil {
70
+ errs = append(errs, err)
71
+ continue
72
+ }
73
+ metrics = append(metrics, ms)
74
+ }
75
+
76
+ if len(metrics) == 0 && len(errs) > 0 {
77
+ return nil, errors.Join(errs...)
78
+ }
79
+ if len(errs) > 0 {
80
+ c.log.Debugf("collecting metrics: %v", errs)
81
+ }
82
+
83
+ return metrics, nil
84
+}
85
+
86
+func (c *Collector) collectProfile(ps *profileState) (*ProfileMetrics, error) {
87
+ if !ps.initialized {
88
+ globalTag, err := c.collectGlobalTags(ps.profile)
89
+ if err != nil {
90
+ return nil, fmt.Errorf("failed to collect global tags: %w", err)
91
+ }
92
+
93
+ deviceMeta, err := c.collectDeviceMetadata(ps.profile)
94
+ if err != nil {
95
+ return nil, fmt.Errorf("failed to collect device metadata: %w", err)
96
+ }
97
+
98
+ ps.globalTags = globalTag
99
+ ps.deviceMetadata = deviceMeta
100
+ ps.initialized = true
101
+ }
102
+
103
+ metrics, err := c.collectScalarMetrics(ps.profile)
104
+ if err != nil {
105
+ return nil, err
106
+ }
107
+
108
+ for _, m := range metrics {
109
+ maps.Copy(m.Tags, ps.globalTags)
110
+ }
111
+
112
+ return &ProfileMetrics{
113
+ Source: ps.profile.SourceFile,
114
+ DeviceMetadata: maps.Clone(ps.deviceMetadata),
115
+ Metrics: metrics,
116
+ }, nil
117
+
118
+}
119
+
120
+func (c *Collector) snmpGet(oids []string) (map[string]gosnmp.SnmpPDU, error) {
121
+ pdus := make(map[string]gosnmp.SnmpPDU)
122
+
123
+ for chunk := range slices.Chunk(oids, c.snmpClient.MaxOids()) {
124
+ result, err := c.snmpClient.Get(chunk)
125
+ if err != nil {
126
+ return nil, err
127
+ }
128
+
129
+ for _, pdu := range result.Variables {
130
+ if isPduWithData(pdu) {
131
+ pdus[trimOID(pdu.Name)] = pdu
132
+ }
133
+ }
134
+ }
135
+
136
+ return pdus, nil
137
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
new
+643
@@ -0,0 +1,643 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "regexp"
8
+ "testing"
9
+
10
+ "github.com/golang/mock/gomock"
11
+ "github.com/gosnmp/gosnmp"
12
+ "github.com/stretchr/testify/assert"
13
+
14
+ snmpmock "github.com/gosnmp/gosnmp/mocks"
15
+
16
+ "github.com/netdata/netdata/go/plugins/logger"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
18
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
19
+)
20
+
21
+func TestCollector_Collect(t *testing.T) {
22
+ tests := map[string]struct {
23
+ name string
24
+ profiles []*ddsnmp.Profile
25
+ setupMock func(m *snmpmock.MockHandler)
26
+ expectedResult []*ProfileMetrics
27
+ expectedError bool
28
+ errorContains string
29
+ }{
30
+ "successful collection with scalar metrics only": {
31
+ profiles: []*ddsnmp.Profile{
32
+ {
33
+ SourceFile: "test-profile.yaml",
34
+ Definition: &ddprofiledefinition.ProfileDefinition{
35
+ Metrics: []ddprofiledefinition.MetricsConfig{
36
+ {
37
+ Symbol: ddprofiledefinition.SymbolConfig{
38
+ OID: "1.3.6.1.2.1.1.3.0",
39
+ Name: "sysUpTime",
40
+ },
41
+ },
42
+ {
43
+ Symbol: ddprofiledefinition.SymbolConfig{
44
+ OID: "1.3.6.1.2.1.1.5.0",
45
+ Name: "sysName",
46
+ },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ },
52
+ setupMock: func(m *snmpmock.MockHandler) {
53
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
54
+ m.EXPECT().Get(gomock.InAnyOrder([]string{"1.3.6.1.2.1.1.3.0", "1.3.6.1.2.1.1.5.0"})).Return(
55
+ &gosnmp.SnmpPacket{
56
+ Variables: []gosnmp.SnmpPDU{
57
+ {
58
+ Name: "1.3.6.1.2.1.1.3.0",
59
+ Type: gosnmp.TimeTicks,
60
+ Value: uint32(123456),
61
+ },
62
+ {
63
+ Name: "1.3.6.1.2.1.1.5.0",
64
+ Type: gosnmp.OctetString,
65
+ Value: []byte("test-host"),
66
+ },
67
+ },
68
+ }, nil,
69
+ )
70
+ },
71
+ expectedResult: []*ProfileMetrics{
72
+ {
73
+ DeviceMetadata: nil,
74
+ Metrics: []Metric{
75
+ {
76
+ Name: "sysUpTime",
77
+ Value: 123456,
78
+ Tags: map[string]string{},
79
+ MetricType: "gauge",
80
+ },
81
+ // sysName will be skipped because it can't be converted to int64
82
+ },
83
+ },
84
+ },
85
+ expectedError: false, // Changed to false - partial success is not an error
86
+ },
87
+ "successful collection with global tags": {
88
+ profiles: []*ddsnmp.Profile{
89
+ {
90
+ SourceFile: "test-profile.yaml",
91
+ Definition: &ddprofiledefinition.ProfileDefinition{
92
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
93
+ {
94
+ Tag: "device_vendor",
95
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
96
+ OID: "1.3.6.1.2.1.1.1.0",
97
+ Name: "sysDescr",
98
+ },
99
+ },
100
+ },
101
+ Metrics: []ddprofiledefinition.MetricsConfig{
102
+ {
103
+ Symbol: ddprofiledefinition.SymbolConfig{
104
+ OID: "1.3.6.1.2.1.1.3.0",
105
+ Name: "sysUpTime",
106
+ },
107
+ },
108
+ },
109
+ },
110
+ },
111
+ },
112
+ setupMock: func(m *snmpmock.MockHandler) {
113
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
114
+ // First call for global tags
115
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
116
+ &gosnmp.SnmpPacket{
117
+ Variables: []gosnmp.SnmpPDU{
118
+ {
119
+ Name: "1.3.6.1.2.1.1.1.0",
120
+ Type: gosnmp.OctetString,
121
+ Value: []byte("Cisco IOS"),
122
+ },
123
+ },
124
+ }, nil,
125
+ )
126
+ // Second call for metrics
127
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
128
+ &gosnmp.SnmpPacket{
129
+ Variables: []gosnmp.SnmpPDU{
130
+ {
131
+ Name: "1.3.6.1.2.1.1.3.0",
132
+ Type: gosnmp.TimeTicks,
133
+ Value: uint32(123456),
134
+ },
135
+ },
136
+ }, nil,
137
+ )
138
+ },
139
+ expectedResult: []*ProfileMetrics{
140
+ {
141
+ DeviceMetadata: nil,
142
+ Metrics: []Metric{
143
+ {
144
+ Name: "sysUpTime",
145
+ Value: 123456,
146
+ Tags: map[string]string{"device_vendor": "Cisco IOS"},
147
+ MetricType: "gauge",
148
+ },
149
+ },
150
+ },
151
+ },
152
+ expectedError: false,
153
+ },
154
+ "successful collection with device metadata": {
155
+ profiles: []*ddsnmp.Profile{
156
+ {
157
+ SourceFile: "test-profile.yaml",
158
+ Definition: &ddprofiledefinition.ProfileDefinition{
159
+ Metadata: ddprofiledefinition.MetadataConfig{
160
+ "device": ddprofiledefinition.MetadataResourceConfig{
161
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
162
+ "vendor": {
163
+ Value: "dell",
164
+ },
165
+ "serial_number": {
166
+ Symbol: ddprofiledefinition.SymbolConfig{
167
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
168
+ Name: "chassisSerialNumber",
169
+ },
170
+ },
171
+ },
172
+ },
173
+ },
174
+ Metrics: []ddprofiledefinition.MetricsConfig{
175
+ {
176
+ Symbol: ddprofiledefinition.SymbolConfig{
177
+ OID: "1.3.6.1.2.1.1.3.0",
178
+ Name: "sysUpTime",
179
+ },
180
+ },
181
+ },
182
+ },
183
+ },
184
+ },
185
+ setupMock: func(m *snmpmock.MockHandler) {
186
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
187
+ // First call for device metadata
188
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.1.3.2.0"}).Return(
189
+ &gosnmp.SnmpPacket{
190
+ Variables: []gosnmp.SnmpPDU{
191
+ {
192
+ Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
193
+ Type: gosnmp.OctetString,
194
+ Value: []byte("ABC123"),
195
+ },
196
+ },
197
+ }, nil,
198
+ )
199
+ // Second call for metrics
200
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
201
+ &gosnmp.SnmpPacket{
202
+ Variables: []gosnmp.SnmpPDU{
203
+ {
204
+ Name: "1.3.6.1.2.1.1.3.0",
205
+ Type: gosnmp.TimeTicks,
206
+ Value: uint32(123456),
207
+ },
208
+ },
209
+ }, nil,
210
+ )
211
+ },
212
+ expectedResult: []*ProfileMetrics{
213
+ {
214
+ DeviceMetadata: map[string]string{
215
+ "vendor": "dell",
216
+ "serial_number": "ABC123",
217
+ },
218
+ Metrics: []Metric{
219
+ {
220
+ Name: "sysUpTime",
221
+ Value: 123456,
222
+ Tags: map[string]string{},
223
+ MetricType: "gauge",
224
+ },
225
+ },
226
+ },
227
+ },
228
+ expectedError: false,
229
+ },
230
+ "metric with scale factor": {
231
+ profiles: []*ddsnmp.Profile{
232
+ {
233
+ SourceFile: "test-profile.yaml",
234
+ Definition: &ddprofiledefinition.ProfileDefinition{
235
+ Metrics: []ddprofiledefinition.MetricsConfig{
236
+ {
237
+ Symbol: ddprofiledefinition.SymbolConfig{
238
+ OID: "1.3.6.1.4.1.12124.1.1.2",
239
+ Name: "memoryKilobytes",
240
+ ScaleFactor: 1000, // Convert KB to bytes
241
+ },
242
+ },
243
+ },
244
+ },
245
+ },
246
+ },
247
+ setupMock: func(m *snmpmock.MockHandler) {
248
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
249
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.2"}).Return(
250
+ &gosnmp.SnmpPacket{
251
+ Variables: []gosnmp.SnmpPDU{
252
+ {
253
+ Name: "1.3.6.1.4.1.12124.1.1.2",
254
+ Type: gosnmp.Gauge32,
255
+ Value: uint(1024),
256
+ },
257
+ },
258
+ }, nil,
259
+ )
260
+ },
261
+ expectedResult: []*ProfileMetrics{
262
+ {
263
+ DeviceMetadata: nil,
264
+ Metrics: []Metric{
265
+ {
266
+ Name: "memoryKilobytes",
267
+ Value: 1024000, // 1024 * 1000
268
+ Tags: map[string]string{},
269
+ MetricType: "gauge",
270
+ },
271
+ },
272
+ },
273
+ },
274
+ expectedError: false,
275
+ },
276
+ "OID not found - returns empty metrics": {
277
+ profiles: []*ddsnmp.Profile{
278
+ {
279
+ SourceFile: "test-profile.yaml",
280
+ Definition: &ddprofiledefinition.ProfileDefinition{
281
+ Metrics: []ddprofiledefinition.MetricsConfig{
282
+ {
283
+ Symbol: ddprofiledefinition.SymbolConfig{
284
+ OID: "1.3.6.1.2.1.1.3.0",
285
+ Name: "sysUpTime",
286
+ },
287
+ },
288
+ },
289
+ },
290
+ },
291
+ },
292
+ setupMock: func(m *snmpmock.MockHandler) {
293
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
294
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
295
+ &gosnmp.SnmpPacket{
296
+ Variables: []gosnmp.SnmpPDU{
297
+ {
298
+ Name: "1.3.6.1.2.1.1.3.0",
299
+ Type: gosnmp.NoSuchObject,
300
+ Value: nil,
301
+ },
302
+ },
303
+ }, nil,
304
+ )
305
+ },
306
+ expectedResult: []*ProfileMetrics{
307
+ {
308
+ DeviceMetadata: nil,
309
+ Metrics: []Metric{},
310
+ },
311
+ },
312
+ expectedError: false,
313
+ },
314
+ "SNMP error": {
315
+ profiles: []*ddsnmp.Profile{
316
+ {
317
+ SourceFile: "test-profile.yaml",
318
+ Definition: &ddprofiledefinition.ProfileDefinition{
319
+ Metrics: []ddprofiledefinition.MetricsConfig{
320
+ {
321
+ Symbol: ddprofiledefinition.SymbolConfig{
322
+ OID: "1.3.6.1.2.1.1.3.0",
323
+ Name: "sysUpTime",
324
+ },
325
+ },
326
+ },
327
+ },
328
+ },
329
+ },
330
+ setupMock: func(m *snmpmock.MockHandler) {
331
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
332
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
333
+ (*gosnmp.SnmpPacket)(nil),
334
+ errors.New("SNMP timeout"),
335
+ )
336
+ },
337
+ expectedResult: nil,
338
+ expectedError: true,
339
+ errorContains: "SNMP timeout",
340
+ },
341
+ "multiple profiles - one fails": {
342
+ profiles: []*ddsnmp.Profile{
343
+ {
344
+ SourceFile: "profile1.yaml",
345
+ Definition: &ddprofiledefinition.ProfileDefinition{
346
+ Metrics: []ddprofiledefinition.MetricsConfig{
347
+ {
348
+ Symbol: ddprofiledefinition.SymbolConfig{
349
+ OID: "1.3.6.1.2.1.1.3.0",
350
+ Name: "sysUpTime",
351
+ },
352
+ },
353
+ },
354
+ },
355
+ },
356
+ {
357
+ SourceFile: "profile2.yaml",
358
+ Definition: &ddprofiledefinition.ProfileDefinition{
359
+ Metrics: []ddprofiledefinition.MetricsConfig{
360
+ {
361
+ Symbol: ddprofiledefinition.SymbolConfig{
362
+ OID: "1.3.6.1.2.1.1.5.0",
363
+ Name: "sysName",
364
+ },
365
+ },
366
+ },
367
+ },
368
+ },
369
+ },
370
+ setupMock: func(m *snmpmock.MockHandler) {
371
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
372
+ // First profile succeeds
373
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
374
+ &gosnmp.SnmpPacket{
375
+ Variables: []gosnmp.SnmpPDU{
376
+ {
377
+ Name: "1.3.6.1.2.1.1.3.0",
378
+ Type: gosnmp.TimeTicks,
379
+ Value: uint32(123456),
380
+ },
381
+ },
382
+ }, nil,
383
+ )
384
+ // Second profile fails
385
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
386
+ (*gosnmp.SnmpPacket)(nil),
387
+ errors.New("connection refused"),
388
+ )
389
+ },
390
+ expectedResult: []*ProfileMetrics{
391
+ {
392
+ DeviceMetadata: nil,
393
+ Metrics: []Metric{
394
+ {
395
+ Name: "sysUpTime",
396
+ Value: 123456,
397
+ Tags: map[string]string{},
398
+ MetricType: "gauge",
399
+ },
400
+ },
401
+ },
402
+ },
403
+ expectedError: false, // Should return partial results
404
+ },
405
+ "metric with extract_value": {
406
+ profiles: []*ddsnmp.Profile{
407
+ {
408
+ SourceFile: "test-profile.yaml",
409
+ Definition: &ddprofiledefinition.ProfileDefinition{
410
+ Metrics: []ddprofiledefinition.MetricsConfig{
411
+ {
412
+ Symbol: ddprofiledefinition.SymbolConfig{
413
+ OID: "1.3.6.1.4.1.12124.1.1.8",
414
+ Name: "temperature",
415
+ ExtractValueCompiled: mustCompileRegex(`(\d+)C`),
416
+ },
417
+ },
418
+ },
419
+ },
420
+ },
421
+ },
422
+ setupMock: func(m *snmpmock.MockHandler) {
423
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
424
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.8"}).Return(
425
+ &gosnmp.SnmpPacket{
426
+ Variables: []gosnmp.SnmpPDU{
427
+ {
428
+ Name: "1.3.6.1.4.1.12124.1.1.8",
429
+ Type: gosnmp.OctetString,
430
+ Value: []byte("25C"),
431
+ },
432
+ },
433
+ }, nil,
434
+ )
435
+ },
436
+ expectedResult: []*ProfileMetrics{
437
+ {
438
+ DeviceMetadata: nil,
439
+ Metrics: []Metric{
440
+ {
441
+ Name: "temperature",
442
+ Value: 25,
443
+ Tags: map[string]string{},
444
+ MetricType: "gauge",
445
+ },
446
+ },
447
+ },
448
+ },
449
+ expectedError: false,
450
+ },
451
+ "metric with mapping": {
452
+ profiles: []*ddsnmp.Profile{
453
+ {
454
+ SourceFile: "test-profile.yaml",
455
+ Definition: &ddprofiledefinition.ProfileDefinition{
456
+ Metrics: []ddprofiledefinition.MetricsConfig{
457
+ {
458
+ Symbol: ddprofiledefinition.SymbolConfig{
459
+ OID: "1.3.6.1.4.1.12124.1.1.2",
460
+ Name: "clusterHealth",
461
+ Mapping: map[string]string{
462
+ "OK": "0",
463
+ "WARNING": "1",
464
+ "CRITICAL": "2",
465
+ },
466
+ },
467
+ },
468
+ },
469
+ },
470
+ },
471
+ },
472
+ setupMock: func(m *snmpmock.MockHandler) {
473
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
474
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.2"}).Return(
475
+ &gosnmp.SnmpPacket{
476
+ Variables: []gosnmp.SnmpPDU{
477
+ {
478
+ Name: "1.3.6.1.4.1.12124.1.1.2",
479
+ Type: gosnmp.OctetString,
480
+ Value: []byte("WARNING"),
481
+ },
482
+ },
483
+ }, nil,
484
+ )
485
+ },
486
+ expectedResult: []*ProfileMetrics{
487
+ {
488
+ DeviceMetadata: nil,
489
+ Metrics: []Metric{
490
+ {
491
+ Name: "clusterHealth",
492
+ Value: 1,
493
+ Tags: map[string]string{},
494
+ MetricType: "gauge",
495
+ },
496
+ },
497
+ },
498
+ },
499
+ expectedError: false,
500
+ },
501
+ "global tags with mapping": {
502
+ profiles: []*ddsnmp.Profile{
503
+ {
504
+ SourceFile: "test-profile.yaml",
505
+ Definition: &ddprofiledefinition.ProfileDefinition{
506
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
507
+ {
508
+ Tag: "device_type",
509
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
510
+ OID: "1.3.6.1.2.1.1.2.0",
511
+ Name: "sysObjectID",
512
+ },
513
+ Mapping: map[string]string{
514
+ "1.3.6.1.4.1.9.1.1": "router",
515
+ "1.3.6.1.4.1.9.1.2": "switch",
516
+ },
517
+ },
518
+ },
519
+ Metrics: []ddprofiledefinition.MetricsConfig{
520
+ {
521
+ Symbol: ddprofiledefinition.SymbolConfig{
522
+ OID: "1.3.6.1.2.1.1.3.0",
523
+ Name: "sysUpTime",
524
+ },
525
+ },
526
+ },
527
+ },
528
+ },
529
+ },
530
+ setupMock: func(m *snmpmock.MockHandler) {
531
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
532
+ // First call for global tags
533
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.2.0"}).Return(
534
+ &gosnmp.SnmpPacket{
535
+ Variables: []gosnmp.SnmpPDU{
536
+ {
537
+ Name: "1.3.6.1.2.1.1.2.0",
538
+ Type: gosnmp.ObjectIdentifier,
539
+ Value: "1.3.6.1.4.1.9.1.1",
540
+ },
541
+ },
542
+ }, nil,
543
+ )
544
+ // Second call for metrics
545
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
546
+ &gosnmp.SnmpPacket{
547
+ Variables: []gosnmp.SnmpPDU{
548
+ {
549
+ Name: "1.3.6.1.2.1.1.3.0",
550
+ Type: gosnmp.TimeTicks,
551
+ Value: uint32(123456),
552
+ },
553
+ },
554
+ }, nil,
555
+ )
556
+ },
557
+ expectedResult: []*ProfileMetrics{
558
+ {
559
+ DeviceMetadata: nil,
560
+ Metrics: []Metric{
561
+ {
562
+ Name: "sysUpTime",
563
+ Value: 123456,
564
+ Tags: map[string]string{"device_type": "router"},
565
+ MetricType: "gauge",
566
+ },
567
+ },
568
+ },
569
+ },
570
+ expectedError: false,
571
+ },
572
+ "empty profile - no metrics defined": {
573
+ profiles: []*ddsnmp.Profile{
574
+ {
575
+ SourceFile: "empty-profile.yaml",
576
+ Definition: &ddprofiledefinition.ProfileDefinition{
577
+ Metrics: []ddprofiledefinition.MetricsConfig{},
578
+ },
579
+ },
580
+ },
581
+ setupMock: func(m *snmpmock.MockHandler) {
582
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
583
+ },
584
+ expectedResult: []*ProfileMetrics{
585
+ {
586
+ DeviceMetadata: nil,
587
+ Metrics: []Metric{},
588
+ },
589
+ },
590
+ expectedError: false,
591
+ },
592
+ }
593
+
594
+ for name, tc := range tests {
595
+ t.Run(name, func(t *testing.T) {
596
+ // Create gomock controller
597
+ ctrl := gomock.NewController(t)
598
+ defer ctrl.Finish()
599
+
600
+ // Create mock SNMP client
601
+ mockHandler := snmpmock.NewMockHandler(ctrl)
602
+ tc.setupMock(mockHandler)
603
+
604
+ // Create logger
605
+ log := logger.New()
606
+
607
+ // Create collector
608
+ collector := New(mockHandler, tc.profiles, log)
609
+
610
+ // Execute
611
+ result, err := collector.Collect()
612
+
613
+ // Verify error
614
+ if tc.expectedError {
615
+ assert.Error(t, err)
616
+ if tc.errorContains != "" {
617
+ assert.Contains(t, err.Error(), tc.errorContains)
618
+ }
619
+ } else {
620
+ assert.NoError(t, err)
621
+ }
622
+
623
+ // Verify result
624
+ if tc.expectedResult != nil {
625
+ assert.Equal(t, len(tc.expectedResult), len(result))
626
+ for i := range tc.expectedResult {
627
+ assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
628
+ assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
629
+ }
630
+ } else {
631
+ assert.Nil(t, result)
632
+ }
633
+ })
634
+ }
635
+}
636
+
637
+func mustCompileRegex(pattern string) *regexp.Regexp {
638
+ re, err := regexp.Compile(pattern)
639
+ if err != nil {
640
+ panic(err)
641
+ }
642
+ return re
643
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/utils.go
new
+216
@@ -0,0 +1,216 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "encoding/hex"
7
+ "fmt"
8
+ "regexp"
9
+ "strconv"
10
+ "strings"
11
+
12
+ "github.com/gosnmp/gosnmp"
13
+
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
+)
16
+
17
+func getMetricType(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) ddprofiledefinition.ProfileMetricType {
18
+ if sym.MetricType != "" {
19
+ return sym.MetricType
20
+ }
21
+ return getMetricTypeFromPDUType(pdu)
22
+}
23
+
24
+func getMetricTypeFromPDUType(pdu gosnmp.SnmpPDU) ddprofiledefinition.ProfileMetricType {
25
+ switch pdu.Type {
26
+ case gosnmp.Counter32, gosnmp.Counter64:
27
+ // Counters are submitted as rates by default
28
+ return ddprofiledefinition.ProfileMetricTypeRate
29
+ case gosnmp.Gauge32, gosnmp.Integer, gosnmp.Uinteger32, gosnmp.OpaqueFloat, gosnmp.OpaqueDouble:
30
+ // Numeric types representing current values are submitted as gauges
31
+ return ddprofiledefinition.ProfileMetricTypeGauge
32
+ case gosnmp.TimeTicks:
33
+ // TimeTicks (hundredths of a second) are typically static or slow changing values
34
+ return ddprofiledefinition.ProfileMetricTypeGauge
35
+ case gosnmp.IPAddress, gosnmp.OctetString, gosnmp.ObjectIdentifier, gosnmp.BitString:
36
+ // String-like types typically get converted to a numeric value to represent presence (1)
37
+ // or some extraction of numeric info from the string
38
+ return ddprofiledefinition.ProfileMetricTypeGauge
39
+ case gosnmp.Boolean:
40
+ // Boolean values (true/false) fit naturally as gauges (1/0)
41
+ return ddprofiledefinition.ProfileMetricTypeGauge
42
+ default:
43
+ return ddprofiledefinition.ProfileMetricTypeGauge
44
+ }
45
+}
46
+
47
+func convPhysAddressToString(pdu gosnmp.SnmpPDU) (string, error) {
48
+ address, ok := pdu.Value.([]uint8)
49
+ if !ok {
50
+ return "", fmt.Errorf("physAddress is not a []uint8 or []byte but %T", pdu.Value)
51
+ }
52
+
53
+ parts := make([]string, 0, len(address))
54
+ for _, v := range address {
55
+ parts = append(parts, fmt.Sprintf("%02X", v))
56
+ }
57
+ return strings.Join(parts, ":"), nil
58
+}
59
+
60
+func convPduToStringf(pdu gosnmp.SnmpPDU, format string) (string, error) {
61
+ switch format {
62
+ case "mac_address":
63
+ return convPhysAddressToString(pdu)
64
+ case "ip_address":
65
+ if pdu.Type == gosnmp.IPAddress {
66
+ // Use the default handler for IP addresses
67
+ return convPduToString(pdu)
68
+ }
69
+
70
+ // Try to handle as bytes that represent an IP
71
+ bs, ok := pdu.Value.([]byte)
72
+ if !ok {
73
+ return "", fmt.Errorf("cannot convert %T to IP address", pdu.Value)
74
+ }
75
+
76
+ if len(bs) == 4 {
77
+ // IPv4
78
+ return fmt.Sprintf("%d.%d.%d.%d", bs[0], bs[1], bs[2], bs[3]), nil
79
+ } else if len(bs) == 16 {
80
+ // IPv6
81
+ parts := make([]string, 0, 8)
82
+ for i := 0; i < 16; i += 2 {
83
+ parts = append(parts, fmt.Sprintf("%02x%02x", bs[i], bs[i+1]))
84
+ }
85
+ return strings.Join(parts, ":"), nil
86
+ }
87
+
88
+ return "", fmt.Errorf("cannot convert %v to IP address (incorrect length)", pdu.Value)
89
+ case "hex":
90
+ // Convert any value to hex string
91
+ bs, ok := pdu.Value.([]byte)
92
+ if !ok {
93
+ return "", fmt.Errorf("cannot convert %T to hex", pdu.Value)
94
+ }
95
+ return hex.EncodeToString(bs), nil
96
+ default:
97
+ // For unknown formats, use the default string conversion
98
+ return convPduToString(pdu)
99
+ }
100
+}
101
+
102
+func convPduToString(pdu gosnmp.SnmpPDU) (string, error) {
103
+ switch pdu.Type {
104
+ case gosnmp.NoSuchObject, gosnmp.NoSuchInstance, gosnmp.Null:
105
+ return "", fmt.Errorf("object not available: %v", pdu.Type)
106
+ case gosnmp.OctetString:
107
+ var bs []byte
108
+ switch v := pdu.Value.(type) {
109
+ case []byte:
110
+ bs = v
111
+ case string:
112
+ return v, nil
113
+ default:
114
+ return "", fmt.Errorf("OctetString has unexpected type %T", pdu.Value)
115
+ }
116
+
117
+ // Convert to string and check if it can be represented as a raw string literal
118
+ s := string(bs)
119
+ if strconv.CanBackquote(s) {
120
+ return s, nil
121
+ }
122
+ return hex.EncodeToString(bs), nil
123
+ case gosnmp.Counter32, gosnmp.Counter64, gosnmp.Integer, gosnmp.Gauge32, gosnmp.Uinteger32, gosnmp.TimeTicks:
124
+ return gosnmp.ToBigInt(pdu.Value).String(), nil
125
+ case gosnmp.IPAddress:
126
+ switch v := pdu.Value.(type) {
127
+ case []byte:
128
+ if len(v) == 4 {
129
+ return fmt.Sprintf("%d.%d.%d.%d", v[0], v[1], v[2], v[3]), nil
130
+ } else if len(v) == 16 {
131
+ parts := make([]string, 0, 8)
132
+ for i := 0; i < 16; i += 2 {
133
+ parts = append(parts, fmt.Sprintf("%02x%02x", v[i], v[i+1]))
134
+ }
135
+ return strings.Join(parts, ":"), nil
136
+ }
137
+ return hex.EncodeToString(v), nil
138
+ case string:
139
+ return v, nil
140
+ default:
141
+ return "", fmt.Errorf("IPAddress has unexpected type %T", pdu.Value)
142
+ }
143
+ case gosnmp.ObjectIdentifier:
144
+ v, ok := pdu.Value.(string)
145
+ if !ok {
146
+ return "", fmt.Errorf("ObjectIdentifier is not a string but %T", pdu.Value)
147
+ }
148
+ return strings.TrimPrefix(v, "."), nil
149
+ case gosnmp.Boolean:
150
+ b, ok := pdu.Value.(bool)
151
+ if !ok {
152
+ return "", fmt.Errorf("boolean is not a bool but %T", pdu.Value)
153
+ }
154
+ if b {
155
+ return "true", nil
156
+ }
157
+ return "false", nil
158
+ default:
159
+ return fmt.Sprintf("%v", pdu.Value), nil
160
+ }
161
+}
162
+
163
+func isPduWithData(pdu gosnmp.SnmpPDU) bool {
164
+ switch pdu.Type {
165
+ case gosnmp.NoSuchObject,
166
+ gosnmp.NoSuchInstance,
167
+ gosnmp.Null,
168
+ gosnmp.EndOfMibView:
169
+ return false
170
+ default:
171
+ return true
172
+ }
173
+}
174
+
175
+func isPduNumericType(pdu gosnmp.SnmpPDU) bool {
176
+ switch pdu.Type {
177
+ case gosnmp.Counter32,
178
+ gosnmp.Counter64,
179
+ gosnmp.Integer,
180
+ gosnmp.Gauge32,
181
+ gosnmp.Uinteger32,
182
+ gosnmp.TimeTicks:
183
+ return true
184
+ default:
185
+ return false
186
+ }
187
+}
188
+
189
+func trimOID(oid string) string {
190
+ return strings.TrimPrefix(oid, ".")
191
+}
192
+
193
+var reBackRef = regexp.MustCompile(`([$\\])(\d+)`)
194
+
195
+func replaceSubmatches(template string, submatches []string) string {
196
+ return reBackRef.ReplaceAllStringFunc(template, func(match string) string {
197
+ parts := reBackRef.FindStringSubmatch(match)
198
+ if len(parts) < 3 {
199
+ return match
200
+ }
201
+
202
+ groupNum, err := strconv.Atoi(parts[2])
203
+ if err != nil || groupNum >= len(submatches) {
204
+ return match
205
+ }
206
+
207
+ return submatches[groupNum]
208
+ })
209
+}
210
+
211
+func ternary[T any](cond bool, a, b T) T {
212
+ if cond {
213
+ return a
214
+ }
215
+ return b
216
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/load.go
deleted
-111
@@ -1,111 +0,0 @@
1
-// SPDX-License-Identifier: GPL-3.0-or-later
2
-
3
-package ddsnmp
4
-
5
-import (
6
- "errors"
7
- "io/fs"
8
- "os"
9
- "path/filepath"
10
- "strings"
11
-
12
- "gopkg.in/yaml.v2"
13
-
14
- "github.com/netdata/netdata/go/plugins/logger"
15
- "github.com/netdata/netdata/go/plugins/pkg/executable"
16
-)
17
-
18
-var log = logger.New().With("component", "snmp/ddsnmp")
19
-
20
-var ddProfiles []*Profile
21
-
22
-func init() {
23
- dir := os.Getenv("NETDATA_STOCK_CONFIG_DIR")
24
- if dir != "" {
25
- dir = filepath.Join(dir, "go.d/snmp.profiles/default")
26
- } else {
27
- if dir, _ = filepath.Abs("../../../config/go.d/snmp.profiles/default"); !isDirExists(dir) {
28
- dir = filepath.Join(executable.Directory, "../../../../usr/lib/netdata/conf.d/go.d/snmp.profiles/default")
29
- }
30
- }
31
- profiles, err := load(dir)
32
- if err != nil {
33
- log.Errorf("failed to load dd snmp profiles: %v", err)
34
- return
35
- }
36
- if len(profiles) == 0 {
37
- log.Warningf("no dd snmp profiles found in '%s'", dir)
38
- return
39
- }
40
-
41
- log.Infof("found %d profiles in '%s'", len(profiles), dir)
42
- ddProfiles = profiles
43
-}
44
-
45
-func load(dirpath string) ([]*Profile, error) {
46
- var profiles []*Profile
47
-
48
- if err := filepath.WalkDir(dirpath, func(path string, d fs.DirEntry, err error) error {
49
- if err != nil {
50
- return err
51
- }
52
- if !(strings.HasSuffix(d.Name(), ".yaml") || strings.HasSuffix(d.Name(), ".yml")) {
53
- return nil
54
- }
55
-
56
- profile, err := loadProfile(path)
57
- if err != nil {
58
- log.Warningf("invalid profile '%s': %v", path, err)
59
- return nil
60
- }
61
-
62
- if err := profile.validate(); err != nil {
63
- log.Warningf("invalid profile '%s': %v", path, err)
64
- return nil
65
- }
66
-
67
- profiles = append(profiles, profile)
68
- return nil
69
- }); err != nil {
70
- return nil, err
71
- }
72
-
73
- return profiles, nil
74
-}
75
-
76
-func loadProfile(filename string) (*Profile, error) {
77
- content, err := os.ReadFile(filename)
78
- if err != nil {
79
- return nil, err
80
- }
81
-
82
- var prof Profile
83
- if err := yaml.Unmarshal(content, &prof.Definition); err != nil {
84
- return nil, err
85
- }
86
-
87
- if prof.SourceFile == "" {
88
- prof.SourceFile, _ = filepath.Abs(filename)
89
- }
90
-
91
- dir := filepath.Dir(filename)
92
-
93
- for _, name := range prof.Definition.Extends {
94
- baseProf, err := loadProfile(filepath.Join(dir, name))
95
- if err != nil {
96
- return nil, err
97
- }
98
-
99
- prof.merge(baseProf)
100
- }
101
-
102
- return &prof, nil
103
-}
104
-
105
-func isDirExists(dir string) bool {
106
- fi, err := os.Stat(dir)
107
- if err != nil {
108
- return !errors.Is(err, fs.ErrNotExist)
109
- }
110
- return fi.Mode().IsDir()
111
-}
src/go/plugin/go.d/collector/snmp/ddsnmp/load_test.go
deleted
-28
@@ -1,28 +0,0 @@
1
-// SPDX-License-Identifier: GPL-3.0-or-later
2
-
3
-package ddsnmp
4
-
5
-import (
6
- "os"
7
- "path/filepath"
8
- "testing"
9
-
10
- "github.com/stretchr/testify/require"
11
-)
12
-
13
-func Test_loadDDSnmpProfiles(t *testing.T) {
14
- dir, _ := filepath.Abs("../../../config/go.d/snmp.profiles/default")
15
-
16
- f, err := os.Open(dir)
17
- require.NoError(t, err)
18
- defer f.Close()
19
-
20
- profiles, err := load(dir)
21
- require.NoError(t, err)
22
-
23
- names, err := f.Readdirnames(-1)
24
- require.NoError(t, err)
25
- require.NotEmpty(t, names)
26
-
27
- require.Equal(t, len(names)-1 /*README.md*/, len(profiles))
28
-}
src/go/plugin/go.d/collector/snmp/ddsnmp/profile.go
+134
-8
@@ -4,12 +4,44 @@ package ddsnmp
4
5
import (
6
"errors"
7
+ "io/fs"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
12
+ "gopkg.in/yaml.v2"
13
+
14
+ "github.com/netdata/netdata/go/plugins/logger"
15
+ "github.com/netdata/netdata/go/plugins/pkg/executable"
16
"github.com/netdata/netdata/go/plugins/pkg/matcher"
17
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
18
)
19
12
-func Find(sysObjId string) []*Profile {
20
+var log = logger.New().With("component", "snmp/ddsnmp")
21
+
22
+var ddProfiles []*Profile
23
+
24
+func init() {
25
+ dir := getProfilesDir()
26
+
27
+ profiles, err := loadProfiles(dir)
28
+ if err != nil {
29
+ log.Errorf("failed to loadProfiles dd snmp profiles: %v", err)
30
+ return
31
+ }
32
+
33
+ if len(profiles) == 0 {
34
+ log.Warningf("no dd snmp profiles found in '%s'", dir)
35
+ return
36
+ }
37
+
38
+ log.Infof("found %d profiles in '%s'", len(profiles), dir)
39
+ ddProfiles = profiles
40
+
41
+ return
42
+}
43
+
44
+func FindProfiles(sysObjId string) []*Profile {
45
var profiles []*Profile
46
47
for _, prof := range ddProfiles {
@@ -41,6 +73,7 @@ func (p *Profile) clone() *Profile {
73
}
74
75
func (p *Profile) merge(base *Profile) {
76
+ // Append metrics (deep clone already handled in the Definition.Clone method)
77
p.Definition.Metrics = append(p.Definition.Metrics, base.Definition.Metrics...)
78
p.Definition.MetricTags = append(p.Definition.MetricTags, base.Definition.MetricTags...)
79
p.Definition.StaticTags = append(p.Definition.StaticTags, base.Definition.StaticTags...)
@@ -74,16 +107,109 @@ func (p *Profile) merge(base *Profile) {
107
func (p *Profile) validate() error {
108
ddprofiledefinition.NormalizeMetrics(p.Definition.Metrics)
109
77
- errs := ddprofiledefinition.ValidateEnrichMetadata(p.Definition.Metadata)
78
- errs = append(errs, ddprofiledefinition.ValidateEnrichMetrics(p.Definition.Metrics)...)
79
- errs = append(errs, ddprofiledefinition.ValidateEnrichMetricTags(p.Definition.MetricTags)...)
110
+ var errs []error
111
+
112
+ for _, err := range ddprofiledefinition.ValidateEnrichMetadata(p.Definition.Metadata) {
113
+ errs = append(errs, errors.New(err))
114
+ }
115
+ for _, err := range ddprofiledefinition.ValidateEnrichMetrics(p.Definition.Metrics) {
116
+ errs = append(errs, errors.New(err))
117
+ }
118
+ for _, err := range ddprofiledefinition.ValidateEnrichMetricTags(p.Definition.MetricTags) {
119
+ errs = append(errs, errors.New(err))
120
+ }
121
if len(errs) > 0 {
81
- errList := make([]error, 0, len(errs))
82
- for _, s := range errs {
83
- errList = append(errList, errors.New(s))
122
+ return errors.Join(errs...)
123
+ }
124
+
125
+ return nil
126
+}
127
+
128
+func loadProfiles(dirpath string) ([]*Profile, error) {
129
+ var profiles []*Profile
130
+
131
+ if err := filepath.WalkDir(dirpath, func(path string, d fs.DirEntry, err error) error {
132
+ if err != nil {
133
+ return err
134
+ }
135
+ if !(strings.HasSuffix(d.Name(), ".yaml") || strings.HasSuffix(d.Name(), ".yml")) {
136
+ return nil
137
+ }
138
+
139
+ profile, err := loadProfile(path)
140
+ if err != nil {
141
+ log.Warningf("invalid profile '%s': %v", path, err)
142
+ return nil
143
+ }
144
+
145
+ if err := profile.validate(); err != nil {
146
+ log.Warningf("invalid profile '%s': %v", path, err)
147
+ return nil
148
}
85
- return errors.Join(errList...)
149
+
150
+ profiles = append(profiles, profile)
151
+ return nil
152
+ }); err != nil {
153
+ return nil, err
154
+ }
155
+
156
+ return profiles, nil
157
+}
158
+
159
+func loadProfile(filename string) (*Profile, error) {
160
+ content, err := os.ReadFile(filename)
161
+ if err != nil {
162
+ return nil, err
163
+ }
164
+
165
+ var prof Profile
166
+ if err := yaml.Unmarshal(content, &prof.Definition); err != nil {
167
+ return nil, err
168
+ }
169
+
170
+ if prof.SourceFile == "" {
171
+ prof.SourceFile, _ = filepath.Abs(filename)
172
+ }
173
+
174
+ dir := filepath.Dir(filename)
175
+
176
+ processedExtends := make(map[string]bool)
177
+ if err := loadProfileExtensions(&prof, dir, processedExtends); err != nil {
178
+ return nil, err
179
+ }
180
+
181
+ return &prof, nil
182
+}
183
+
184
+func loadProfileExtensions(profile *Profile, dir string, processedExtends map[string]bool) error {
185
+ for _, name := range profile.Definition.Extends {
186
+ if processedExtends[name] {
187
+ continue
188
+ }
189
+ processedExtends[name] = true
190
+
191
+ baseProf, err := loadProfile(filepath.Join(dir, name))
192
+ if err != nil {
193
+ return err
194
+ }
195
+
196
+ if err := loadProfileExtensions(baseProf, dir, processedExtends); err != nil {
197
+ return err
198
+ }
199
+
200
+ profile.merge(baseProf)
201
}
202
203
return nil
204
}
205
+
206
+func getProfilesDir() string {
207
+ if executable.Name == "test" {
208
+ dir, _ := filepath.Abs("../../../config/go.d/snmp.profiles/default")
209
+ return dir
210
+ }
211
+ if dir := os.Getenv("NETDATA_STOCK_CONFIG_DIR"); dir != "" {
212
+ return filepath.Join(dir, "go.d/snmp.profiles/default")
213
+ }
214
+ return filepath.Join(executable.Directory, "../../../config/go.d/snmp.profiles/default")
215
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/profile_test.go
+22
-2
@@ -3,12 +3,32 @@
3
package ddsnmp
4
5
import (
6
+ "os"
7
+ "path/filepath"
8
"testing"
9
10
"github.com/stretchr/testify/require"
11
)
12
11
-func Test_Find(t *testing.T) {
13
+func Test_loadDDSnmpProfiles(t *testing.T) {
14
+ dir, _ := filepath.Abs("../../../config/go.d/snmp.profiles/default")
15
+
16
+ f, err := os.Open(dir)
17
+ require.NoError(t, err)
18
+ defer f.Close()
19
+
20
+ profiles, err := loadProfiles(dir)
21
+ require.NoError(t, err)
22
+
23
+ require.NotEmpty(t, profiles)
24
+
25
+ names, err := f.Readdirnames(-1)
26
+ require.NoError(t, err)
27
+
28
+ require.Equal(t, len(names)-1 /*README.md*/, len(profiles))
29
+}
30
+
31
+func Test_FindProfiles(t *testing.T) {
32
test := map[string]struct {
33
sysObjOId string
34
wanProfiles int
@@ -25,7 +45,7 @@ func Test_Find(t *testing.T) {
45
46
for name, test := range test {
47
t.Run(name, func(t *testing.T) {
28
- profiles := Find(test.sysObjOId)
48
+ profiles := FindProfiles(test.sysObjOId)
49
50
require.Len(t, profiles, test.wanProfiles)
51
})
src/go/plugin/go.d/collector/snmp/parse_profiles.go
deleted
-296
@@ -1,296 +0,0 @@
1
-package snmp
2
-
3
-import (
4
- "errors"
5
- "fmt"
6
- "regexp"
7
-
8
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
9
-)
10
-
11
-type (
12
- parsedResult struct {
13
- OIDs []string
14
- nextOIDs []string
15
- bulkOIDs []string
16
- parsedMetrics []parsedMetric
17
- }
18
- parsedMetric any
19
-)
20
-
21
-type (
22
- tableBatches map[tableBatchKey]tableBatch
23
- tableBatchKey struct {
24
- mib string
25
- table string
26
- }
27
- tableBatch struct {
28
- tableOID string
29
- oids []string
30
- }
31
-)
32
-
33
-type indexTag struct {
34
- parsedMetricTag parsedMetricTag
35
- index int
36
-}
37
-
38
-type columnTag struct {
39
- parsedMetricTag parsedMetricTag
40
- column string
41
- indexSlices []indexSlice
42
-}
43
-
44
-type indexMapping struct {
45
- tag string
46
- index int
47
- mapping map[int]string
48
-}
49
-
50
-type parsedSymbol struct {
51
- name string
52
- oid string
53
- extractValuePattern *regexp.Regexp
54
- oidsToResolve map[string]string
55
-}
56
-
57
-type parsedSymbolMetric struct {
58
- name string
59
- tags []string
60
- forcedType string
61
- enforceScalar bool
62
- options map[string]string
63
- extractValuePattern *regexp.Regexp
64
- baseoid string //TODO consider changing this to OID, it will not have nested OIDs as it is a symbol
65
- unit string
66
- description string
67
-}
68
-
69
-type parsedTableMetric struct {
70
- name string
71
- indexTags []indexTag
72
- columnTags []columnTag
73
- forcedType string
74
- options map[string]string
75
- extractValuePattern *regexp.Regexp
76
- rowOID string
77
- tableName string
78
- tableOID string
79
-}
80
-
81
-// union of two above
82
-
83
-type parsedMetricTag struct {
84
- name string
85
-
86
- tags []string
87
- pattern *regexp.Regexp
88
- // symbol Symbol not used yet
89
-}
90
-
91
-type metricParseResult struct {
92
- oidsToFetch []string
93
- oidsToResolve map[string]string
94
- indexMappings []indexMapping
95
- tableBatches tableBatches
96
- parsedMetrics []parsedMetric
97
-}
98
-
99
-type indexSlice struct {
100
- Start int
101
- End int
102
-}
103
-
104
-func parseMetrics(metrics []ddprofiledefinition.MetricsConfig) (parsedResult, error) {
105
- var (
106
- OIDs, nextOIDs, bulkOIDs []string
107
- OIDsToResolve []map[string]string
108
- parsedMetrics []parsedMetric
109
- indexesToResolve []indexMapping
110
- )
111
-
112
- bulkThreshold := 0
113
- for _, metric := range metrics {
114
- result, err := parseMetric(metric)
115
-
116
- if err != nil {
117
- return parsedResult{}, err
118
- }
119
-
120
- OIDs = append(OIDs, result.oidsToFetch...)
121
-
122
- for name, oid := range result.oidsToResolve {
123
- // here in the python implementation a registration happens to their OIDResolver. I will not support this atm
124
- OIDsToResolve = append(OIDsToResolve, map[string]string{name: oid})
125
- }
126
-
127
- // here in the python implementation a registration happens to their OIDResolver. I will not support this atm
128
- indexesToResolve = append(indexesToResolve, result.indexMappings...)
129
-
130
- for _, batch := range result.tableBatches {
131
- should_query_in_bulk := bulkThreshold > 0 && len(batch.oids) > bulkThreshold
132
- if should_query_in_bulk {
133
- bulkOIDs = append(bulkOIDs, batch.tableOID)
134
- } else {
135
- nextOIDs = append(nextOIDs, batch.oids...)
136
- }
137
- }
138
-
139
- parsedMetrics = append(parsedMetrics, result.parsedMetrics...)
140
-
141
- }
142
- return parsedResult{
143
- OIDs: OIDs,
144
- nextOIDs: nextOIDs,
145
- bulkOIDs: bulkOIDs,
146
- parsedMetrics: parsedMetrics}, nil
147
-}
148
-
149
-func parseMetric(metric ddprofiledefinition.MetricsConfig) (metricParseResult, error) {
150
- /*Can either be:
151
-
152
- * An OID metric:
153
-
154
- ```
155
- metrics:
156
- - OID: 1.3.6.1.2.1.2.2.1.14
157
- name: ifInErrors
158
- ```
159
-
160
- * A symbol metric:
161
-
162
- ```
163
- metrics:
164
- - MIB: IF-MIB
165
- symbol: ifInErrors
166
- # OR:
167
- symbol:
168
- OID: 1.3.6.1.2.1.2.2.1.14
169
- name: ifInErrors
170
- ```
171
-
172
- * A table metric (see parsing for table metrics for all possible options):
173
-
174
- ```
175
- metrics:
176
- - MIB: IF-MIB
177
- table: ifTable
178
- symbols:
179
- - OID: 1.3.6.1.2.1.2.2.1.14
180
- name: ifInErrors
181
- ```*/
182
-
183
- // Can't support tags at the moment
184
-
185
- if len(metric.OID) > 0 {
186
- // TODO investigate if this exists in the yamls
187
- // return (parseOIDMetric(oidMetric{name: metric.Name, oid: metric.OID, metricTags: castedStringMetricTags, forcedType: string(metric.MetricType), options: metric.Options})), nil
188
- return metricParseResult{}, nil
189
- }
190
- if len(metric.MIB) == 0 {
191
- return metricParseResult{}, fmt.Errorf("unsupported metric {%v}", metric)
192
- }
193
- if metric.Symbol != (ddprofiledefinition.SymbolConfig{}) {
194
- // Single Metric
195
- return parseSymbolMetric(metric.Symbol, metric.MIB) // TODO metric tags might be needed here.
196
- //Can't support tables at the moment
197
- }
198
- return metricParseResult{}, nil
199
-
200
-}
201
-
202
-// TODO error outs on functions
203
-func parseSymbolMetric(symbol ddprofiledefinition.SymbolConfig, mib string) (metricParseResult, error) {
204
- /* Parse a symbol metric (= an OID in a MIB).
205
- Example:
206
-
207
- ```
208
- metrics:
209
- - MIB: IF-MIB
210
- symbol: <string or OID/name object>
211
- - MIB: IF-MIB
212
- symbol: # MIB-less syntax
213
- OID: 1.3.6.1.2.1.6.5.0
214
- name: tcpActiveOpens
215
- - MIB: IF-MIB
216
- symbol: tcpActiveOpens # require MIB syntax
217
- ```*/
218
-
219
- parsedSymbol, err := parseSymbol(symbol)
220
- if err != nil {
221
- return metricParseResult{}, err
222
- }
223
-
224
- parsedSymbolMetric := parsedSymbolMetric{
225
- name: parsedSymbol.name,
226
- tags: nil,
227
- forcedType: string(symbol.MetricType),
228
- enforceScalar: false,
229
- options: nil,
230
- extractValuePattern: parsedSymbol.extractValuePattern,
231
- baseoid: parsedSymbol.oid,
232
- unit: symbol.Unit,
233
- description: symbol.Description,
234
- }
235
-
236
- return metricParseResult{
237
- oidsToFetch: []string{parsedSymbol.oid},
238
- oidsToResolve: parsedSymbol.oidsToResolve,
239
- parsedMetrics: []parsedMetric{parsedSymbolMetric},
240
- tableBatches: nil,
241
- indexMappings: nil,
242
- }, nil
243
-}
244
-
245
-func parseSymbol(symbol interface{}) (parsedSymbol, error) {
246
- /*
247
- Parse an OID symbol.
248
-
249
- This can either be the unresolved name of a symbol:
250
-
251
- ```
252
- symbol: ifNumber
253
- ```
254
-
255
- Or a resolved OID/name object:
256
-
257
- ```
258
- symbol:
259
- OID: 1.3.6.1.2.1.2.1
260
- name: ifNumber
261
- ```
262
- */
263
-
264
- switch s := symbol.(type) {
265
- case ddprofiledefinition.SymbolConfig:
266
- ps := parsedSymbol{
267
- name: s.Name,
268
- oid: s.OID,
269
- oidsToResolve: map[string]string{s.Name: s.OID},
270
- }
271
- if s.ExtractValue != "" {
272
- if v, err := regexp.Compile(s.ExtractValue); err != nil {
273
- ps.extractValuePattern = v
274
- }
275
- }
276
- return ps, nil
277
- case string:
278
- return parsedSymbol{}, errors.New("string only symbol, can't support yet")
279
- case map[string]interface{}:
280
- oid, okOID := s["OID"].(string)
281
- name, okName := s["name"].(string)
282
-
283
- if !okOID || !okName {
284
- return parsedSymbol{}, fmt.Errorf("invalid symbol format: %+v", s)
285
- }
286
-
287
- return parsedSymbol{
288
- name: name,
289
- oid: oid,
290
- extractValuePattern: nil,
291
- oidsToResolve: map[string]string{name: oid},
292
- }, nil
293
- default:
294
- return parsedSymbol{}, fmt.Errorf("unsupported symbol type: %T", symbol)
295
- }
296
-}
src/go/plugin/go.d/config/go.d/snmp.profiles/default/mikrotik-router.yaml
+10
-10
@@ -116,16 +116,16 @@ metrics:
116
2: waiting_for_load
117
3: powered_on
118
4: overload
119
-# - MIB: MIKROTIK-MIB
120
-# symbol:
121
-# OID: 1.3.6.1.4.1.14988.1.1.3.17
122
-# name: mtxrHlFanSpeed1
123
-# string metric is not supported yet (keep this metric and this comment in profile until it's fixed)
124
-# - MIB: MIKROTIK-MIB
125
-# symbol:
126
-# OID: 1.3.6.1.4.1.14988.1.1.3.18
127
-# name: mtxrHlFanSpeed2
128
-# string metric is not supported yet (keep this metric and this comment in profile until it's fixed)
119
+ # - MIB: MIKROTIK-MIB
120
+ # symbol:
121
+ # OID: 1.3.6.1.4.1.14988.1.1.3.17
122
+ # name: mtxrHlFanSpeed1
123
+ # string metric is not supported yet (keep this metric and this comment in profile until it's fixed)
124
+ # - MIB: MIKROTIK-MIB
125
+ # symbol:
126
+ # OID: 1.3.6.1.4.1.14988.1.1.3.18
127
+ # name: mtxrHlFanSpeed2
128
+ # string metric is not supported yet (keep this metric and this comment in profile until it's fixed)
129
- MIB: HOST-RESOURCES-MIB
130
symbol:
131
OID: 1.3.6.1.2.1.25.3.3.1.2.1