@cryptotaxi247 / netdata-1 / commits / f0eecb8c5

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