refactor(go.d/ddsnmpcollector): restructure into components (#20543)
Ilya Mashchenko committed
Jun 21, 2025 at 21:46 UTC
0ff31d55eef690700275699c018d643b3b6f3b1c
21 files changed
+7415
-6149
src/go/plugin/go.d/collector/snmp/charts.go
+4
-1
@@ -333,7 +333,7 @@ func (c *Collector) addProfileScalarMetricChart(m ddsnmp.Metric) {
333
if chart.Fam == "" {
334
chart.Fam = m.Name
335
}
336
- if chart.Units == "bits/s" {
336
+ if chart.Units == "bit/s" {
337
chart.Type = module.Area
338
}
339
@@ -389,6 +389,9 @@ func (c *Collector) addProfileTableMetricChart(m ddsnmp.Metric) {
389
if chart.Fam == "" {
390
chart.Fam = m.Name
391
}
392
+ if chart.Units == "bit/s" {
393
+ chart.Type = module.Area
394
+ }
395
396
tags := map[string]string{
397
"vendor": c.sysInfo.Organization,
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_device_meta.go
deleted
-118
@@ -1,118 +0,0 @@
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, cfg := range prof.Definition.Metadata {
25
- if !ddprofiledefinition.IsMetadataResourceWithScalarOids(resName) {
26
- continue
27
- }
28
- for name, field := range cfg.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, cfg := range prof.Definition.Metadata {
59
- if !ddprofiledefinition.IsMetadataResourceWithScalarOids(resName) {
60
- continue
61
- }
62
- for name, field := range cfg.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
- mergeTagsWithEmptyFallback(tags, map[string]string{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
- mergeTagsWithEmptyFallback(tags, map[string]string{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(cfg ddprofiledefinition.SymbolConfig, result map[string]gosnmp.SnmpPDU) (string, error) {
92
- pdu, ok := result[trimOID(cfg.OID)]
93
- if !ok {
94
- return "", nil
95
- }
96
-
97
- val, err := convPduToStringf(pdu, cfg.Format)
98
- if err != nil {
99
- return "", err
100
- }
101
-
102
- switch {
103
- case cfg.ExtractValueCompiled != nil:
104
- if sm := cfg.ExtractValueCompiled.FindStringSubmatch(val); len(sm) > 1 {
105
- val = sm[1]
106
- }
107
- case cfg.MatchPatternCompiled != nil:
108
- if sm := cfg.MatchPatternCompiled.FindStringSubmatch(val); len(sm) > 0 {
109
- val = replaceSubmatches(cfg.MatchValue, sm)
110
- }
111
- }
112
-
113
- if v, ok := cfg.Mapping[val]; ok {
114
- val = v
115
- }
116
-
117
- return val, nil
118
-}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_global_tags.go
deleted
-111
@@ -1,111 +0,0 @@
1
-// SPDX-License-Identifier: GPL-3.0-or-later
2
-
3
-package ddsnmpcollector
4
-
5
-import (
6
- "errors"
7
- "fmt"
8
- "slices"
9
- "strings"
10
-
11
- "github.com/gosnmp/gosnmp"
12
-
13
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
14
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
-)
16
-
17
-func (c *Collector) collectGlobalTags(prof *ddsnmp.Profile) (map[string]string, error) {
18
- if len(prof.Definition.MetricTags) == 0 {
19
- return nil, nil
20
- }
21
-
22
- var oids []string
23
-
24
- for _, tagCfg := range prof.Definition.MetricTags {
25
- if tagCfg.Symbol.OID != "" {
26
- oids = append(oids, tagCfg.Symbol.OID)
27
- }
28
- }
29
-
30
- slices.Sort(oids)
31
- oids = slices.Compact(oids)
32
-
33
- if len(oids) == 0 {
34
- return nil, nil
35
- }
36
-
37
- pdus, err := c.snmpGet(oids)
38
- if err != nil {
39
- return nil, err
40
- }
41
-
42
- globalTags := make(map[string]string)
43
- var errs []error
44
-
45
- for _, tag := range prof.Definition.StaticTags {
46
- parts := strings.SplitN(tag, ":", 2)
47
- if len(parts) == 2 {
48
- globalTags[parts[0]] = parts[1]
49
- }
50
- }
51
-
52
- for _, cfg := range prof.Definition.MetricTags {
53
- tagValues, err := processMetricTagValue(cfg, pdus)
54
- if err != nil {
55
- errs = append(errs, fmt.Errorf("failed to process tag value for '%s/%s': %v", cfg.Tag, cfg.Symbol.Name, err))
56
- continue
57
- }
58
- mergeTagsWithEmptyFallback(globalTags, tagValues)
59
- }
60
-
61
- if len(errs) > 0 && len(globalTags) == 0 {
62
- return nil, fmt.Errorf("failed to process any global tags: %v", errors.Join(errs...))
63
- }
64
-
65
- return globalTags, nil
66
-}
67
-
68
-func processMetricTagValue(cfg ddprofiledefinition.MetricTagConfig, pdus map[string]gosnmp.SnmpPDU) (map[string]string, error) {
69
- tagName := ternary(cfg.Tag != "", cfg.Tag, cfg.Symbol.Name)
70
- if tagName == "" {
71
- return nil, nil
72
- }
73
-
74
- pdu, ok := pdus[trimOID(cfg.Symbol.OID)]
75
- if !ok {
76
- return nil, nil
77
- }
78
-
79
- val, err := convPduToStringf(pdu, cfg.Symbol.Format)
80
- if err != nil {
81
- return nil, err
82
- }
83
-
84
- tags := make(map[string]string)
85
-
86
- switch {
87
- case len(cfg.Mapping) > 0:
88
- if v, ok := cfg.Mapping[val]; ok {
89
- val = v
90
- }
91
- tags[tagName] = val
92
- case cfg.Pattern != nil:
93
- if sm := cfg.Pattern.FindStringSubmatch(val); len(sm) > 0 {
94
- for name, tmpl := range cfg.Tags {
95
- tags[name] = replaceSubmatches(tmpl, sm)
96
- }
97
- }
98
- case cfg.Symbol.ExtractValueCompiled != nil:
99
- if sm := cfg.Symbol.ExtractValueCompiled.FindStringSubmatch(val); len(sm) > 1 {
100
- tags[tagName] = sm[1]
101
- }
102
- case cfg.Symbol.MatchPatternCompiled != nil:
103
- if sm := cfg.Symbol.MatchPatternCompiled.FindStringSubmatch(val); len(sm) > 0 {
104
- tags[tagName] = replaceSubmatches(cfg.Symbol.MatchValue, sm)
105
- }
106
- default:
107
- tags[tagName] = val
108
- }
109
-
110
- return tags, nil
111
-}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_scalar.go
deleted
-182
@@ -1,182 +0,0 @@
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) ([]ddsnmp.Metric, error) {
19
- var oids []string
20
- var missingOIDs []string
21
-
22
- for _, m := range prof.Definition.Metrics {
23
- if !m.IsScalar() {
24
- continue
25
- }
26
- if c.missingOIDs[trimOID(m.Symbol.OID)] {
27
- missingOIDs = append(missingOIDs, m.Symbol.OID)
28
- continue
29
- }
30
- oids = append(oids, m.Symbol.OID)
31
- }
32
-
33
- if len(missingOIDs) > 0 {
34
- c.log.Debugf("scalar metrics missing OIDs: %v", missingOIDs)
35
- }
36
-
37
- slices.Sort(oids)
38
- oids = slices.Compact(oids)
39
-
40
- if len(oids) == 0 {
41
- return nil, nil
42
- }
43
-
44
- pdus, err := c.snmpGet(oids)
45
- if err != nil {
46
- return nil, err
47
- }
48
-
49
- metrics := make([]ddsnmp.Metric, 0, len(prof.Definition.Metrics))
50
- var errs []error
51
-
52
- for _, cfg := range prof.Definition.Metrics {
53
- if !cfg.IsScalar() {
54
- continue
55
- }
56
-
57
- metric, err := c.collectScalarMetric(cfg, pdus)
58
- if err != nil {
59
- errs = append(errs, fmt.Errorf("metric '%s': %w", cfg.Symbol.Name, err))
60
- c.log.Debugf("Error processing scalar metric '%s': %v", cfg.Symbol.Name, err)
61
- continue
62
- }
63
-
64
- if metric != nil {
65
- metrics = append(metrics, *metric)
66
- }
67
- }
68
-
69
- if len(metrics) == 0 && len(errs) > 0 {
70
- return nil, errors.Join(errs...)
71
- }
72
-
73
- return metrics, nil
74
-}
75
-
76
-func (c *Collector) collectScalarMetric(cfg ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU) (*ddsnmp.Metric, error) {
77
- pdu, ok := pdus[trimOID(cfg.Symbol.OID)]
78
- if !ok {
79
- return nil, nil
80
- }
81
-
82
- value, err := processSymbolValue(cfg.Symbol, pdu)
83
- if err != nil {
84
- return nil, fmt.Errorf("error processing value for OID %s (%s): %w", cfg.Symbol.Name, cfg.Symbol.OID, err)
85
- }
86
-
87
- staticTags := make(map[string]string)
88
-
89
- for _, tag := range cfg.StaticTags {
90
- if n, v, _ := strings.Cut(tag, ":"); n != "" && v != "" {
91
- staticTags[n] = v
92
- }
93
- }
94
-
95
- m := &ddsnmp.Metric{
96
- Name: cfg.Symbol.Name,
97
- Value: value,
98
- StaticTags: ternary(len(staticTags) > 0, staticTags, nil),
99
- Unit: cfg.Symbol.Unit,
100
- Description: cfg.Symbol.Description,
101
- Family: cfg.Symbol.Family,
102
- Mappings: convSymMappingToNumeric(cfg.Symbol),
103
- MetricType: getMetricType(cfg.Symbol, pdu),
104
- }
105
-
106
- if cfg.Symbol.TransformCompiled != nil {
107
- if err := applyTransform(m, cfg.Symbol); err != nil {
108
- return nil, fmt.Errorf("failed to apply transform for scalar metric '%s': %w", cfg.Symbol.Name, err)
109
- }
110
- }
111
-
112
- return m, nil
113
-}
114
-
115
-func processSymbolValue(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) (int64, error) {
116
- var value int64
117
-
118
- if isPduNumericType(pdu) {
119
- switch pdu.Type {
120
- case gosnmp.OpaqueFloat:
121
- floatVal, ok := pdu.Value.(float32)
122
- if !ok {
123
- return 0, fmt.Errorf("OpaqueFloat has unexpected type %T", pdu.Value)
124
- }
125
- value = ternary(sym.ScaleFactor != 0, int64(float64(floatVal)*sym.ScaleFactor), int64(floatVal))
126
- return value, nil
127
- case gosnmp.OpaqueDouble:
128
- floatVal, ok := pdu.Value.(float64)
129
- if !ok {
130
- return 0, fmt.Errorf("OpaqueFloat has unexpected type %T", pdu.Value)
131
- }
132
- value = ternary(sym.ScaleFactor != 0, int64(floatVal*sym.ScaleFactor), int64(floatVal))
133
- return value, nil
134
- default:
135
- value = gosnmp.ToBigInt(pdu.Value).Int64()
136
- if len(sym.Mapping) > 0 {
137
- s := strconv.FormatInt(value, 10)
138
- if v, ok := sym.Mapping[s]; ok && isInt(v) {
139
- value, _ = strconv.ParseInt(v, 10, 64)
140
- }
141
- }
142
- }
143
- } else {
144
- s, err := convPduToStringf(pdu, sym.Format)
145
- if err != nil {
146
- return 0, err
147
- }
148
-
149
- switch {
150
- case sym.ExtractValueCompiled != nil:
151
- if sm := sym.ExtractValueCompiled.FindStringSubmatch(s); len(sm) > 1 {
152
- s = sm[1]
153
- }
154
- case sym.MatchPatternCompiled != nil:
155
- if sm := sym.MatchPatternCompiled.FindStringSubmatch(s); len(sm) > 0 {
156
- s = replaceSubmatches(sym.MatchValue, sm)
157
- }
158
- }
159
-
160
- // Handle mapping based on the mapping type:
161
- // 1. Int -> String mapping (e.g., {"1": "up", "2": "down"}):
162
- // - Used for creating dimensions later
163
- // - Value remains numeric, no conversion needed here
164
- // 2. String -> Int mapping (e.g., {"OK": "0", "WARNING": "1"}):
165
- // - Used to convert string values to numeric values
166
- // - Apply the mapping to get the numeric representation
167
- if v, ok := sym.Mapping[s]; ok && isInt(v) {
168
- s = v
169
- }
170
-
171
- value, err = strconv.ParseInt(s, 10, 64)
172
- if err != nil {
173
- return 0, err
174
- }
175
- }
176
-
177
- if sym.ScaleFactor != 0 {
178
- value = int64(float64(value) * sym.ScaleFactor)
179
- }
180
-
181
- return value, nil
182
-}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_table.go
deleted
-626
@@ -1,626 +0,0 @@
1
-// SPDX-License-Identifier: GPL-3.0-or-later
2
-
3
-package ddsnmpcollector
4
-
5
-import (
6
- "errors"
7
- "fmt"
8
- "maps"
9
- "strings"
10
-
11
- "github.com/gosnmp/gosnmp"
12
-
13
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
14
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
-)
16
-
17
-// tableWalkResult holds the walked data for a single table
18
-type tableWalkResult struct {
19
- tableOID string
20
- pdus map[string]gosnmp.SnmpPDU
21
- config ddprofiledefinition.MetricsConfig
22
-}
23
-
24
-func (c *Collector) collectTableMetrics(prof *ddsnmp.Profile) ([]ddsnmp.Metric, error) {
25
- walkResults, err := c.walkTablesAsNeeded(prof)
26
- if err != nil {
27
- return nil, err
28
- }
29
-
30
- return c.processTableWalkResults(walkResults)
31
-}
32
-
33
-// Phase 1: Walk only tables that aren't fully cached
34
-func (c *Collector) walkTablesAsNeeded(prof *ddsnmp.Profile) ([]tableWalkResult, error) {
35
- var results []tableWalkResult
36
- var errs []error
37
- var missingOIDs []string
38
-
39
- // Map to store walked data by table OID
40
- walkedTables := make(map[string]map[string]gosnmp.SnmpPDU)
41
-
42
- // Map to track which tables need to be walked
43
- tablesToWalk := make(map[string]bool)
44
-
45
- // Map configs by table OID
46
- tableConfigs := make(map[string][]ddprofiledefinition.MetricsConfig)
47
-
48
- // First pass: identify tables and check cache status
49
- for _, cfg := range prof.Definition.Metrics {
50
- if cfg.IsScalar() || cfg.Table.OID == "" {
51
- continue
52
- }
53
-
54
- tableOID := cfg.Table.OID
55
- if c.missingOIDs[trimOID(tableOID)] {
56
- missingOIDs = append(missingOIDs, tableOID)
57
- continue
58
- }
59
-
60
- tableConfigs[tableOID] = append(tableConfigs[tableOID], cfg)
61
-
62
- if !c.tableCache.isConfigCached(cfg) {
63
- tablesToWalk[tableOID] = true
64
- }
65
- }
66
-
67
- // Walk each table that needs it (only once per table)
68
- c.log.Debugf("Tables walking %d cached %d (%d total)",
69
- len(tablesToWalk), len(tableConfigs)-len(tablesToWalk), len(tableConfigs))
70
- for tableOID := range tablesToWalk {
71
- pdus, err := c.snmpWalk(tableOID)
72
- if err != nil {
73
- errs = append(errs, fmt.Errorf("failed to walk table OID '%s': %w", tableOID, err))
74
- continue
75
- }
76
-
77
- if len(pdus) > 0 {
78
- walkedTables[tableOID] = pdus
79
- }
80
- }
81
-
82
- // Second pass: create results for ALL configs
83
- for _, cfg := range prof.Definition.Metrics {
84
- if cfg.IsScalar() || cfg.Table.OID == "" {
85
- continue
86
- }
87
-
88
- if c.missingOIDs[trimOID(cfg.Table.OID)] {
89
- continue
90
- }
91
-
92
- // Add to results if we have walked data OR if it's cached
93
- if pdus, ok := walkedTables[cfg.Table.OID]; ok {
94
- results = append(results, tableWalkResult{
95
- tableOID: cfg.Table.OID,
96
- pdus: pdus,
97
- config: cfg,
98
- })
99
- } else if c.tableCache.isConfigCached(cfg) {
100
- // Add config without PDUs - will use cache in processing
101
- results = append(results, tableWalkResult{
102
- tableOID: cfg.Table.OID,
103
- pdus: nil,
104
- config: cfg,
105
- })
106
- }
107
- }
108
-
109
- if len(missingOIDs) > 0 {
110
- c.log.Debugf("table metrics missing OIDs: %v", missingOIDs)
111
- }
112
-
113
- if len(results) == 0 && len(errs) > 0 {
114
- return nil, errors.Join(errs...)
115
- }
116
-
117
- return results, nil
118
-}
119
-
120
-// Phase 2: Process walked data
121
-func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]ddsnmp.Metric, error) {
122
- var metrics []ddsnmp.Metric
123
- var errs []error
124
-
125
- // Build a map for quick lookup of walked data by table OID
126
- walkedData := make(map[string]map[string]gosnmp.SnmpPDU)
127
- for _, result := range walkResults {
128
- if result.pdus != nil {
129
- walkedData[result.tableOID] = result.pdus
130
- }
131
- }
132
-
133
- // Build a map of table name to OID for cross-table lookups
134
- tableNameToOID := make(map[string]string)
135
- for _, result := range walkResults {
136
- if result.config.Table.Name != "" {
137
- tableNameToOID[result.config.Table.Name] = result.tableOID
138
- }
139
- }
140
-
141
- // Process each table's walked data
142
- for _, result := range walkResults {
143
- // Try cache first
144
- if cachedOIDs, cachedTags, ok := c.tableCache.getCachedData(result.config); ok {
145
- columnOIDs := buildColumnOIDs(result.config)
146
- cacheMetrics, err := c.collectTableWithCache(result.config, cachedOIDs, cachedTags, columnOIDs)
147
- if err == nil {
148
- c.log.Debugf("Successfully collected table %s using cache", result.config.Table.Name)
149
- metrics = append(metrics, cacheMetrics...)
150
- continue
151
- }
152
- c.log.Debugf("Cached collection failed for table %s, falling back to process walked data: %v", result.config.Table.Name, err)
153
- }
154
-
155
- // Process walked data if available
156
- if result.pdus != nil {
157
- tableMetrics, err := c.processTableData(result.config, result.pdus, walkedData, tableNameToOID)
158
- if err != nil {
159
- errs = append(errs, fmt.Errorf("table '%s': %w", result.config.Table.Name, err))
160
- continue
161
- }
162
- metrics = append(metrics, tableMetrics...)
163
- }
164
- }
165
-
166
- if len(metrics) == 0 && len(errs) > 0 {
167
- return nil, errors.Join(errs...)
168
- }
169
-
170
- return metrics, nil
171
-}
172
-
173
-// Process a single table's data
174
-func (c *Collector) processTableData(
175
- cfg ddprofiledefinition.MetricsConfig,
176
- pdus map[string]gosnmp.SnmpPDU,
177
- allWalkedData map[string]map[string]gosnmp.SnmpPDU,
178
- tableNameToOID map[string]string) ([]ddsnmp.Metric, error) {
179
- columnOIDs := buildColumnOIDs(cfg)
180
- tagColumnOIDs := buildTagColumnOIDs(cfg)
181
-
182
- allColumnOIDs := make([]string, 0, len(columnOIDs)+len(tagColumnOIDs))
183
- for oid := range columnOIDs {
184
- allColumnOIDs = append(allColumnOIDs, oid)
185
- }
186
- for oid := range tagColumnOIDs {
187
- allColumnOIDs = append(allColumnOIDs, oid)
188
- }
189
-
190
- // Group PDUs by row index and build cache structure
191
- rows := make(map[string]map[string]gosnmp.SnmpPDU)
192
- oidCache := make(map[string]map[string]string) // For caching: index -> column OID -> full OID
193
- tagCache := make(map[string]map[string]string) // For caching: index -> tag name -> value
194
-
195
- for oid, pdu := range pdus {
196
- for _, columnOID := range allColumnOIDs {
197
- if strings.HasPrefix(oid, columnOID+".") {
198
- index := strings.TrimPrefix(oid, columnOID+".")
199
-
200
- if rows[index] == nil {
201
- rows[index] = make(map[string]gosnmp.SnmpPDU)
202
- oidCache[index] = make(map[string]string)
203
- tagCache[index] = make(map[string]string)
204
- }
205
- rows[index][columnOID] = pdu
206
- oidCache[index][columnOID] = oid
207
- break
208
- }
209
- }
210
- }
211
-
212
- rowStaticTags := make(map[string]string)
213
- for _, tag := range cfg.StaticTags {
214
- if n, v, _ := strings.Cut(tag, ":"); n != "" && v != "" {
215
- rowStaticTags[n] = v
216
- }
217
- }
218
-
219
- var metrics []ddsnmp.Metric
220
- var errs []error
221
-
222
- for index, rowPDUs := range rows {
223
- rowTags := make(map[string]string)
224
-
225
- // Process tags for this row
226
- for columnOID, tagCfgs := range tagColumnOIDs {
227
- pdu, ok := rowPDUs[columnOID]
228
- if !ok {
229
- continue
230
- }
231
-
232
- for _, tagCfg := range tagCfgs {
233
- tags, err := processTableMetricTagValue(tagCfg, pdu)
234
- if err != nil {
235
- c.log.Debugf("Error processing tag %s: %v", tagCfg.Tag, err)
236
- continue
237
- }
238
-
239
- mergeTagsWithEmptyFallback(rowTags, tags)
240
- mergeTagsWithEmptyFallback(tagCache[index], tags)
241
- }
242
- }
243
-
244
- // Process cross-table tags
245
- for _, tagCfg := range cfg.MetricTags {
246
- // Skip if not a cross-table tag
247
- if tagCfg.Table == "" || tagCfg.Table == cfg.Table.Name {
248
- continue
249
- }
250
-
251
- // Skip if it's an index-based tag (handled separately)
252
- if tagCfg.Index != 0 {
253
- continue
254
- }
255
-
256
- // Find the referenced table's OID
257
- refTableOID, ok := tableNameToOID[tagCfg.Table]
258
- if !ok {
259
- c.log.Debugf("Cannot find table OID for referenced table %s", tagCfg.Table)
260
- continue
261
- }
262
-
263
- // Get the walked data for the referenced table
264
- refTablePDUs, ok := allWalkedData[refTableOID]
265
- if !ok {
266
- c.log.Debugf("No walked data for referenced table %s (OID: %s)", tagCfg.Table, refTableOID)
267
- continue
268
- }
269
-
270
- lookupIndex := index
271
- if len(tagCfg.IndexTransform) > 0 {
272
- lookupIndex = applyIndexTransform(index, tagCfg.IndexTransform)
273
- if lookupIndex == "" {
274
- c.log.Debugf("Index transformation failed for index %s with transforms %v", index, tagCfg.IndexTransform)
275
- continue
276
- }
277
- }
278
-
279
- // Look up the value from the referenced table using the same index
280
- refColumnOID := trimOID(tagCfg.Symbol.OID)
281
- refFullOID := refColumnOID + "." + lookupIndex
282
-
283
- pdu, ok := refTablePDUs[refFullOID]
284
- if !ok {
285
- c.log.Debugf("Cannot find cross-table tag value at OID %s for table %s (lookup index: %s)", refFullOID, tagCfg.Table, lookupIndex)
286
- continue
287
- }
288
-
289
- // Process the cross-table tag value
290
- tags, err := processTableMetricTagValue(tagCfg, pdu)
291
- if err != nil {
292
- c.log.Debugf("Error processing cross-table tag %s from table %s: %v", tagCfg.Tag, tagCfg.Table, err)
293
- continue
294
- }
295
-
296
- mergeTagsWithEmptyFallback(rowTags, tags)
297
- mergeTagsWithEmptyFallback(tagCache[index], tags)
298
- }
299
-
300
- // Process index-based tags
301
- for _, tagCfg := range cfg.MetricTags {
302
- // Skip if not an index-based tag
303
- if tagCfg.Index == 0 {
304
- continue
305
- }
306
-
307
- indexValue, ok := getIndexPosition(index, tagCfg.Index)
308
- if !ok {
309
- c.log.Debugf("Cannot extract position %d from index %s", tagCfg.Index, index)
310
- continue
311
- }
312
-
313
- tagName := ternary(tagCfg.Tag != "", tagCfg.Tag, fmt.Sprintf("index%d", tagCfg.Index))
314
-
315
- if v, ok := tagCfg.Mapping[indexValue]; ok {
316
- indexValue = v
317
- }
318
-
319
- rowTags[tagName] = indexValue
320
- tagCache[index][tagName] = indexValue
321
- }
322
-
323
- // Process metrics for this row
324
- for columnOID, sym := range columnOIDs {
325
- pdu, ok := rowPDUs[columnOID]
326
- if !ok {
327
- continue
328
- }
329
-
330
- value, err := processSymbolValue(sym, pdu)
331
- if err != nil {
332
- c.log.Debugf("Error processing value for %s: %v", sym.Name, err)
333
- continue
334
- }
335
-
336
- metric := ddsnmp.Metric{
337
- Name: sym.Name,
338
- Value: value,
339
- StaticTags: ternary(len(rowStaticTags) > 0, rowStaticTags, nil),
340
- Tags: maps.Clone(ternary(len(rowTags) > 0, rowTags, nil)),
341
- Unit: sym.Unit,
342
- Description: sym.Description,
343
- MetricType: getMetricType(sym, pdu),
344
- Family: sym.Family,
345
- Mappings: convSymMappingToNumeric(sym),
346
- IsTable: true,
347
- }
348
-
349
- if sym.TransformCompiled != nil {
350
- if err := applyTransform(&metric, sym); err != nil {
351
- errs = append(errs, fmt.Errorf("failed to apply transform for table metric '%s:%s': %w",
352
- cfg.Table.Name, sym.Name, err))
353
- continue
354
- }
355
- }
356
-
357
- metrics = append(metrics, metric)
358
- }
359
- }
360
-
361
- if len(errs) > 0 {
362
- c.log.Warningf("failed to collect table metrics: %v", errors.Join(errs...))
363
- }
364
-
365
- deps := extractTableDependencies(cfg, tableNameToOID)
366
-
367
- // Cache the processed data
368
- c.tableCache.cacheData(cfg, oidCache, tagCache, deps)
369
- c.log.Debugf("Cached table %s structure with %d rows", cfg.Table.Name, len(oidCache))
370
-
371
- return metrics, nil
372
-}
373
-
374
-func buildColumnOIDs(cfg ddprofiledefinition.MetricsConfig) map[string]ddprofiledefinition.SymbolConfig {
375
- columnOIDs := make(map[string]ddprofiledefinition.SymbolConfig)
376
- for _, sym := range cfg.Symbols {
377
- columnOIDs[trimOID(sym.OID)] = sym
378
- }
379
- return columnOIDs
380
-}
381
-
382
-func buildTagColumnOIDs(cfg ddprofiledefinition.MetricsConfig) map[string][]ddprofiledefinition.MetricTagConfig {
383
- tagColumnOIDs := make(map[string][]ddprofiledefinition.MetricTagConfig)
384
- for _, tagCfg := range cfg.MetricTags {
385
- if tagCfg.Table == "" || tagCfg.Table == cfg.Table.Name {
386
- oid := trimOID(tagCfg.Symbol.OID)
387
- tagColumnOIDs[oid] = append(tagColumnOIDs[oid], tagCfg)
388
- }
389
- }
390
- return tagColumnOIDs
391
-}
392
-
393
-func (c *Collector) collectTableWithCache(
394
- cfg ddprofiledefinition.MetricsConfig,
395
- cachedOIDs map[string]map[string]string,
396
- cachedTags map[string]map[string]string,
397
- columnOIDs map[string]ddprofiledefinition.SymbolConfig,
398
-) ([]ddsnmp.Metric, error) {
399
- var oidsToGet []string
400
-
401
- for _, columns := range cachedOIDs {
402
- for columnOID, fullOID := range columns {
403
- // Only GET metric columns, tags are cached
404
- if _, isMetric := columnOIDs[columnOID]; isMetric {
405
- oidsToGet = append(oidsToGet, fullOID)
406
- }
407
- }
408
- }
409
-
410
- if len(oidsToGet) == 0 {
411
- return nil, nil
412
- }
413
-
414
- pdus, err := c.snmpGet(oidsToGet)
415
- if err != nil {
416
- return nil, fmt.Errorf("failed to get cached OIDs: %w", err)
417
- }
418
-
419
- if len(pdus) < len(oidsToGet)/2 { // If we got less than half, probably table structure changed
420
- return nil, fmt.Errorf("table structure may have changed, got %d/%d PDUs", len(pdus), len(oidsToGet))
421
- }
422
-
423
- rowStaticTags := make(map[string]string)
424
-
425
- for _, tag := range cfg.StaticTags {
426
- if n, v, _ := strings.Cut(tag, ":"); n != "" && v != "" {
427
- rowStaticTags[n] = v
428
- }
429
- }
430
-
431
- var metrics []ddsnmp.Metric
432
- var errs []error
433
-
434
- for index, columns := range cachedOIDs {
435
- rowTags := make(map[string]string)
436
-
437
- if tags, ok := cachedTags[index]; ok {
438
- for k, v := range tags {
439
- rowTags[k] = v
440
- }
441
- }
442
-
443
- for columnOID, fullOID := range columns {
444
- sym, isMetric := columnOIDs[columnOID]
445
- if !isMetric {
446
- continue
447
- }
448
-
449
- pdu, ok := pdus[trimOID(fullOID)]
450
- if !ok {
451
- c.log.Debugf("Missing PDU for cached OID %s", fullOID)
452
- continue
453
- }
454
-
455
- value, err := processSymbolValue(sym, pdu)
456
- if err != nil {
457
- c.log.Debugf("Error processing value for %s: %v", sym.Name, err)
458
- continue
459
- }
460
-
461
- metric := ddsnmp.Metric{
462
- Name: sym.Name,
463
- Value: value,
464
- StaticTags: ternary(len(rowStaticTags) > 0, rowStaticTags, nil),
465
- Tags: ternary(len(rowTags) > 0, rowTags, nil),
466
- Unit: sym.Unit,
467
- Description: sym.Description,
468
- MetricType: getMetricType(sym, pdu),
469
- Family: sym.Family,
470
- Mappings: convSymMappingToNumeric(sym),
471
- IsTable: true,
472
- }
473
-
474
- if sym.TransformCompiled != nil {
475
- if err := applyTransform(&metric, sym); err != nil {
476
- errs = append(errs, fmt.Errorf("failed to apply transform for metric '%s:%s': %w",
477
- cfg.Table.Name, sym.Name, err))
478
- continue
479
- }
480
- }
481
-
482
- metrics = append(metrics, metric)
483
- }
484
- }
485
-
486
- if len(errs) > 0 {
487
- c.log.Warningf("failed to collect table metrics: %v", errors.Join(errs...))
488
- }
489
-
490
- return metrics, nil
491
-}
492
-
493
-func processTableMetricTagValue(cfg ddprofiledefinition.MetricTagConfig, pdu gosnmp.SnmpPDU) (map[string]string, error) {
494
- val, err := convPduToStringf(pdu, cfg.Symbol.Format)
495
- if err != nil {
496
- return nil, err
497
- }
498
-
499
- tags := make(map[string]string)
500
- tagName := ternary(cfg.Tag != "", cfg.Tag, cfg.Symbol.Name)
501
-
502
- switch {
503
- case len(cfg.Mapping) > 0:
504
- if v, ok := cfg.Mapping[val]; ok {
505
- val = v
506
- }
507
- tags[tagName] = val
508
- case cfg.Pattern != nil:
509
- if sm := cfg.Pattern.FindStringSubmatch(val); len(sm) > 0 {
510
- for name, tmpl := range cfg.Tags {
511
- tags[name] = replaceSubmatches(tmpl, sm)
512
- }
513
- }
514
- case cfg.Symbol.ExtractValueCompiled != nil:
515
- if sm := cfg.Symbol.ExtractValueCompiled.FindStringSubmatch(val); len(sm) > 1 {
516
- tags[tagName] = sm[1]
517
- }
518
- case cfg.Symbol.MatchPatternCompiled != nil:
519
- if sm := cfg.Symbol.MatchPatternCompiled.FindStringSubmatch(val); len(sm) > 0 {
520
- tags[tagName] = replaceSubmatches(cfg.Symbol.MatchValue, sm)
521
- }
522
- default:
523
- tags[tagName] = val
524
- }
525
-
526
- return tags, nil
527
-}
528
-
529
-func (c *Collector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error) {
530
- pdus := make(map[string]gosnmp.SnmpPDU)
531
-
532
- var resp []gosnmp.SnmpPDU
533
- var err error
534
-
535
- if c.snmpClient.Version() == gosnmp.Version1 {
536
- resp, err = c.snmpClient.WalkAll(oid)
537
- } else {
538
- resp, err = c.snmpClient.BulkWalkAll(oid)
539
- }
540
- if err != nil {
541
- return nil, err
542
- }
543
-
544
- for _, pdu := range resp {
545
- if isPduWithData(pdu) {
546
- pdus[trimOID(pdu.Name)] = pdu
547
- }
548
- }
549
-
550
- if len(pdus) == 0 {
551
- c.missingOIDs[trimOID(oid)] = true
552
- }
553
-
554
- return pdus, nil
555
-}
556
-
557
-// getIndexPosition extracts a specific position from an index
558
-// Position uses 1-based indexing as per the profile format
559
-// Example: index "7.8.9", position 2 → "8"
560
-func getIndexPosition(index string, position uint) (string, bool) {
561
- if position == 0 {
562
- return "", false
563
- }
564
-
565
- var n uint
566
- for {
567
- n++
568
- i := strings.IndexByte(index, '.')
569
- if i == -1 {
570
- break
571
- }
572
- if n == position {
573
- return index[:i], true
574
- }
575
- index = index[i+1:]
576
- }
577
-
578
- return index, n == position && index != ""
579
-}
580
-
581
-// applyIndexTransform applies index transformation rules to extract a subset of the index
582
-// Example: index "1.6.0.36.155.53.3.246", transform [{start: 1, end: 7}] → "6.0.36.155.53.3.246"
583
-func applyIndexTransform(index string, transforms []ddprofiledefinition.MetricIndexTransform) string {
584
- if len(transforms) == 0 {
585
- return index
586
- }
587
-
588
- parts := strings.Split(index, ".")
589
- var result []string
590
-
591
- for _, transform := range transforms {
592
- start := transform.Start
593
- end := transform.End
594
-
595
- if int(start) >= len(parts) || end < start || int(end) >= len(parts) {
596
- continue
597
- }
598
-
599
- // Extract the range (inclusive)
600
- result = append(result, parts[start:end+1]...)
601
- }
602
-
603
- return strings.Join(result, ".")
604
-}
605
-
606
-func extractTableDependencies(cfg ddprofiledefinition.MetricsConfig, tableNameToOID map[string]string) []string {
607
- deps := make(map[string]bool)
608
-
609
- for _, tagCfg := range cfg.MetricTags {
610
- // Skip if not a cross-table tag
611
- if tagCfg.Table == "" || tagCfg.Table == cfg.Table.Name {
612
- continue
613
- }
614
-
615
- if tableOID, ok := tableNameToOID[tagCfg.Table]; ok {
616
- deps[tableOID] = true
617
- }
618
- }
619
-
620
- result := make([]string, 0, len(deps))
621
- for oid := range deps {
622
- result = append(result, oid)
623
- }
624
-
625
- return result
626
-}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+18
-5
@@ -23,7 +23,7 @@ func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logg
23
snmpClient: snmpClient,
24
profiles: make(map[string]*profileState),
25
missingOIDs: make(map[string]bool),
26
- tableCache: newTableCache(30*time.Minute, 1), // 100% jitter
26
+ tableCache: newTableCache(30*time.Minute, 1),
27
}
28
29
for _, prof := range profiles {
@@ -31,6 +31,11 @@ func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logg
31
coll.profiles[prof.SourceFile] = &profileState{profile: prof}
32
}
33
34
+ coll.globalTagsCollector = newGlobalTagsCollector(snmpClient, coll.missingOIDs, coll.log)
35
+ coll.deviceMetadataCollector = newDeviceMetadataCollector(snmpClient, coll.missingOIDs, coll.log)
36
+ coll.scalarCollector = newScalarCollector(snmpClient, coll.missingOIDs, coll.log)
37
+ coll.tableCollector = newTableCollector(snmpClient, coll.missingOIDs, coll.tableCache, coll.log)
38
+
39
return coll
40
}
41
@@ -42,6 +47,11 @@ type (
47
missingOIDs map[string]bool
48
tableCache *tableCache
49
50
+ globalTagsCollector *globalTagsCollector
51
+ deviceMetadataCollector *deviceMetadataCollector
52
+ scalarCollector *scalarCollector
53
+ tableCollector *tableCollector
54
+
55
DoTableMetrics bool
56
}
57
profileState struct {
@@ -82,12 +92,12 @@ func (c *Collector) Collect() ([]*ddsnmp.ProfileMetrics, error) {
92
93
func (c *Collector) collectProfile(ps *profileState) (*ddsnmp.ProfileMetrics, error) {
94
if !ps.initialized {
85
- globalTag, err := c.collectGlobalTags(ps.profile)
95
+ globalTag, err := c.globalTagsCollector.Collect(ps.profile)
96
if err != nil {
97
return nil, fmt.Errorf("failed to collect global tags: %w", err)
98
}
99
90
- deviceMeta, err := c.collectDeviceMetadata(ps.profile)
100
+ deviceMeta, err := c.deviceMetadataCollector.Collect(ps.profile)
101
if err != nil {
102
return nil, fmt.Errorf("failed to collect device metadata: %w", err)
103
}
@@ -99,14 +109,14 @@ func (c *Collector) collectProfile(ps *profileState) (*ddsnmp.ProfileMetrics, er
109
110
var metrics []ddsnmp.Metric
111
102
- scalarMetrics, err := c.collectScalarMetrics(ps.profile)
112
+ scalarMetrics, err := c.scalarCollector.Collect(ps.profile)
113
if err != nil {
114
return nil, err
115
}
116
metrics = append(metrics, scalarMetrics...)
117
118
if c.DoTableMetrics {
109
- tableMetrics, err := c.collectTableMetrics(ps.profile)
119
+ tableMetrics, err := c.tableCollector.Collect(ps.profile)
120
if err != nil {
121
return nil, err
122
}
@@ -136,6 +146,9 @@ func (c *Collector) updateMetrics(pms []*ddsnmp.ProfileMetrics) {
146
if !ps.initialized {
147
continue
148
}
149
+ if ps.profile.Definition == nil {
150
+ continue
151
+ }
152
res, ok := ps.profile.Definition.Metadata["device"]
153
if !ok {
154
continue
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_scalar.go
new
+146
@@ -0,0 +1,146 @@
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/logger"
13
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
+)
16
+
17
+// scalarCollector handles collection of scalar (non-table) metrics
18
+type scalarCollector struct {
19
+ snmpClient gosnmp.Handler
20
+ missingOIDs map[string]bool
21
+ log *logger.Logger
22
+ valProc *valueProcessor
23
+}
24
+
25
+func newScalarCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, log *logger.Logger) *scalarCollector {
26
+ return &scalarCollector{
27
+ snmpClient: snmpClient,
28
+ missingOIDs: missingOIDs,
29
+ log: log,
30
+ valProc: newValueProcessor(),
31
+ }
32
+}
33
+
34
+// Collect gathers all scalar metrics from the profile
35
+func (sc *scalarCollector) Collect(prof *ddsnmp.Profile) ([]ddsnmp.Metric, error) {
36
+ oids, missingOIDs := sc.identifyScalarOIDs(prof.Definition.Metrics)
37
+
38
+ if len(missingOIDs) > 0 {
39
+ sc.log.Debugf("scalar metrics missing OIDs: %v", missingOIDs)
40
+ }
41
+
42
+ if len(oids) == 0 {
43
+ return nil, nil
44
+ }
45
+
46
+ pdus, err := sc.getScalarValues(oids)
47
+ if err != nil {
48
+ return nil, err
49
+ }
50
+
51
+ return sc.processScalarMetrics(prof.Definition.Metrics, pdus)
52
+}
53
+
54
+// identifyScalarOIDs returns OIDs to collect and OIDs that are known to be missing
55
+func (sc *scalarCollector) identifyScalarOIDs(configs []ddprofiledefinition.MetricsConfig) ([]string, []string) {
56
+ var oids []string
57
+ var missingOIDs []string
58
+
59
+ for _, cfg := range configs {
60
+ if !cfg.IsScalar() {
61
+ continue
62
+ }
63
+
64
+ oid := trimOID(cfg.Symbol.OID)
65
+ if sc.missingOIDs[oid] {
66
+ missingOIDs = append(missingOIDs, cfg.Symbol.OID)
67
+ continue
68
+ }
69
+
70
+ oids = append(oids, cfg.Symbol.OID)
71
+ }
72
+
73
+ // Sort and deduplicate
74
+ slices.Sort(oids)
75
+ oids = slices.Compact(oids)
76
+
77
+ return oids, missingOIDs
78
+}
79
+
80
+func (sc *scalarCollector) getScalarValues(oids []string) (map[string]gosnmp.SnmpPDU, error) {
81
+ pdus := make(map[string]gosnmp.SnmpPDU)
82
+ maxOids := sc.snmpClient.MaxOids()
83
+
84
+ for chunk := range slices.Chunk(oids, maxOids) {
85
+ result, err := sc.snmpClient.Get(chunk)
86
+ if err != nil {
87
+ return nil, err
88
+ }
89
+
90
+ for _, pdu := range result.Variables {
91
+ if !isPduWithData(pdu) {
92
+ sc.missingOIDs[trimOID(pdu.Name)] = true
93
+ continue
94
+ }
95
+ pdus[trimOID(pdu.Name)] = pdu
96
+ }
97
+ }
98
+
99
+ return pdus, nil
100
+}
101
+
102
+// processScalarMetrics converts PDUs into metrics
103
+func (sc *scalarCollector) processScalarMetrics(configs []ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU) ([]ddsnmp.Metric, error) {
104
+ var metrics []ddsnmp.Metric
105
+ var errs []error
106
+
107
+ for _, cfg := range configs {
108
+ if !cfg.IsScalar() {
109
+ continue
110
+ }
111
+
112
+ metric, err := sc.processScalarMetric(cfg, pdus)
113
+ if err != nil {
114
+ errs = append(errs, fmt.Errorf("metric '%s': %w", cfg.Symbol.Name, err))
115
+ sc.log.Debugf("Error processing scalar metric '%s': %v", cfg.Symbol.Name, err)
116
+ continue
117
+ }
118
+
119
+ if metric != nil {
120
+ metrics = append(metrics, *metric)
121
+ }
122
+ }
123
+
124
+ if len(metrics) == 0 && len(errs) > 0 {
125
+ return nil, errors.Join(errs...)
126
+ }
127
+
128
+ return metrics, nil
129
+}
130
+
131
+// processScalarMetric processes a single scalar metric configuration
132
+func (sc *scalarCollector) processScalarMetric(cfg ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU) (*ddsnmp.Metric, error) {
133
+ pdu, ok := pdus[trimOID(cfg.Symbol.OID)]
134
+ if !ok {
135
+ return nil, nil
136
+ }
137
+
138
+ value, err := sc.valProc.processValue(cfg.Symbol, pdu)
139
+ if err != nil {
140
+ return nil, fmt.Errorf("error processing value for OID %s (%s): %w", cfg.Symbol.Name, cfg.Symbol.OID, err)
141
+ }
142
+
143
+ staticTags := parseStaticTags(cfg.StaticTags)
144
+
145
+ return buildScalarMetric(cfg.Symbol, pdu, value, staticTags)
146
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_scalar_test.go
new
+735
@@ -0,0 +1,735 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "testing"
9
+
10
+ "github.com/gosnmp/gosnmp"
11
+ snmpmock "github.com/gosnmp/gosnmp/mocks"
12
+ "github.com/stretchr/testify/assert"
13
+
14
+ "github.com/netdata/netdata/go/plugins/logger"
15
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
16
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
17
+)
18
+
19
+func TestScalarCollector_Collect(t *testing.T) {
20
+ tests := map[string]struct {
21
+ profile *ddsnmp.Profile
22
+ setupMock func(m *snmpmock.MockHandler)
23
+ expectedResult []ddsnmp.Metric
24
+ expectedError bool
25
+ errorContains string
26
+ }{
27
+ "successful collection with scalar metrics only": {
28
+ profile: createTestProfile("test-profile.yaml", []ddprofiledefinition.MetricsConfig{
29
+ createScalarMetric("1.3.6.1.2.1.1.3.0", "sysUpTime"),
30
+ createScalarMetric("1.3.6.1.2.1.1.5.0", "sysName"),
31
+ }),
32
+ setupMock: func(m *snmpmock.MockHandler) {
33
+ expectSNMPGet(m, []string{"1.3.6.1.2.1.1.3.0", "1.3.6.1.2.1.1.5.0"}, []gosnmp.SnmpPDU{
34
+ createTimeTicksPDU("1.3.6.1.2.1.1.3.0", 123456),
35
+ createStringPDU("1.3.6.1.2.1.1.5.0", "test-host"),
36
+ })
37
+ },
38
+ expectedResult: []ddsnmp.Metric{
39
+ {
40
+ Name: "sysUpTime",
41
+ Value: 123456,
42
+ MetricType: "gauge",
43
+ },
44
+ // sysName will be skipped because it can't be converted to int64
45
+ },
46
+ expectedError: false,
47
+ },
48
+ "metric with scale factor": {
49
+ profile: &ddsnmp.Profile{
50
+ SourceFile: "test-profile.yaml",
51
+ Definition: &ddprofiledefinition.ProfileDefinition{
52
+ Metrics: []ddprofiledefinition.MetricsConfig{
53
+ {
54
+ Symbol: ddprofiledefinition.SymbolConfig{
55
+ OID: "1.3.6.1.4.1.12124.1.1.2",
56
+ Name: "memoryKilobytes",
57
+ ScaleFactor: 1000, // Convert KB to bytes
58
+ },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ setupMock: func(m *snmpmock.MockHandler) {
64
+ expectSNMPGet(m, []string{"1.3.6.1.4.1.12124.1.1.2"}, []gosnmp.SnmpPDU{
65
+ createGauge32PDU("1.3.6.1.4.1.12124.1.1.2", 1024),
66
+ })
67
+ },
68
+ expectedResult: []ddsnmp.Metric{
69
+ {
70
+ Name: "memoryKilobytes",
71
+ Value: 1024000, // 1024 * 1000
72
+ MetricType: "gauge",
73
+ },
74
+ },
75
+ expectedError: false,
76
+ },
77
+ "metric with extract_value": {
78
+ profile: &ddsnmp.Profile{
79
+ SourceFile: "test-profile.yaml",
80
+ Definition: &ddprofiledefinition.ProfileDefinition{
81
+ Metrics: []ddprofiledefinition.MetricsConfig{
82
+ {
83
+ Symbol: ddprofiledefinition.SymbolConfig{
84
+ OID: "1.3.6.1.4.1.12124.1.1.8",
85
+ Name: "temperature",
86
+ ExtractValueCompiled: mustCompileRegex(`(\d+)C`),
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ setupMock: func(m *snmpmock.MockHandler) {
93
+ expectSNMPGet(m, []string{"1.3.6.1.4.1.12124.1.1.8"}, []gosnmp.SnmpPDU{
94
+ createStringPDU("1.3.6.1.4.1.12124.1.1.8", "25C"),
95
+ })
96
+ },
97
+ expectedResult: []ddsnmp.Metric{
98
+ {
99
+ Name: "temperature",
100
+ Value: 25,
101
+ MetricType: "gauge",
102
+ },
103
+ },
104
+ expectedError: false,
105
+ },
106
+ "OID not found - returns empty metrics": {
107
+ profile: createTestProfile("test-profile.yaml", []ddprofiledefinition.MetricsConfig{
108
+ createScalarMetric("1.3.6.1.2.1.1.3.0", "sysUpTime"),
109
+ }),
110
+ setupMock: func(m *snmpmock.MockHandler) {
111
+ expectSNMPGet(m, []string{"1.3.6.1.2.1.1.3.0"}, []gosnmp.SnmpPDU{
112
+ createNoSuchObjectPDU("1.3.6.1.2.1.1.3.0"),
113
+ })
114
+ },
115
+ expectedResult: []ddsnmp.Metric{},
116
+ expectedError: false,
117
+ },
118
+ "empty profile - no metrics defined": {
119
+ profile: createTestProfile("empty-profile.yaml", []ddprofiledefinition.MetricsConfig{}),
120
+ setupMock: func(m *snmpmock.MockHandler) {
121
+ // No SNMP calls expected
122
+ },
123
+ expectedResult: nil,
124
+ expectedError: false,
125
+ },
126
+ "SNMP error": {
127
+ profile: createTestProfile("test-profile.yaml", []ddprofiledefinition.MetricsConfig{
128
+ createScalarMetric("1.3.6.1.2.1.1.3.0", "sysUpTime"),
129
+ }),
130
+ setupMock: func(m *snmpmock.MockHandler) {
131
+ expectSNMPGetError(m, []string{"1.3.6.1.2.1.1.3.0"}, errors.New("SNMP timeout"))
132
+ },
133
+ expectedResult: nil,
134
+ expectedError: true,
135
+ errorContains: "SNMP timeout",
136
+ },
137
+ "opaque float metric": {
138
+ profile: createTestProfile("test-profile.yaml", []ddprofiledefinition.MetricsConfig{
139
+ createScalarMetric("1.3.6.1.4.1.9.9.305.1.1.1.0", "cpmCPULoadAvg1min"),
140
+ createScalarMetric("1.3.6.1.4.1.9.9.305.1.1.2.0", "cpmCPULoadAvg5min"),
141
+ createScalarMetric("1.3.6.1.4.1.9.9.305.1.1.3.0", "cpmCPULoadAvg15min"),
142
+ }),
143
+ setupMock: func(m *snmpmock.MockHandler) {
144
+ expectSNMPGet(m, []string{
145
+ "1.3.6.1.4.1.9.9.305.1.1.1.0",
146
+ "1.3.6.1.4.1.9.9.305.1.1.2.0",
147
+ "1.3.6.1.4.1.9.9.305.1.1.3.0",
148
+ }, []gosnmp.SnmpPDU{
149
+ createPDU("1.3.6.1.4.1.9.9.305.1.1.1.0", gosnmp.OpaqueFloat, float32(0.75)),
150
+ createPDU("1.3.6.1.4.1.9.9.305.1.1.2.0", gosnmp.OpaqueFloat, float32(1.23)),
151
+ createPDU("1.3.6.1.4.1.9.9.305.1.1.3.0", gosnmp.OpaqueFloat, float32(2.45)),
152
+ })
153
+ },
154
+ expectedResult: []ddsnmp.Metric{
155
+ {
156
+ Name: "cpmCPULoadAvg1min",
157
+ Value: 0, // 0.75 truncated to int64
158
+ MetricType: "gauge",
159
+ },
160
+ {
161
+ Name: "cpmCPULoadAvg5min",
162
+ Value: 1, // 1.23 truncated to int64
163
+ MetricType: "gauge",
164
+ },
165
+ {
166
+ Name: "cpmCPULoadAvg15min",
167
+ Value: 2, // 2.45 truncated to int64
168
+ MetricType: "gauge",
169
+ },
170
+ },
171
+ expectedError: false,
172
+ },
173
+ "opaque double metric": {
174
+ profile: createTestProfile("test-profile.yaml", []ddprofiledefinition.MetricsConfig{
175
+ createScalarMetric("1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.1", "cpqHeSysBatteryVoltage"),
176
+ createScalarMetric("1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.2", "cpqHeSysBatteryCurrent"),
177
+ createScalarMetric("1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.3", "cpqHeSysBatteryCapacity"),
178
+ }),
179
+ setupMock: func(m *snmpmock.MockHandler) {
180
+ expectSNMPGet(m, []string{
181
+ "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.1",
182
+ "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.2",
183
+ "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.3",
184
+ }, []gosnmp.SnmpPDU{
185
+ createPDU("1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.1", gosnmp.OpaqueDouble, 12.6),
186
+ createPDU("1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.2", gosnmp.OpaqueDouble, 2.4),
187
+ createPDU("1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.3", gosnmp.OpaqueDouble, 98.5),
188
+ })
189
+ },
190
+ expectedResult: []ddsnmp.Metric{
191
+ {
192
+ Name: "cpqHeSysBatteryVoltage",
193
+ Value: 12, // 12.6 truncated to int64
194
+ MetricType: "gauge",
195
+ },
196
+ {
197
+ Name: "cpqHeSysBatteryCurrent",
198
+ Value: 2, // 2.4 truncated to int64
199
+ MetricType: "gauge",
200
+ },
201
+ {
202
+ Name: "cpqHeSysBatteryCapacity",
203
+ Value: 98, // 98.5 truncated to int64
204
+ MetricType: "gauge",
205
+ },
206
+ },
207
+ expectedError: false,
208
+ },
209
+ "multiple scalar metrics with different types": {
210
+ profile: &ddsnmp.Profile{
211
+ SourceFile: "test-profile.yaml",
212
+ Definition: &ddprofiledefinition.ProfileDefinition{
213
+ Metrics: []ddprofiledefinition.MetricsConfig{
214
+ {
215
+ Symbol: ddprofiledefinition.SymbolConfig{
216
+ OID: "1.3.6.1.2.1.1.3.0",
217
+ Name: "sysUpTime",
218
+ },
219
+ },
220
+ {
221
+ Symbol: ddprofiledefinition.SymbolConfig{
222
+ OID: "1.3.6.1.2.1.1.7.0",
223
+ Name: "sysServices",
224
+ MetricType: "gauge",
225
+ },
226
+ },
227
+ {
228
+ Symbol: ddprofiledefinition.SymbolConfig{
229
+ OID: "1.3.6.1.2.1.2.1.0",
230
+ Name: "ifNumber",
231
+ MetricType: "gauge",
232
+ },
233
+ },
234
+ },
235
+ },
236
+ },
237
+ setupMock: func(m *snmpmock.MockHandler) {
238
+ expectSNMPGet(m, []string{
239
+ "1.3.6.1.2.1.1.3.0",
240
+ "1.3.6.1.2.1.1.7.0",
241
+ "1.3.6.1.2.1.2.1.0",
242
+ }, []gosnmp.SnmpPDU{
243
+ createTimeTicksPDU("1.3.6.1.2.1.1.3.0", 123456),
244
+ createIntegerPDU("1.3.6.1.2.1.1.7.0", 72),
245
+ createIntegerPDU("1.3.6.1.2.1.2.1.0", 4),
246
+ })
247
+ },
248
+ expectedResult: []ddsnmp.Metric{
249
+ {
250
+ Name: "sysUpTime",
251
+ Value: 123456,
252
+ MetricType: "gauge",
253
+ },
254
+ {
255
+ Name: "sysServices",
256
+ Value: 72,
257
+ MetricType: "gauge",
258
+ },
259
+ {
260
+ Name: "ifNumber",
261
+ Value: 4,
262
+ MetricType: "gauge",
263
+ },
264
+ },
265
+ expectedError: false,
266
+ },
267
+ "scalar metrics with static tags": {
268
+ profile: &ddsnmp.Profile{
269
+ SourceFile: "test-profile.yaml",
270
+ Definition: &ddprofiledefinition.ProfileDefinition{
271
+ Metrics: []ddprofiledefinition.MetricsConfig{
272
+ {
273
+ Symbol: ddprofiledefinition.SymbolConfig{
274
+ OID: "1.3.6.1.2.1.1.3.0",
275
+ Name: "sysUpTime",
276
+ },
277
+ StaticTags: []string{
278
+ "source:system",
279
+ "type:uptime",
280
+ },
281
+ },
282
+ },
283
+ },
284
+ },
285
+ setupMock: func(m *snmpmock.MockHandler) {
286
+ expectSNMPGet(m, []string{"1.3.6.1.2.1.1.3.0"}, []gosnmp.SnmpPDU{
287
+ createTimeTicksPDU("1.3.6.1.2.1.1.3.0", 123456),
288
+ })
289
+ },
290
+ expectedResult: []ddsnmp.Metric{
291
+ {
292
+ Name: "sysUpTime",
293
+ Value: 123456,
294
+ StaticTags: map[string]string{
295
+ "source": "system",
296
+ "type": "uptime",
297
+ },
298
+ MetricType: "gauge",
299
+ },
300
+ },
301
+ expectedError: false,
302
+ },
303
+
304
+ "metric with string to int mapping": {
305
+ profile: &ddsnmp.Profile{
306
+ SourceFile: "test-profile.yaml",
307
+ Definition: &ddprofiledefinition.ProfileDefinition{
308
+ Metrics: []ddprofiledefinition.MetricsConfig{
309
+ {
310
+ Symbol: ddprofiledefinition.SymbolConfig{
311
+ OID: "1.3.6.1.4.1.12124.1.1.2",
312
+ Name: "clusterHealth",
313
+ Mapping: map[string]string{
314
+ "OK": "0",
315
+ "WARNING": "1",
316
+ "CRITICAL": "2",
317
+ },
318
+ },
319
+ },
320
+ },
321
+ },
322
+ },
323
+ setupMock: func(m *snmpmock.MockHandler) {
324
+ expectSNMPGet(m, []string{"1.3.6.1.4.1.12124.1.1.2"}, []gosnmp.SnmpPDU{
325
+ createStringPDU("1.3.6.1.4.1.12124.1.1.2", "WARNING"),
326
+ })
327
+ },
328
+ expectedResult: []ddsnmp.Metric{
329
+ {
330
+ Name: "clusterHealth",
331
+ Value: 1,
332
+ MetricType: "gauge",
333
+ Mappings: map[int64]string{
334
+ 0: "OK",
335
+ 1: "WARNING",
336
+ 2: "CRITICAL",
337
+ },
338
+ },
339
+ },
340
+ expectedError: false,
341
+ },
342
+ "metric with int to string mapping": {
343
+ profile: &ddsnmp.Profile{
344
+ SourceFile: "test-profile.yaml",
345
+ Definition: &ddprofiledefinition.ProfileDefinition{
346
+ Metrics: []ddprofiledefinition.MetricsConfig{
347
+ {
348
+ Symbol: ddprofiledefinition.SymbolConfig{
349
+ OID: "1.3.6.1.2.1.2.2.1.8",
350
+ Name: "ifOperStatus",
351
+ Mapping: map[string]string{
352
+ "1": "up",
353
+ "2": "down",
354
+ "3": "testing",
355
+ "4": "unknown",
356
+ "5": "dormant",
357
+ },
358
+ },
359
+ },
360
+ },
361
+ },
362
+ },
363
+ setupMock: func(m *snmpmock.MockHandler) {
364
+ expectSNMPGet(m, []string{"1.3.6.1.2.1.2.2.1.8"}, []gosnmp.SnmpPDU{
365
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.8", 2), // down
366
+ })
367
+ },
368
+ expectedResult: []ddsnmp.Metric{
369
+ {
370
+ Name: "ifOperStatus",
371
+ Value: 2,
372
+ MetricType: "gauge",
373
+ Mappings: map[int64]string{
374
+ 1: "up",
375
+ 2: "down",
376
+ 3: "testing",
377
+ 4: "unknown",
378
+ 5: "dormant",
379
+ },
380
+ },
381
+ },
382
+ expectedError: false,
383
+ },
384
+ "metric with extract_value and int to string mapping": {
385
+ profile: &ddsnmp.Profile{
386
+ SourceFile: "test-profile.yaml",
387
+ Definition: &ddprofiledefinition.ProfileDefinition{
388
+ Metrics: []ddprofiledefinition.MetricsConfig{
389
+ {
390
+ Symbol: ddprofiledefinition.SymbolConfig{
391
+ OID: "1.3.6.1.4.1.12124.1.1.8",
392
+ Name: "fanStatus",
393
+ ExtractValueCompiled: mustCompileRegex(`Fan(\d+)`),
394
+ Mapping: map[string]string{
395
+ "1": "normal",
396
+ "2": "warning",
397
+ "3": "critical",
398
+ },
399
+ },
400
+ },
401
+ },
402
+ },
403
+ },
404
+ setupMock: func(m *snmpmock.MockHandler) {
405
+ expectSNMPGet(m, []string{"1.3.6.1.4.1.12124.1.1.8"}, []gosnmp.SnmpPDU{
406
+ createStringPDU("1.3.6.1.4.1.12124.1.1.8", "Fan2"),
407
+ })
408
+ },
409
+ expectedResult: []ddsnmp.Metric{
410
+ {
411
+ Name: "fanStatus",
412
+ Value: 2,
413
+ MetricType: "gauge",
414
+ Mappings: map[int64]string{
415
+ 1: "normal",
416
+ 2: "warning",
417
+ 3: "critical",
418
+ },
419
+ },
420
+ },
421
+ expectedError: false,
422
+ },
423
+ "metric with int to int mapping": {
424
+ profile: &ddsnmp.Profile{
425
+ SourceFile: "test-profile.yaml",
426
+ Definition: &ddprofiledefinition.ProfileDefinition{
427
+ Metrics: []ddprofiledefinition.MetricsConfig{
428
+ {
429
+ Symbol: ddprofiledefinition.SymbolConfig{
430
+ OID: "1.3.6.1.2.1.2.2.1.7",
431
+ Name: "ifAdminStatus",
432
+ Mapping: map[string]string{
433
+ "1": "1", // up -> 1
434
+ "2": "0", // down -> 0
435
+ "3": "0", // testing -> 0
436
+ },
437
+ },
438
+ },
439
+ },
440
+ },
441
+ },
442
+ setupMock: func(m *snmpmock.MockHandler) {
443
+ expectSNMPGet(m, []string{"1.3.6.1.2.1.2.2.1.7"}, []gosnmp.SnmpPDU{
444
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.7", 2), // down
445
+ })
446
+ },
447
+ expectedResult: []ddsnmp.Metric{
448
+ {
449
+ Name: "ifAdminStatus",
450
+ Value: 0, // mapped from 2 -> 0
451
+ MetricType: "gauge",
452
+ Mappings: map[int64]string{
453
+ 1: "1",
454
+ 2: "0",
455
+ 3: "0",
456
+ },
457
+ },
458
+ },
459
+ expectedError: false,
460
+ },
461
+ "metric with partial string to int mapping": {
462
+ profile: &ddsnmp.Profile{
463
+ SourceFile: "test-profile.yaml",
464
+ Definition: &ddprofiledefinition.ProfileDefinition{
465
+ Metrics: []ddprofiledefinition.MetricsConfig{
466
+ {
467
+ Symbol: ddprofiledefinition.SymbolConfig{
468
+ OID: "1.3.6.1.4.1.12124.1.1.2",
469
+ Name: "clusterHealth",
470
+ Mapping: map[string]string{
471
+ "OK": "0",
472
+ "WARNING": "1",
473
+ // CRITICAL is not mapped
474
+ },
475
+ },
476
+ },
477
+ },
478
+ },
479
+ },
480
+ setupMock: func(m *snmpmock.MockHandler) {
481
+ expectSNMPGet(m, []string{"1.3.6.1.4.1.12124.1.1.2"}, []gosnmp.SnmpPDU{
482
+ createStringPDU("1.3.6.1.4.1.12124.1.1.2", "CRITICAL"),
483
+ })
484
+ },
485
+ expectedResult: nil,
486
+ expectedError: true,
487
+ errorContains: "strconv.ParseInt",
488
+ },
489
+ "metric with mixed mapping values": {
490
+ profile: &ddsnmp.Profile{
491
+ SourceFile: "test-profile.yaml",
492
+ Definition: &ddprofiledefinition.ProfileDefinition{
493
+ Metrics: []ddprofiledefinition.MetricsConfig{
494
+ {
495
+ Symbol: ddprofiledefinition.SymbolConfig{
496
+ OID: "1.3.6.1.4.1.12124.1.1.2",
497
+ Name: "deviceStatus",
498
+ Mapping: map[string]string{
499
+ "OK": "0",
500
+ "WARNING": "1",
501
+ "ERROR": "invalid", // This will cause metric to be skipped
502
+ },
503
+ },
504
+ },
505
+ },
506
+ },
507
+ },
508
+ setupMock: func(m *snmpmock.MockHandler) {
509
+ expectSNMPGet(m, []string{"1.3.6.1.4.1.12124.1.1.2"}, []gosnmp.SnmpPDU{
510
+ createStringPDU("1.3.6.1.4.1.12124.1.1.2", "ERROR"),
511
+ })
512
+ },
513
+ expectedResult: nil,
514
+ expectedError: true,
515
+ errorContains: "strconv.ParseInt",
516
+ },
517
+ "metric with no mapping": {
518
+ profile: createTestProfile("test-profile.yaml", []ddprofiledefinition.MetricsConfig{
519
+ createScalarMetric("1.3.6.1.2.1.1.3.0", "sysUpTime"),
520
+ }),
521
+ setupMock: func(m *snmpmock.MockHandler) {
522
+ expectSNMPGet(m, []string{"1.3.6.1.2.1.1.3.0"}, []gosnmp.SnmpPDU{
523
+ createTimeTicksPDU("1.3.6.1.2.1.1.3.0", 123456),
524
+ })
525
+ },
526
+ expectedResult: []ddsnmp.Metric{
527
+ {
528
+ Name: "sysUpTime",
529
+ Value: 123456,
530
+ MetricType: "gauge",
531
+ Mappings: nil, // No mappings
532
+ },
533
+ },
534
+ expectedError: false,
535
+ },
536
+ "metric with numeric value and int to string mapping": {
537
+ profile: &ddsnmp.Profile{
538
+ SourceFile: "test-profile.yaml",
539
+ Definition: &ddprofiledefinition.ProfileDefinition{
540
+ Metrics: []ddprofiledefinition.MetricsConfig{
541
+ {
542
+ Symbol: ddprofiledefinition.SymbolConfig{
543
+ OID: "1.3.6.1.2.1.2.2.1.7",
544
+ Name: "ifAdminStatus",
545
+ Mapping: map[string]string{
546
+ "1": "up",
547
+ "2": "down",
548
+ "3": "testing",
549
+ },
550
+ },
551
+ },
552
+ },
553
+ },
554
+ },
555
+ setupMock: func(m *snmpmock.MockHandler) {
556
+ expectSNMPGet(m, []string{"1.3.6.1.2.1.2.2.1.7"}, []gosnmp.SnmpPDU{
557
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.7", 1), // up
558
+ })
559
+ },
560
+ expectedResult: []ddsnmp.Metric{
561
+ {
562
+ Name: "ifAdminStatus",
563
+ Value: 1,
564
+ MetricType: "gauge",
565
+ Mappings: map[int64]string{
566
+ 1: "up",
567
+ 2: "down",
568
+ 3: "testing",
569
+ },
570
+ },
571
+ },
572
+ expectedError: false,
573
+ },
574
+ "metric with string value and string to int mapping": {
575
+ profile: &ddsnmp.Profile{
576
+ SourceFile: "test-profile.yaml",
577
+ Definition: &ddprofiledefinition.ProfileDefinition{
578
+ Metrics: []ddprofiledefinition.MetricsConfig{
579
+ {
580
+ Symbol: ddprofiledefinition.SymbolConfig{
581
+ OID: "1.3.6.1.4.1.318.1.1.1.2.2.1.0",
582
+ Name: "upsBasicBatteryStatus",
583
+ Mapping: map[string]string{
584
+ "batteryNormal": "0",
585
+ "batteryLow": "1",
586
+ "batteryDepleted": "2",
587
+ "batteryCharging": "3",
588
+ },
589
+ },
590
+ },
591
+ },
592
+ },
593
+ },
594
+ setupMock: func(m *snmpmock.MockHandler) {
595
+ expectSNMPGet(m, []string{"1.3.6.1.4.1.318.1.1.1.2.2.1.0"}, []gosnmp.SnmpPDU{
596
+ createStringPDU("1.3.6.1.4.1.318.1.1.1.2.2.1.0", "batteryLow"),
597
+ })
598
+ },
599
+ expectedResult: []ddsnmp.Metric{
600
+ {
601
+ Name: "upsBasicBatteryStatus",
602
+ Value: 1,
603
+ MetricType: "gauge",
604
+ Mappings: map[int64]string{
605
+ 0: "batteryNormal",
606
+ 1: "batteryLow",
607
+ 2: "batteryDepleted",
608
+ 3: "batteryCharging",
609
+ },
610
+ },
611
+ },
612
+ expectedError: false,
613
+ },
614
+ "partial success with missing OIDs": {
615
+ profile: &ddsnmp.Profile{
616
+ SourceFile: "test-profile.yaml",
617
+ Definition: &ddprofiledefinition.ProfileDefinition{
618
+ Metrics: []ddprofiledefinition.MetricsConfig{
619
+ createScalarMetric("1.3.6.1.2.1.1.3.0", "sysUpTime"),
620
+ createScalarMetric("1.3.6.1.2.1.1.5.0", "sysName"),
621
+ createScalarMetric("1.3.6.1.2.1.1.6.0", "sysLocation"),
622
+ },
623
+ },
624
+ },
625
+ setupMock: func(m *snmpmock.MockHandler) {
626
+ expectSNMPGet(m, []string{
627
+ "1.3.6.1.2.1.1.3.0",
628
+ "1.3.6.1.2.1.1.5.0",
629
+ "1.3.6.1.2.1.1.6.0",
630
+ }, []gosnmp.SnmpPDU{
631
+ createTimeTicksPDU("1.3.6.1.2.1.1.3.0", 123456),
632
+ createNoSuchObjectPDU("1.3.6.1.2.1.1.5.0"),
633
+ createStringPDU("1.3.6.1.2.1.1.6.0", "DataCenter"),
634
+ })
635
+ },
636
+ expectedResult: []ddsnmp.Metric{
637
+ {
638
+ Name: "sysUpTime",
639
+ Value: 123456,
640
+ MetricType: "gauge",
641
+ },
642
+ // sysName is missing (NoSuchObject)
643
+ // sysLocation is string and can't be converted to int64
644
+ },
645
+ expectedError: false,
646
+ },
647
+ "chunked requests with many metrics": {
648
+ profile: func() *ddsnmp.Profile {
649
+ // Create a profile with many metrics to force chunking
650
+ var metrics []ddprofiledefinition.MetricsConfig
651
+ for i := 0; i < 25; i++ {
652
+ metrics = append(metrics, createScalarMetric(
653
+ fmt.Sprintf("1.3.6.1.2.1.1.%02d.0", i),
654
+ fmt.Sprintf("metric%d", i),
655
+ ))
656
+ }
657
+ return &ddsnmp.Profile{
658
+ SourceFile: "test-profile.yaml",
659
+ Definition: &ddprofiledefinition.ProfileDefinition{
660
+ Metrics: metrics,
661
+ },
662
+ }
663
+ }(),
664
+ setupMock: func(m *snmpmock.MockHandler) {
665
+ // Expect 3 chunks (10 + 10 + 5)
666
+ chunk1OIDs := make([]string, 10)
667
+ chunk1PDUs := make([]gosnmp.SnmpPDU, 10)
668
+ for i := 0; i < 10; i++ {
669
+ chunk1OIDs[i] = fmt.Sprintf("1.3.6.1.2.1.1.%02d.0", i)
670
+ chunk1PDUs[i] = createIntegerPDU(chunk1OIDs[i], i*100)
671
+ }
672
+
673
+ chunk2OIDs := make([]string, 10)
674
+ chunk2PDUs := make([]gosnmp.SnmpPDU, 10)
675
+ for i := 0; i < 10; i++ {
676
+ chunk2OIDs[i] = fmt.Sprintf("1.3.6.1.2.1.1.%02d.0", i+10)
677
+ chunk2PDUs[i] = createIntegerPDU(chunk2OIDs[i], (i+10)*100)
678
+ }
679
+
680
+ chunk3OIDs := make([]string, 5)
681
+ chunk3PDUs := make([]gosnmp.SnmpPDU, 5)
682
+ for i := 0; i < 5; i++ {
683
+ chunk3OIDs[i] = fmt.Sprintf("1.3.6.1.2.1.1.%02d.0", i+20)
684
+ chunk3PDUs[i] = createIntegerPDU(chunk3OIDs[i], (i+20)*100)
685
+ }
686
+
687
+ m.EXPECT().Get(chunk1OIDs).Return(&gosnmp.SnmpPacket{Variables: chunk1PDUs}, nil)
688
+ m.EXPECT().Get(chunk2OIDs).Return(&gosnmp.SnmpPacket{Variables: chunk2PDUs}, nil)
689
+ m.EXPECT().Get(chunk3OIDs).Return(&gosnmp.SnmpPacket{Variables: chunk3PDUs}, nil)
690
+ },
691
+ expectedResult: func() []ddsnmp.Metric {
692
+ // Generate expected metrics
693
+ var metrics []ddsnmp.Metric
694
+ for i := 0; i < 25; i++ {
695
+ metrics = append(metrics, ddsnmp.Metric{
696
+ Name: fmt.Sprintf("metric%d", i),
697
+ Value: int64(i * 100),
698
+ MetricType: "gauge",
699
+ })
700
+ }
701
+ return metrics
702
+ }(),
703
+ expectedError: false,
704
+ },
705
+ }
706
+
707
+ for name, tc := range tests {
708
+ t.Run(name, func(t *testing.T) {
709
+ ctrl, mockHandler := setupMockHandler(t)
710
+ defer ctrl.Finish()
711
+
712
+ tc.setupMock(mockHandler)
713
+
714
+ missingOIDs := make(map[string]bool)
715
+ collector := newScalarCollector(mockHandler, missingOIDs, logger.New())
716
+
717
+ result, err := collector.Collect(tc.profile)
718
+
719
+ if tc.expectedError {
720
+ assert.Error(t, err)
721
+ if tc.errorContains != "" {
722
+ assert.Contains(t, err.Error(), tc.errorContains)
723
+ }
724
+ } else {
725
+ assert.NoError(t, err)
726
+ }
727
+
728
+ if tc.expectedResult != nil {
729
+ assertMetricsEqual(t, tc.expectedResult, result)
730
+ } else {
731
+ assert.Nil(t, result)
732
+ }
733
+ })
734
+ }
735
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table.go
new
+626
@@ -0,0 +1,626 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "slices"
9
+ "strings"
10
+
11
+ "github.com/gosnmp/gosnmp"
12
+
13
+ "github.com/netdata/netdata/go/plugins/logger"
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
+// tableCollector handles collection of SNMP table metrics
19
+type tableCollector struct {
20
+ snmpClient gosnmp.Handler
21
+ missingOIDs map[string]bool
22
+ tableCache *tableCache
23
+ log *logger.Logger
24
+ valProc *valueProcessor
25
+ rowProcessor *tableRowProcessor
26
+}
27
+
28
+// newTableCollector creates a new table collector
29
+func newTableCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, tableCache *tableCache, log *logger.Logger) *tableCollector {
30
+ return &tableCollector{
31
+ snmpClient: snmpClient,
32
+ missingOIDs: missingOIDs,
33
+ tableCache: tableCache,
34
+ log: log,
35
+ valProc: newValueProcessor(),
36
+ rowProcessor: newTableRowProcessor(log),
37
+ }
38
+}
39
+
40
+// Collect gathers all table metrics from the profile
41
+func (tc *tableCollector) Collect(prof *ddsnmp.Profile) ([]ddsnmp.Metric, error) {
42
+ walkResults, err := tc.walkTablesAsNeeded(prof)
43
+ if err != nil {
44
+ return nil, err
45
+ }
46
+
47
+ return tc.processWalkResults(walkResults)
48
+}
49
+
50
+// tableWalkResult holds the walked data for a single table
51
+// tableWalkResult holds the walked data for a single table
52
+type tableWalkResult struct {
53
+ // tableOID is the base OID of the table (e.g., "1.3.6.1.2.1.2.2" for ifTable)
54
+ // Used to identify which table this result belongs to
55
+ tableOID string
56
+
57
+ // pdus contains all PDUs retrieved from walking this table
58
+ // Key: full OID (e.g., "1.3.6.1.2.1.2.2.1.10.1"), Value: SNMP PDU
59
+ // nil when using cached data (table structure already known)
60
+ pdus map[string]gosnmp.SnmpPDU
61
+
62
+ // config is the metric configuration that requested this table
63
+ // Contains symbols to collect, tags to apply, and metric metadata
64
+ config ddprofiledefinition.MetricsConfig
65
+}
66
+
67
+type tableProcessingContext struct {
68
+ // === Input data (set when context is created) ===
69
+
70
+ // config is the metric configuration for this table
71
+ // Contains table OID, symbols to collect, and tag configurations
72
+ config ddprofiledefinition.MetricsConfig
73
+
74
+ // pdus contains all PDUs from walking this specific table
75
+ // Key: full OID (e.g., "1.3.6.1.2.1.2.2.1.10.1"), Value: SNMP PDU
76
+ pdus map[string]gosnmp.SnmpPDU
77
+
78
+ // walkedData contains PDUs from ALL walked tables in this collection
79
+ // Used for cross-table tag resolution
80
+ // Key: table OID → map[full OID]PDU
81
+ walkedData map[string]map[string]gosnmp.SnmpPDU
82
+
83
+ // tableNameToOID maps table names to their OIDs
84
+ // Used to resolve cross-table references (e.g., "ifXTable" → "1.3.6.1.2.1.31.1.1")
85
+ tableNameToOID map[string]string
86
+
87
+ // === Computed during processing (set by various methods) ===
88
+
89
+ // columnOIDs maps column OIDs to their symbol configurations
90
+ // Built from config.Symbols, used to identify which columns contain metrics
91
+ // Key: column OID (e.g., "1.3.6.1.2.1.2.2.1.10"), Value: symbol config
92
+ columnOIDs map[string]ddprofiledefinition.SymbolConfig
93
+
94
+ // sameTableTagOIDs maps column OIDs to tag configurations for same-table tags only
95
+ // Built from config.MetricTags, excludes cross-table tags
96
+ // Key: column OID → list of tag configs (multiple tags can use same column)
97
+ sameTableTagOIDs map[string][]ddprofiledefinition.MetricTagConfig
98
+
99
+ // staticTags contains tags that apply to all metrics from this table
100
+ // Parsed from config.StaticTags (e.g., "source:network")
101
+ staticTags map[string]string
102
+
103
+ // rows contains PDUs organized by row index
104
+ // Created by organizePDUsByRow from flat PDU list
105
+ // Key: row index (e.g., "1", "2.3") → map[column OID]PDU
106
+ rows map[string]map[string]gosnmp.SnmpPDU
107
+
108
+ // oidCache stores the mapping of column OID to full OID for each row
109
+ // Used for caching table structure
110
+ // Key: row index → map[column OID]full OID
111
+ oidCache map[string]map[string]string
112
+
113
+ // tagCache stores computed tag values for each row
114
+ // Populated during row processing, used for caching
115
+ // Key: row index → map[tag name]tag value
116
+ tagCache map[string]map[string]string
117
+}
118
+
119
+type cacheProcessingContext struct {
120
+ // config is the metric configuration for this table
121
+ // Used to determine which columns are metrics and parse static tags
122
+ config ddprofiledefinition.MetricsConfig
123
+
124
+ // cachedOIDs contains the cached table structure
125
+ // Retrieved from tableCache, maps row index → column OID → full OID
126
+ // Key: row index → map[column OID]full OID
127
+ cachedOIDs map[string]map[string]string
128
+
129
+ // cachedTags contains previously computed tag values
130
+ // Retrieved from tableCache, avoids re-processing tag columns
131
+ // Key: row index → map[tag name]tag value
132
+ cachedTags map[string]map[string]string
133
+
134
+ // columnOIDs identifies which columns contain metrics (not tags)
135
+ // Built from config.Symbols, used to filter which OIDs to GET
136
+ // Key: column OID, Value: symbol configuration
137
+ columnOIDs map[string]ddprofiledefinition.SymbolConfig
138
+
139
+ // pdus contains current metric values fetched via SNMP GET
140
+ // Only contains metric columns (not tag columns, which are cached)
141
+ // Key: full OID (trimmed), Value: current PDU value
142
+ pdus map[string]gosnmp.SnmpPDU
143
+}
144
+
145
+// walkTablesAsNeeded walks only tables that aren't fully cached
146
+func (tc *tableCollector) walkTablesAsNeeded(prof *ddsnmp.Profile) ([]tableWalkResult, error) {
147
+ toWalk := tc.identifyTablesToWalk(prof)
148
+
149
+ walkedData, errs := tc.walkTables(toWalk.tablesToWalk)
150
+
151
+ results := tc.buildWalkResults(walkedData, toWalk)
152
+
153
+ if len(results) == 0 && len(errs) > 0 {
154
+ return nil, errors.Join(errs...)
155
+ }
156
+
157
+ return results, nil
158
+}
159
+
160
+// tablesToWalkInfo holds information about which tables need walking
161
+type tablesToWalkInfo struct {
162
+ tablesToWalk map[string]bool
163
+ tableConfigs map[string][]ddprofiledefinition.MetricsConfig
164
+ missingOIDs []string
165
+}
166
+
167
+// identifyTablesToWalk determines which tables need to be walked
168
+func (tc *tableCollector) identifyTablesToWalk(prof *ddsnmp.Profile) *tablesToWalkInfo {
169
+ info := &tablesToWalkInfo{
170
+ tablesToWalk: make(map[string]bool),
171
+ tableConfigs: make(map[string][]ddprofiledefinition.MetricsConfig),
172
+ }
173
+
174
+ for _, cfg := range prof.Definition.Metrics {
175
+ if cfg.IsScalar() || cfg.Table.OID == "" {
176
+ continue
177
+ }
178
+
179
+ tableOID := cfg.Table.OID
180
+ if tc.missingOIDs[trimOID(tableOID)] {
181
+ info.missingOIDs = append(info.missingOIDs, tableOID)
182
+ continue
183
+ }
184
+
185
+ info.tableConfigs[tableOID] = append(info.tableConfigs[tableOID], cfg)
186
+
187
+ if !tc.tableCache.isConfigCached(cfg) {
188
+ info.tablesToWalk[tableOID] = true
189
+ }
190
+ }
191
+
192
+ tc.log.Debugf("Tables walking %d cached %d (%d total)",
193
+ len(info.tablesToWalk), len(info.tableConfigs)-len(info.tablesToWalk), len(info.tableConfigs))
194
+
195
+ if len(info.missingOIDs) > 0 {
196
+ tc.log.Debugf("table metrics missing OIDs: %v", info.missingOIDs)
197
+ }
198
+
199
+ return info
200
+}
201
+
202
+// walkTables performs SNMP walks for the specified tables
203
+func (tc *tableCollector) walkTables(tablesToWalk map[string]bool) (map[string]map[string]gosnmp.SnmpPDU, []error) {
204
+ walkedData := make(map[string]map[string]gosnmp.SnmpPDU)
205
+ var errs []error
206
+
207
+ for tableOID := range tablesToWalk {
208
+ pdus, err := tc.snmpWalk(tableOID)
209
+ if err != nil {
210
+ errs = append(errs, fmt.Errorf("failed to walk table OID '%s': %w", tableOID, err))
211
+ continue
212
+ }
213
+
214
+ if len(pdus) > 0 {
215
+ walkedData[tableOID] = pdus
216
+ }
217
+ }
218
+
219
+ return walkedData, errs
220
+}
221
+
222
+// buildWalkResults creates results for all configurations
223
+func (tc *tableCollector) buildWalkResults(walkedData map[string]map[string]gosnmp.SnmpPDU, info *tablesToWalkInfo) []tableWalkResult {
224
+ var results []tableWalkResult
225
+
226
+ for tableOID, configs := range info.tableConfigs {
227
+ for _, cfg := range configs {
228
+ // Add to results if we have walked data OR if it's cached
229
+ if pdus, ok := walkedData[tableOID]; ok {
230
+ results = append(results, tableWalkResult{
231
+ tableOID: tableOID,
232
+ pdus: pdus,
233
+ config: cfg,
234
+ })
235
+ } else if tc.tableCache.isConfigCached(cfg) {
236
+ // Add config without PDUs - will use cache in processing
237
+ results = append(results, tableWalkResult{
238
+ tableOID: tableOID,
239
+ pdus: nil,
240
+ config: cfg,
241
+ })
242
+ }
243
+ }
244
+ }
245
+
246
+ return results
247
+}
248
+
249
+// processWalkResults processes all table walk results
250
+func (tc *tableCollector) processWalkResults(walkResults []tableWalkResult) ([]ddsnmp.Metric, error) {
251
+ // Build lookup maps
252
+ walkedData := tc.buildWalkedDataMap(walkResults)
253
+ tableNameToOID := tc.buildTableNameMap(walkResults)
254
+
255
+ var metrics []ddsnmp.Metric
256
+ var errs []error
257
+
258
+ for _, result := range walkResults {
259
+ tableMetrics, err := tc.processTableResult(result, walkedData, tableNameToOID)
260
+ if err != nil {
261
+ errs = append(errs, fmt.Errorf("table '%s': %w", result.config.Table.Name, err))
262
+ continue
263
+ }
264
+ metrics = append(metrics, tableMetrics...)
265
+ }
266
+
267
+ if len(metrics) == 0 && len(errs) > 0 {
268
+ return nil, errors.Join(errs...)
269
+ }
270
+
271
+ return metrics, nil
272
+}
273
+
274
+// buildWalkedDataMap creates a map of table OID to PDUs
275
+func (tc *tableCollector) buildWalkedDataMap(walkResults []tableWalkResult) map[string]map[string]gosnmp.SnmpPDU {
276
+ walkedData := make(map[string]map[string]gosnmp.SnmpPDU)
277
+ for _, result := range walkResults {
278
+ if result.pdus != nil {
279
+ walkedData[result.tableOID] = result.pdus
280
+ }
281
+ }
282
+ return walkedData
283
+}
284
+
285
+// buildTableNameMap creates a map of table name to OID
286
+func (tc *tableCollector) buildTableNameMap(walkResults []tableWalkResult) map[string]string {
287
+ tableNameToOID := make(map[string]string)
288
+ for _, result := range walkResults {
289
+ if result.config.Table.Name != "" {
290
+ tableNameToOID[result.config.Table.Name] = result.tableOID
291
+ }
292
+ }
293
+ return tableNameToOID
294
+}
295
+
296
+// processTableResult processes a single table result
297
+func (tc *tableCollector) processTableResult(result tableWalkResult, walkedData map[string]map[string]gosnmp.SnmpPDU, tableNameToOID map[string]string) ([]ddsnmp.Metric, error) {
298
+ // Try cache first
299
+ if metrics := tc.tryCollectFromCache(result.config); metrics != nil {
300
+ return metrics, nil
301
+ }
302
+
303
+ // Process walked data if available
304
+ if result.pdus != nil {
305
+ ctx := &tableProcessingContext{
306
+ config: result.config,
307
+ pdus: result.pdus,
308
+ walkedData: walkedData,
309
+ tableNameToOID: tableNameToOID,
310
+ }
311
+ return tc.processTableData(ctx)
312
+ }
313
+
314
+ return nil, nil
315
+}
316
+
317
+// tryCollectFromCache attempts to collect metrics using cached data
318
+func (tc *tableCollector) tryCollectFromCache(cfg ddprofiledefinition.MetricsConfig) []ddsnmp.Metric {
319
+ cachedOIDs, cachedTags, ok := tc.tableCache.getCachedData(cfg)
320
+ if !ok {
321
+ return nil
322
+ }
323
+
324
+ columnOIDs := buildColumnOIDs(cfg)
325
+
326
+ ctx := &cacheProcessingContext{
327
+ config: cfg,
328
+ cachedOIDs: cachedOIDs,
329
+ cachedTags: cachedTags,
330
+ columnOIDs: columnOIDs,
331
+ }
332
+
333
+ metrics, err := tc.collectWithCache(ctx)
334
+ if err == nil {
335
+ tc.log.Debugf("Successfully collected table %s using cache", cfg.Table.Name)
336
+ return metrics
337
+ }
338
+
339
+ tc.log.Debugf("Cached collection failed for table %s: %v", cfg.Table.Name, err)
340
+ return nil
341
+}
342
+
343
+// processTableData processes walked table data
344
+func (tc *tableCollector) processTableData(ctx *tableProcessingContext) ([]ddsnmp.Metric, error) {
345
+ ctx.columnOIDs = buildColumnOIDs(ctx.config)
346
+ ctx.sameTableTagOIDs = buildSameTableTagOIDs(ctx.config)
347
+
348
+ ctx.rows, ctx.oidCache, ctx.tagCache = tc.organizePDUsByRow(ctx)
349
+
350
+ ctx.staticTags = parseStaticTags(ctx.config.StaticTags)
351
+
352
+ metrics, err := tc.processRows(ctx)
353
+
354
+ // Cache the processed data
355
+ deps := extractTableDependencies(ctx.config, ctx.tableNameToOID)
356
+ tc.tableCache.cacheData(ctx.config, ctx.oidCache, ctx.tagCache, deps)
357
+ tc.log.Debugf("Cached table %s structure with %d rows", ctx.config.Table.Name, len(ctx.oidCache))
358
+
359
+ return metrics, err
360
+}
361
+
362
+// organizePDUsByRow groups PDUs by their row index
363
+func (tc *tableCollector) organizePDUsByRow(ctx *tableProcessingContext) (rows map[string]map[string]gosnmp.SnmpPDU, oidCache, tagCache map[string]map[string]string) {
364
+ // Combine all column OIDs
365
+ allColumnOIDs := make([]string, 0, len(ctx.columnOIDs)+len(ctx.sameTableTagOIDs))
366
+ for oid := range ctx.columnOIDs {
367
+ allColumnOIDs = append(allColumnOIDs, oid)
368
+ }
369
+ for oid := range ctx.sameTableTagOIDs {
370
+ allColumnOIDs = append(allColumnOIDs, oid)
371
+ }
372
+
373
+ rows = make(map[string]map[string]gosnmp.SnmpPDU)
374
+ oidCache = make(map[string]map[string]string)
375
+ tagCache = make(map[string]map[string]string)
376
+
377
+ for oid, pdu := range ctx.pdus {
378
+ for _, columnOID := range allColumnOIDs {
379
+ if strings.HasPrefix(oid, columnOID+".") {
380
+ index := strings.TrimPrefix(oid, columnOID+".")
381
+
382
+ if rows[index] == nil {
383
+ rows[index] = make(map[string]gosnmp.SnmpPDU)
384
+ oidCache[index] = make(map[string]string)
385
+ tagCache[index] = make(map[string]string)
386
+ }
387
+ rows[index][columnOID] = pdu
388
+ oidCache[index][columnOID] = oid
389
+ break
390
+ }
391
+ }
392
+ }
393
+
394
+ return rows, oidCache, tagCache
395
+}
396
+
397
+// processRows processes all rows and returns metrics
398
+func (tc *tableCollector) processRows(ctx *tableProcessingContext) ([]ddsnmp.Metric, error) {
399
+ var metrics []ddsnmp.Metric
400
+ var errs []error
401
+
402
+ crossTableCtx := &crossTableContext{
403
+ walkedData: ctx.walkedData,
404
+ tableNameToOID: ctx.tableNameToOID,
405
+ }
406
+
407
+ for index, rowPDUs := range ctx.rows {
408
+ row := &tableRowData{
409
+ index: index,
410
+ pdus: rowPDUs,
411
+ tags: make(map[string]string),
412
+ staticTags: ctx.staticTags,
413
+ }
414
+ crossTableCtx.rowTags = row.tags
415
+
416
+ rowCtx := &tableRowProcessingContext{
417
+ config: ctx.config,
418
+ columnOIDs: ctx.columnOIDs,
419
+ sameTableTagOIDs: ctx.sameTableTagOIDs,
420
+ crossTableCtx: crossTableCtx,
421
+ }
422
+ rowMetrics, err := tc.rowProcessor.processRow(row, rowCtx)
423
+ if err != nil {
424
+ errs = append(errs, err)
425
+ continue
426
+ }
427
+
428
+ // Copy processed tags to cache
429
+ for k, v := range row.tags {
430
+ ctx.tagCache[index][k] = v
431
+ }
432
+
433
+ metrics = append(metrics, rowMetrics...)
434
+ }
435
+
436
+ if len(errs) > 0 {
437
+ tc.log.Warningf("failed to collect table metrics: %v", errors.Join(errs...))
438
+ }
439
+
440
+ return metrics, nil
441
+}
442
+
443
+// collectWithCache collects metrics using cached structure
444
+func (tc *tableCollector) collectWithCache(ctx *cacheProcessingContext) ([]ddsnmp.Metric, error) {
445
+ // Build list of OIDs to GET
446
+ var oidsToGet []string
447
+ for _, columns := range ctx.cachedOIDs {
448
+ for columnOID, fullOID := range columns {
449
+ if _, isMetric := ctx.columnOIDs[columnOID]; isMetric {
450
+ oidsToGet = append(oidsToGet, fullOID)
451
+ }
452
+ }
453
+ }
454
+
455
+ if len(oidsToGet) == 0 {
456
+ return nil, nil
457
+ }
458
+
459
+ // GET current values
460
+ pdus, err := tc.snmpGet(oidsToGet)
461
+ if err != nil {
462
+ return nil, fmt.Errorf("failed to get cached OIDs: %w", err)
463
+ }
464
+
465
+ // Validate response
466
+ if len(pdus) < len(oidsToGet)/2 {
467
+ return nil, fmt.Errorf("table structure may have changed, got %d/%d PDUs", len(pdus), len(oidsToGet))
468
+ }
469
+
470
+ // Add PDUs to context and build metrics
471
+ ctx.pdus = pdus
472
+ return tc.buildMetricsFromCache(ctx)
473
+}
474
+
475
+// buildMetricsFromCache builds metrics from cached structure and current values
476
+func (tc *tableCollector) buildMetricsFromCache(ctx *cacheProcessingContext) ([]ddsnmp.Metric, error) {
477
+ staticTags := parseStaticTags(ctx.config.StaticTags)
478
+ var metrics []ddsnmp.Metric
479
+ var errs []error
480
+
481
+ for index, columns := range ctx.cachedOIDs {
482
+ // Get cached tags for this row
483
+ rowTags := make(map[string]string)
484
+ if tags, ok := ctx.cachedTags[index]; ok {
485
+ for k, v := range tags {
486
+ rowTags[k] = v
487
+ }
488
+ }
489
+
490
+ // Process each metric column
491
+ for columnOID, fullOID := range columns {
492
+ sym, isMetric := ctx.columnOIDs[columnOID]
493
+ if !isMetric {
494
+ continue
495
+ }
496
+
497
+ pdu, ok := ctx.pdus[trimOID(fullOID)]
498
+ if !ok {
499
+ tc.log.Debugf("Missing PDU for cached OID %s", fullOID)
500
+ continue
501
+ }
502
+
503
+ value, err := tc.valProc.processValue(sym, pdu)
504
+ if err != nil {
505
+ tc.log.Debugf("Error processing value for %s: %v", sym.Name, err)
506
+ continue
507
+ }
508
+
509
+ metric, err := buildTableMetric(sym, pdu, value, rowTags, staticTags)
510
+ if err != nil {
511
+ errs = append(errs, err)
512
+ continue
513
+ }
514
+
515
+ metrics = append(metrics, *metric)
516
+ }
517
+ }
518
+
519
+ if len(errs) > 0 {
520
+ tc.log.Warningf("failed to collect table metrics: %v", errors.Join(errs...))
521
+ }
522
+
523
+ return metrics, nil
524
+}
525
+
526
+// SNMP operations
527
+
528
+func (tc *tableCollector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error) {
529
+ pdus := make(map[string]gosnmp.SnmpPDU)
530
+
531
+ var resp []gosnmp.SnmpPDU
532
+ var err error
533
+
534
+ if tc.snmpClient.Version() == gosnmp.Version1 {
535
+ resp, err = tc.snmpClient.WalkAll(oid)
536
+ } else {
537
+ resp, err = tc.snmpClient.BulkWalkAll(oid)
538
+ }
539
+ if err != nil {
540
+ return nil, err
541
+ }
542
+
543
+ for _, pdu := range resp {
544
+ if isPduWithData(pdu) {
545
+ pdus[trimOID(pdu.Name)] = pdu
546
+ }
547
+ }
548
+
549
+ if len(pdus) == 0 {
550
+ tc.missingOIDs[trimOID(oid)] = true
551
+ }
552
+
553
+ return pdus, nil
554
+}
555
+
556
+func (tc *tableCollector) snmpGet(oids []string) (map[string]gosnmp.SnmpPDU, error) {
557
+ pdus := make(map[string]gosnmp.SnmpPDU)
558
+
559
+ for chunk := range slices.Chunk(oids, tc.snmpClient.MaxOids()) {
560
+ result, err := tc.snmpClient.Get(chunk)
561
+ if err != nil {
562
+ return nil, err
563
+ }
564
+
565
+ for _, pdu := range result.Variables {
566
+ if !isPduWithData(pdu) {
567
+ tc.missingOIDs[trimOID(pdu.Name)] = true
568
+ continue
569
+ }
570
+ pdus[trimOID(pdu.Name)] = pdu
571
+ }
572
+ }
573
+
574
+ return pdus, nil
575
+}
576
+
577
+func parseStaticTags(staticTags []string) map[string]string {
578
+ tags := make(map[string]string)
579
+ for _, tag := range staticTags {
580
+ if n, v, _ := strings.Cut(tag, ":"); n != "" && v != "" {
581
+ tags[n] = v
582
+ }
583
+ }
584
+ return tags
585
+}
586
+
587
+func buildColumnOIDs(cfg ddprofiledefinition.MetricsConfig) map[string]ddprofiledefinition.SymbolConfig {
588
+ columnOIDs := make(map[string]ddprofiledefinition.SymbolConfig)
589
+ for _, sym := range cfg.Symbols {
590
+ columnOIDs[trimOID(sym.OID)] = sym
591
+ }
592
+ return columnOIDs
593
+}
594
+
595
+func buildSameTableTagOIDs(cfg ddprofiledefinition.MetricsConfig) map[string][]ddprofiledefinition.MetricTagConfig {
596
+ tagColumnOIDs := make(map[string][]ddprofiledefinition.MetricTagConfig)
597
+ for _, tagCfg := range cfg.MetricTags {
598
+ if tagCfg.Table == "" || tagCfg.Table == cfg.Table.Name {
599
+ oid := trimOID(tagCfg.Symbol.OID)
600
+ tagColumnOIDs[oid] = append(tagColumnOIDs[oid], tagCfg)
601
+ }
602
+ }
603
+ return tagColumnOIDs
604
+}
605
+
606
+func extractTableDependencies(cfg ddprofiledefinition.MetricsConfig, tableNameToOID map[string]string) []string {
607
+ deps := make(map[string]bool)
608
+
609
+ for _, tagCfg := range cfg.MetricTags {
610
+ // Skip if not a cross-table tag
611
+ if tagCfg.Table == "" || tagCfg.Table == cfg.Table.Name {
612
+ continue
613
+ }
614
+
615
+ if tableOID, ok := tableNameToOID[tagCfg.Table]; ok {
616
+ deps[tableOID] = true
617
+ }
618
+ }
619
+
620
+ result := make([]string, 0, len(deps))
621
+ for oid := range deps {
622
+ result = append(result, oid)
623
+ }
624
+
625
+ return result
626
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table_test.go
new
+3836
@@ -0,0 +1,3836 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "strings"
8
+ "testing"
9
+ "time"
10
+
11
+ "github.com/golang/mock/gomock"
12
+ "github.com/gosnmp/gosnmp"
13
+ snmpmock "github.com/gosnmp/gosnmp/mocks"
14
+ "github.com/stretchr/testify/assert"
15
+ "github.com/stretchr/testify/require"
16
+
17
+ "github.com/netdata/netdata/go/plugins/logger"
18
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
19
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
20
+)
21
+
22
+func TestTableCollector_Collect(t *testing.T) {
23
+ tests := map[string]struct {
24
+ profile *ddsnmp.Profile
25
+ setupMock func(m *snmpmock.MockHandler)
26
+ expectedResult []ddsnmp.Metric
27
+ expectedError bool
28
+ errorContains string
29
+ checkMissing map[string]bool // OIDs that should be marked as missing
30
+ }{
31
+ "basic table with single metric": {
32
+ profile: &ddsnmp.Profile{
33
+ SourceFile: "test-profile.yaml",
34
+ Definition: &ddprofiledefinition.ProfileDefinition{
35
+ Metrics: []ddprofiledefinition.MetricsConfig{
36
+ {
37
+ Table: ddprofiledefinition.SymbolConfig{
38
+ OID: "1.3.6.1.2.1.2.2",
39
+ Name: "ifTable",
40
+ },
41
+ Symbols: []ddprofiledefinition.SymbolConfig{
42
+ {
43
+ OID: "1.3.6.1.2.1.2.2.1.10",
44
+ Name: "ifInOctets",
45
+ },
46
+ },
47
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
48
+ {
49
+ Tag: "interface",
50
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
51
+ OID: "1.3.6.1.2.1.2.2.1.2",
52
+ Name: "ifDescr",
53
+ },
54
+ },
55
+ },
56
+ },
57
+ },
58
+ },
59
+ },
60
+ setupMock: func(m *snmpmock.MockHandler) {
61
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
62
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
63
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.2", 2000),
64
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
65
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "eth1"),
66
+ })
67
+ },
68
+ expectedResult: []ddsnmp.Metric{
69
+ {
70
+ Name: "ifInOctets",
71
+ Value: 1000,
72
+ Tags: map[string]string{"interface": "eth0"},
73
+ MetricType: "rate",
74
+ IsTable: true,
75
+ },
76
+ {
77
+ Name: "ifInOctets",
78
+ Value: 2000,
79
+ Tags: map[string]string{"interface": "eth1"},
80
+ MetricType: "rate",
81
+ IsTable: true,
82
+ },
83
+ },
84
+ expectedError: false,
85
+ },
86
+ "table with multiple metrics": {
87
+ profile: &ddsnmp.Profile{
88
+ SourceFile: "test-profile.yaml",
89
+ Definition: &ddprofiledefinition.ProfileDefinition{
90
+ Metrics: []ddprofiledefinition.MetricsConfig{
91
+ {
92
+ Table: ddprofiledefinition.SymbolConfig{
93
+ OID: "1.3.6.1.2.1.2.2",
94
+ Name: "ifTable",
95
+ },
96
+ Symbols: []ddprofiledefinition.SymbolConfig{
97
+ {
98
+ OID: "1.3.6.1.2.1.2.2.1.10",
99
+ Name: "ifInOctets",
100
+ },
101
+ {
102
+ OID: "1.3.6.1.2.1.2.2.1.16",
103
+ Name: "ifOutOctets",
104
+ },
105
+ {
106
+ OID: "1.3.6.1.2.1.2.2.1.8",
107
+ Name: "ifOperStatus",
108
+ },
109
+ },
110
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
111
+ {
112
+ Tag: "interface",
113
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
114
+ OID: "1.3.6.1.2.1.2.2.1.2",
115
+ Name: "ifDescr",
116
+ },
117
+ },
118
+ },
119
+ },
120
+ },
121
+ },
122
+ },
123
+ setupMock: func(m *snmpmock.MockHandler) {
124
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
125
+ // Row 1
126
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
127
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.16.1", 500),
128
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.8.1", 1), // up
129
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
130
+ // Row 2
131
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.2", 2000),
132
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.16.2", 1500),
133
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.8.2", 2), // down
134
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "eth1"),
135
+ })
136
+ },
137
+ expectedResult: []ddsnmp.Metric{
138
+ // Row 1
139
+ {
140
+ Name: "ifInOctets",
141
+ Value: 1000,
142
+ Tags: map[string]string{"interface": "eth0"},
143
+ MetricType: "rate",
144
+ IsTable: true,
145
+ },
146
+ {
147
+ Name: "ifOutOctets",
148
+ Value: 500,
149
+ Tags: map[string]string{"interface": "eth0"},
150
+ MetricType: "rate",
151
+ IsTable: true,
152
+ },
153
+ {
154
+ Name: "ifOperStatus",
155
+ Value: 1,
156
+ Tags: map[string]string{"interface": "eth0"},
157
+ MetricType: "gauge",
158
+ IsTable: true,
159
+ },
160
+ // Row 2
161
+ {
162
+ Name: "ifInOctets",
163
+ Value: 2000,
164
+ Tags: map[string]string{"interface": "eth1"},
165
+ MetricType: "rate",
166
+ IsTable: true,
167
+ },
168
+ {
169
+ Name: "ifOutOctets",
170
+ Value: 1500,
171
+ Tags: map[string]string{"interface": "eth1"},
172
+ MetricType: "rate",
173
+ IsTable: true,
174
+ },
175
+ {
176
+ Name: "ifOperStatus",
177
+ Value: 2,
178
+ Tags: map[string]string{"interface": "eth1"},
179
+ MetricType: "gauge",
180
+ IsTable: true,
181
+ },
182
+ },
183
+ expectedError: false,
184
+ },
185
+ "empty table": {
186
+ profile: &ddsnmp.Profile{
187
+ SourceFile: "test-profile.yaml",
188
+ Definition: &ddprofiledefinition.ProfileDefinition{
189
+ Metrics: []ddprofiledefinition.MetricsConfig{
190
+ {
191
+ Table: ddprofiledefinition.SymbolConfig{
192
+ OID: "1.3.6.1.2.1.2.2",
193
+ Name: "ifTable",
194
+ },
195
+ Symbols: []ddprofiledefinition.SymbolConfig{
196
+ {
197
+ OID: "1.3.6.1.2.1.2.2.1.10",
198
+ Name: "ifInOctets",
199
+ },
200
+ },
201
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
202
+ {
203
+ Tag: "interface",
204
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
205
+ OID: "1.3.6.1.2.1.2.2.1.2",
206
+ Name: "ifDescr",
207
+ },
208
+ },
209
+ },
210
+ },
211
+ },
212
+ },
213
+ },
214
+ setupMock: func(m *snmpmock.MockHandler) {
215
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{})
216
+ },
217
+ expectedResult: []ddsnmp.Metric{},
218
+ expectedError: false,
219
+ checkMissing: map[string]bool{"1.3.6.1.2.1.2.2": true},
220
+ },
221
+ "table walk error": {
222
+ profile: &ddsnmp.Profile{
223
+ SourceFile: "test-profile.yaml",
224
+ Definition: &ddprofiledefinition.ProfileDefinition{
225
+ Metrics: []ddprofiledefinition.MetricsConfig{
226
+ {
227
+ Table: ddprofiledefinition.SymbolConfig{
228
+ OID: "1.3.6.1.2.1.2.2",
229
+ Name: "ifTable",
230
+ },
231
+ Symbols: []ddprofiledefinition.SymbolConfig{
232
+ {
233
+ OID: "1.3.6.1.2.1.2.2.1.10",
234
+ Name: "ifInOctets",
235
+ },
236
+ },
237
+ },
238
+ },
239
+ },
240
+ },
241
+ setupMock: func(m *snmpmock.MockHandler) {
242
+ expectSNMPWalkError(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", errors.New("timeout walking table"))
243
+ },
244
+ expectedResult: nil,
245
+ expectedError: true,
246
+ errorContains: "timeout walking table",
247
+ },
248
+ "table with snmpv1": {
249
+ profile: &ddsnmp.Profile{
250
+ SourceFile: "test-profile.yaml",
251
+ Definition: &ddprofiledefinition.ProfileDefinition{
252
+ Metrics: []ddprofiledefinition.MetricsConfig{
253
+ {
254
+ Table: ddprofiledefinition.SymbolConfig{
255
+ OID: "1.3.6.1.2.1.2.2",
256
+ Name: "ifTable",
257
+ },
258
+ Symbols: []ddprofiledefinition.SymbolConfig{
259
+ {
260
+ OID: "1.3.6.1.2.1.2.2.1.10",
261
+ Name: "ifInOctets",
262
+ },
263
+ },
264
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
265
+ {
266
+ Tag: "interface",
267
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
268
+ OID: "1.3.6.1.2.1.2.2.1.2",
269
+ Name: "ifDescr",
270
+ },
271
+ },
272
+ },
273
+ },
274
+ },
275
+ },
276
+ },
277
+ setupMock: func(m *snmpmock.MockHandler) {
278
+ expectSNMPWalk(m, gosnmp.Version1, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
279
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
280
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
281
+ })
282
+ },
283
+ expectedResult: []ddsnmp.Metric{
284
+ {
285
+ Name: "ifInOctets",
286
+ Value: 1000,
287
+ Tags: map[string]string{"interface": "eth0"},
288
+ MetricType: "rate",
289
+ IsTable: true,
290
+ },
291
+ },
292
+ expectedError: false,
293
+ },
294
+ "multiple tables in profile": {
295
+ profile: &ddsnmp.Profile{
296
+ SourceFile: "test-profile.yaml",
297
+ Definition: &ddprofiledefinition.ProfileDefinition{
298
+ Metrics: []ddprofiledefinition.MetricsConfig{
299
+ {
300
+ Table: ddprofiledefinition.SymbolConfig{
301
+ OID: "1.3.6.1.2.1.2.2",
302
+ Name: "ifTable",
303
+ },
304
+ Symbols: []ddprofiledefinition.SymbolConfig{
305
+ {
306
+ OID: "1.3.6.1.2.1.2.2.1.10",
307
+ Name: "ifInOctets",
308
+ },
309
+ },
310
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
311
+ {
312
+ Tag: "interface",
313
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
314
+ OID: "1.3.6.1.2.1.2.2.1.2",
315
+ Name: "ifDescr",
316
+ },
317
+ },
318
+ },
319
+ },
320
+ {
321
+ Table: ddprofiledefinition.SymbolConfig{
322
+ OID: "1.3.6.1.2.1.4.31.1",
323
+ Name: "ipSystemStatsTable",
324
+ },
325
+ Symbols: []ddprofiledefinition.SymbolConfig{
326
+ {
327
+ OID: "1.3.6.1.2.1.4.31.1.1.4",
328
+ Name: "ipSystemStatsHCInReceives",
329
+ },
330
+ },
331
+ },
332
+ },
333
+ },
334
+ },
335
+ setupMock: func(m *snmpmock.MockHandler) {
336
+ // First table walk
337
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
338
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
339
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
340
+ })
341
+ // Second table walk
342
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.4.31.1", []gosnmp.SnmpPDU{
343
+ createCounter64PDU("1.3.6.1.2.1.4.31.1.1.4.1", 5000),
344
+ createCounter64PDU("1.3.6.1.2.1.4.31.1.1.4.2", 6000),
345
+ })
346
+ },
347
+ expectedResult: []ddsnmp.Metric{
348
+ {
349
+ Name: "ifInOctets",
350
+ Value: 1000,
351
+ Tags: map[string]string{"interface": "eth0"},
352
+ MetricType: "rate",
353
+ IsTable: true,
354
+ },
355
+ {
356
+ Name: "ipSystemStatsHCInReceives",
357
+ Value: 5000,
358
+ Tags: nil,
359
+ MetricType: "rate",
360
+ IsTable: true,
361
+ },
362
+ {
363
+ Name: "ipSystemStatsHCInReceives",
364
+ Value: 6000,
365
+ Tags: nil,
366
+ MetricType: "rate",
367
+ IsTable: true,
368
+ },
369
+ },
370
+ expectedError: false,
371
+ },
372
+ "partial table data with missing rows": {
373
+ profile: &ddsnmp.Profile{
374
+ SourceFile: "test-profile.yaml",
375
+ Definition: &ddprofiledefinition.ProfileDefinition{
376
+ Metrics: []ddprofiledefinition.MetricsConfig{
377
+ {
378
+ Table: ddprofiledefinition.SymbolConfig{
379
+ OID: "1.3.6.1.2.1.2.2",
380
+ Name: "ifTable",
381
+ },
382
+ Symbols: []ddprofiledefinition.SymbolConfig{
383
+ {
384
+ OID: "1.3.6.1.2.1.2.2.1.10",
385
+ Name: "ifInOctets",
386
+ },
387
+ {
388
+ OID: "1.3.6.1.2.1.2.2.1.16",
389
+ Name: "ifOutOctets",
390
+ },
391
+ },
392
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
393
+ {
394
+ Tag: "interface",
395
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
396
+ OID: "1.3.6.1.2.1.2.2.1.2",
397
+ Name: "ifDescr",
398
+ },
399
+ },
400
+ },
401
+ },
402
+ },
403
+ },
404
+ },
405
+ setupMock: func(m *snmpmock.MockHandler) {
406
+ // Walk returns partial data - some metrics missing for some rows
407
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
408
+ // Row 1 - complete
409
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
410
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.16.1", 500),
411
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
412
+ // Row 2 - missing ifOutOctets
413
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.2", 2000),
414
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "eth1"),
415
+ // Row 3 - missing tag but has metrics
416
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.3", 3000),
417
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.16.3", 1500),
418
+ })
419
+ },
420
+ expectedResult: []ddsnmp.Metric{
421
+ // Row 1 - complete
422
+ {
423
+ Name: "ifInOctets",
424
+ Value: 1000,
425
+ Tags: map[string]string{"interface": "eth0"},
426
+ MetricType: "rate",
427
+ IsTable: true,
428
+ },
429
+ {
430
+ Name: "ifOutOctets",
431
+ Value: 500,
432
+ Tags: map[string]string{"interface": "eth0"},
433
+ MetricType: "rate",
434
+ IsTable: true,
435
+ },
436
+ // Row 2 - only ifInOctets
437
+ {
438
+ Name: "ifInOctets",
439
+ Value: 2000,
440
+ Tags: map[string]string{"interface": "eth1"},
441
+ MetricType: "rate",
442
+ IsTable: true,
443
+ },
444
+ // Row 3 - both metrics but no tag
445
+ {
446
+ Name: "ifInOctets",
447
+ Value: 3000,
448
+ Tags: nil,
449
+ MetricType: "rate",
450
+ IsTable: true,
451
+ },
452
+ {
453
+ Name: "ifOutOctets",
454
+ Value: 1500,
455
+ Tags: nil,
456
+ MetricType: "rate",
457
+ IsTable: true,
458
+ },
459
+ },
460
+ expectedError: false,
461
+ },
462
+ "no symbols defined": {
463
+ profile: &ddsnmp.Profile{
464
+ SourceFile: "test-profile.yaml",
465
+ Definition: &ddprofiledefinition.ProfileDefinition{
466
+ Metrics: []ddprofiledefinition.MetricsConfig{
467
+ {
468
+ Table: ddprofiledefinition.SymbolConfig{
469
+ OID: "1.3.6.1.2.1.2.2",
470
+ Name: "ifTable",
471
+ },
472
+ Symbols: []ddprofiledefinition.SymbolConfig{}, // No symbols
473
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
474
+ {
475
+ Tag: "interface",
476
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
477
+ OID: "1.3.6.1.2.1.2.2.1.2",
478
+ Name: "ifDescr",
479
+ },
480
+ },
481
+ },
482
+ },
483
+ },
484
+ },
485
+ },
486
+ setupMock: func(m *snmpmock.MockHandler) {
487
+ // Table walk happens even without symbols (for tags)
488
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
489
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
490
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "eth1"),
491
+ })
492
+ },
493
+ expectedResult: []ddsnmp.Metric{}, // No metrics because no symbols defined
494
+ expectedError: false,
495
+ },
496
+ "table with non-numeric values": {
497
+ profile: &ddsnmp.Profile{
498
+ SourceFile: "test-profile.yaml",
499
+ Definition: &ddprofiledefinition.ProfileDefinition{
500
+ Metrics: []ddprofiledefinition.MetricsConfig{
501
+ {
502
+ Table: ddprofiledefinition.SymbolConfig{
503
+ OID: "1.3.6.1.2.1.2.2",
504
+ Name: "ifTable",
505
+ },
506
+ Symbols: []ddprofiledefinition.SymbolConfig{
507
+ {
508
+ OID: "1.3.6.1.2.1.2.2.1.2",
509
+ Name: "ifDescr", // String column, should be skipped
510
+ },
511
+ {
512
+ OID: "1.3.6.1.2.1.2.2.1.10",
513
+ Name: "ifInOctets",
514
+ },
515
+ },
516
+ },
517
+ },
518
+ },
519
+ },
520
+ setupMock: func(m *snmpmock.MockHandler) {
521
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
522
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
523
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
524
+ })
525
+ },
526
+ expectedResult: []ddsnmp.Metric{
527
+ {
528
+ Name: "ifInOctets",
529
+ Value: 1000,
530
+ Tags: nil,
531
+ MetricType: "rate",
532
+ IsTable: true,
533
+ },
534
+ },
535
+ expectedError: false,
536
+ },
537
+
538
+ "table with tag mapping": {
539
+ profile: &ddsnmp.Profile{
540
+ SourceFile: "test-profile.yaml",
541
+ Definition: &ddprofiledefinition.ProfileDefinition{
542
+ Metrics: []ddprofiledefinition.MetricsConfig{
543
+ {
544
+ Table: ddprofiledefinition.SymbolConfig{
545
+ OID: "1.3.6.1.2.1.2.2",
546
+ Name: "ifTable",
547
+ },
548
+ Symbols: []ddprofiledefinition.SymbolConfig{
549
+ {
550
+ OID: "1.3.6.1.2.1.2.2.1.14",
551
+ Name: "ifInErrors",
552
+ },
553
+ },
554
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
555
+ {
556
+ Tag: "interface",
557
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
558
+ OID: "1.3.6.1.2.1.2.2.1.2",
559
+ Name: "ifDescr",
560
+ },
561
+ },
562
+ {
563
+ Tag: "if_type",
564
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
565
+ OID: "1.3.6.1.2.1.2.2.1.3",
566
+ Name: "ifType",
567
+ },
568
+ Mapping: map[string]string{
569
+ "1": "other",
570
+ "6": "ethernetCsmacd",
571
+ "24": "softwareLoopback",
572
+ },
573
+ },
574
+ },
575
+ },
576
+ },
577
+ },
578
+ },
579
+ setupMock: func(m *snmpmock.MockHandler) {
580
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
581
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.1", 10),
582
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "lo0"),
583
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.3.1", 24), // softwareLoopback
584
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.2", 5),
585
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "eth0"),
586
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.3.2", 6), // ethernetCsmacd
587
+ })
588
+ },
589
+ expectedResult: []ddsnmp.Metric{
590
+ {
591
+ Name: "ifInErrors",
592
+ Value: 10,
593
+ Tags: map[string]string{"interface": "lo0", "if_type": "softwareLoopback"},
594
+ MetricType: "rate",
595
+ IsTable: true,
596
+ },
597
+ {
598
+ Name: "ifInErrors",
599
+ Value: 5,
600
+ Tags: map[string]string{"interface": "eth0", "if_type": "ethernetCsmacd"},
601
+ MetricType: "rate",
602
+ IsTable: true,
603
+ },
604
+ },
605
+ expectedError: false,
606
+ },
607
+ "table with extract_value in tag": {
608
+ profile: &ddsnmp.Profile{
609
+ SourceFile: "test-profile.yaml",
610
+ Definition: &ddprofiledefinition.ProfileDefinition{
611
+ Metrics: []ddprofiledefinition.MetricsConfig{
612
+ {
613
+ Table: ddprofiledefinition.SymbolConfig{
614
+ OID: "1.3.6.1.2.1.2.2",
615
+ Name: "ifTable",
616
+ },
617
+ Symbols: []ddprofiledefinition.SymbolConfig{
618
+ {
619
+ OID: "1.3.6.1.2.1.2.2.1.14",
620
+ Name: "ifInErrors",
621
+ },
622
+ },
623
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
624
+ {
625
+ Tag: "interface",
626
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
627
+ OID: "1.3.6.1.2.1.2.2.1.2",
628
+ Name: "ifDescr",
629
+ ExtractValueCompiled: mustCompileRegex(`^(\S+)`), // Extract first word
630
+ },
631
+ },
632
+ },
633
+ },
634
+ },
635
+ },
636
+ },
637
+ setupMock: func(m *snmpmock.MockHandler) {
638
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
639
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.1", 10),
640
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0 - Primary Network Interface"),
641
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.2", 5),
642
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "lo0 - Loopback"),
643
+ })
644
+ },
645
+ expectedResult: []ddsnmp.Metric{
646
+ {
647
+ Name: "ifInErrors",
648
+ Value: 10,
649
+ Tags: map[string]string{"interface": "eth0"},
650
+ MetricType: "rate",
651
+ IsTable: true,
652
+ },
653
+ {
654
+ Name: "ifInErrors",
655
+ Value: 5,
656
+ Tags: map[string]string{"interface": "lo0"},
657
+ MetricType: "rate",
658
+ IsTable: true,
659
+ },
660
+ },
661
+ expectedError: false,
662
+ },
663
+ "table with multiple tag columns": {
664
+ profile: &ddsnmp.Profile{
665
+ SourceFile: "test-profile.yaml",
666
+ Definition: &ddprofiledefinition.ProfileDefinition{
667
+ Metrics: []ddprofiledefinition.MetricsConfig{
668
+ {
669
+ Table: ddprofiledefinition.SymbolConfig{
670
+ OID: "1.3.6.1.2.1.2.2",
671
+ Name: "ifTable",
672
+ },
673
+ Symbols: []ddprofiledefinition.SymbolConfig{
674
+ {
675
+ OID: "1.3.6.1.2.1.2.2.1.10",
676
+ Name: "ifInOctets",
677
+ },
678
+ },
679
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
680
+ {
681
+ Tag: "interface",
682
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
683
+ OID: "1.3.6.1.2.1.2.2.1.2",
684
+ Name: "ifDescr",
685
+ },
686
+ },
687
+ {
688
+ Tag: "if_type",
689
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
690
+ OID: "1.3.6.1.2.1.2.2.1.3",
691
+ Name: "ifType",
692
+ },
693
+ },
694
+ {
695
+ Tag: "admin_status",
696
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
697
+ OID: "1.3.6.1.2.1.2.2.1.7",
698
+ Name: "ifAdminStatus",
699
+ },
700
+ Mapping: map[string]string{
701
+ "1": "up",
702
+ "2": "down",
703
+ "3": "testing",
704
+ },
705
+ },
706
+ },
707
+ },
708
+ },
709
+ },
710
+ },
711
+ setupMock: func(m *snmpmock.MockHandler) {
712
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
713
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
714
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
715
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.3.1", 6),
716
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.7.1", 1), // up
717
+ })
718
+ },
719
+ expectedResult: []ddsnmp.Metric{
720
+ {
721
+ Name: "ifInOctets",
722
+ Value: 1000,
723
+ Tags: map[string]string{
724
+ "interface": "eth0",
725
+ "if_type": "6",
726
+ "admin_status": "up",
727
+ },
728
+ MetricType: "rate",
729
+ IsTable: true,
730
+ },
731
+ },
732
+ expectedError: false,
733
+ },
734
+ "table with static tags": {
735
+ profile: &ddsnmp.Profile{
736
+ SourceFile: "test-profile.yaml",
737
+ Definition: &ddprofiledefinition.ProfileDefinition{
738
+ Metrics: []ddprofiledefinition.MetricsConfig{
739
+ {
740
+ Table: ddprofiledefinition.SymbolConfig{
741
+ OID: "1.3.6.1.2.1.2.2",
742
+ Name: "ifTable",
743
+ },
744
+ Symbols: []ddprofiledefinition.SymbolConfig{
745
+ {
746
+ OID: "1.3.6.1.2.1.2.2.1.10",
747
+ Name: "ifInOctets",
748
+ },
749
+ },
750
+ StaticTags: []string{
751
+ "source:interface",
752
+ "table:if",
753
+ },
754
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
755
+ {
756
+ Tag: "interface",
757
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
758
+ OID: "1.3.6.1.2.1.2.2.1.2",
759
+ Name: "ifDescr",
760
+ },
761
+ },
762
+ },
763
+ },
764
+ },
765
+ },
766
+ },
767
+ setupMock: func(m *snmpmock.MockHandler) {
768
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
769
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
770
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
771
+ })
772
+ },
773
+ expectedResult: []ddsnmp.Metric{
774
+ {
775
+ Name: "ifInOctets",
776
+ Value: 1000,
777
+ StaticTags: map[string]string{
778
+ "source": "interface",
779
+ "table": "if",
780
+ },
781
+ Tags: map[string]string{"interface": "eth0"},
782
+ MetricType: "rate",
783
+ IsTable: true,
784
+ },
785
+ },
786
+ expectedError: false,
787
+ },
788
+ "table with pattern matching tag": {
789
+ profile: &ddsnmp.Profile{
790
+ SourceFile: "test-profile.yaml",
791
+ Definition: &ddprofiledefinition.ProfileDefinition{
792
+ Metrics: []ddprofiledefinition.MetricsConfig{
793
+ {
794
+ Table: ddprofiledefinition.SymbolConfig{
795
+ OID: "1.3.6.1.2.1.2.2",
796
+ Name: "ifTable",
797
+ },
798
+ Symbols: []ddprofiledefinition.SymbolConfig{
799
+ {
800
+ OID: "1.3.6.1.2.1.2.2.1.10",
801
+ Name: "ifInOctets",
802
+ },
803
+ },
804
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
805
+ {
806
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
807
+ OID: "1.3.6.1.2.1.2.2.1.2",
808
+ Name: "ifDescr",
809
+ },
810
+ Pattern: mustCompileRegex(`(\w+)(\d+)/(\d+)`),
811
+ Tags: map[string]string{
812
+ "interface_type": "$1",
813
+ "slot": "$2",
814
+ "port": "$3",
815
+ },
816
+ },
817
+ },
818
+ },
819
+ },
820
+ },
821
+ },
822
+ setupMock: func(m *snmpmock.MockHandler) {
823
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
824
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
825
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "GigabitEthernet1/0"),
826
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.2", 2000),
827
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "FastEthernet2/1"),
828
+ })
829
+ },
830
+ expectedResult: []ddsnmp.Metric{
831
+ {
832
+ Name: "ifInOctets",
833
+ Value: 1000,
834
+ Tags: map[string]string{
835
+ "interface_type": "GigabitEthernet",
836
+ "slot": "1",
837
+ "port": "0",
838
+ },
839
+ MetricType: "rate",
840
+ IsTable: true,
841
+ },
842
+ {
843
+ Name: "ifInOctets",
844
+ Value: 2000,
845
+ Tags: map[string]string{
846
+ "interface_type": "FastEthernet",
847
+ "slot": "2",
848
+ "port": "1",
849
+ },
850
+ MetricType: "rate",
851
+ IsTable: true,
852
+ },
853
+ },
854
+ expectedError: false,
855
+ },
856
+ "table with format mac_address in tag": {
857
+ profile: &ddsnmp.Profile{
858
+ SourceFile: "test-profile.yaml",
859
+ Definition: &ddprofiledefinition.ProfileDefinition{
860
+ Metrics: []ddprofiledefinition.MetricsConfig{
861
+ {
862
+ Table: ddprofiledefinition.SymbolConfig{
863
+ OID: "1.3.6.1.4.1.29671.1.1.4",
864
+ Name: "devTable",
865
+ },
866
+ Symbols: []ddprofiledefinition.SymbolConfig{
867
+ {
868
+ OID: "1.3.6.1.4.1.29671.1.1.4.1.5",
869
+ Name: "devClientCount",
870
+ },
871
+ },
872
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
873
+ {
874
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
875
+ OID: "1.3.6.1.4.1.29671.1.1.4.1.1",
876
+ Name: "devMac",
877
+ Format: "mac_address",
878
+ },
879
+ Tag: "mac_address",
880
+ },
881
+ },
882
+ },
883
+ },
884
+ },
885
+ },
886
+ setupMock: func(m *snmpmock.MockHandler) {
887
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.29671.1.1.4", []gosnmp.SnmpPDU{
888
+ createCounter32PDU("1.3.6.1.4.1.29671.1.1.4.1.5.1", 10),
889
+ createPDU("1.3.6.1.4.1.29671.1.1.4.1.1.1", gosnmp.OctetString, []byte{0x00, 0x50, 0x56, 0xAB, 0xCD, 0xEF}),
890
+ })
891
+ },
892
+ expectedResult: []ddsnmp.Metric{
893
+ {
894
+ Name: "devClientCount",
895
+ Value: 10,
896
+ Tags: map[string]string{"mac_address": "00:50:56:AB:CD:EF"},
897
+ MetricType: "rate",
898
+ IsTable: true,
899
+ },
900
+ },
901
+ expectedError: false,
902
+ },
903
+ "table with format ip_address in tag": {
904
+ profile: &ddsnmp.Profile{
905
+ SourceFile: "test-profile.yaml",
906
+ Definition: &ddprofiledefinition.ProfileDefinition{
907
+ Metrics: []ddprofiledefinition.MetricsConfig{
908
+ {
909
+ Table: ddprofiledefinition.SymbolConfig{
910
+ OID: "1.3.6.1.2.1.4.20",
911
+ Name: "ipAddrTable",
912
+ },
913
+ Symbols: []ddprofiledefinition.SymbolConfig{
914
+ {
915
+ OID: "1.3.6.1.2.1.4.20.1.3",
916
+ Name: "ipAdEntBcastAddr",
917
+ },
918
+ },
919
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
920
+ {
921
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
922
+ OID: "1.3.6.1.2.1.4.20.1.1",
923
+ Name: "ipAdEntAddr",
924
+ Format: "ip_address",
925
+ },
926
+ Tag: "ip_address",
927
+ },
928
+ },
929
+ },
930
+ },
931
+ },
932
+ },
933
+ setupMock: func(m *snmpmock.MockHandler) {
934
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.4.20", []gosnmp.SnmpPDU{
935
+ createIntegerPDU("1.3.6.1.2.1.4.20.1.3.1", 1),
936
+ createPDU("1.3.6.1.2.1.4.20.1.1.1", gosnmp.IPAddress, "192.168.1.1"),
937
+ })
938
+ },
939
+ expectedResult: []ddsnmp.Metric{
940
+ {
941
+ Name: "ipAdEntBcastAddr",
942
+ Value: 1,
943
+ Tags: map[string]string{"ip_address": "192.168.1.1"},
944
+ MetricType: "gauge",
945
+ IsTable: true,
946
+ },
947
+ },
948
+ expectedError: false,
949
+ },
950
+ "table with mixed tag types": {
951
+ profile: &ddsnmp.Profile{
952
+ SourceFile: "test-profile.yaml",
953
+ Definition: &ddprofiledefinition.ProfileDefinition{
954
+ Metrics: []ddprofiledefinition.MetricsConfig{
955
+ {
956
+ Table: ddprofiledefinition.SymbolConfig{
957
+ OID: "1.3.6.1.2.1.2.2",
958
+ Name: "ifTable",
959
+ },
960
+ Symbols: []ddprofiledefinition.SymbolConfig{
961
+ {
962
+ OID: "1.3.6.1.2.1.2.2.1.10",
963
+ Name: "ifInOctets",
964
+ },
965
+ },
966
+ StaticTags: []string{
967
+ "source:network",
968
+ },
969
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
970
+ {
971
+ Tag: "interface",
972
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
973
+ OID: "1.3.6.1.2.1.2.2.1.2",
974
+ Name: "ifDescr",
975
+ ExtractValueCompiled: mustCompileRegex(`^(\S+)`),
976
+ },
977
+ },
978
+ {
979
+ Tag: "if_type",
980
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
981
+ OID: "1.3.6.1.2.1.2.2.1.3",
982
+ Name: "ifType",
983
+ },
984
+ Mapping: map[string]string{
985
+ "6": "ethernet",
986
+ },
987
+ },
988
+ },
989
+ },
990
+ },
991
+ },
992
+ },
993
+ setupMock: func(m *snmpmock.MockHandler) {
994
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
995
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
996
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0 - Primary Interface"),
997
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.3.1", 6),
998
+ })
999
+ },
1000
+ expectedResult: []ddsnmp.Metric{
1001
+ {
1002
+ Name: "ifInOctets",
1003
+ Value: 1000,
1004
+ StaticTags: map[string]string{"source": "network"},
1005
+ Tags: map[string]string{"interface": "eth0", "if_type": "ethernet"},
1006
+ MetricType: "rate",
1007
+ IsTable: true,
1008
+ },
1009
+ },
1010
+ expectedError: false,
1011
+ },
1012
+ "table with tag but no mapping match": {
1013
+ profile: &ddsnmp.Profile{
1014
+ SourceFile: "test-profile.yaml",
1015
+ Definition: &ddprofiledefinition.ProfileDefinition{
1016
+ Metrics: []ddprofiledefinition.MetricsConfig{
1017
+ {
1018
+ Table: ddprofiledefinition.SymbolConfig{
1019
+ OID: "1.3.6.1.2.1.2.2",
1020
+ Name: "ifTable",
1021
+ },
1022
+ Symbols: []ddprofiledefinition.SymbolConfig{
1023
+ {
1024
+ OID: "1.3.6.1.2.1.2.2.1.14",
1025
+ Name: "ifInErrors",
1026
+ },
1027
+ },
1028
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1029
+ {
1030
+ Tag: "interface",
1031
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1032
+ OID: "1.3.6.1.2.1.2.2.1.2",
1033
+ Name: "ifDescr",
1034
+ },
1035
+ },
1036
+ {
1037
+ Tag: "if_type",
1038
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1039
+ OID: "1.3.6.1.2.1.2.2.1.3",
1040
+ Name: "ifType",
1041
+ },
1042
+ Mapping: map[string]string{
1043
+ "6": "ethernet",
1044
+ // No mapping for value 131
1045
+ },
1046
+ },
1047
+ },
1048
+ },
1049
+ },
1050
+ },
1051
+ },
1052
+ setupMock: func(m *snmpmock.MockHandler) {
1053
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1054
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.1", 10),
1055
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "tunnel0"),
1056
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.3.1", 131), // No mapping for this value
1057
+ })
1058
+ },
1059
+ expectedResult: []ddsnmp.Metric{
1060
+ {
1061
+ Name: "ifInErrors",
1062
+ Value: 10,
1063
+ Tags: map[string]string{"interface": "tunnel0", "if_type": "131"}, // Raw value when no mapping
1064
+ MetricType: "rate",
1065
+ IsTable: true,
1066
+ },
1067
+ },
1068
+ expectedError: false,
1069
+ },
1070
+ "table with empty tag value": {
1071
+ profile: &ddsnmp.Profile{
1072
+ SourceFile: "test-profile.yaml",
1073
+ Definition: &ddprofiledefinition.ProfileDefinition{
1074
+ Metrics: []ddprofiledefinition.MetricsConfig{
1075
+ {
1076
+ Table: ddprofiledefinition.SymbolConfig{
1077
+ OID: "1.3.6.1.2.1.2.2",
1078
+ Name: "ifTable",
1079
+ },
1080
+ Symbols: []ddprofiledefinition.SymbolConfig{
1081
+ {
1082
+ OID: "1.3.6.1.2.1.2.2.1.10",
1083
+ Name: "ifInOctets",
1084
+ },
1085
+ },
1086
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1087
+ {
1088
+ Tag: "interface",
1089
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1090
+ OID: "1.3.6.1.2.1.2.2.1.2",
1091
+ Name: "ifDescr",
1092
+ },
1093
+ },
1094
+ },
1095
+ },
1096
+ },
1097
+ },
1098
+ },
1099
+ setupMock: func(m *snmpmock.MockHandler) {
1100
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1101
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
1102
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", ""), // Empty string
1103
+ })
1104
+ },
1105
+ expectedResult: []ddsnmp.Metric{
1106
+ {
1107
+ Name: "ifInOctets",
1108
+ Value: 1000,
1109
+ Tags: map[string]string{"interface": ""}, // Empty tag value
1110
+ MetricType: "rate",
1111
+ IsTable: true,
1112
+ },
1113
+ },
1114
+ expectedError: false,
1115
+ },
1116
+
1117
+ "table with missing rows": {
1118
+ profile: &ddsnmp.Profile{
1119
+ SourceFile: "test-profile.yaml",
1120
+ Definition: &ddprofiledefinition.ProfileDefinition{
1121
+ Metrics: []ddprofiledefinition.MetricsConfig{
1122
+ {
1123
+ Table: ddprofiledefinition.SymbolConfig{
1124
+ OID: "1.3.6.1.2.1.2.2",
1125
+ Name: "ifTable",
1126
+ },
1127
+ Symbols: []ddprofiledefinition.SymbolConfig{
1128
+ {
1129
+ OID: "1.3.6.1.2.1.2.2.1.10",
1130
+ Name: "ifInOctets",
1131
+ },
1132
+ {
1133
+ OID: "1.3.6.1.2.1.2.2.1.16",
1134
+ Name: "ifOutOctets",
1135
+ },
1136
+ },
1137
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1138
+ {
1139
+ Tag: "interface",
1140
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1141
+ OID: "1.3.6.1.2.1.2.2.1.2",
1142
+ Name: "ifDescr",
1143
+ },
1144
+ },
1145
+ },
1146
+ },
1147
+ },
1148
+ },
1149
+ },
1150
+ setupMock: func(m *snmpmock.MockHandler) {
1151
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1152
+ // Row 1 - complete
1153
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
1154
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.16.1", 500),
1155
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
1156
+ // Row 2 - missing ifOutOctets
1157
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.2", 2000),
1158
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "eth1"),
1159
+ // Row 3 - missing tag (ifDescr)
1160
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.3", 3000),
1161
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.16.3", 1500),
1162
+ })
1163
+ },
1164
+ expectedResult: []ddsnmp.Metric{
1165
+ // Row 1 - complete
1166
+ {
1167
+ Name: "ifInOctets",
1168
+ Value: 1000,
1169
+ Tags: map[string]string{"interface": "eth0"},
1170
+ MetricType: "rate",
1171
+ IsTable: true,
1172
+ },
1173
+ {
1174
+ Name: "ifOutOctets",
1175
+ Value: 500,
1176
+ Tags: map[string]string{"interface": "eth0"},
1177
+ MetricType: "rate",
1178
+ IsTable: true,
1179
+ },
1180
+ // Row 2 - only ifInOctets
1181
+ {
1182
+ Name: "ifInOctets",
1183
+ Value: 2000,
1184
+ Tags: map[string]string{"interface": "eth1"},
1185
+ MetricType: "rate",
1186
+ IsTable: true,
1187
+ },
1188
+ // Row 3 - both metrics but no tag
1189
+ {
1190
+ Name: "ifInOctets",
1191
+ Value: 3000,
1192
+ Tags: nil,
1193
+ MetricType: "rate",
1194
+ IsTable: true,
1195
+ },
1196
+ {
1197
+ Name: "ifOutOctets",
1198
+ Value: 1500,
1199
+ Tags: nil,
1200
+ MetricType: "rate",
1201
+ IsTable: true,
1202
+ },
1203
+ },
1204
+ expectedError: false,
1205
+ },
1206
+ "table with scale factor": {
1207
+ profile: &ddsnmp.Profile{
1208
+ SourceFile: "test-profile.yaml",
1209
+ Definition: &ddprofiledefinition.ProfileDefinition{
1210
+ Metrics: []ddprofiledefinition.MetricsConfig{
1211
+ {
1212
+ Table: ddprofiledefinition.SymbolConfig{
1213
+ OID: "1.3.6.1.4.1.9.9.109.1.1.1",
1214
+ Name: "cpmCPUTotalTable",
1215
+ },
1216
+ Symbols: []ddprofiledefinition.SymbolConfig{
1217
+ {
1218
+ OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.12",
1219
+ Name: "cpmCPUMemoryUsed",
1220
+ ScaleFactor: 1024, // Convert KB to bytes
1221
+ },
1222
+ },
1223
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1224
+ {
1225
+ Tag: "cpu_id",
1226
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1227
+ OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.2",
1228
+ Name: "cpmCPUTotalPhysicalIndex",
1229
+ },
1230
+ },
1231
+ },
1232
+ },
1233
+ },
1234
+ },
1235
+ },
1236
+ setupMock: func(m *snmpmock.MockHandler) {
1237
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.9.9.109.1.1.1", []gosnmp.SnmpPDU{
1238
+ createGauge32PDU("1.3.6.1.4.1.9.9.109.1.1.1.1.12.1", 2048), // 2048 KB
1239
+ createIntegerPDU("1.3.6.1.4.1.9.9.109.1.1.1.1.2.1", 1),
1240
+ })
1241
+ },
1242
+ expectedResult: []ddsnmp.Metric{
1243
+ {
1244
+ Name: "cpmCPUMemoryUsed",
1245
+ Value: 2097152, // 2048 * 1024
1246
+ Tags: map[string]string{"cpu_id": "1"},
1247
+ MetricType: "gauge",
1248
+ IsTable: true,
1249
+ },
1250
+ },
1251
+ expectedError: false,
1252
+ },
1253
+ "table with value mapping": {
1254
+ profile: &ddsnmp.Profile{
1255
+ SourceFile: "test-profile.yaml",
1256
+ Definition: &ddprofiledefinition.ProfileDefinition{
1257
+ Metrics: []ddprofiledefinition.MetricsConfig{
1258
+ {
1259
+ Table: ddprofiledefinition.SymbolConfig{
1260
+ OID: "1.3.6.1.2.1.2.2",
1261
+ Name: "ifTable",
1262
+ },
1263
+ Symbols: []ddprofiledefinition.SymbolConfig{
1264
+ {
1265
+ OID: "1.3.6.1.2.1.2.2.1.8",
1266
+ Name: "ifOperStatus",
1267
+ Mapping: map[string]string{
1268
+ "1": "up",
1269
+ "2": "down",
1270
+ "3": "testing",
1271
+ "4": "unknown",
1272
+ "5": "dormant",
1273
+ },
1274
+ },
1275
+ },
1276
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1277
+ {
1278
+ Tag: "interface",
1279
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1280
+ OID: "1.3.6.1.2.1.2.2.1.2",
1281
+ Name: "ifDescr",
1282
+ },
1283
+ },
1284
+ },
1285
+ },
1286
+ },
1287
+ },
1288
+ },
1289
+ setupMock: func(m *snmpmock.MockHandler) {
1290
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1291
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.8.1", 1), // up
1292
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
1293
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.8.2", 2), // down
1294
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "eth1"),
1295
+ })
1296
+ },
1297
+ expectedResult: []ddsnmp.Metric{
1298
+ {
1299
+ Name: "ifOperStatus",
1300
+ Value: 1,
1301
+ Tags: map[string]string{"interface": "eth0"},
1302
+ MetricType: "gauge",
1303
+ IsTable: true,
1304
+ Mappings: map[int64]string{
1305
+ 1: "up",
1306
+ 2: "down",
1307
+ 3: "testing",
1308
+ 4: "unknown",
1309
+ 5: "dormant",
1310
+ },
1311
+ },
1312
+ {
1313
+ Name: "ifOperStatus",
1314
+ Value: 2,
1315
+ Tags: map[string]string{"interface": "eth1"},
1316
+ MetricType: "gauge",
1317
+ IsTable: true,
1318
+ Mappings: map[int64]string{
1319
+ 1: "up",
1320
+ 2: "down",
1321
+ 3: "testing",
1322
+ 4: "unknown",
1323
+ 5: "dormant",
1324
+ },
1325
+ },
1326
+ },
1327
+ expectedError: false,
1328
+ },
1329
+ "table with extract value and mapping": {
1330
+ profile: &ddsnmp.Profile{
1331
+ SourceFile: "test-profile.yaml",
1332
+ Definition: &ddprofiledefinition.ProfileDefinition{
1333
+ Metrics: []ddprofiledefinition.MetricsConfig{
1334
+ {
1335
+ Table: ddprofiledefinition.SymbolConfig{
1336
+ OID: "1.3.6.1.4.1.12124.1.13",
1337
+ Name: "fanTable",
1338
+ },
1339
+ Symbols: []ddprofiledefinition.SymbolConfig{
1340
+ {
1341
+ OID: "1.3.6.1.4.1.12124.1.13.1.3",
1342
+ Name: "fanStatus",
1343
+ ExtractValueCompiled: mustCompileRegex(`Status(\d+)`),
1344
+ Mapping: map[string]string{
1345
+ "1": "normal",
1346
+ "2": "warning",
1347
+ "3": "critical",
1348
+ },
1349
+ },
1350
+ },
1351
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1352
+ {
1353
+ Tag: "fan_name",
1354
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1355
+ OID: "1.3.6.1.4.1.12124.1.13.1.2",
1356
+ Name: "fanName",
1357
+ },
1358
+ },
1359
+ },
1360
+ },
1361
+ },
1362
+ },
1363
+ },
1364
+ setupMock: func(m *snmpmock.MockHandler) {
1365
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.12124.1.13", []gosnmp.SnmpPDU{
1366
+ createStringPDU("1.3.6.1.4.1.12124.1.13.1.3.1", "Status1"), // normal
1367
+ createStringPDU("1.3.6.1.4.1.12124.1.13.1.2.1", "Fan1"),
1368
+ createStringPDU("1.3.6.1.4.1.12124.1.13.1.3.2", "Status3"), // critical
1369
+ createStringPDU("1.3.6.1.4.1.12124.1.13.1.2.2", "Fan2"),
1370
+ })
1371
+ },
1372
+ expectedResult: []ddsnmp.Metric{
1373
+ {
1374
+ Name: "fanStatus",
1375
+ Value: 1,
1376
+ Tags: map[string]string{"fan_name": "Fan1"},
1377
+ MetricType: "gauge",
1378
+ IsTable: true,
1379
+ Mappings: map[int64]string{
1380
+ 1: "normal",
1381
+ 2: "warning",
1382
+ 3: "critical",
1383
+ },
1384
+ },
1385
+ {
1386
+ Name: "fanStatus",
1387
+ Value: 3,
1388
+ Tags: map[string]string{"fan_name": "Fan2"},
1389
+ MetricType: "gauge",
1390
+ IsTable: true,
1391
+ Mappings: map[int64]string{
1392
+ 1: "normal",
1393
+ 2: "warning",
1394
+ 3: "critical",
1395
+ },
1396
+ },
1397
+ },
1398
+ expectedError: false,
1399
+ },
1400
+ "table with multiple scale factors": {
1401
+ profile: &ddsnmp.Profile{
1402
+ SourceFile: "test-profile.yaml",
1403
+ Definition: &ddprofiledefinition.ProfileDefinition{
1404
+ Metrics: []ddprofiledefinition.MetricsConfig{
1405
+ {
1406
+ Table: ddprofiledefinition.SymbolConfig{
1407
+ OID: "1.3.6.1.4.1.9.9.305.1.1.1",
1408
+ Name: "cempMemPoolTable",
1409
+ },
1410
+ Symbols: []ddprofiledefinition.SymbolConfig{
1411
+ {
1412
+ OID: "1.3.6.1.4.1.9.9.305.1.1.1.1.5",
1413
+ Name: "cempMemPoolUsed",
1414
+ ScaleFactor: 1000, // Convert to larger unit
1415
+ },
1416
+ {
1417
+ OID: "1.3.6.1.4.1.9.9.305.1.1.1.1.6",
1418
+ Name: "cempMemPoolFree",
1419
+ ScaleFactor: 0.001, // Convert to smaller unit
1420
+ },
1421
+ },
1422
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1423
+ {
1424
+ Tag: "pool_name",
1425
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1426
+ OID: "1.3.6.1.4.1.9.9.305.1.1.1.1.2",
1427
+ Name: "cempMemPoolName",
1428
+ },
1429
+ },
1430
+ },
1431
+ },
1432
+ },
1433
+ },
1434
+ },
1435
+ setupMock: func(m *snmpmock.MockHandler) {
1436
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.9.9.305.1.1.1", []gosnmp.SnmpPDU{
1437
+ createGauge32PDU("1.3.6.1.4.1.9.9.305.1.1.1.1.5.1", 1024),
1438
+ createGauge32PDU("1.3.6.1.4.1.9.9.305.1.1.1.1.6.1", 2048000),
1439
+ createStringPDU("1.3.6.1.4.1.9.9.305.1.1.1.1.2.1", "Processor"),
1440
+ })
1441
+ },
1442
+ expectedResult: []ddsnmp.Metric{
1443
+ {
1444
+ Name: "cempMemPoolUsed",
1445
+ Value: 1024000, // 1024 * 1000
1446
+ Tags: map[string]string{"pool_name": "Processor"},
1447
+ MetricType: "gauge",
1448
+ IsTable: true,
1449
+ },
1450
+ {
1451
+ Name: "cempMemPoolFree",
1452
+ Value: 2048, // 2048000 * 0.001
1453
+ Tags: map[string]string{"pool_name": "Processor"},
1454
+ MetricType: "gauge",
1455
+ IsTable: true,
1456
+ },
1457
+ },
1458
+ expectedError: false,
1459
+ },
1460
+ "table with missing value in mapping": {
1461
+ profile: &ddsnmp.Profile{
1462
+ SourceFile: "test-profile.yaml",
1463
+ Definition: &ddprofiledefinition.ProfileDefinition{
1464
+ Metrics: []ddprofiledefinition.MetricsConfig{
1465
+ {
1466
+ Table: ddprofiledefinition.SymbolConfig{
1467
+ OID: "1.3.6.1.4.1.318.1.1.10.4.3.3",
1468
+ Name: "upsPhaseInputTable",
1469
+ },
1470
+ Symbols: []ddprofiledefinition.SymbolConfig{
1471
+ {
1472
+ OID: "1.3.6.1.4.1.318.1.1.10.4.3.3.1.4",
1473
+ Name: "upsPhaseInputCurrent",
1474
+ },
1475
+ },
1476
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1477
+ {
1478
+ Tag: "phase",
1479
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1480
+ OID: "1.3.6.1.4.1.318.1.1.10.4.3.3.1.1",
1481
+ Name: "upsPhaseInputPhaseIndex",
1482
+ },
1483
+ Mapping: map[string]string{
1484
+ "1": "L1",
1485
+ "2": "L2",
1486
+ "3": "L3",
1487
+ // No mapping for value 4
1488
+ },
1489
+ },
1490
+ },
1491
+ },
1492
+ },
1493
+ },
1494
+ },
1495
+ setupMock: func(m *snmpmock.MockHandler) {
1496
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.318.1.1.10.4.3.3", []gosnmp.SnmpPDU{
1497
+ createGauge32PDU("1.3.6.1.4.1.318.1.1.10.4.3.3.1.4.1", 150),
1498
+ createIntegerPDU("1.3.6.1.4.1.318.1.1.10.4.3.3.1.1.1", 1), // L1
1499
+ createGauge32PDU("1.3.6.1.4.1.318.1.1.10.4.3.3.1.4.2", 160),
1500
+ createIntegerPDU("1.3.6.1.4.1.318.1.1.10.4.3.3.1.1.2", 4), // No mapping
1501
+ })
1502
+ },
1503
+ expectedResult: []ddsnmp.Metric{
1504
+ {
1505
+ Name: "upsPhaseInputCurrent",
1506
+ Value: 150,
1507
+ Tags: map[string]string{"phase": "L1"},
1508
+ MetricType: "gauge",
1509
+ IsTable: true,
1510
+ },
1511
+ {
1512
+ Name: "upsPhaseInputCurrent",
1513
+ Value: 160,
1514
+ Tags: map[string]string{"phase": "4"}, // Raw value when no mapping
1515
+ MetricType: "gauge",
1516
+ IsTable: true,
1517
+ },
1518
+ },
1519
+ expectedError: false,
1520
+ },
1521
+ "table with string to int mapping": {
1522
+ profile: &ddsnmp.Profile{
1523
+ SourceFile: "test-profile.yaml",
1524
+ Definition: &ddprofiledefinition.ProfileDefinition{
1525
+ Metrics: []ddprofiledefinition.MetricsConfig{
1526
+ {
1527
+ Table: ddprofiledefinition.SymbolConfig{
1528
+ OID: "1.3.6.1.4.1.12124.1.1",
1529
+ Name: "nodeTable",
1530
+ },
1531
+ Symbols: []ddprofiledefinition.SymbolConfig{
1532
+ {
1533
+ OID: "1.3.6.1.4.1.12124.1.1.1.5",
1534
+ Name: "nodeHealth",
1535
+ Mapping: map[string]string{
1536
+ "OK": "0",
1537
+ "ATTN": "1",
1538
+ "DOWN": "2",
1539
+ "INVALID": "3",
1540
+ },
1541
+ },
1542
+ },
1543
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1544
+ {
1545
+ Tag: "node_name",
1546
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1547
+ OID: "1.3.6.1.4.1.12124.1.1.1.2",
1548
+ Name: "nodeName",
1549
+ },
1550
+ },
1551
+ },
1552
+ },
1553
+ },
1554
+ },
1555
+ },
1556
+ setupMock: func(m *snmpmock.MockHandler) {
1557
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.12124.1.1", []gosnmp.SnmpPDU{
1558
+ createStringPDU("1.3.6.1.4.1.12124.1.1.1.5.1", "OK"),
1559
+ createStringPDU("1.3.6.1.4.1.12124.1.1.1.2.1", "node1"),
1560
+ createStringPDU("1.3.6.1.4.1.12124.1.1.1.5.2", "DOWN"),
1561
+ createStringPDU("1.3.6.1.4.1.12124.1.1.1.2.2", "node2"),
1562
+ })
1563
+ },
1564
+ expectedResult: []ddsnmp.Metric{
1565
+ {
1566
+ Name: "nodeHealth",
1567
+ Value: 0,
1568
+ Tags: map[string]string{"node_name": "node1"},
1569
+ MetricType: "gauge",
1570
+ IsTable: true,
1571
+ Mappings: map[int64]string{
1572
+ 0: "OK",
1573
+ 1: "ATTN",
1574
+ 2: "DOWN",
1575
+ 3: "INVALID",
1576
+ },
1577
+ },
1578
+ {
1579
+ Name: "nodeHealth",
1580
+ Value: 2,
1581
+ Tags: map[string]string{"node_name": "node2"},
1582
+ MetricType: "gauge",
1583
+ IsTable: true,
1584
+ Mappings: map[int64]string{
1585
+ 0: "OK",
1586
+ 1: "ATTN",
1587
+ 2: "DOWN",
1588
+ 3: "INVALID",
1589
+ },
1590
+ },
1591
+ },
1592
+ expectedError: false,
1593
+ },
1594
+ "table with OpaqueFloat values": {
1595
+ profile: &ddsnmp.Profile{
1596
+ SourceFile: "test-profile.yaml",
1597
+ Definition: &ddprofiledefinition.ProfileDefinition{
1598
+ Metrics: []ddprofiledefinition.MetricsConfig{
1599
+ {
1600
+ Table: ddprofiledefinition.SymbolConfig{
1601
+ OID: "1.3.6.1.4.1.9.9.305.1.1.2",
1602
+ Name: "cpmCPULoadTable",
1603
+ },
1604
+ Symbols: []ddprofiledefinition.SymbolConfig{
1605
+ {
1606
+ OID: "1.3.6.1.4.1.9.9.305.1.1.2.1.1",
1607
+ Name: "cpmCPULoadAvg1min",
1608
+ },
1609
+ {
1610
+ OID: "1.3.6.1.4.1.9.9.305.1.1.2.1.2",
1611
+ Name: "cpmCPULoadAvg5min",
1612
+ ScaleFactor: 100, // Convert to percentage
1613
+ },
1614
+ },
1615
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1616
+ {
1617
+ Tag: "cpu_index",
1618
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1619
+ OID: "1.3.6.1.4.1.9.9.305.1.1.2.1.0",
1620
+ Name: "cpmCPULoadIndex",
1621
+ },
1622
+ },
1623
+ },
1624
+ },
1625
+ },
1626
+ },
1627
+ },
1628
+ setupMock: func(m *snmpmock.MockHandler) {
1629
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.9.9.305.1.1.2", []gosnmp.SnmpPDU{
1630
+ createPDU("1.3.6.1.4.1.9.9.305.1.1.2.1.1.1", gosnmp.OpaqueFloat, float32(0.75)),
1631
+ createPDU("1.3.6.1.4.1.9.9.305.1.1.2.1.2.1", gosnmp.OpaqueFloat, float32(0.82)),
1632
+ createIntegerPDU("1.3.6.1.4.1.9.9.305.1.1.2.1.0.1", 1),
1633
+ })
1634
+ },
1635
+ expectedResult: []ddsnmp.Metric{
1636
+ {
1637
+ Name: "cpmCPULoadAvg1min",
1638
+ Value: 0, // 0.75 truncated to int64
1639
+ Tags: map[string]string{"cpu_index": "1"},
1640
+ MetricType: "gauge",
1641
+ IsTable: true,
1642
+ },
1643
+ {
1644
+ Name: "cpmCPULoadAvg5min",
1645
+ Value: 81, // 0.82 * 100 = 82
1646
+ Tags: map[string]string{"cpu_index": "1"},
1647
+ MetricType: "gauge",
1648
+ IsTable: true,
1649
+ },
1650
+ },
1651
+ expectedError: false,
1652
+ },
1653
+ "table with non-convertible string values": {
1654
+ profile: &ddsnmp.Profile{
1655
+ SourceFile: "test-profile.yaml",
1656
+ Definition: &ddprofiledefinition.ProfileDefinition{
1657
+ Metrics: []ddprofiledefinition.MetricsConfig{
1658
+ {
1659
+ Table: ddprofiledefinition.SymbolConfig{
1660
+ OID: "1.3.6.1.2.1.2.2",
1661
+ Name: "ifTable",
1662
+ },
1663
+ Symbols: []ddprofiledefinition.SymbolConfig{
1664
+ {
1665
+ OID: "1.3.6.1.2.1.2.2.1.10",
1666
+ Name: "ifInOctets",
1667
+ },
1668
+ {
1669
+ OID: "1.3.6.1.2.1.2.2.1.2",
1670
+ Name: "ifDescr", // This is a string, should be skipped
1671
+ },
1672
+ },
1673
+ },
1674
+ },
1675
+ },
1676
+ },
1677
+ setupMock: func(m *snmpmock.MockHandler) {
1678
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1679
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
1680
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"), // Can't convert to int64
1681
+ })
1682
+ },
1683
+ expectedResult: []ddsnmp.Metric{
1684
+ {
1685
+ Name: "ifInOctets",
1686
+ Value: 1000,
1687
+ Tags: nil,
1688
+ MetricType: "rate",
1689
+ IsTable: true,
1690
+ },
1691
+ // ifDescr metric is skipped because string can't be converted to int64
1692
+ },
1693
+ expectedError: false,
1694
+ },
1695
+
1696
+ "cross-table tag from ifXTable": {
1697
+ profile: &ddsnmp.Profile{
1698
+ SourceFile: "test-profile.yaml",
1699
+ Definition: &ddprofiledefinition.ProfileDefinition{
1700
+ Metrics: []ddprofiledefinition.MetricsConfig{
1701
+ {
1702
+ Table: ddprofiledefinition.SymbolConfig{
1703
+ OID: "1.3.6.1.2.1.2.2",
1704
+ Name: "ifTable",
1705
+ },
1706
+ Symbols: []ddprofiledefinition.SymbolConfig{
1707
+ {
1708
+ OID: "1.3.6.1.2.1.2.2.1.14",
1709
+ Name: "ifInErrors",
1710
+ },
1711
+ },
1712
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1713
+ {
1714
+ Tag: "interface",
1715
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1716
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1717
+ Name: "ifName",
1718
+ },
1719
+ Table: "ifXTable",
1720
+ },
1721
+ },
1722
+ },
1723
+ {
1724
+ Table: ddprofiledefinition.SymbolConfig{
1725
+ OID: "1.3.6.1.2.1.31.1.1",
1726
+ Name: "ifXTable",
1727
+ },
1728
+ Symbols: []ddprofiledefinition.SymbolConfig{
1729
+ {
1730
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1731
+ Name: "ifName",
1732
+ },
1733
+ },
1734
+ },
1735
+ },
1736
+ },
1737
+ },
1738
+ setupMock: func(m *snmpmock.MockHandler) {
1739
+ // Walk ifTable
1740
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1741
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.1", 10),
1742
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.2", 20),
1743
+ })
1744
+ // Walk ifXTable
1745
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.31.1.1", []gosnmp.SnmpPDU{
1746
+ createStringPDU("1.3.6.1.2.1.31.1.1.1.1.1", "GigabitEthernet0/0"),
1747
+ createStringPDU("1.3.6.1.2.1.31.1.1.1.1.2", "GigabitEthernet0/1"),
1748
+ })
1749
+ },
1750
+ expectedResult: []ddsnmp.Metric{
1751
+ {
1752
+ Name: "ifInErrors",
1753
+ Value: 10,
1754
+ Tags: map[string]string{"interface": "GigabitEthernet0/0"},
1755
+ MetricType: "rate",
1756
+ IsTable: true,
1757
+ },
1758
+ {
1759
+ Name: "ifInErrors",
1760
+ Value: 20,
1761
+ Tags: map[string]string{"interface": "GigabitEthernet0/1"},
1762
+ MetricType: "rate",
1763
+ IsTable: true,
1764
+ },
1765
+ },
1766
+ expectedError: false,
1767
+ },
1768
+ "cross-table tag when referenced table not walked": {
1769
+ profile: &ddsnmp.Profile{
1770
+ SourceFile: "test-profile.yaml",
1771
+ Definition: &ddprofiledefinition.ProfileDefinition{
1772
+ Metrics: []ddprofiledefinition.MetricsConfig{
1773
+ {
1774
+ Table: ddprofiledefinition.SymbolConfig{
1775
+ OID: "1.3.6.1.2.1.2.2",
1776
+ Name: "ifTable",
1777
+ },
1778
+ Symbols: []ddprofiledefinition.SymbolConfig{
1779
+ {
1780
+ OID: "1.3.6.1.2.1.2.2.1.14",
1781
+ Name: "ifInErrors",
1782
+ },
1783
+ },
1784
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1785
+ {
1786
+ Tag: "interface",
1787
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1788
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1789
+ Name: "ifName",
1790
+ },
1791
+ Table: "ifXTable",
1792
+ },
1793
+ },
1794
+ },
1795
+ // Note: ifXTable is NOT in the metrics list
1796
+ },
1797
+ },
1798
+ },
1799
+ setupMock: func(m *snmpmock.MockHandler) {
1800
+ // Only walk ifTable
1801
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1802
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.1", 10),
1803
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.2", 20),
1804
+ })
1805
+ },
1806
+ expectedResult: []ddsnmp.Metric{
1807
+ {
1808
+ Name: "ifInErrors",
1809
+ Value: 10,
1810
+ Tags: nil, // No cross-table tag because ifXTable wasn't walked
1811
+ MetricType: "rate",
1812
+ IsTable: true,
1813
+ },
1814
+ {
1815
+ Name: "ifInErrors",
1816
+ Value: 20,
1817
+ Tags: nil,
1818
+ MetricType: "rate",
1819
+ IsTable: true,
1820
+ },
1821
+ },
1822
+ expectedError: false,
1823
+ },
1824
+ "cross-table tag with missing row in referenced table": {
1825
+ profile: &ddsnmp.Profile{
1826
+ SourceFile: "test-profile.yaml",
1827
+ Definition: &ddprofiledefinition.ProfileDefinition{
1828
+ Metrics: []ddprofiledefinition.MetricsConfig{
1829
+ {
1830
+ Table: ddprofiledefinition.SymbolConfig{
1831
+ OID: "1.3.6.1.2.1.2.2",
1832
+ Name: "ifTable",
1833
+ },
1834
+ Symbols: []ddprofiledefinition.SymbolConfig{
1835
+ {
1836
+ OID: "1.3.6.1.2.1.2.2.1.14",
1837
+ Name: "ifInErrors",
1838
+ },
1839
+ },
1840
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1841
+ {
1842
+ Tag: "interface",
1843
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1844
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1845
+ Name: "ifName",
1846
+ },
1847
+ Table: "ifXTable",
1848
+ },
1849
+ },
1850
+ },
1851
+ {
1852
+ Table: ddprofiledefinition.SymbolConfig{
1853
+ OID: "1.3.6.1.2.1.31.1.1",
1854
+ Name: "ifXTable",
1855
+ },
1856
+ Symbols: []ddprofiledefinition.SymbolConfig{
1857
+ {
1858
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1859
+ Name: "ifName",
1860
+ },
1861
+ },
1862
+ },
1863
+ },
1864
+ },
1865
+ },
1866
+ setupMock: func(m *snmpmock.MockHandler) {
1867
+ // Walk ifTable - has 3 interfaces
1868
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1869
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.1", 10),
1870
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.2", 20),
1871
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.3", 30),
1872
+ })
1873
+ // Walk ifXTable - only has 2 interfaces (missing index 3)
1874
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.31.1.1", []gosnmp.SnmpPDU{
1875
+ createStringPDU("1.3.6.1.2.1.31.1.1.1.1.1", "GigabitEthernet0/0"),
1876
+ createStringPDU("1.3.6.1.2.1.31.1.1.1.1.2", "GigabitEthernet0/1"),
1877
+ })
1878
+ },
1879
+ expectedResult: []ddsnmp.Metric{
1880
+ {
1881
+ Name: "ifInErrors",
1882
+ Value: 10,
1883
+ Tags: map[string]string{"interface": "GigabitEthernet0/0"},
1884
+ MetricType: "rate",
1885
+ IsTable: true,
1886
+ },
1887
+ {
1888
+ Name: "ifInErrors",
1889
+ Value: 20,
1890
+ Tags: map[string]string{"interface": "GigabitEthernet0/1"},
1891
+ MetricType: "rate",
1892
+ IsTable: true,
1893
+ },
1894
+ {
1895
+ Name: "ifInErrors",
1896
+ Value: 30,
1897
+ Tags: nil, // No cross-table tag found for index 3
1898
+ MetricType: "rate",
1899
+ IsTable: true,
1900
+ },
1901
+ },
1902
+ expectedError: false,
1903
+ },
1904
+ "multiple cross-table tags": {
1905
+ profile: &ddsnmp.Profile{
1906
+ SourceFile: "test-profile.yaml",
1907
+ Definition: &ddprofiledefinition.ProfileDefinition{
1908
+ Metrics: []ddprofiledefinition.MetricsConfig{
1909
+ {
1910
+ Table: ddprofiledefinition.SymbolConfig{
1911
+ OID: "1.3.6.1.2.1.2.2",
1912
+ Name: "ifTable",
1913
+ },
1914
+ Symbols: []ddprofiledefinition.SymbolConfig{
1915
+ {
1916
+ OID: "1.3.6.1.2.1.2.2.1.14",
1917
+ Name: "ifInErrors",
1918
+ },
1919
+ },
1920
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1921
+ {
1922
+ Tag: "interface_desc",
1923
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1924
+ OID: "1.3.6.1.2.1.2.2.1.2",
1925
+ Name: "ifDescr",
1926
+ },
1927
+ // No Table field = same table
1928
+ },
1929
+ {
1930
+ Tag: "interface_name",
1931
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1932
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1933
+ Name: "ifName",
1934
+ },
1935
+ Table: "ifXTable",
1936
+ },
1937
+ {
1938
+ Tag: "interface_alias",
1939
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1940
+ OID: "1.3.6.1.2.1.31.1.1.1.18",
1941
+ Name: "ifAlias",
1942
+ },
1943
+ Table: "ifXTable",
1944
+ },
1945
+ },
1946
+ },
1947
+ {
1948
+ Table: ddprofiledefinition.SymbolConfig{
1949
+ OID: "1.3.6.1.2.1.31.1.1",
1950
+ Name: "ifXTable",
1951
+ },
1952
+ Symbols: []ddprofiledefinition.SymbolConfig{
1953
+ {
1954
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1955
+ Name: "ifName",
1956
+ },
1957
+ {
1958
+ OID: "1.3.6.1.2.1.31.1.1.1.18",
1959
+ Name: "ifAlias",
1960
+ },
1961
+ },
1962
+ },
1963
+ },
1964
+ },
1965
+ },
1966
+ setupMock: func(m *snmpmock.MockHandler) {
1967
+ // Walk ifTable
1968
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1969
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.14.1", 10),
1970
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
1971
+ })
1972
+ // Walk ifXTable
1973
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.31.1.1", []gosnmp.SnmpPDU{
1974
+ createStringPDU("1.3.6.1.2.1.31.1.1.1.1.1", "GigabitEthernet0/0"),
1975
+ createStringPDU("1.3.6.1.2.1.31.1.1.1.18.1", "Uplink to Core"),
1976
+ })
1977
+ },
1978
+ expectedResult: []ddsnmp.Metric{
1979
+ {
1980
+ Name: "ifInErrors",
1981
+ Value: 10,
1982
+ Tags: map[string]string{
1983
+ "interface_desc": "eth0",
1984
+ "interface_name": "GigabitEthernet0/0",
1985
+ "interface_alias": "Uplink to Core",
1986
+ },
1987
+ MetricType: "rate",
1988
+ IsTable: true,
1989
+ },
1990
+ },
1991
+ expectedError: false,
1992
+ },
1993
+ "cross-table tag with extract_value": {
1994
+ profile: &ddsnmp.Profile{
1995
+ SourceFile: "test-profile.yaml",
1996
+ Definition: &ddprofiledefinition.ProfileDefinition{
1997
+ Metrics: []ddprofiledefinition.MetricsConfig{
1998
+ {
1999
+ Table: ddprofiledefinition.SymbolConfig{
2000
+ OID: "1.3.6.1.2.1.2.2",
2001
+ Name: "ifTable",
2002
+ },
2003
+ Symbols: []ddprofiledefinition.SymbolConfig{
2004
+ {
2005
+ OID: "1.3.6.1.2.1.2.2.1.10",
2006
+ Name: "ifInOctets",
2007
+ },
2008
+ },
2009
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2010
+ {
2011
+ Tag: "interface_clean",
2012
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2013
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
2014
+ Name: "ifName",
2015
+ ExtractValueCompiled: mustCompileRegex("^([a-zA-Z0-9]+)"),
2016
+ },
2017
+ Table: "ifXTable",
2018
+ },
2019
+ },
2020
+ },
2021
+ {
2022
+ Table: ddprofiledefinition.SymbolConfig{
2023
+ OID: "1.3.6.1.2.1.31.1.1",
2024
+ Name: "ifXTable",
2025
+ },
2026
+ Symbols: []ddprofiledefinition.SymbolConfig{
2027
+ {
2028
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
2029
+ Name: "ifName",
2030
+ },
2031
+ },
2032
+ },
2033
+ },
2034
+ },
2035
+ },
2036
+ setupMock: func(m *snmpmock.MockHandler) {
2037
+ // Walk ifTable
2038
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
2039
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
2040
+ })
2041
+ // Walk ifXTable
2042
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.31.1.1", []gosnmp.SnmpPDU{
2043
+ createStringPDU("1.3.6.1.2.1.31.1.1.1.1.1", "GigabitEthernet0/0.100"),
2044
+ })
2045
+ },
2046
+ expectedResult: []ddsnmp.Metric{
2047
+ {
2048
+ Name: "ifInOctets",
2049
+ Value: 1000,
2050
+ Tags: map[string]string{"interface_clean": "GigabitEthernet0"},
2051
+ MetricType: "rate",
2052
+ IsTable: true,
2053
+ },
2054
+ },
2055
+ expectedError: false,
2056
+ },
2057
+ "cross-table tag with mapping": {
2058
+ profile: &ddsnmp.Profile{
2059
+ SourceFile: "test-profile.yaml",
2060
+ Definition: &ddprofiledefinition.ProfileDefinition{
2061
+ Metrics: []ddprofiledefinition.MetricsConfig{
2062
+ {
2063
+ Table: ddprofiledefinition.SymbolConfig{
2064
+ OID: "1.3.6.1.4.1.9.9.276.1.1.2",
2065
+ Name: "cieIfInterfaceTable",
2066
+ },
2067
+ Symbols: []ddprofiledefinition.SymbolConfig{
2068
+ {
2069
+ OID: "1.3.6.1.4.1.9.9.276.1.1.2.1.1",
2070
+ Name: "cieIfResetCount",
2071
+ },
2072
+ },
2073
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2074
+ {
2075
+ Tag: "interface",
2076
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2077
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
2078
+ Name: "ifName",
2079
+ },
2080
+ Table: "ifXTable",
2081
+ },
2082
+ {
2083
+ Tag: "if_type",
2084
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2085
+ OID: "1.3.6.1.2.1.2.2.1.3",
2086
+ Name: "ifType",
2087
+ },
2088
+ Table: "ifTable",
2089
+ Mapping: map[string]string{
2090
+ "6": "ethernet",
2091
+ "24": "loopback",
2092
+ },
2093
+ },
2094
+ },
2095
+ },
2096
+ {
2097
+ Table: ddprofiledefinition.SymbolConfig{
2098
+ OID: "1.3.6.1.2.1.31.1.1",
2099
+ Name: "ifXTable",
2100
+ },
2101
+ Symbols: []ddprofiledefinition.SymbolConfig{
2102
+ {
2103
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
2104
+ Name: "ifName",
2105
+ },
2106
+ },
2107
+ },
2108
+ {
2109
+ Table: ddprofiledefinition.SymbolConfig{
2110
+ OID: "1.3.6.1.2.1.2.2",
2111
+ Name: "ifTable",
2112
+ },
2113
+ Symbols: []ddprofiledefinition.SymbolConfig{
2114
+ {
2115
+ OID: "1.3.6.1.2.1.2.2.1.3",
2116
+ Name: "ifType",
2117
+ },
2118
+ },
2119
+ },
2120
+ },
2121
+ },
2122
+ },
2123
+ setupMock: func(m *snmpmock.MockHandler) {
2124
+ // Walk cieIfInterfaceTable
2125
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.9.9.276.1.1.2", []gosnmp.SnmpPDU{
2126
+ createCounter64PDU("1.3.6.1.4.1.9.9.276.1.1.2.1.1.1", 5),
2127
+ })
2128
+ // Walk ifXTable
2129
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.31.1.1", []gosnmp.SnmpPDU{
2130
+ createStringPDU("1.3.6.1.2.1.31.1.1.1.1.1", "GigabitEthernet0/0"),
2131
+ })
2132
+ // Walk ifTable
2133
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
2134
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.3.1", 6),
2135
+ })
2136
+ },
2137
+ expectedResult: []ddsnmp.Metric{
2138
+ {
2139
+ Name: "cieIfResetCount",
2140
+ Value: 5,
2141
+ Tags: map[string]string{
2142
+ "interface": "GigabitEthernet0/0",
2143
+ "if_type": "ethernet",
2144
+ },
2145
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
2146
+ IsTable: true,
2147
+ },
2148
+ {
2149
+ Name: "ifType",
2150
+ MetricType: "gauge",
2151
+ IsTable: true,
2152
+ Tags: nil,
2153
+ Value: 6,
2154
+ },
2155
+ },
2156
+ expectedError: false,
2157
+ },
2158
+ "cross-table tag with index transformation": {
2159
+ profile: &ddsnmp.Profile{
2160
+ SourceFile: "test-profile.yaml",
2161
+ Definition: &ddprofiledefinition.ProfileDefinition{
2162
+ Metrics: []ddprofiledefinition.MetricsConfig{
2163
+ {
2164
+ Table: ddprofiledefinition.SymbolConfig{
2165
+ OID: "1.3.6.1.4.1.30932.1.10.1.3.110",
2166
+ Name: "cpiPduBranchTable",
2167
+ },
2168
+ Symbols: []ddprofiledefinition.SymbolConfig{
2169
+ {
2170
+ OID: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3",
2171
+ Name: "cpiPduBranchCurrent",
2172
+ },
2173
+ },
2174
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2175
+ {
2176
+ Tag: "pdu_name",
2177
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2178
+ OID: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3",
2179
+ Name: "cpiPduName",
2180
+ },
2181
+ Table: "cpiPduTable",
2182
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2183
+ {
2184
+ Start: 1,
2185
+ End: 7,
2186
+ },
2187
+ },
2188
+ },
2189
+ },
2190
+ },
2191
+ {
2192
+ Table: ddprofiledefinition.SymbolConfig{
2193
+ OID: "1.3.6.1.4.1.30932.1.10.1.2.10",
2194
+ Name: "cpiPduTable",
2195
+ },
2196
+ Symbols: []ddprofiledefinition.SymbolConfig{
2197
+ {
2198
+ OID: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3",
2199
+ Name: "cpiPduName",
2200
+ },
2201
+ },
2202
+ },
2203
+ },
2204
+ },
2205
+ },
2206
+ setupMock: func(m *snmpmock.MockHandler) {
2207
+ // Walk cpiPduBranchTable with complex index: 1.6.0.36.155.53.3.246
2208
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.30932.1.10.1.3.110", []gosnmp.SnmpPDU{
2209
+ createGauge32PDU("1.3.6.1.4.1.30932.1.10.1.3.110.1.3.1.6.0.36.155.53.3.246", 15),
2210
+ })
2211
+ // Walk cpiPduTable with simpler index: 6.0.36.155.53.3.246
2212
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.30932.1.10.1.2.10", []gosnmp.SnmpPDU{
2213
+ createStringPDU("1.3.6.1.4.1.30932.1.10.1.2.10.1.3.6.0.36.155.53.3.246", "PDU-A1"),
2214
+ })
2215
+ },
2216
+ expectedResult: []ddsnmp.Metric{
2217
+ {
2218
+ Name: "cpiPduBranchCurrent",
2219
+ Value: 15,
2220
+ Tags: map[string]string{"pdu_name": "PDU-A1"},
2221
+ MetricType: "gauge",
2222
+ IsTable: true,
2223
+ },
2224
+ },
2225
+ expectedError: false,
2226
+ },
2227
+ "cross-table tag with complex index transformation": {
2228
+ profile: &ddsnmp.Profile{
2229
+ SourceFile: "test-profile.yaml",
2230
+ Definition: &ddprofiledefinition.ProfileDefinition{
2231
+ Metrics: []ddprofiledefinition.MetricsConfig{
2232
+ {
2233
+ Table: ddprofiledefinition.SymbolConfig{
2234
+ OID: "1.3.6.1.4.1.12345.1.1",
2235
+ Name: "customTable",
2236
+ },
2237
+ Symbols: []ddprofiledefinition.SymbolConfig{
2238
+ {
2239
+ OID: "1.3.6.1.4.1.12345.1.1.1.1",
2240
+ Name: "customMetric",
2241
+ },
2242
+ },
2243
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2244
+ {
2245
+ Tag: "device_name",
2246
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2247
+ OID: "1.3.6.1.4.1.12345.2.1.1.1",
2248
+ Name: "deviceName",
2249
+ },
2250
+ Table: "deviceTable",
2251
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2252
+ {
2253
+ Start: 1,
2254
+ End: 2,
2255
+ },
2256
+ {
2257
+ Start: 4,
2258
+ End: 6,
2259
+ },
2260
+ },
2261
+ },
2262
+ },
2263
+ },
2264
+ {
2265
+ Table: ddprofiledefinition.SymbolConfig{
2266
+ OID: "1.3.6.1.4.1.12345.2.1",
2267
+ Name: "deviceTable",
2268
+ },
2269
+ Symbols: []ddprofiledefinition.SymbolConfig{
2270
+ {
2271
+ OID: "1.3.6.1.4.1.12345.2.1.1.1",
2272
+ Name: "deviceName",
2273
+ },
2274
+ },
2275
+ },
2276
+ },
2277
+ },
2278
+ },
2279
+ setupMock: func(m *snmpmock.MockHandler) {
2280
+ // Walk customTable with index: 1.2.3.4.5.6.7
2281
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.12345.1.1", []gosnmp.SnmpPDU{
2282
+ createGauge32PDU("1.3.6.1.4.1.12345.1.1.1.1.1.2.3.4.5.6.7", 100),
2283
+ })
2284
+ // Walk deviceTable with transformed index: 2.3.5.6.7
2285
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.12345.2.1", []gosnmp.SnmpPDU{
2286
+ createStringPDU("1.3.6.1.4.1.12345.2.1.1.1.2.3.5.6.7", "Device-123"),
2287
+ })
2288
+ },
2289
+ expectedResult: []ddsnmp.Metric{
2290
+ {
2291
+ Name: "customMetric",
2292
+ Value: 100,
2293
+ Tags: map[string]string{"device_name": "Device-123"},
2294
+ MetricType: "gauge",
2295
+ IsTable: true,
2296
+ },
2297
+ },
2298
+ expectedError: false,
2299
+ },
2300
+ "cross-table tag error when index transform fails": {
2301
+ profile: &ddsnmp.Profile{
2302
+ SourceFile: "test-profile.yaml",
2303
+ Definition: &ddprofiledefinition.ProfileDefinition{
2304
+ Metrics: []ddprofiledefinition.MetricsConfig{
2305
+ {
2306
+ Table: ddprofiledefinition.SymbolConfig{
2307
+ OID: "1.3.6.1.4.1.12345.1.1",
2308
+ Name: "customTable",
2309
+ },
2310
+ Symbols: []ddprofiledefinition.SymbolConfig{
2311
+ {
2312
+ OID: "1.3.6.1.4.1.12345.1.1.1.1",
2313
+ Name: "customMetric",
2314
+ },
2315
+ },
2316
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2317
+ {
2318
+ Tag: "device_name",
2319
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2320
+ OID: "1.3.6.1.4.1.12345.2.1.1.1",
2321
+ Name: "deviceName",
2322
+ },
2323
+ Table: "deviceTable",
2324
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2325
+ {
2326
+ Start: 10, // Out of bounds
2327
+ End: 15,
2328
+ },
2329
+ },
2330
+ },
2331
+ },
2332
+ },
2333
+ {
2334
+ Table: ddprofiledefinition.SymbolConfig{
2335
+ OID: "1.3.6.1.4.1.12345.2.1",
2336
+ Name: "deviceTable",
2337
+ },
2338
+ Symbols: []ddprofiledefinition.SymbolConfig{
2339
+ {
2340
+ OID: "1.3.6.1.4.1.12345.2.1.1.1",
2341
+ Name: "deviceName",
2342
+ },
2343
+ },
2344
+ },
2345
+ },
2346
+ },
2347
+ },
2348
+ setupMock: func(m *snmpmock.MockHandler) {
2349
+ // Walk customTable with short index: 1.2.3
2350
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.12345.1.1", []gosnmp.SnmpPDU{
2351
+ createGauge32PDU("1.3.6.1.4.1.12345.1.1.1.1.1.2.3", 100),
2352
+ })
2353
+ // Walk deviceTable
2354
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.12345.2.1", []gosnmp.SnmpPDU{
2355
+ createStringPDU("1.3.6.1.4.1.12345.2.1.1.1.1", "Device-1"),
2356
+ })
2357
+ },
2358
+ expectedResult: []ddsnmp.Metric{
2359
+ {
2360
+ Name: "customMetric",
2361
+ Value: 100,
2362
+ Tags: nil, // No tag because index transform failed
2363
+ MetricType: "gauge",
2364
+ IsTable: true,
2365
+ },
2366
+ },
2367
+ expectedError: false,
2368
+ },
2369
+
2370
+ "basic index tag": {
2371
+ profile: &ddsnmp.Profile{
2372
+ SourceFile: "test-profile.yaml",
2373
+ Definition: &ddprofiledefinition.ProfileDefinition{
2374
+ Metrics: []ddprofiledefinition.MetricsConfig{
2375
+ {
2376
+ Table: ddprofiledefinition.SymbolConfig{
2377
+ OID: "1.3.6.1.2.1.4.31.1",
2378
+ Name: "ipSystemStatsTable",
2379
+ },
2380
+ Symbols: []ddprofiledefinition.SymbolConfig{
2381
+ {
2382
+ OID: "1.3.6.1.2.1.4.31.1.1.4",
2383
+ Name: "ipSystemStatsHCInReceives",
2384
+ },
2385
+ },
2386
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2387
+ {
2388
+ Index: 1,
2389
+ Tag: "ip_version",
2390
+ },
2391
+ },
2392
+ },
2393
+ },
2394
+ },
2395
+ },
2396
+ setupMock: func(m *snmpmock.MockHandler) {
2397
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.4.31.1", []gosnmp.SnmpPDU{
2398
+ createCounter64PDU("1.3.6.1.2.1.4.31.1.1.4.1", 1000), // IPv4
2399
+ createCounter64PDU("1.3.6.1.2.1.4.31.1.1.4.2", 2000), // IPv6
2400
+ })
2401
+ },
2402
+ expectedResult: []ddsnmp.Metric{
2403
+ {
2404
+ Name: "ipSystemStatsHCInReceives",
2405
+ Value: 1000,
2406
+ Tags: map[string]string{"ip_version": "1"},
2407
+ MetricType: "rate",
2408
+ IsTable: true,
2409
+ },
2410
+ {
2411
+ Name: "ipSystemStatsHCInReceives",
2412
+ Value: 2000,
2413
+ Tags: map[string]string{"ip_version": "2"},
2414
+ MetricType: "rate",
2415
+ IsTable: true,
2416
+ },
2417
+ },
2418
+ expectedError: false,
2419
+ },
2420
+ "multiple index positions": {
2421
+ profile: &ddsnmp.Profile{
2422
+ SourceFile: "test-profile.yaml",
2423
+ Definition: &ddprofiledefinition.ProfileDefinition{
2424
+ Metrics: []ddprofiledefinition.MetricsConfig{
2425
+ {
2426
+ Table: ddprofiledefinition.SymbolConfig{
2427
+ OID: "1.3.6.1.4.1.9.9.147.1.2.2.2",
2428
+ Name: "cfwConnectionStatTable",
2429
+ },
2430
+ Symbols: []ddprofiledefinition.SymbolConfig{
2431
+ {
2432
+ OID: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5",
2433
+ Name: "cfwConnectionStatValue",
2434
+ },
2435
+ },
2436
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2437
+ {
2438
+ Index: 1,
2439
+ Tag: "service_type",
2440
+ },
2441
+ {
2442
+ Index: 2,
2443
+ Tag: "stat_type",
2444
+ },
2445
+ },
2446
+ },
2447
+ },
2448
+ },
2449
+ },
2450
+ setupMock: func(m *snmpmock.MockHandler) {
2451
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.9.9.147.1.2.2.2", []gosnmp.SnmpPDU{
2452
+ createCounter64PDU("1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.20.2", 4087850099),
2453
+ createCounter64PDU("1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.21.3", 1234567890),
2454
+ })
2455
+ },
2456
+ expectedResult: []ddsnmp.Metric{
2457
+ {
2458
+ Name: "cfwConnectionStatValue",
2459
+ Value: 4087850099,
2460
+ Tags: map[string]string{"service_type": "20", "stat_type": "2"},
2461
+ MetricType: "rate",
2462
+ IsTable: true,
2463
+ },
2464
+ {
2465
+ Name: "cfwConnectionStatValue",
2466
+ Value: 1234567890,
2467
+ Tags: map[string]string{"service_type": "21", "stat_type": "3"},
2468
+ MetricType: "rate",
2469
+ IsTable: true,
2470
+ },
2471
+ },
2472
+ expectedError: false,
2473
+ },
2474
+ "index tag with mapping": {
2475
+ profile: &ddsnmp.Profile{
2476
+ SourceFile: "test-profile.yaml",
2477
+ Definition: &ddprofiledefinition.ProfileDefinition{
2478
+ Metrics: []ddprofiledefinition.MetricsConfig{
2479
+ {
2480
+ Table: ddprofiledefinition.SymbolConfig{
2481
+ OID: "1.3.6.1.2.1.4.31.1",
2482
+ Name: "ipSystemStatsTable",
2483
+ },
2484
+ Symbols: []ddprofiledefinition.SymbolConfig{
2485
+ {
2486
+ OID: "1.3.6.1.2.1.4.31.1.1.4",
2487
+ Name: "ipSystemStatsHCInReceives",
2488
+ },
2489
+ },
2490
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2491
+ {
2492
+ Index: 1,
2493
+ Tag: "ip_version",
2494
+ Mapping: map[string]string{
2495
+ "1": "ipv4",
2496
+ "2": "ipv6",
2497
+ },
2498
+ },
2499
+ },
2500
+ },
2501
+ },
2502
+ },
2503
+ },
2504
+ setupMock: func(m *snmpmock.MockHandler) {
2505
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.4.31.1", []gosnmp.SnmpPDU{
2506
+ createCounter64PDU("1.3.6.1.2.1.4.31.1.1.4.1", 1000),
2507
+ createCounter64PDU("1.3.6.1.2.1.4.31.1.1.4.2", 2000),
2508
+ createCounter64PDU("1.3.6.1.2.1.4.31.1.1.4.0", 10), // Unknown
2509
+ })
2510
+ },
2511
+ expectedResult: []ddsnmp.Metric{
2512
+ {
2513
+ Name: "ipSystemStatsHCInReceives",
2514
+ Value: 1000,
2515
+ Tags: map[string]string{"ip_version": "ipv4"},
2516
+ MetricType: "rate",
2517
+ IsTable: true,
2518
+ },
2519
+ {
2520
+ Name: "ipSystemStatsHCInReceives",
2521
+ Value: 2000,
2522
+ Tags: map[string]string{"ip_version": "ipv6"},
2523
+ MetricType: "rate",
2524
+ IsTable: true,
2525
+ },
2526
+ {
2527
+ Name: "ipSystemStatsHCInReceives",
2528
+ Value: 10,
2529
+ Tags: map[string]string{"ip_version": "0"}, // No mapping for 0
2530
+ MetricType: "rate",
2531
+ IsTable: true,
2532
+ },
2533
+ },
2534
+ expectedError: false,
2535
+ },
2536
+ "index tag with missing position": {
2537
+ profile: &ddsnmp.Profile{
2538
+ SourceFile: "test-profile.yaml",
2539
+ Definition: &ddprofiledefinition.ProfileDefinition{
2540
+ Metrics: []ddprofiledefinition.MetricsConfig{
2541
+ {
2542
+ Table: ddprofiledefinition.SymbolConfig{
2543
+ OID: "1.3.6.1.2.1.2.2",
2544
+ Name: "ifTable",
2545
+ },
2546
+ Symbols: []ddprofiledefinition.SymbolConfig{
2547
+ {
2548
+ OID: "1.3.6.1.2.1.2.2.1.10",
2549
+ Name: "ifInOctets",
2550
+ },
2551
+ },
2552
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2553
+ {
2554
+ Index: 1,
2555
+ Tag: "interface_index",
2556
+ },
2557
+ {
2558
+ Index: 2,
2559
+ Tag: "sub_interface", // Won't exist for single index
2560
+ },
2561
+ },
2562
+ },
2563
+ },
2564
+ },
2565
+ },
2566
+ setupMock: func(m *snmpmock.MockHandler) {
2567
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
2568
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000), // Single index
2569
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.2.3", 2000), // Multi-part index
2570
+ })
2571
+ },
2572
+ expectedResult: []ddsnmp.Metric{
2573
+ {
2574
+ Name: "ifInOctets",
2575
+ Value: 1000,
2576
+ Tags: map[string]string{"interface_index": "1"}, // No sub_interface tag
2577
+ MetricType: "rate",
2578
+ IsTable: true,
2579
+ },
2580
+ {
2581
+ Name: "ifInOctets",
2582
+ Value: 2000,
2583
+ Tags: map[string]string{"interface_index": "2", "sub_interface": "3"},
2584
+ MetricType: "rate",
2585
+ IsTable: true,
2586
+ },
2587
+ },
2588
+ expectedError: false,
2589
+ },
2590
+ "index tag with default tag name": {
2591
+ profile: &ddsnmp.Profile{
2592
+ SourceFile: "test-profile.yaml",
2593
+ Definition: &ddprofiledefinition.ProfileDefinition{
2594
+ Metrics: []ddprofiledefinition.MetricsConfig{
2595
+ {
2596
+ Table: ddprofiledefinition.SymbolConfig{
2597
+ OID: "1.3.6.1.2.1.4.24.4",
2598
+ Name: "ipMRouteTable",
2599
+ },
2600
+ Symbols: []ddprofiledefinition.SymbolConfig{
2601
+ {
2602
+ OID: "1.3.6.1.2.1.4.24.4.1.3",
2603
+ Name: "ipMRouteInIfIndex",
2604
+ },
2605
+ },
2606
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2607
+ {
2608
+ Index: 1, // No Tag field, should use "index1"
2609
+ },
2610
+ {
2611
+ Index: 2, // No Tag field, should use "index2"
2612
+ },
2613
+ },
2614
+ },
2615
+ },
2616
+ },
2617
+ },
2618
+ setupMock: func(m *snmpmock.MockHandler) {
2619
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.4.24.4", []gosnmp.SnmpPDU{
2620
+ createIntegerPDU("1.3.6.1.2.1.4.24.4.1.3.192.168.1.0.255.255.255.0", 1),
2621
+ })
2622
+ },
2623
+ expectedResult: []ddsnmp.Metric{
2624
+ {
2625
+ Name: "ipMRouteInIfIndex",
2626
+ Value: 1,
2627
+ Tags: map[string]string{"index1": "192", "index2": "168"},
2628
+ MetricType: "gauge",
2629
+ IsTable: true,
2630
+ },
2631
+ },
2632
+ expectedError: false,
2633
+ },
2634
+ "complex multi-part index": {
2635
+ profile: &ddsnmp.Profile{
2636
+ SourceFile: "test-profile.yaml",
2637
+ Definition: &ddprofiledefinition.ProfileDefinition{
2638
+ Metrics: []ddprofiledefinition.MetricsConfig{
2639
+ {
2640
+ Table: ddprofiledefinition.SymbolConfig{
2641
+ OID: "1.3.6.1.4.1.12345.1.1",
2642
+ Name: "customTable",
2643
+ },
2644
+ Symbols: []ddprofiledefinition.SymbolConfig{
2645
+ {
2646
+ OID: "1.3.6.1.4.1.12345.1.1.1.1",
2647
+ Name: "customMetric",
2648
+ },
2649
+ },
2650
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2651
+ {
2652
+ Index: 1,
2653
+ Tag: "device_id",
2654
+ },
2655
+ {
2656
+ Index: 3,
2657
+ Tag: "sensor_id",
2658
+ },
2659
+ {
2660
+ Index: 5,
2661
+ Tag: "reading_type",
2662
+ },
2663
+ {
2664
+ Index: 10,
2665
+ Tag: "last_octet", // Should not exist
2666
+ },
2667
+ },
2668
+ },
2669
+ },
2670
+ },
2671
+ },
2672
+ setupMock: func(m *snmpmock.MockHandler) {
2673
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.12345.1.1", []gosnmp.SnmpPDU{
2674
+ createGauge32PDU("1.3.6.1.4.1.12345.1.1.1.1.10.20.30.40.50", 100),
2675
+ })
2676
+ },
2677
+ expectedResult: []ddsnmp.Metric{
2678
+ {
2679
+ Name: "customMetric",
2680
+ Value: 100,
2681
+ Tags: map[string]string{
2682
+ "device_id": "10",
2683
+ "sensor_id": "30",
2684
+ "reading_type": "50",
2685
+ // No last_octet tag as position 10 doesn't exist
2686
+ },
2687
+ MetricType: "gauge",
2688
+ IsTable: true,
2689
+ },
2690
+ },
2691
+ expectedError: false,
2692
+ },
2693
+ "index tag with zero position": {
2694
+ profile: &ddsnmp.Profile{
2695
+ SourceFile: "test-profile.yaml",
2696
+ Definition: &ddprofiledefinition.ProfileDefinition{
2697
+ Metrics: []ddprofiledefinition.MetricsConfig{
2698
+ {
2699
+ Table: ddprofiledefinition.SymbolConfig{
2700
+ OID: "1.3.6.1.2.1.2.2",
2701
+ Name: "ifTable",
2702
+ },
2703
+ Symbols: []ddprofiledefinition.SymbolConfig{
2704
+ {
2705
+ OID: "1.3.6.1.2.1.2.2.1.10",
2706
+ Name: "ifInOctets",
2707
+ },
2708
+ },
2709
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2710
+ {
2711
+ Index: 0, // Invalid, should be ignored
2712
+ Tag: "invalid_tag",
2713
+ },
2714
+ {
2715
+ Index: 1,
2716
+ Tag: "interface_index",
2717
+ },
2718
+ },
2719
+ },
2720
+ },
2721
+ },
2722
+ },
2723
+ setupMock: func(m *snmpmock.MockHandler) {
2724
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
2725
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
2726
+ })
2727
+ },
2728
+ expectedResult: []ddsnmp.Metric{
2729
+ {
2730
+ Name: "ifInOctets",
2731
+ Value: 1000,
2732
+ Tags: map[string]string{"interface_index": "1"}, // No invalid_tag
2733
+ MetricType: "rate",
2734
+ IsTable: true,
2735
+ },
2736
+ },
2737
+ expectedError: false,
2738
+ },
2739
+ "index overflow handling": {
2740
+ profile: &ddsnmp.Profile{
2741
+ SourceFile: "test-profile.yaml",
2742
+ Definition: &ddprofiledefinition.ProfileDefinition{
2743
+ Metrics: []ddprofiledefinition.MetricsConfig{
2744
+ {
2745
+ Table: ddprofiledefinition.SymbolConfig{
2746
+ OID: "1.3.6.1.2.1.2.2",
2747
+ Name: "ifTable",
2748
+ },
2749
+ Symbols: []ddprofiledefinition.SymbolConfig{
2750
+ {
2751
+ OID: "1.3.6.1.2.1.2.2.1.10",
2752
+ Name: "ifInOctets",
2753
+ },
2754
+ },
2755
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2756
+ {
2757
+ Index: 999, // Way out of bounds
2758
+ Tag: "impossible_tag",
2759
+ },
2760
+ {
2761
+ Index: 1,
2762
+ Tag: "interface_index",
2763
+ },
2764
+ },
2765
+ },
2766
+ },
2767
+ },
2768
+ },
2769
+ setupMock: func(m *snmpmock.MockHandler) {
2770
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
2771
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.5", 5000),
2772
+ })
2773
+ },
2774
+ expectedResult: []ddsnmp.Metric{
2775
+ {
2776
+ Name: "ifInOctets",
2777
+ Value: 5000,
2778
+ Tags: map[string]string{"interface_index": "5"}, // No impossible_tag
2779
+ MetricType: "rate",
2780
+ IsTable: true,
2781
+ },
2782
+ },
2783
+ expectedError: false,
2784
+ },
2785
+
2786
+ "table metric with sensor type transformation": {
2787
+ profile: &ddsnmp.Profile{
2788
+ SourceFile: "test-profile.yaml",
2789
+ Definition: &ddprofiledefinition.ProfileDefinition{
2790
+ Metrics: []ddprofiledefinition.MetricsConfig{
2791
+ {
2792
+ Table: ddprofiledefinition.SymbolConfig{
2793
+ OID: "1.3.6.1.4.1.14988.1.1.3.100",
2794
+ Name: "mtxrHlTable",
2795
+ },
2796
+ Symbols: []ddprofiledefinition.SymbolConfig{
2797
+ {
2798
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.3",
2799
+ Name: "mtxrHlSensorValue",
2800
+ Transform: `
2801
+{{- $sensorType := index .Metric.Tags "sensor_type" | default "" -}}
2802
+{{- if eq $sensorType "1" -}}
2803
+ {{- setName .Metric (printf "%s_temperature" .Metric.Name) -}}
2804
+ {{- setUnit .Metric "celsius" -}}
2805
+ {{- setFamily .Metric "Health/Temperature" -}}
2806
+{{- else if eq $sensorType "3" -}}
2807
+ {{- setName .Metric (printf "%s_voltage" .Metric.Name) -}}
2808
+ {{- setUnit .Metric "volts" -}}
2809
+ {{- setValue .Metric (int64 (div (float64 .Metric.Value) 10.0)) -}}
2810
+ {{- setFamily .Metric "Health/Power" -}}
2811
+{{- end -}}
2812
+{{- deleteTag .Metric "sensor_type" -}}`,
2813
+ },
2814
+ },
2815
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2816
+ {
2817
+ Tag: "sensor_name",
2818
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2819
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.2",
2820
+ Name: "mtxrHlSensorName",
2821
+ },
2822
+ },
2823
+ {
2824
+ Tag: "sensor_type",
2825
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2826
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.4",
2827
+ Name: "mtxrHlSensorType",
2828
+ },
2829
+ },
2830
+ },
2831
+ },
2832
+ },
2833
+ },
2834
+ },
2835
+ setupMock: func(m *snmpmock.MockHandler) {
2836
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.14988.1.1.3.100", []gosnmp.SnmpPDU{
2837
+ // Temperature sensor (type 1)
2838
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.3.1", 25),
2839
+ createStringPDU("1.3.6.1.4.1.14988.1.1.3.100.1.2.1", "cpu-temperature"),
2840
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.4.1", 1),
2841
+ // Voltage sensor (type 3)
2842
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.3.2", 120), // 12.0V in decivolts
2843
+ createStringPDU("1.3.6.1.4.1.14988.1.1.3.100.1.2.2", "input-voltage"),
2844
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.4.2", 3),
2845
+ })
2846
+ },
2847
+ expectedResult: []ddsnmp.Metric{
2848
+ {
2849
+ Name: "mtxrHlSensorValue_temperature",
2850
+ Value: 25,
2851
+ Tags: map[string]string{"sensor_name": "cpu-temperature"},
2852
+ Unit: "celsius",
2853
+ Family: "Health/Temperature",
2854
+ MetricType: "gauge",
2855
+ IsTable: true,
2856
+ },
2857
+ {
2858
+ Name: "mtxrHlSensorValue_voltage",
2859
+ Value: 12, // Converted from decivolts
2860
+ Tags: map[string]string{"sensor_name": "input-voltage"},
2861
+ Unit: "volts",
2862
+ Family: "Health/Power",
2863
+ MetricType: "gauge",
2864
+ IsTable: true,
2865
+ },
2866
+ },
2867
+ expectedError: false,
2868
+ },
2869
+ "mikrotik sensor table real-world example": {
2870
+ profile: &ddsnmp.Profile{
2871
+ SourceFile: "mikrotik-profile.yaml",
2872
+ Definition: &ddprofiledefinition.ProfileDefinition{
2873
+ Metrics: []ddprofiledefinition.MetricsConfig{
2874
+ {
2875
+ Table: ddprofiledefinition.SymbolConfig{
2876
+ OID: "1.3.6.1.4.1.14988.1.1.3.100",
2877
+ Name: "mtxrHlTable",
2878
+ },
2879
+ Symbols: []ddprofiledefinition.SymbolConfig{
2880
+ {
2881
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.3",
2882
+ Name: "mtxrHlSensorValue",
2883
+ Transform: `
2884
+{{- $config := get (dict
2885
+ "1" (dict "name" "temperature" "unit" "celsius" "family" "Health/Temperature")
2886
+ "2" (dict "name" "fan_speed" "unit" "rpm" "family" "Health/Cooling")
2887
+ "3" (dict "name" "voltage" "unit" "volts" "family" "Health/Power" "divisor" 10.0)
2888
+ "6" (dict "name" "sensor_status" "family" "Health/Status"
2889
+ "mapping" (i64map 0 "not_ok" 1 "ok"))
2890
+) (index .Metric.Tags "sensor_type" | default "") -}}
2891
+
2892
+{{- if $config -}}
2893
+ {{- setName .Metric (printf "%s_%s" .Metric.Name (get $config "name")) -}}
2894
+ {{- setFamily .Metric (get $config "family") -}}
2895
+ {{- with get $config "unit" -}}{{- setUnit $.Metric . -}}{{- end -}}
2896
+ {{- with get $config "divisor" -}}{{- setValue $.Metric (int64 (div (float64 $.Metric.Value) .)) -}}{{- end -}}
2897
+ {{- with get $config "mapping" -}}{{- setMappings $.Metric . -}}{{- end -}}
2898
+{{- end -}}
2899
+
2900
+{{- deleteTag .Metric "sensor_type" -}}`,
2901
+ },
2902
+ },
2903
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2904
+ {
2905
+ Tag: "sensor_name",
2906
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2907
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.2",
2908
+ Name: "mtxrHlSensorName",
2909
+ },
2910
+ },
2911
+ {
2912
+ Tag: "sensor_type",
2913
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2914
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.4",
2915
+ Name: "mtxrHlSensorType",
2916
+ },
2917
+ },
2918
+ },
2919
+ },
2920
+ },
2921
+ },
2922
+ },
2923
+ setupMock: func(m *snmpmock.MockHandler) {
2924
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.14988.1.1.3.100", []gosnmp.SnmpPDU{
2925
+ // Temperature sensor
2926
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.3.1", 45),
2927
+ createStringPDU("1.3.6.1.4.1.14988.1.1.3.100.1.2.1", "cpu-temperature"),
2928
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.4.1", 1),
2929
+ // PSU status sensor
2930
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.3.2", 1),
2931
+ createStringPDU("1.3.6.1.4.1.14988.1.1.3.100.1.2.2", "psu1-state"),
2932
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.4.2", 6),
2933
+ // Voltage sensor
2934
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.3.3", 240), // 24.0V
2935
+ createStringPDU("1.3.6.1.4.1.14988.1.1.3.100.1.2.3", "psu-voltage"),
2936
+ createIntegerPDU("1.3.6.1.4.1.14988.1.1.3.100.1.4.3", 3),
2937
+ })
2938
+ },
2939
+ expectedResult: []ddsnmp.Metric{
2940
+ {
2941
+ Name: "mtxrHlSensorValue_temperature",
2942
+ Value: 45,
2943
+ Tags: map[string]string{"sensor_name": "cpu-temperature"},
2944
+ Unit: "celsius",
2945
+ Family: "Health/Temperature",
2946
+ MetricType: "gauge",
2947
+ IsTable: true,
2948
+ },
2949
+ {
2950
+ Name: "mtxrHlSensorValue_sensor_status",
2951
+ Value: 1,
2952
+ Tags: map[string]string{"sensor_name": "psu1-state"},
2953
+ Family: "Health/Status",
2954
+ Mappings: map[int64]string{
2955
+ 0: "not_ok",
2956
+ 1: "ok",
2957
+ },
2958
+ MetricType: "gauge",
2959
+ IsTable: true,
2960
+ },
2961
+ {
2962
+ Name: "mtxrHlSensorValue_voltage",
2963
+ Value: 24,
2964
+ Tags: map[string]string{"sensor_name": "psu-voltage"},
2965
+ Unit: "volts",
2966
+ Family: "Health/Power",
2967
+ MetricType: "gauge",
2968
+ IsTable: true,
2969
+ },
2970
+ },
2971
+ expectedError: false,
2972
+ },
2973
+ "table transformation with dynamic tags": {
2974
+ profile: &ddsnmp.Profile{
2975
+ SourceFile: "test-profile.yaml",
2976
+ Definition: &ddprofiledefinition.ProfileDefinition{
2977
+ Metrics: []ddprofiledefinition.MetricsConfig{
2978
+ {
2979
+ Table: ddprofiledefinition.SymbolConfig{
2980
+ OID: "1.3.6.1.2.1.2.2",
2981
+ Name: "ifTable",
2982
+ },
2983
+ Symbols: []ddprofiledefinition.SymbolConfig{
2984
+ {
2985
+ OID: "1.3.6.1.2.1.2.2.1.10",
2986
+ Name: "ifInOctets",
2987
+ Transform: `
2988
+{{- $ifName := index .Metric.Tags "interface" | lower -}}
2989
+{{- if contains "eth" $ifName -}}
2990
+ {{- setFamily .Metric "Interfaces/Ethernet" -}}
2991
+ {{- setTag .Metric "interface_type" "ethernet" -}}
2992
+{{- else if contains "lo" $ifName -}}
2993
+ {{- setFamily .Metric "Interfaces/Loopback" -}}
2994
+ {{- setTag .Metric "interface_type" "loopback" -}}
2995
+{{- else -}}
2996
+ {{- setFamily .Metric "Interfaces/Other" -}}
2997
+ {{- setTag .Metric "interface_type" "other" -}}
2998
+{{- end -}}
2999
+{{- setDesc .Metric (printf "Traffic on %s interface" (index .Metric.Tags "interface")) -}}`,
3000
+ },
3001
+ },
3002
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3003
+ {
3004
+ Tag: "interface",
3005
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3006
+ OID: "1.3.6.1.2.1.2.2.1.2",
3007
+ Name: "ifDescr",
3008
+ },
3009
+ },
3010
+ },
3011
+ },
3012
+ },
3013
+ },
3014
+ },
3015
+ setupMock: func(m *snmpmock.MockHandler) {
3016
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
3017
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000),
3018
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
3019
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.2", 2000),
3020
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "lo0"),
3021
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.3", 3000),
3022
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.3", "tun0"),
3023
+ })
3024
+ },
3025
+ expectedResult: []ddsnmp.Metric{
3026
+ {
3027
+ Name: "ifInOctets",
3028
+ Value: 1000,
3029
+ Tags: map[string]string{"interface": "eth0", "interface_type": "ethernet"},
3030
+ Family: "Interfaces/Ethernet",
3031
+ Description: "Traffic on eth0 interface",
3032
+ MetricType: "rate",
3033
+ IsTable: true,
3034
+ },
3035
+ {
3036
+ Name: "ifInOctets",
3037
+ Value: 2000,
3038
+ Tags: map[string]string{"interface": "lo0", "interface_type": "loopback"},
3039
+ Family: "Interfaces/Loopback",
3040
+ Description: "Traffic on lo0 interface",
3041
+ MetricType: "rate",
3042
+ IsTable: true,
3043
+ },
3044
+ {
3045
+ Name: "ifInOctets",
3046
+ Value: 3000,
3047
+ Tags: map[string]string{"interface": "tun0", "interface_type": "other"},
3048
+ Family: "Interfaces/Other",
3049
+ Description: "Traffic on tun0 interface",
3050
+ MetricType: "rate",
3051
+ IsTable: true,
3052
+ },
3053
+ },
3054
+ expectedError: false,
3055
+ },
3056
+ "table transformation with conditional metrics": {
3057
+ profile: &ddsnmp.Profile{
3058
+ SourceFile: "test-profile.yaml",
3059
+ Definition: &ddprofiledefinition.ProfileDefinition{
3060
+ Metrics: []ddprofiledefinition.MetricsConfig{
3061
+ {
3062
+ Table: ddprofiledefinition.SymbolConfig{
3063
+ OID: "1.3.6.1.2.1.2.2",
3064
+ Name: "ifTable",
3065
+ },
3066
+ Symbols: []ddprofiledefinition.SymbolConfig{
3067
+ {
3068
+ OID: "1.3.6.1.2.1.2.2.1.8",
3069
+ Name: "ifOperStatus",
3070
+ Transform: `
3071
+{{- $status := .Metric.Value -}}
3072
+{{- if eq $status 1 -}}
3073
+ {{- setValue .Metric 1 -}}
3074
+{{- else -}}
3075
+ {{- setValue .Metric 0 -}}
3076
+{{- end -}}
3077
+{{- setMappings .Metric (i64map 0 "down" 1 "up") -}}
3078
+{{- setName .Metric "interface_status" -}}`,
3079
+ },
3080
+ },
3081
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3082
+ {
3083
+ Tag: "interface",
3084
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3085
+ OID: "1.3.6.1.2.1.2.2.1.2",
3086
+ Name: "ifDescr",
3087
+ },
3088
+ },
3089
+ },
3090
+ },
3091
+ },
3092
+ },
3093
+ },
3094
+ setupMock: func(m *snmpmock.MockHandler) {
3095
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
3096
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.8.1", 1), // up
3097
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.1", "eth0"),
3098
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.8.2", 2), // down
3099
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.2", "eth1"),
3100
+ createIntegerPDU("1.3.6.1.2.1.2.2.1.8.3", 3), // testing
3101
+ createStringPDU("1.3.6.1.2.1.2.2.1.2.3", "eth2"),
3102
+ })
3103
+ },
3104
+ expectedResult: []ddsnmp.Metric{
3105
+ {
3106
+ Name: "interface_status",
3107
+ Value: 1,
3108
+ Tags: map[string]string{"interface": "eth0"},
3109
+ Mappings: map[int64]string{
3110
+ 0: "down",
3111
+ 1: "up",
3112
+ },
3113
+ MetricType: "gauge",
3114
+ IsTable: true,
3115
+ },
3116
+ {
3117
+ Name: "interface_status",
3118
+ Value: 0,
3119
+ Tags: map[string]string{"interface": "eth1"},
3120
+ Mappings: map[int64]string{
3121
+ 0: "down",
3122
+ 1: "up",
3123
+ },
3124
+ MetricType: "gauge",
3125
+ IsTable: true,
3126
+ },
3127
+ {
3128
+ Name: "interface_status",
3129
+ Value: 0,
3130
+ Tags: map[string]string{"interface": "eth2"},
3131
+ Mappings: map[int64]string{
3132
+ 0: "down",
3133
+ 1: "up",
3134
+ },
3135
+ MetricType: "gauge",
3136
+ IsTable: true,
3137
+ },
3138
+ },
3139
+ expectedError: false,
3140
+ },
3141
+ "table transformation with sprig string functions": {
3142
+ profile: &ddsnmp.Profile{
3143
+ SourceFile: "test-profile.yaml",
3144
+ Definition: &ddprofiledefinition.ProfileDefinition{
3145
+ Metrics: []ddprofiledefinition.MetricsConfig{
3146
+ {
3147
+ Table: ddprofiledefinition.SymbolConfig{
3148
+ OID: "1.3.6.1.2.1.25.2.3",
3149
+ Name: "hrStorageTable",
3150
+ },
3151
+ Symbols: []ddprofiledefinition.SymbolConfig{
3152
+ {
3153
+ OID: "1.3.6.1.2.1.25.2.3.1.5",
3154
+ Name: "hrStorageSize",
3155
+ Transform: `
3156
+{{- $descr := index .Metric.Tags "storage_descr" -}}
3157
+{{- if hasPrefix "/" $descr -}}
3158
+ {{- setFamily .Metric "Storage/FileSystem" -}}
3159
+ {{- setTag .Metric "mount_point" $descr -}}
3160
+ {{- setName .Metric "filesystem_size" -}}
3161
+{{- else if contains "Memory" $descr -}}
3162
+ {{- setFamily .Metric "Storage/Memory" -}}
3163
+ {{- setName .Metric "memory_size" -}}
3164
+{{- else -}}
3165
+ {{- setFamily .Metric "Storage/Other" -}}
3166
+{{- end -}}`,
3167
+ },
3168
+ },
3169
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3170
+ {
3171
+ Tag: "storage_descr",
3172
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3173
+ OID: "1.3.6.1.2.1.25.2.3.1.3",
3174
+ Name: "hrStorageDescr",
3175
+ },
3176
+ },
3177
+ },
3178
+ },
3179
+ },
3180
+ },
3181
+ },
3182
+ setupMock: func(m *snmpmock.MockHandler) {
3183
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.25.2.3", []gosnmp.SnmpPDU{
3184
+ createGauge32PDU("1.3.6.1.2.1.25.2.3.1.5.1", 1000000),
3185
+ createStringPDU("1.3.6.1.2.1.25.2.3.1.3.1", "/"),
3186
+ createGauge32PDU("1.3.6.1.2.1.25.2.3.1.5.2", 500000),
3187
+ createStringPDU("1.3.6.1.2.1.25.2.3.1.3.2", "/var"),
3188
+ createGauge32PDU("1.3.6.1.2.1.25.2.3.1.5.3", 2000000),
3189
+ createStringPDU("1.3.6.1.2.1.25.2.3.1.3.3", "Physical Memory"),
3190
+ createGauge32PDU("1.3.6.1.2.1.25.2.3.1.5.4", 100000),
3191
+ createStringPDU("1.3.6.1.2.1.25.2.3.1.3.4", "Swap Space"),
3192
+ })
3193
+ },
3194
+ expectedResult: []ddsnmp.Metric{
3195
+ {
3196
+ Name: "filesystem_size",
3197
+ Value: 1000000,
3198
+ Tags: map[string]string{"storage_descr": "/", "mount_point": "/"},
3199
+ Family: "Storage/FileSystem",
3200
+ MetricType: "gauge",
3201
+ IsTable: true,
3202
+ },
3203
+ {
3204
+ Name: "filesystem_size",
3205
+ Value: 500000,
3206
+ Tags: map[string]string{"storage_descr": "/var", "mount_point": "/var"},
3207
+ Family: "Storage/FileSystem",
3208
+ MetricType: "gauge",
3209
+ IsTable: true,
3210
+ },
3211
+ {
3212
+ Name: "memory_size",
3213
+ Value: 2000000,
3214
+ Tags: map[string]string{"storage_descr": "Physical Memory"},
3215
+ Family: "Storage/Memory",
3216
+ MetricType: "gauge",
3217
+ IsTable: true,
3218
+ },
3219
+ {
3220
+ Name: "hrStorageSize",
3221
+ Value: 100000,
3222
+ Tags: map[string]string{"storage_descr": "Swap Space"},
3223
+ Family: "Storage/Other",
3224
+ MetricType: "gauge",
3225
+ IsTable: true,
3226
+ },
3227
+ },
3228
+ expectedError: false,
3229
+ },
3230
+ }
3231
+
3232
+ for name, tc := range tests {
3233
+ t.Run(name, func(t *testing.T) {
3234
+ ctrl, mockHandler := setupMockHandler(t)
3235
+ defer ctrl.Finish()
3236
+
3237
+ tc.setupMock(mockHandler)
3238
+
3239
+ if err := ddsnmp.CompileTransforms(tc.profile); err != nil {
3240
+ if tc.expectedError && tc.errorContains != "" && strings.Contains(err.Error(), tc.errorContains) {
3241
+ return // Expected error during compilation
3242
+ }
3243
+ t.Fatalf("Failed to compile transforms: %v", err)
3244
+ }
3245
+
3246
+ missingOIDs := make(map[string]bool)
3247
+ tableCache := newTableCache(0, 0) // Cache disabled
3248
+ collector := newTableCollector(mockHandler, missingOIDs, tableCache, logger.New())
3249
+
3250
+ result, err := collector.Collect(tc.profile)
3251
+
3252
+ if tc.expectedError {
3253
+ assert.Error(t, err)
3254
+ if tc.errorContains != "" {
3255
+ assert.Contains(t, err.Error(), tc.errorContains)
3256
+ }
3257
+ } else {
3258
+ assert.NoError(t, err)
3259
+ }
3260
+
3261
+ assertTableMetricsEqual(t, tc.expectedResult, result)
3262
+
3263
+ // Check missing OIDs if specified
3264
+ if tc.checkMissing != nil {
3265
+ for oid, shouldBeMissing := range tc.checkMissing {
3266
+ assert.Equal(t, shouldBeMissing, missingOIDs[oid], "OID %s missing status", oid)
3267
+ }
3268
+ }
3269
+ })
3270
+ }
3271
+}
3272
+
3273
+func TestCollector_Collect_TableCaching(t *testing.T) {
3274
+ tests := map[string]struct {
3275
+ profiles []*ddsnmp.Profile
3276
+ setupMock func(m *snmpmock.MockHandler)
3277
+ expectedResult []*ddsnmp.ProfileMetrics
3278
+ expectedError bool
3279
+ errorContains string
3280
+ enableCache bool
3281
+ cacheTTL time.Duration
3282
+ collectCount int // Number of times to call Collect()
3283
+ sleepBetween time.Duration
3284
+ }{
3285
+ "table cache basic operation": {
3286
+ profiles: []*ddsnmp.Profile{
3287
+ {
3288
+ SourceFile: "test-profile.yaml",
3289
+ Definition: &ddprofiledefinition.ProfileDefinition{
3290
+ Metrics: []ddprofiledefinition.MetricsConfig{
3291
+ {
3292
+ Table: ddprofiledefinition.SymbolConfig{
3293
+ OID: "1.3.6.1.2.1.2.2",
3294
+ Name: "ifTable",
3295
+ },
3296
+ Symbols: []ddprofiledefinition.SymbolConfig{
3297
+ {
3298
+ OID: "1.3.6.1.2.1.2.2.1.10",
3299
+ Name: "ifInOctets",
3300
+ },
3301
+ {
3302
+ OID: "1.3.6.1.2.1.2.2.1.16",
3303
+ Name: "ifOutOctets",
3304
+ },
3305
+ },
3306
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3307
+ {
3308
+ Tag: "interface",
3309
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3310
+ OID: "1.3.6.1.2.1.2.2.1.2",
3311
+ Name: "ifDescr",
3312
+ },
3313
+ },
3314
+ },
3315
+ },
3316
+ },
3317
+ },
3318
+ },
3319
+ },
3320
+ setupMock: func(m *snmpmock.MockHandler) {
3321
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3322
+
3323
+ // First collection: Full walk
3324
+ m.EXPECT().Version().Return(gosnmp.Version2c).Times(1)
3325
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3326
+ []gosnmp.SnmpPDU{
3327
+ {
3328
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
3329
+ Type: gosnmp.Counter32,
3330
+ Value: uint(1000),
3331
+ },
3332
+ {
3333
+ Name: "1.3.6.1.2.1.2.2.1.16.1",
3334
+ Type: gosnmp.Counter32,
3335
+ Value: uint(500),
3336
+ },
3337
+ {
3338
+ Name: "1.3.6.1.2.1.2.2.1.2.1",
3339
+ Type: gosnmp.OctetString,
3340
+ Value: []byte("eth0"),
3341
+ },
3342
+ {
3343
+ Name: "1.3.6.1.2.1.2.2.1.10.2",
3344
+ Type: gosnmp.Counter32,
3345
+ Value: uint(2000),
3346
+ },
3347
+ {
3348
+ Name: "1.3.6.1.2.1.2.2.1.16.2",
3349
+ Type: gosnmp.Counter32,
3350
+ Value: uint(1500),
3351
+ },
3352
+ {
3353
+ Name: "1.3.6.1.2.1.2.2.1.2.2",
3354
+ Type: gosnmp.OctetString,
3355
+ Value: []byte("eth1"),
3356
+ },
3357
+ }, nil,
3358
+ ).Times(1)
3359
+
3360
+ // Second collection: Only GET metrics (not tags)
3361
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
3362
+ "1.3.6.1.2.1.2.2.1.10.1",
3363
+ "1.3.6.1.2.1.2.2.1.16.1",
3364
+ "1.3.6.1.2.1.2.2.1.10.2",
3365
+ "1.3.6.1.2.1.2.2.1.16.2",
3366
+ })).Return(
3367
+ &gosnmp.SnmpPacket{
3368
+ Variables: []gosnmp.SnmpPDU{
3369
+ {
3370
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
3371
+ Type: gosnmp.Counter32,
3372
+ Value: uint(1100), // Values changed
3373
+ },
3374
+ {
3375
+ Name: "1.3.6.1.2.1.2.2.1.16.1",
3376
+ Type: gosnmp.Counter32,
3377
+ Value: uint(600),
3378
+ },
3379
+ {
3380
+ Name: "1.3.6.1.2.1.2.2.1.10.2",
3381
+ Type: gosnmp.Counter32,
3382
+ Value: uint(2200),
3383
+ },
3384
+ {
3385
+ Name: "1.3.6.1.2.1.2.2.1.16.2",
3386
+ Type: gosnmp.Counter32,
3387
+ Value: uint(1700),
3388
+ },
3389
+ },
3390
+ }, nil,
3391
+ ).Times(1)
3392
+
3393
+ // Third collection: Still using cache
3394
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
3395
+ "1.3.6.1.2.1.2.2.1.10.1",
3396
+ "1.3.6.1.2.1.2.2.1.16.1",
3397
+ "1.3.6.1.2.1.2.2.1.10.2",
3398
+ "1.3.6.1.2.1.2.2.1.16.2",
3399
+ })).Return(
3400
+ &gosnmp.SnmpPacket{
3401
+ Variables: []gosnmp.SnmpPDU{
3402
+ {
3403
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
3404
+ Type: gosnmp.Counter32,
3405
+ Value: uint(1200),
3406
+ },
3407
+ {
3408
+ Name: "1.3.6.1.2.1.2.2.1.16.1",
3409
+ Type: gosnmp.Counter32,
3410
+ Value: uint(700),
3411
+ },
3412
+ {
3413
+ Name: "1.3.6.1.2.1.2.2.1.10.2",
3414
+ Type: gosnmp.Counter32,
3415
+ Value: uint(2400),
3416
+ },
3417
+ {
3418
+ Name: "1.3.6.1.2.1.2.2.1.16.2",
3419
+ Type: gosnmp.Counter32,
3420
+ Value: uint(1900),
3421
+ },
3422
+ },
3423
+ }, nil,
3424
+ ).Times(1)
3425
+ },
3426
+ expectedResult: []*ddsnmp.ProfileMetrics{
3427
+ {
3428
+ Source: "test-profile.yaml",
3429
+ DeviceMetadata: nil,
3430
+ Metrics: []ddsnmp.Metric{
3431
+ {
3432
+ Name: "ifInOctets",
3433
+ Value: 1200, // Latest value
3434
+ Tags: map[string]string{"interface": "eth0"},
3435
+ MetricType: "rate",
3436
+ IsTable: true,
3437
+ },
3438
+ {
3439
+ Name: "ifOutOctets",
3440
+ Value: 700,
3441
+ Tags: map[string]string{"interface": "eth0"},
3442
+ MetricType: "rate",
3443
+ IsTable: true,
3444
+ },
3445
+ {
3446
+ Name: "ifInOctets",
3447
+ Value: 2400,
3448
+ Tags: map[string]string{"interface": "eth1"},
3449
+ MetricType: "rate",
3450
+ IsTable: true,
3451
+ },
3452
+ {
3453
+ Name: "ifOutOctets",
3454
+ Value: 1900,
3455
+ Tags: map[string]string{"interface": "eth1"},
3456
+ MetricType: "rate",
3457
+ IsTable: true,
3458
+ },
3459
+ },
3460
+ },
3461
+ },
3462
+ expectedError: false,
3463
+ enableCache: true,
3464
+ cacheTTL: 30 * time.Second,
3465
+ collectCount: 3,
3466
+ sleepBetween: 10 * time.Millisecond,
3467
+ },
3468
+ "table cache expiration": {
3469
+ profiles: []*ddsnmp.Profile{
3470
+ {
3471
+ SourceFile: "test-profile.yaml",
3472
+ Definition: &ddprofiledefinition.ProfileDefinition{
3473
+ Metrics: []ddprofiledefinition.MetricsConfig{
3474
+ {
3475
+ Table: ddprofiledefinition.SymbolConfig{
3476
+ OID: "1.3.6.1.2.1.2.2",
3477
+ Name: "ifTable",
3478
+ },
3479
+ Symbols: []ddprofiledefinition.SymbolConfig{
3480
+ {
3481
+ OID: "1.3.6.1.2.1.2.2.1.10",
3482
+ Name: "ifInOctets",
3483
+ },
3484
+ },
3485
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3486
+ {
3487
+ Tag: "interface",
3488
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3489
+ OID: "1.3.6.1.2.1.2.2.1.2",
3490
+ Name: "ifDescr",
3491
+ },
3492
+ },
3493
+ },
3494
+ },
3495
+ },
3496
+ },
3497
+ },
3498
+ },
3499
+ setupMock: func(m *snmpmock.MockHandler) {
3500
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3501
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
3502
+
3503
+ // First collection: Full walk
3504
+ gomock.InOrder(
3505
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3506
+ []gosnmp.SnmpPDU{
3507
+ {
3508
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
3509
+ Type: gosnmp.Counter32,
3510
+ Value: uint(1000),
3511
+ },
3512
+ {
3513
+ Name: "1.3.6.1.2.1.2.2.1.2.1",
3514
+ Type: gosnmp.OctetString,
3515
+ Value: []byte("eth0"),
3516
+ },
3517
+ }, nil,
3518
+ ),
3519
+
3520
+ // Second collection: Using cache
3521
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.10.1"}).Return(
3522
+ &gosnmp.SnmpPacket{
3523
+ Variables: []gosnmp.SnmpPDU{
3524
+ {
3525
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
3526
+ Type: gosnmp.Counter32,
3527
+ Value: uint(1100),
3528
+ },
3529
+ },
3530
+ }, nil,
3531
+ ),
3532
+
3533
+ // Third collection: After expiration, full walk again
3534
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3535
+ []gosnmp.SnmpPDU{
3536
+ {
3537
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
3538
+ Type: gosnmp.Counter32,
3539
+ Value: uint(1200),
3540
+ },
3541
+ {
3542
+ Name: "1.3.6.1.2.1.2.2.1.2.1",
3543
+ Type: gosnmp.OctetString,
3544
+ Value: []byte("eth0"),
3545
+ },
3546
+ {
3547
+ Name: "1.3.6.1.2.1.2.2.1.10.2",
3548
+ Type: gosnmp.Counter32,
3549
+ Value: uint(2000),
3550
+ },
3551
+ {
3552
+ Name: "1.3.6.1.2.1.2.2.1.2.2",
3553
+ Type: gosnmp.OctetString,
3554
+ Value: []byte("eth1"),
3555
+ },
3556
+ }, nil,
3557
+ ),
3558
+ )
3559
+ },
3560
+ expectedResult: []*ddsnmp.ProfileMetrics{
3561
+ {
3562
+ Source: "test-profile.yaml",
3563
+ DeviceMetadata: nil,
3564
+ Metrics: []ddsnmp.Metric{
3565
+ {
3566
+ Name: "ifInOctets",
3567
+ Value: 1200,
3568
+ Tags: map[string]string{"interface": "eth0"},
3569
+ MetricType: "rate",
3570
+ IsTable: true,
3571
+ },
3572
+ {
3573
+ Name: "ifInOctets",
3574
+ Value: 2000,
3575
+ Tags: map[string]string{"interface": "eth1"},
3576
+ MetricType: "rate",
3577
+ IsTable: true,
3578
+ },
3579
+ },
3580
+ },
3581
+ },
3582
+ expectedError: false,
3583
+ enableCache: true,
3584
+ cacheTTL: 100 * time.Millisecond,
3585
+ collectCount: 3,
3586
+ sleepBetween: 60 * time.Millisecond, // Sleep less than TTL for second collection, but total > TTL for third
3587
+ },
3588
+ "table cache with multiple configs same table": {
3589
+ profiles: []*ddsnmp.Profile{
3590
+ {
3591
+ SourceFile: "test-profile.yaml",
3592
+ Definition: &ddprofiledefinition.ProfileDefinition{
3593
+ Metrics: []ddprofiledefinition.MetricsConfig{
3594
+ {
3595
+ Table: ddprofiledefinition.SymbolConfig{
3596
+ OID: "1.3.6.1.2.1.2.2",
3597
+ Name: "ifTable",
3598
+ },
3599
+ Symbols: []ddprofiledefinition.SymbolConfig{
3600
+ {
3601
+ OID: "1.3.6.1.2.1.2.2.1.10",
3602
+ Name: "ifInOctets",
3603
+ },
3604
+ {
3605
+ OID: "1.3.6.1.2.1.2.2.1.16",
3606
+ Name: "ifOutOctets",
3607
+ },
3608
+ },
3609
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3610
+ {
3611
+ Tag: "interface",
3612
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3613
+ OID: "1.3.6.1.2.1.2.2.1.2",
3614
+ Name: "ifDescr",
3615
+ },
3616
+ },
3617
+ },
3618
+ },
3619
+ {
3620
+ Table: ddprofiledefinition.SymbolConfig{
3621
+ OID: "1.3.6.1.2.1.2.2",
3622
+ Name: "ifTable",
3623
+ },
3624
+ Symbols: []ddprofiledefinition.SymbolConfig{
3625
+ {
3626
+ OID: "1.3.6.1.2.1.2.2.1.14",
3627
+ Name: "ifInErrors",
3628
+ },
3629
+ {
3630
+ OID: "1.3.6.1.2.1.2.2.1.20",
3631
+ Name: "ifOutErrors",
3632
+ },
3633
+ },
3634
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3635
+ {
3636
+ Tag: "interface",
3637
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3638
+ OID: "1.3.6.1.2.1.2.2.1.2",
3639
+ Name: "ifDescr",
3640
+ },
3641
+ },
3642
+ },
3643
+ },
3644
+ },
3645
+ },
3646
+ },
3647
+ },
3648
+ setupMock: func(m *snmpmock.MockHandler) {
3649
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3650
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
3651
+
3652
+ // First collection: Walk table once
3653
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3654
+ []gosnmp.SnmpPDU{
3655
+ // Interface 1
3656
+ {
3657
+ Name: "1.3.6.1.2.1.2.2.1.2.1",
3658
+ Type: gosnmp.OctetString,
3659
+ Value: []byte("eth0"),
3660
+ },
3661
+ {
3662
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
3663
+ Type: gosnmp.Counter32,
3664
+ Value: uint(1000),
3665
+ },
3666
+ {
3667
+ Name: "1.3.6.1.2.1.2.2.1.16.1",
3668
+ Type: gosnmp.Counter32,
3669
+ Value: uint(500),
3670
+ },
3671
+ {
3672
+ Name: "1.3.6.1.2.1.2.2.1.14.1",
3673
+ Type: gosnmp.Counter32,
3674
+ Value: uint(10),
3675
+ },
3676
+ {
3677
+ Name: "1.3.6.1.2.1.2.2.1.20.1",
3678
+ Type: gosnmp.Counter32,
3679
+ Value: uint(5),
3680
+ },
3681
+ }, nil,
3682
+ ).Times(1)
3683
+
3684
+ // Second collection: Each config uses cache separately
3685
+ // First config GET
3686
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
3687
+ "1.3.6.1.2.1.2.2.1.10.1",
3688
+ "1.3.6.1.2.1.2.2.1.16.1",
3689
+ })).Return(
3690
+ &gosnmp.SnmpPacket{
3691
+ Variables: []gosnmp.SnmpPDU{
3692
+ {
3693
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
3694
+ Type: gosnmp.Counter32,
3695
+ Value: uint(1100),
3696
+ },
3697
+ {
3698
+ Name: "1.3.6.1.2.1.2.2.1.16.1",
3699
+ Type: gosnmp.Counter32,
3700
+ Value: uint(600),
3701
+ },
3702
+ },
3703
+ }, nil,
3704
+ ).Times(1)
3705
+
3706
+ // Second config GET
3707
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
3708
+ "1.3.6.1.2.1.2.2.1.14.1",
3709
+ "1.3.6.1.2.1.2.2.1.20.1",
3710
+ })).Return(
3711
+ &gosnmp.SnmpPacket{
3712
+ Variables: []gosnmp.SnmpPDU{
3713
+ {
3714
+ Name: "1.3.6.1.2.1.2.2.1.14.1",
3715
+ Type: gosnmp.Counter32,
3716
+ Value: uint(12),
3717
+ },
3718
+ {
3719
+ Name: "1.3.6.1.2.1.2.2.1.20.1",
3720
+ Type: gosnmp.Counter32,
3721
+ Value: uint(6),
3722
+ },
3723
+ },
3724
+ }, nil,
3725
+ ).Times(1)
3726
+ },
3727
+ expectedResult: []*ddsnmp.ProfileMetrics{
3728
+ {
3729
+ Source: "test-profile.yaml",
3730
+ DeviceMetadata: nil,
3731
+ Metrics: []ddsnmp.Metric{
3732
+ // From first config
3733
+ {
3734
+ Name: "ifInOctets",
3735
+ Value: 1100,
3736
+ Tags: map[string]string{"interface": "eth0"},
3737
+ MetricType: "rate",
3738
+ IsTable: true,
3739
+ },
3740
+ {
3741
+ Name: "ifOutOctets",
3742
+ Value: 600,
3743
+ Tags: map[string]string{"interface": "eth0"},
3744
+ MetricType: "rate",
3745
+ IsTable: true,
3746
+ },
3747
+ // From second config
3748
+ {
3749
+ Name: "ifInErrors",
3750
+ Value: 12,
3751
+ Tags: map[string]string{"interface": "eth0"},
3752
+ MetricType: "rate",
3753
+ IsTable: true,
3754
+ },
3755
+ {
3756
+ Name: "ifOutErrors",
3757
+ Value: 6,
3758
+ Tags: map[string]string{"interface": "eth0"},
3759
+ MetricType: "rate",
3760
+ IsTable: true,
3761
+ },
3762
+ },
3763
+ },
3764
+ },
3765
+ expectedError: false,
3766
+ enableCache: true,
3767
+ cacheTTL: 30 * time.Second,
3768
+ collectCount: 2,
3769
+ sleepBetween: 10 * time.Millisecond,
3770
+ },
3771
+ }
3772
+
3773
+ for name, tc := range tests {
3774
+ t.Run(name, func(t *testing.T) {
3775
+ ctrl := gomock.NewController(t)
3776
+ defer ctrl.Finish()
3777
+
3778
+ mockHandler := snmpmock.NewMockHandler(ctrl)
3779
+ tc.setupMock(mockHandler)
3780
+
3781
+ collector := New(mockHandler, tc.profiles, logger.New())
3782
+ collector.DoTableMetrics = true
3783
+
3784
+ // Configure cache based on test requirements
3785
+ if tc.enableCache {
3786
+ collector.tableCache.setTTL(tc.cacheTTL, 0)
3787
+ } else {
3788
+ collector.tableCache.setTTL(0, 0) // Disable cache
3789
+ }
3790
+
3791
+ var result []*ddsnmp.ProfileMetrics
3792
+ var err error
3793
+
3794
+ // Perform multiple collections to test caching behavior
3795
+ for i := 0; i < tc.collectCount; i++ {
3796
+ if i > 0 && tc.sleepBetween > 0 {
3797
+ time.Sleep(tc.sleepBetween)
3798
+ }
3799
+
3800
+ result, err = collector.Collect()
3801
+
3802
+ // For intermediate collections, just verify no error
3803
+ if i < tc.collectCount-1 {
3804
+ assert.NoError(t, err)
3805
+ }
3806
+ }
3807
+
3808
+ // Clear circular references in final result
3809
+ for _, profile := range result {
3810
+ for i := range profile.Metrics {
3811
+ profile.Metrics[i].Profile = nil
3812
+ }
3813
+ }
3814
+
3815
+ if tc.expectedError {
3816
+ assert.Error(t, err)
3817
+ if tc.errorContains != "" {
3818
+ assert.Contains(t, err.Error(), tc.errorContains)
3819
+ }
3820
+ } else {
3821
+ assert.NoError(t, err)
3822
+ }
3823
+
3824
+ if tc.expectedResult != nil {
3825
+ require.Equal(t, len(tc.expectedResult), len(result))
3826
+ for i := range tc.expectedResult {
3827
+ assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
3828
+ assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
3829
+ assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
3830
+ }
3831
+ } else {
3832
+ assert.Nil(t, result)
3833
+ }
3834
+ })
3835
+ }
3836
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
deleted
-5046
@@ -1,5046 +0,0 @@
1
-// SPDX-License-Identifier: GPL-3.0-or-later
2
-
3
-package ddsnmpcollector
4
-
5
-import (
6
- "errors"
7
- "regexp"
8
- "strings"
9
- "testing"
10
- "time"
11
-
12
- "github.com/golang/mock/gomock"
13
- "github.com/gosnmp/gosnmp"
14
- "github.com/stretchr/testify/assert"
15
- "github.com/stretchr/testify/require"
16
-
17
- snmpmock "github.com/gosnmp/gosnmp/mocks"
18
-
19
- "github.com/netdata/netdata/go/plugins/logger"
20
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
21
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
22
-)
23
-
24
-func TestCollector_Collect_ScalarMetrics(t *testing.T) {
25
- tests := map[string]struct {
26
- profiles []*ddsnmp.Profile
27
- setupMock func(m *snmpmock.MockHandler)
28
- expectedResult []*ddsnmp.ProfileMetrics
29
- expectedError bool
30
- errorContains string
31
- enableCache bool // Whether to enable cache for this test
32
- }{
33
- "successful collection with scalar metrics only": {
34
- profiles: []*ddsnmp.Profile{
35
- {
36
- SourceFile: "test-profile.yaml",
37
- Definition: &ddprofiledefinition.ProfileDefinition{
38
- Metrics: []ddprofiledefinition.MetricsConfig{
39
- {
40
- Symbol: ddprofiledefinition.SymbolConfig{
41
- OID: "1.3.6.1.2.1.1.3.0",
42
- Name: "sysUpTime",
43
- },
44
- },
45
- {
46
- Symbol: ddprofiledefinition.SymbolConfig{
47
- OID: "1.3.6.1.2.1.1.5.0",
48
- Name: "sysName",
49
- },
50
- },
51
- },
52
- },
53
- },
54
- },
55
- setupMock: func(m *snmpmock.MockHandler) {
56
- m.EXPECT().MaxOids().Return(10).AnyTimes()
57
- 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(
58
- &gosnmp.SnmpPacket{
59
- Variables: []gosnmp.SnmpPDU{
60
- {
61
- Name: "1.3.6.1.2.1.1.3.0",
62
- Type: gosnmp.TimeTicks,
63
- Value: uint32(123456),
64
- },
65
- {
66
- Name: "1.3.6.1.2.1.1.5.0",
67
- Type: gosnmp.OctetString,
68
- Value: []byte("test-host"),
69
- },
70
- },
71
- }, nil,
72
- )
73
- },
74
- expectedResult: []*ddsnmp.ProfileMetrics{
75
- {
76
- Source: "test-profile.yaml",
77
- DeviceMetadata: nil,
78
- Metrics: []ddsnmp.Metric{
79
- {
80
- Name: "sysUpTime",
81
- Value: 123456,
82
- MetricType: "gauge",
83
- },
84
- // sysName will be skipped because it can't be converted to int64
85
- },
86
- },
87
- },
88
- expectedError: false,
89
- },
90
- "successful collection with global tags": {
91
- profiles: []*ddsnmp.Profile{
92
- {
93
- SourceFile: "test-profile.yaml",
94
- Definition: &ddprofiledefinition.ProfileDefinition{
95
- MetricTags: []ddprofiledefinition.MetricTagConfig{
96
- {
97
- Tag: "device_vendor",
98
- Symbol: ddprofiledefinition.SymbolConfigCompat{
99
- OID: "1.3.6.1.2.1.1.1.0",
100
- Name: "sysDescr",
101
- },
102
- },
103
- },
104
- Metrics: []ddprofiledefinition.MetricsConfig{
105
- {
106
- Symbol: ddprofiledefinition.SymbolConfig{
107
- OID: "1.3.6.1.2.1.1.3.0",
108
- Name: "sysUpTime",
109
- },
110
- },
111
- },
112
- },
113
- },
114
- },
115
- setupMock: func(m *snmpmock.MockHandler) {
116
- m.EXPECT().MaxOids().Return(10).AnyTimes()
117
- // First call for global tags
118
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
119
- &gosnmp.SnmpPacket{
120
- Variables: []gosnmp.SnmpPDU{
121
- {
122
- Name: "1.3.6.1.2.1.1.1.0",
123
- Type: gosnmp.OctetString,
124
- Value: []byte("Cisco IOS"),
125
- },
126
- },
127
- }, nil,
128
- )
129
- // Second call for metrics
130
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
131
- &gosnmp.SnmpPacket{
132
- Variables: []gosnmp.SnmpPDU{
133
- {
134
- Name: "1.3.6.1.2.1.1.3.0",
135
- Type: gosnmp.TimeTicks,
136
- Value: uint32(123456),
137
- },
138
- },
139
- }, nil,
140
- )
141
- },
142
- expectedResult: []*ddsnmp.ProfileMetrics{
143
- {
144
- Source: "test-profile.yaml",
145
- DeviceMetadata: nil,
146
- Tags: map[string]string{"device_vendor": "Cisco IOS"},
147
- Metrics: []ddsnmp.Metric{
148
- {
149
- Name: "sysUpTime",
150
- Value: 123456,
151
- MetricType: "gauge",
152
- },
153
- },
154
- },
155
- },
156
- expectedError: false,
157
- },
158
- "successful collection with device metadata": {
159
- profiles: []*ddsnmp.Profile{
160
- {
161
- SourceFile: "test-profile.yaml",
162
- Definition: &ddprofiledefinition.ProfileDefinition{
163
- Metadata: ddprofiledefinition.MetadataConfig{
164
- "device": ddprofiledefinition.MetadataResourceConfig{
165
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
166
- "vendor": {
167
- Value: "dell",
168
- },
169
- "serial_number": {
170
- Symbol: ddprofiledefinition.SymbolConfig{
171
- OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
172
- Name: "chassisSerialNumber",
173
- },
174
- },
175
- },
176
- },
177
- },
178
- Metrics: []ddprofiledefinition.MetricsConfig{
179
- {
180
- Symbol: ddprofiledefinition.SymbolConfig{
181
- OID: "1.3.6.1.2.1.1.3.0",
182
- Name: "sysUpTime",
183
- },
184
- },
185
- },
186
- },
187
- },
188
- },
189
- setupMock: func(m *snmpmock.MockHandler) {
190
- m.EXPECT().MaxOids().Return(10).AnyTimes()
191
- // First call for device metadata
192
- m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.1.3.2.0"}).Return(
193
- &gosnmp.SnmpPacket{
194
- Variables: []gosnmp.SnmpPDU{
195
- {
196
- Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
197
- Type: gosnmp.OctetString,
198
- Value: []byte("ABC123"),
199
- },
200
- },
201
- }, nil,
202
- )
203
- // Second call for metrics
204
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
205
- &gosnmp.SnmpPacket{
206
- Variables: []gosnmp.SnmpPDU{
207
- {
208
- Name: "1.3.6.1.2.1.1.3.0",
209
- Type: gosnmp.TimeTicks,
210
- Value: uint32(123456),
211
- },
212
- },
213
- }, nil,
214
- )
215
- },
216
- expectedResult: []*ddsnmp.ProfileMetrics{
217
- {
218
- Source: "test-profile.yaml",
219
- DeviceMetadata: map[string]string{
220
- "vendor": "dell",
221
- "serial_number": "ABC123",
222
- },
223
- Metrics: []ddsnmp.Metric{
224
- {
225
- Name: "sysUpTime",
226
- Value: 123456,
227
- MetricType: "gauge",
228
- },
229
- },
230
- },
231
- },
232
- expectedError: false,
233
- },
234
- "metric with scale factor": {
235
- profiles: []*ddsnmp.Profile{
236
- {
237
- SourceFile: "test-profile.yaml",
238
- Definition: &ddprofiledefinition.ProfileDefinition{
239
- Metrics: []ddprofiledefinition.MetricsConfig{
240
- {
241
- Symbol: ddprofiledefinition.SymbolConfig{
242
- OID: "1.3.6.1.4.1.12124.1.1.2",
243
- Name: "memoryKilobytes",
244
- ScaleFactor: 1000, // Convert KB to bytes
245
- },
246
- },
247
- },
248
- },
249
- },
250
- },
251
- setupMock: func(m *snmpmock.MockHandler) {
252
- m.EXPECT().MaxOids().Return(10).AnyTimes()
253
- m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.2"}).Return(
254
- &gosnmp.SnmpPacket{
255
- Variables: []gosnmp.SnmpPDU{
256
- {
257
- Name: "1.3.6.1.4.1.12124.1.1.2",
258
- Type: gosnmp.Gauge32,
259
- Value: uint(1024),
260
- },
261
- },
262
- }, nil,
263
- )
264
- },
265
- expectedResult: []*ddsnmp.ProfileMetrics{
266
- {
267
- Source: "test-profile.yaml",
268
- DeviceMetadata: nil,
269
- Metrics: []ddsnmp.Metric{
270
- {
271
- Name: "memoryKilobytes",
272
- Value: 1024000, // 1024 * 1000
273
- MetricType: "gauge",
274
- },
275
- },
276
- },
277
- },
278
- expectedError: false,
279
- },
280
- "metric with extract_value": {
281
- profiles: []*ddsnmp.Profile{
282
- {
283
- SourceFile: "test-profile.yaml",
284
- Definition: &ddprofiledefinition.ProfileDefinition{
285
- Metrics: []ddprofiledefinition.MetricsConfig{
286
- {
287
- Symbol: ddprofiledefinition.SymbolConfig{
288
- OID: "1.3.6.1.4.1.12124.1.1.8",
289
- Name: "temperature",
290
- ExtractValueCompiled: mustCompileRegex(`(\d+)C`),
291
- },
292
- },
293
- },
294
- },
295
- },
296
- },
297
- setupMock: func(m *snmpmock.MockHandler) {
298
- m.EXPECT().MaxOids().Return(10).AnyTimes()
299
- m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.8"}).Return(
300
- &gosnmp.SnmpPacket{
301
- Variables: []gosnmp.SnmpPDU{
302
- {
303
- Name: "1.3.6.1.4.1.12124.1.1.8",
304
- Type: gosnmp.OctetString,
305
- Value: []byte("25C"),
306
- },
307
- },
308
- }, nil,
309
- )
310
- },
311
- expectedResult: []*ddsnmp.ProfileMetrics{
312
- {
313
- Source: "test-profile.yaml",
314
- DeviceMetadata: nil,
315
- Metrics: []ddsnmp.Metric{
316
- {
317
- Name: "temperature",
318
- Value: 25,
319
- MetricType: "gauge",
320
- },
321
- },
322
- },
323
- },
324
- expectedError: false,
325
- },
326
- "global tags with mapping": {
327
- profiles: []*ddsnmp.Profile{
328
- {
329
- SourceFile: "test-profile.yaml",
330
- Definition: &ddprofiledefinition.ProfileDefinition{
331
- MetricTags: []ddprofiledefinition.MetricTagConfig{
332
- {
333
- Tag: "device_type",
334
- Symbol: ddprofiledefinition.SymbolConfigCompat{
335
- OID: "1.3.6.1.2.1.1.2.0",
336
- Name: "sysObjectID",
337
- },
338
- Mapping: map[string]string{
339
- "1.3.6.1.4.1.9.1.1": "router",
340
- "1.3.6.1.4.1.9.1.2": "switch",
341
- },
342
- },
343
- },
344
- Metrics: []ddprofiledefinition.MetricsConfig{
345
- {
346
- Symbol: ddprofiledefinition.SymbolConfig{
347
- OID: "1.3.6.1.2.1.1.3.0",
348
- Name: "sysUpTime",
349
- },
350
- },
351
- },
352
- },
353
- },
354
- },
355
- setupMock: func(m *snmpmock.MockHandler) {
356
- m.EXPECT().MaxOids().Return(10).AnyTimes()
357
- // First call for global tags
358
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.2.0"}).Return(
359
- &gosnmp.SnmpPacket{
360
- Variables: []gosnmp.SnmpPDU{
361
- {
362
- Name: "1.3.6.1.2.1.1.2.0",
363
- Type: gosnmp.ObjectIdentifier,
364
- Value: "1.3.6.1.4.1.9.1.1",
365
- },
366
- },
367
- }, nil,
368
- )
369
- // Second call for metrics
370
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
371
- &gosnmp.SnmpPacket{
372
- Variables: []gosnmp.SnmpPDU{
373
- {
374
- Name: "1.3.6.1.2.1.1.3.0",
375
- Type: gosnmp.TimeTicks,
376
- Value: uint32(123456),
377
- },
378
- },
379
- }, nil,
380
- )
381
- },
382
- expectedResult: []*ddsnmp.ProfileMetrics{
383
- {
384
- Source: "test-profile.yaml",
385
- DeviceMetadata: nil,
386
- Tags: map[string]string{"device_type": "router"},
387
- Metrics: []ddsnmp.Metric{
388
- {
389
- Name: "sysUpTime",
390
- Value: 123456,
391
- MetricType: "gauge",
392
- },
393
- },
394
- },
395
- },
396
- expectedError: false,
397
- },
398
- "OID not found - returns empty metrics": {
399
- profiles: []*ddsnmp.Profile{
400
- {
401
- SourceFile: "test-profile.yaml",
402
- Definition: &ddprofiledefinition.ProfileDefinition{
403
- Metrics: []ddprofiledefinition.MetricsConfig{
404
- {
405
- Symbol: ddprofiledefinition.SymbolConfig{
406
- OID: "1.3.6.1.2.1.1.3.0",
407
- Name: "sysUpTime",
408
- },
409
- },
410
- },
411
- },
412
- },
413
- },
414
- setupMock: func(m *snmpmock.MockHandler) {
415
- m.EXPECT().MaxOids().Return(10).AnyTimes()
416
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
417
- &gosnmp.SnmpPacket{
418
- Variables: []gosnmp.SnmpPDU{
419
- {
420
- Name: "1.3.6.1.2.1.1.3.0",
421
- Type: gosnmp.NoSuchObject,
422
- Value: nil,
423
- },
424
- },
425
- }, nil,
426
- )
427
- },
428
- expectedResult: []*ddsnmp.ProfileMetrics{
429
- {
430
- DeviceMetadata: nil,
431
- Metrics: []ddsnmp.Metric{},
432
- },
433
- },
434
- expectedError: false,
435
- },
436
- "SNMP error": {
437
- profiles: []*ddsnmp.Profile{
438
- {
439
- SourceFile: "test-profile.yaml",
440
- Definition: &ddprofiledefinition.ProfileDefinition{
441
- Metrics: []ddprofiledefinition.MetricsConfig{
442
- {
443
- Symbol: ddprofiledefinition.SymbolConfig{
444
- OID: "1.3.6.1.2.1.1.3.0",
445
- Name: "sysUpTime",
446
- },
447
- },
448
- },
449
- },
450
- },
451
- },
452
- setupMock: func(m *snmpmock.MockHandler) {
453
- m.EXPECT().MaxOids().Return(10).AnyTimes()
454
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
455
- (*gosnmp.SnmpPacket)(nil),
456
- errors.New("SNMP timeout"),
457
- )
458
- },
459
- expectedResult: nil,
460
- expectedError: true,
461
- errorContains: "SNMP timeout",
462
- },
463
- "empty profile - no metrics defined": {
464
- profiles: []*ddsnmp.Profile{
465
- {
466
- SourceFile: "empty-profile.yaml",
467
- Definition: &ddprofiledefinition.ProfileDefinition{
468
- Metrics: []ddprofiledefinition.MetricsConfig{},
469
- },
470
- },
471
- },
472
- setupMock: func(m *snmpmock.MockHandler) {
473
- m.EXPECT().MaxOids().Return(10).AnyTimes()
474
- },
475
- expectedResult: []*ddsnmp.ProfileMetrics{
476
- {
477
- DeviceMetadata: nil,
478
- Metrics: []ddsnmp.Metric{},
479
- },
480
- },
481
- expectedError: false,
482
- },
483
- "multiple profiles - one fails": {
484
- profiles: []*ddsnmp.Profile{
485
- {
486
- SourceFile: "profile1.yaml",
487
- Definition: &ddprofiledefinition.ProfileDefinition{
488
- Metrics: []ddprofiledefinition.MetricsConfig{
489
- {
490
- Symbol: ddprofiledefinition.SymbolConfig{
491
- OID: "1.3.6.1.2.1.1.3.0",
492
- Name: "sysUpTime",
493
- },
494
- },
495
- },
496
- },
497
- },
498
- {
499
- SourceFile: "profile2.yaml",
500
- Definition: &ddprofiledefinition.ProfileDefinition{
501
- Metrics: []ddprofiledefinition.MetricsConfig{
502
- {
503
- Symbol: ddprofiledefinition.SymbolConfig{
504
- OID: "1.3.6.1.2.1.1.5.0",
505
- Name: "sysName",
506
- },
507
- },
508
- },
509
- },
510
- },
511
- },
512
- setupMock: func(m *snmpmock.MockHandler) {
513
- m.EXPECT().MaxOids().Return(10).AnyTimes()
514
- // First profile succeeds
515
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
516
- &gosnmp.SnmpPacket{
517
- Variables: []gosnmp.SnmpPDU{
518
- {
519
- Name: "1.3.6.1.2.1.1.3.0",
520
- Type: gosnmp.TimeTicks,
521
- Value: uint32(123456),
522
- },
523
- },
524
- }, nil,
525
- )
526
- // Second profile fails
527
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
528
- (*gosnmp.SnmpPacket)(nil),
529
- errors.New("connection refused"),
530
- )
531
- },
532
- expectedResult: []*ddsnmp.ProfileMetrics{
533
- {
534
- Source: "profile1.yaml",
535
- DeviceMetadata: nil,
536
- Metrics: []ddsnmp.Metric{
537
- {
538
- Name: "sysUpTime",
539
- Value: 123456,
540
- MetricType: "gauge",
541
- },
542
- },
543
- },
544
- },
545
- expectedError: false, // Should return partial results
546
- },
547
- }
548
-
549
- for name, tc := range tests {
550
- t.Run(name, func(t *testing.T) {
551
- ctrl := gomock.NewController(t)
552
- defer ctrl.Finish()
553
-
554
- mockHandler := snmpmock.NewMockHandler(ctrl)
555
- tc.setupMock(mockHandler)
556
-
557
- collector := New(mockHandler, tc.profiles, logger.New())
558
- collector.DoTableMetrics = true
559
-
560
- // Configure cache based on test requirements
561
- if tc.enableCache {
562
- collector.tableCache.setTTL(30*time.Second, 0)
563
- } else {
564
- collector.tableCache.setTTL(0, 0) // Disable cache
565
- }
566
-
567
- result, err := collector.Collect()
568
-
569
- // Clear circular references
570
- for _, profile := range result {
571
- for i := range profile.Metrics {
572
- profile.Metrics[i].Profile = nil
573
- }
574
- }
575
-
576
- if tc.expectedError {
577
- assert.Error(t, err)
578
- if tc.errorContains != "" {
579
- assert.Contains(t, err.Error(), tc.errorContains)
580
- }
581
- } else {
582
- assert.NoError(t, err)
583
- }
584
-
585
- if tc.expectedResult != nil {
586
- require.Equal(t, len(tc.expectedResult), len(result))
587
- for i := range tc.expectedResult {
588
- assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
589
- assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
590
- assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
591
- }
592
- } else {
593
- assert.Nil(t, result)
594
- }
595
- })
596
- }
597
-}
598
-
599
-func TestCollector_Collect_ValueMappings(t *testing.T) {
600
- tests := map[string]struct {
601
- profiles []*ddsnmp.Profile
602
- setupMock func(m *snmpmock.MockHandler)
603
- expectedResult []*ddsnmp.ProfileMetrics
604
- expectedError bool
605
- errorContains string
606
- }{
607
- "metric with string to int mapping": {
608
- profiles: []*ddsnmp.Profile{
609
- {
610
- SourceFile: "test-profile.yaml",
611
- Definition: &ddprofiledefinition.ProfileDefinition{
612
- Metrics: []ddprofiledefinition.MetricsConfig{
613
- {
614
- Symbol: ddprofiledefinition.SymbolConfig{
615
- OID: "1.3.6.1.4.1.12124.1.1.2",
616
- Name: "clusterHealth",
617
- Mapping: map[string]string{
618
- "OK": "0",
619
- "WARNING": "1",
620
- "CRITICAL": "2",
621
- },
622
- },
623
- },
624
- },
625
- },
626
- },
627
- },
628
- setupMock: func(m *snmpmock.MockHandler) {
629
- m.EXPECT().MaxOids().Return(10).AnyTimes()
630
- m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.2"}).Return(
631
- &gosnmp.SnmpPacket{
632
- Variables: []gosnmp.SnmpPDU{
633
- {
634
- Name: "1.3.6.1.4.1.12124.1.1.2",
635
- Type: gosnmp.OctetString,
636
- Value: []byte("WARNING"),
637
- },
638
- },
639
- }, nil,
640
- )
641
- },
642
- expectedResult: []*ddsnmp.ProfileMetrics{
643
- {
644
- Source: "test-profile.yaml",
645
- DeviceMetadata: nil,
646
- Metrics: []ddsnmp.Metric{
647
- {
648
- Name: "clusterHealth",
649
- Value: 1,
650
- MetricType: "gauge",
651
- Mappings: map[int64]string{
652
- 0: "OK",
653
- 1: "WARNING",
654
- 2: "CRITICAL",
655
- },
656
- },
657
- },
658
- },
659
- },
660
- expectedError: false,
661
- },
662
- "metric with int to string mapping": {
663
- profiles: []*ddsnmp.Profile{
664
- {
665
- SourceFile: "test-profile.yaml",
666
- Definition: &ddprofiledefinition.ProfileDefinition{
667
- Metrics: []ddprofiledefinition.MetricsConfig{
668
- {
669
- Symbol: ddprofiledefinition.SymbolConfig{
670
- OID: "1.3.6.1.2.1.2.2.1.8",
671
- Name: "ifOperStatus",
672
- Mapping: map[string]string{
673
- "1": "up",
674
- "2": "down",
675
- "3": "testing",
676
- "4": "unknown",
677
- "5": "dormant",
678
- },
679
- },
680
- },
681
- },
682
- },
683
- },
684
- },
685
- setupMock: func(m *snmpmock.MockHandler) {
686
- m.EXPECT().MaxOids().Return(10).AnyTimes()
687
- m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.8"}).Return(
688
- &gosnmp.SnmpPacket{
689
- Variables: []gosnmp.SnmpPDU{
690
- {
691
- Name: "1.3.6.1.2.1.2.2.1.8",
692
- Type: gosnmp.Integer,
693
- Value: 2, // down
694
- },
695
- },
696
- }, nil,
697
- )
698
- },
699
- expectedResult: []*ddsnmp.ProfileMetrics{
700
- {
701
- Source: "test-profile.yaml",
702
- DeviceMetadata: nil,
703
- Metrics: []ddsnmp.Metric{
704
- {
705
- Name: "ifOperStatus",
706
- Value: 2,
707
- MetricType: "gauge",
708
- Mappings: map[int64]string{
709
- 1: "up",
710
- 2: "down",
711
- 3: "testing",
712
- 4: "unknown",
713
- 5: "dormant",
714
- },
715
- },
716
- },
717
- },
718
- },
719
- expectedError: false,
720
- },
721
- "metric with extract_value and int to string mapping": {
722
- profiles: []*ddsnmp.Profile{
723
- {
724
- SourceFile: "test-profile.yaml",
725
- Definition: &ddprofiledefinition.ProfileDefinition{
726
- Metrics: []ddprofiledefinition.MetricsConfig{
727
- {
728
- Symbol: ddprofiledefinition.SymbolConfig{
729
- OID: "1.3.6.1.4.1.12124.1.1.8",
730
- Name: "fanStatus",
731
- ExtractValueCompiled: mustCompileRegex(`Fan(\d+)`),
732
- Mapping: map[string]string{
733
- "1": "normal",
734
- "2": "warning",
735
- "3": "critical",
736
- },
737
- },
738
- },
739
- },
740
- },
741
- },
742
- },
743
- setupMock: func(m *snmpmock.MockHandler) {
744
- m.EXPECT().MaxOids().Return(10).AnyTimes()
745
- m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.8"}).Return(
746
- &gosnmp.SnmpPacket{
747
- Variables: []gosnmp.SnmpPDU{
748
- {
749
- Name: "1.3.6.1.4.1.12124.1.1.8",
750
- Type: gosnmp.OctetString,
751
- Value: []byte("Fan2"),
752
- },
753
- },
754
- }, nil,
755
- )
756
- },
757
- expectedResult: []*ddsnmp.ProfileMetrics{
758
- {
759
- Source: "test-profile.yaml",
760
- DeviceMetadata: nil,
761
- Metrics: []ddsnmp.Metric{
762
- {
763
- Name: "fanStatus",
764
- Value: 2,
765
- MetricType: "gauge",
766
- Mappings: map[int64]string{
767
- 1: "normal",
768
- 2: "warning",
769
- 3: "critical",
770
- },
771
- },
772
- },
773
- },
774
- },
775
- expectedError: false,
776
- },
777
- "metric with int to int mapping": {
778
- profiles: []*ddsnmp.Profile{
779
- {
780
- SourceFile: "test-profile.yaml",
781
- Definition: &ddprofiledefinition.ProfileDefinition{
782
- Metrics: []ddprofiledefinition.MetricsConfig{
783
- {
784
- Symbol: ddprofiledefinition.SymbolConfig{
785
- OID: "1.3.6.1.2.1.2.2.1.7",
786
- Name: "ifAdminStatus",
787
- Mapping: map[string]string{
788
- "1": "1", // up -> 1
789
- "2": "0", // down -> 0
790
- "3": "0", // testing -> 0
791
- },
792
- },
793
- },
794
- },
795
- },
796
- },
797
- },
798
- setupMock: func(m *snmpmock.MockHandler) {
799
- m.EXPECT().MaxOids().Return(10).AnyTimes()
800
- m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.7"}).Return(
801
- &gosnmp.SnmpPacket{
802
- Variables: []gosnmp.SnmpPDU{
803
- {
804
- Name: "1.3.6.1.2.1.2.2.1.7",
805
- Type: gosnmp.Integer,
806
- Value: 2, // down
807
- },
808
- },
809
- }, nil,
810
- )
811
- },
812
- expectedResult: []*ddsnmp.ProfileMetrics{
813
- {
814
- Source: "test-profile.yaml",
815
- DeviceMetadata: nil,
816
- Metrics: []ddsnmp.Metric{
817
- {
818
- Name: "ifAdminStatus",
819
- Value: 0, // mapped from 2 -> 0
820
- MetricType: "gauge",
821
- Mappings: map[int64]string{
822
- 1: "1",
823
- 2: "0",
824
- 3: "0",
825
- },
826
- },
827
- },
828
- },
829
- },
830
- expectedError: false,
831
- },
832
- "metric with partial string to int mapping": {
833
- profiles: []*ddsnmp.Profile{
834
- {
835
- SourceFile: "test-profile.yaml",
836
- Definition: &ddprofiledefinition.ProfileDefinition{
837
- Metrics: []ddprofiledefinition.MetricsConfig{
838
- {
839
- Symbol: ddprofiledefinition.SymbolConfig{
840
- OID: "1.3.6.1.4.1.12124.1.1.2",
841
- Name: "clusterHealth",
842
- Mapping: map[string]string{
843
- "OK": "0",
844
- "WARNING": "1",
845
- // CRITICAL is not mapped
846
- },
847
- },
848
- },
849
- },
850
- },
851
- },
852
- },
853
- setupMock: func(m *snmpmock.MockHandler) {
854
- m.EXPECT().MaxOids().Return(10).AnyTimes()
855
- m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.2"}).Return(
856
- &gosnmp.SnmpPacket{
857
- Variables: []gosnmp.SnmpPDU{
858
- {
859
- Name: "1.3.6.1.4.1.12124.1.1.2",
860
- Type: gosnmp.OctetString,
861
- Value: []byte("CRITICAL"),
862
- },
863
- },
864
- }, nil,
865
- )
866
- },
867
- expectedResult: nil,
868
- expectedError: true,
869
- errorContains: "strconv.ParseInt",
870
- },
871
- "metric with mixed mapping values": {
872
- profiles: []*ddsnmp.Profile{
873
- {
874
- SourceFile: "test-profile.yaml",
875
- Definition: &ddprofiledefinition.ProfileDefinition{
876
- Metrics: []ddprofiledefinition.MetricsConfig{
877
- {
878
- Symbol: ddprofiledefinition.SymbolConfig{
879
- OID: "1.3.6.1.4.1.12124.1.1.2",
880
- Name: "deviceStatus",
881
- Mapping: map[string]string{
882
- "OK": "0",
883
- "WARNING": "1",
884
- "ERROR": "invalid", // This will cause metric to be skipped
885
- },
886
- },
887
- },
888
- },
889
- },
890
- },
891
- },
892
- setupMock: func(m *snmpmock.MockHandler) {
893
- m.EXPECT().MaxOids().Return(10).AnyTimes()
894
- m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.2"}).Return(
895
- &gosnmp.SnmpPacket{
896
- Variables: []gosnmp.SnmpPDU{
897
- {
898
- Name: "1.3.6.1.4.1.12124.1.1.2",
899
- Type: gosnmp.OctetString,
900
- Value: []byte("ERROR"),
901
- },
902
- },
903
- }, nil,
904
- )
905
- },
906
- expectedResult: nil,
907
- expectedError: true,
908
- errorContains: "strconv.ParseInt",
909
- },
910
- "metric with no mapping": {
911
- profiles: []*ddsnmp.Profile{
912
- {
913
- SourceFile: "test-profile.yaml",
914
- Definition: &ddprofiledefinition.ProfileDefinition{
915
- Metrics: []ddprofiledefinition.MetricsConfig{
916
- {
917
- Symbol: ddprofiledefinition.SymbolConfig{
918
- OID: "1.3.6.1.2.1.1.3.0",
919
- Name: "sysUpTime",
920
- },
921
- },
922
- },
923
- },
924
- },
925
- },
926
- setupMock: func(m *snmpmock.MockHandler) {
927
- m.EXPECT().MaxOids().Return(10).AnyTimes()
928
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
929
- &gosnmp.SnmpPacket{
930
- Variables: []gosnmp.SnmpPDU{
931
- {
932
- Name: "1.3.6.1.2.1.1.3.0",
933
- Type: gosnmp.TimeTicks,
934
- Value: uint32(123456),
935
- },
936
- },
937
- }, nil,
938
- )
939
- },
940
- expectedResult: []*ddsnmp.ProfileMetrics{
941
- {
942
- Source: "test-profile.yaml",
943
- DeviceMetadata: nil,
944
- Metrics: []ddsnmp.Metric{
945
- {
946
- Name: "sysUpTime",
947
- Value: 123456,
948
- MetricType: "gauge",
949
- Mappings: nil, // No mappings
950
- },
951
- },
952
- },
953
- },
954
- expectedError: false,
955
- },
956
- "metric with numeric value and int to string mapping": {
957
- profiles: []*ddsnmp.Profile{
958
- {
959
- SourceFile: "test-profile.yaml",
960
- Definition: &ddprofiledefinition.ProfileDefinition{
961
- Metrics: []ddprofiledefinition.MetricsConfig{
962
- {
963
- Symbol: ddprofiledefinition.SymbolConfig{
964
- OID: "1.3.6.1.2.1.2.2.1.7",
965
- Name: "ifAdminStatus",
966
- Mapping: map[string]string{
967
- "1": "up",
968
- "2": "down",
969
- "3": "testing",
970
- },
971
- },
972
- },
973
- },
974
- },
975
- },
976
- },
977
- setupMock: func(m *snmpmock.MockHandler) {
978
- m.EXPECT().MaxOids().Return(10).AnyTimes()
979
- m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.7"}).Return(
980
- &gosnmp.SnmpPacket{
981
- Variables: []gosnmp.SnmpPDU{
982
- {
983
- Name: "1.3.6.1.2.1.2.2.1.7",
984
- Type: gosnmp.Integer,
985
- Value: 1, // up
986
- },
987
- },
988
- }, nil,
989
- )
990
- },
991
- expectedResult: []*ddsnmp.ProfileMetrics{
992
- {
993
- Source: "test-profile.yaml",
994
- DeviceMetadata: nil,
995
- Metrics: []ddsnmp.Metric{
996
- {
997
- Name: "ifAdminStatus",
998
- Value: 1,
999
- MetricType: "gauge",
1000
- Mappings: map[int64]string{
1001
- 1: "up",
1002
- 2: "down",
1003
- 3: "testing",
1004
- },
1005
- },
1006
- },
1007
- },
1008
- },
1009
- expectedError: false,
1010
- },
1011
- "metric with string value and string to int mapping": {
1012
- profiles: []*ddsnmp.Profile{
1013
- {
1014
- SourceFile: "test-profile.yaml",
1015
- Definition: &ddprofiledefinition.ProfileDefinition{
1016
- Metrics: []ddprofiledefinition.MetricsConfig{
1017
- {
1018
- Symbol: ddprofiledefinition.SymbolConfig{
1019
- OID: "1.3.6.1.4.1.318.1.1.1.2.2.1.0",
1020
- Name: "upsBasicBatteryStatus",
1021
- Mapping: map[string]string{
1022
- "batteryNormal": "0",
1023
- "batteryLow": "1",
1024
- "batteryDepleted": "2",
1025
- "batteryCharging": "3",
1026
- },
1027
- },
1028
- },
1029
- },
1030
- },
1031
- },
1032
- },
1033
- setupMock: func(m *snmpmock.MockHandler) {
1034
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1035
- m.EXPECT().Get([]string{"1.3.6.1.4.1.318.1.1.1.2.2.1.0"}).Return(
1036
- &gosnmp.SnmpPacket{
1037
- Variables: []gosnmp.SnmpPDU{
1038
- {
1039
- Name: "1.3.6.1.4.1.318.1.1.1.2.2.1.0",
1040
- Type: gosnmp.OctetString,
1041
- Value: []byte("batteryLow"),
1042
- },
1043
- },
1044
- }, nil,
1045
- )
1046
- },
1047
- expectedResult: []*ddsnmp.ProfileMetrics{
1048
- {
1049
- Source: "test-profile.yaml",
1050
- DeviceMetadata: nil,
1051
- Metrics: []ddsnmp.Metric{
1052
- {
1053
- Name: "upsBasicBatteryStatus",
1054
- Value: 1,
1055
- MetricType: "gauge",
1056
- Mappings: map[int64]string{
1057
- 0: "batteryNormal",
1058
- 1: "batteryLow",
1059
- 2: "batteryDepleted",
1060
- 3: "batteryCharging",
1061
- },
1062
- },
1063
- },
1064
- },
1065
- },
1066
- expectedError: false,
1067
- },
1068
- }
1069
-
1070
- for name, tc := range tests {
1071
- t.Run(name, func(t *testing.T) {
1072
- ctrl := gomock.NewController(t)
1073
- defer ctrl.Finish()
1074
-
1075
- mockHandler := snmpmock.NewMockHandler(ctrl)
1076
- tc.setupMock(mockHandler)
1077
-
1078
- collector := New(mockHandler, tc.profiles, logger.New())
1079
- collector.DoTableMetrics = true
1080
- collector.tableCache.setTTL(0, 0) // Disable cache
1081
-
1082
- result, err := collector.Collect()
1083
-
1084
- // Clear circular references
1085
- for _, profile := range result {
1086
- for i := range profile.Metrics {
1087
- profile.Metrics[i].Profile = nil
1088
- }
1089
- }
1090
-
1091
- if tc.expectedError {
1092
- assert.Error(t, err)
1093
- if tc.errorContains != "" {
1094
- assert.Contains(t, err.Error(), tc.errorContains)
1095
- }
1096
- } else {
1097
- assert.NoError(t, err)
1098
- }
1099
-
1100
- if tc.expectedResult != nil {
1101
- require.Equal(t, len(tc.expectedResult), len(result))
1102
- for i := range tc.expectedResult {
1103
- assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
1104
- assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
1105
- assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
1106
- }
1107
- } else {
1108
- assert.Nil(t, result)
1109
- }
1110
- })
1111
- }
1112
-}
1113
-
1114
-func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1115
- tests := map[string]struct {
1116
- profiles []*ddsnmp.Profile
1117
- setupMock func(m *snmpmock.MockHandler)
1118
- expectedResult []*ddsnmp.ProfileMetrics
1119
- expectedError bool
1120
- errorContains string
1121
- enableCache bool
1122
- }{
1123
- "basic table with single metric": {
1124
- profiles: []*ddsnmp.Profile{
1125
- {
1126
- SourceFile: "test-profile.yaml",
1127
- Definition: &ddprofiledefinition.ProfileDefinition{
1128
- Metrics: []ddprofiledefinition.MetricsConfig{
1129
- {
1130
- Table: ddprofiledefinition.SymbolConfig{
1131
- OID: "1.3.6.1.2.1.2.2",
1132
- Name: "ifTable",
1133
- },
1134
- Symbols: []ddprofiledefinition.SymbolConfig{
1135
- {
1136
- OID: "1.3.6.1.2.1.2.2.1.10",
1137
- Name: "ifInOctets",
1138
- },
1139
- },
1140
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1141
- {
1142
- Tag: "interface",
1143
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1144
- OID: "1.3.6.1.2.1.2.2.1.2",
1145
- Name: "ifDescr",
1146
- },
1147
- },
1148
- },
1149
- },
1150
- },
1151
- },
1152
- },
1153
- },
1154
- setupMock: func(m *snmpmock.MockHandler) {
1155
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1156
- m.EXPECT().Version().Return(gosnmp.Version2c)
1157
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1158
- []gosnmp.SnmpPDU{
1159
- {
1160
- Name: "1.3.6.1.2.1.2.2.1.10.1",
1161
- Type: gosnmp.Counter32,
1162
- Value: uint(1000),
1163
- },
1164
- {
1165
- Name: "1.3.6.1.2.1.2.2.1.10.2",
1166
- Type: gosnmp.Counter32,
1167
- Value: uint(2000),
1168
- },
1169
- {
1170
- Name: "1.3.6.1.2.1.2.2.1.2.1",
1171
- Type: gosnmp.OctetString,
1172
- Value: []byte("eth0"),
1173
- },
1174
- {
1175
- Name: "1.3.6.1.2.1.2.2.1.2.2",
1176
- Type: gosnmp.OctetString,
1177
- Value: []byte("eth1"),
1178
- },
1179
- }, nil,
1180
- )
1181
- },
1182
- expectedResult: []*ddsnmp.ProfileMetrics{
1183
- {
1184
- Source: "test-profile.yaml",
1185
- DeviceMetadata: nil,
1186
- Metrics: []ddsnmp.Metric{
1187
- {
1188
- Name: "ifInOctets",
1189
- Value: 1000,
1190
- Tags: map[string]string{"interface": "eth0"},
1191
- MetricType: "rate",
1192
- IsTable: true,
1193
- },
1194
- {
1195
- Name: "ifInOctets",
1196
- Value: 2000,
1197
- Tags: map[string]string{"interface": "eth1"},
1198
- MetricType: "rate",
1199
- IsTable: true,
1200
- },
1201
- },
1202
- },
1203
- },
1204
- expectedError: false,
1205
- },
1206
- "table with multiple metrics": {
1207
- profiles: []*ddsnmp.Profile{
1208
- {
1209
- SourceFile: "test-profile.yaml",
1210
- Definition: &ddprofiledefinition.ProfileDefinition{
1211
- Metrics: []ddprofiledefinition.MetricsConfig{
1212
- {
1213
- Table: ddprofiledefinition.SymbolConfig{
1214
- OID: "1.3.6.1.2.1.2.2",
1215
- Name: "ifTable",
1216
- },
1217
- Symbols: []ddprofiledefinition.SymbolConfig{
1218
- {
1219
- OID: "1.3.6.1.2.1.2.2.1.10",
1220
- Name: "ifInOctets",
1221
- },
1222
- {
1223
- OID: "1.3.6.1.2.1.2.2.1.16",
1224
- Name: "ifOutOctets",
1225
- },
1226
- {
1227
- OID: "1.3.6.1.2.1.2.2.1.8",
1228
- Name: "ifOperStatus",
1229
- },
1230
- },
1231
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1232
- {
1233
- Tag: "interface",
1234
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1235
- OID: "1.3.6.1.2.1.2.2.1.2",
1236
- Name: "ifDescr",
1237
- },
1238
- },
1239
- },
1240
- },
1241
- },
1242
- },
1243
- },
1244
- },
1245
- setupMock: func(m *snmpmock.MockHandler) {
1246
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1247
- m.EXPECT().Version().Return(gosnmp.Version2c)
1248
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1249
- []gosnmp.SnmpPDU{
1250
- // Row 1
1251
- {
1252
- Name: "1.3.6.1.2.1.2.2.1.10.1",
1253
- Type: gosnmp.Counter32,
1254
- Value: uint(1000),
1255
- },
1256
- {
1257
- Name: "1.3.6.1.2.1.2.2.1.16.1",
1258
- Type: gosnmp.Counter32,
1259
- Value: uint(500),
1260
- },
1261
- {
1262
- Name: "1.3.6.1.2.1.2.2.1.8.1",
1263
- Type: gosnmp.Integer,
1264
- Value: 1, // up
1265
- },
1266
- {
1267
- Name: "1.3.6.1.2.1.2.2.1.2.1",
1268
- Type: gosnmp.OctetString,
1269
- Value: []byte("eth0"),
1270
- },
1271
- // Row 2
1272
- {
1273
- Name: "1.3.6.1.2.1.2.2.1.10.2",
1274
- Type: gosnmp.Counter32,
1275
- Value: uint(2000),
1276
- },
1277
- {
1278
- Name: "1.3.6.1.2.1.2.2.1.16.2",
1279
- Type: gosnmp.Counter32,
1280
- Value: uint(1500),
1281
- },
1282
- {
1283
- Name: "1.3.6.1.2.1.2.2.1.8.2",
1284
- Type: gosnmp.Integer,
1285
- Value: 2, // down
1286
- },
1287
- {
1288
- Name: "1.3.6.1.2.1.2.2.1.2.2",
1289
- Type: gosnmp.OctetString,
1290
- Value: []byte("eth1"),
1291
- },
1292
- }, nil,
1293
- )
1294
- },
1295
- expectedResult: []*ddsnmp.ProfileMetrics{
1296
- {
1297
- Source: "test-profile.yaml",
1298
- DeviceMetadata: nil,
1299
- Metrics: []ddsnmp.Metric{
1300
- // Row 1
1301
- {
1302
- Name: "ifInOctets",
1303
- Value: 1000,
1304
- Tags: map[string]string{"interface": "eth0"},
1305
- MetricType: "rate",
1306
- IsTable: true,
1307
- },
1308
- {
1309
- Name: "ifOutOctets",
1310
- Value: 500,
1311
- Tags: map[string]string{"interface": "eth0"},
1312
- MetricType: "rate",
1313
- IsTable: true,
1314
- },
1315
- {
1316
- Name: "ifOperStatus",
1317
- Value: 1,
1318
- Tags: map[string]string{"interface": "eth0"},
1319
- MetricType: "gauge",
1320
- IsTable: true,
1321
- },
1322
- // Row 2
1323
- {
1324
- Name: "ifInOctets",
1325
- Value: 2000,
1326
- Tags: map[string]string{"interface": "eth1"},
1327
- MetricType: "rate",
1328
- IsTable: true,
1329
- },
1330
- {
1331
- Name: "ifOutOctets",
1332
- Value: 1500,
1333
- Tags: map[string]string{"interface": "eth1"},
1334
- MetricType: "rate",
1335
- IsTable: true,
1336
- },
1337
- {
1338
- Name: "ifOperStatus",
1339
- Value: 2,
1340
- Tags: map[string]string{"interface": "eth1"},
1341
- MetricType: "gauge",
1342
- IsTable: true,
1343
- },
1344
- },
1345
- },
1346
- },
1347
- expectedError: false,
1348
- },
1349
- "table with tag mapping": {
1350
- profiles: []*ddsnmp.Profile{
1351
- {
1352
- SourceFile: "test-profile.yaml",
1353
- Definition: &ddprofiledefinition.ProfileDefinition{
1354
- Metrics: []ddprofiledefinition.MetricsConfig{
1355
- {
1356
- Table: ddprofiledefinition.SymbolConfig{
1357
- OID: "1.3.6.1.2.1.2.2",
1358
- Name: "ifTable",
1359
- },
1360
- Symbols: []ddprofiledefinition.SymbolConfig{
1361
- {
1362
- OID: "1.3.6.1.2.1.2.2.1.14",
1363
- Name: "ifInErrors",
1364
- },
1365
- },
1366
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1367
- {
1368
- Tag: "interface",
1369
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1370
- OID: "1.3.6.1.2.1.2.2.1.2",
1371
- Name: "ifDescr",
1372
- },
1373
- },
1374
- {
1375
- Tag: "if_type",
1376
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1377
- OID: "1.3.6.1.2.1.2.2.1.3",
1378
- Name: "ifType",
1379
- },
1380
- Mapping: map[string]string{
1381
- "1": "other",
1382
- "6": "ethernetCsmacd",
1383
- "24": "softwareLoopback",
1384
- },
1385
- },
1386
- },
1387
- },
1388
- },
1389
- },
1390
- },
1391
- },
1392
- setupMock: func(m *snmpmock.MockHandler) {
1393
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1394
- m.EXPECT().Version().Return(gosnmp.Version2c)
1395
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1396
- []gosnmp.SnmpPDU{
1397
- {
1398
- Name: "1.3.6.1.2.1.2.2.1.14.1",
1399
- Type: gosnmp.Counter32,
1400
- Value: uint(10),
1401
- },
1402
- {
1403
- Name: "1.3.6.1.2.1.2.2.1.2.1",
1404
- Type: gosnmp.OctetString,
1405
- Value: []byte("lo0"),
1406
- },
1407
- {
1408
- Name: "1.3.6.1.2.1.2.2.1.3.1",
1409
- Type: gosnmp.Integer,
1410
- Value: 24, // softwareLoopback
1411
- },
1412
- {
1413
- Name: "1.3.6.1.2.1.2.2.1.14.2",
1414
- Type: gosnmp.Counter32,
1415
- Value: uint(5),
1416
- },
1417
- {
1418
- Name: "1.3.6.1.2.1.2.2.1.2.2",
1419
- Type: gosnmp.OctetString,
1420
- Value: []byte("eth0"),
1421
- },
1422
- {
1423
- Name: "1.3.6.1.2.1.2.2.1.3.2",
1424
- Type: gosnmp.Integer,
1425
- Value: 6, // ethernetCsmacd
1426
- },
1427
- }, nil,
1428
- )
1429
- },
1430
- expectedResult: []*ddsnmp.ProfileMetrics{
1431
- {
1432
- Source: "test-profile.yaml",
1433
- DeviceMetadata: nil,
1434
- Metrics: []ddsnmp.Metric{
1435
- {
1436
- Name: "ifInErrors",
1437
- Value: 10,
1438
- Tags: map[string]string{"interface": "lo0", "if_type": "softwareLoopback"},
1439
- MetricType: "rate",
1440
- IsTable: true,
1441
- },
1442
- {
1443
- Name: "ifInErrors",
1444
- Value: 5,
1445
- Tags: map[string]string{"interface": "eth0", "if_type": "ethernetCsmacd"},
1446
- MetricType: "rate",
1447
- IsTable: true,
1448
- },
1449
- },
1450
- },
1451
- },
1452
- expectedError: false,
1453
- },
1454
- "table with missing rows": {
1455
- profiles: []*ddsnmp.Profile{
1456
- {
1457
- SourceFile: "test-profile.yaml",
1458
- Definition: &ddprofiledefinition.ProfileDefinition{
1459
- Metrics: []ddprofiledefinition.MetricsConfig{
1460
- {
1461
- Table: ddprofiledefinition.SymbolConfig{
1462
- OID: "1.3.6.1.2.1.2.2",
1463
- Name: "ifTable",
1464
- },
1465
- Symbols: []ddprofiledefinition.SymbolConfig{
1466
- {
1467
- OID: "1.3.6.1.2.1.2.2.1.10",
1468
- Name: "ifInOctets",
1469
- },
1470
- {
1471
- OID: "1.3.6.1.2.1.2.2.1.16",
1472
- Name: "ifOutOctets",
1473
- },
1474
- },
1475
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1476
- {
1477
- Tag: "interface",
1478
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1479
- OID: "1.3.6.1.2.1.2.2.1.2",
1480
- Name: "ifDescr",
1481
- },
1482
- },
1483
- },
1484
- },
1485
- },
1486
- },
1487
- },
1488
- },
1489
- setupMock: func(m *snmpmock.MockHandler) {
1490
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1491
- m.EXPECT().Version().Return(gosnmp.Version2c)
1492
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1493
- []gosnmp.SnmpPDU{
1494
- // Row 1 - complete
1495
- {
1496
- Name: "1.3.6.1.2.1.2.2.1.10.1",
1497
- Type: gosnmp.Counter32,
1498
- Value: uint(1000),
1499
- },
1500
- {
1501
- Name: "1.3.6.1.2.1.2.2.1.16.1",
1502
- Type: gosnmp.Counter32,
1503
- Value: uint(500),
1504
- },
1505
- {
1506
- Name: "1.3.6.1.2.1.2.2.1.2.1",
1507
- Type: gosnmp.OctetString,
1508
- Value: []byte("eth0"),
1509
- },
1510
- // Row 2 - missing ifOutOctets
1511
- {
1512
- Name: "1.3.6.1.2.1.2.2.1.10.2",
1513
- Type: gosnmp.Counter32,
1514
- Value: uint(2000),
1515
- },
1516
- {
1517
- Name: "1.3.6.1.2.1.2.2.1.2.2",
1518
- Type: gosnmp.OctetString,
1519
- Value: []byte("eth1"),
1520
- },
1521
- // Row 3 - missing tag (ifDescr)
1522
- {
1523
- Name: "1.3.6.1.2.1.2.2.1.10.3",
1524
- Type: gosnmp.Counter32,
1525
- Value: uint(3000),
1526
- },
1527
- {
1528
- Name: "1.3.6.1.2.1.2.2.1.16.3",
1529
- Type: gosnmp.Counter32,
1530
- Value: uint(1500),
1531
- },
1532
- }, nil,
1533
- )
1534
- },
1535
- expectedResult: []*ddsnmp.ProfileMetrics{
1536
- {
1537
- Source: "test-profile.yaml",
1538
- DeviceMetadata: nil,
1539
- Metrics: []ddsnmp.Metric{
1540
- // Row 1 - complete
1541
- {
1542
- Name: "ifInOctets",
1543
- Value: 1000,
1544
- Tags: map[string]string{"interface": "eth0"},
1545
- MetricType: "rate",
1546
- IsTable: true,
1547
- },
1548
- {
1549
- Name: "ifOutOctets",
1550
- Value: 500,
1551
- Tags: map[string]string{"interface": "eth0"},
1552
- MetricType: "rate",
1553
- IsTable: true,
1554
- },
1555
- // Row 2 - only ifInOctets
1556
- {
1557
- Name: "ifInOctets",
1558
- Value: 2000,
1559
- Tags: map[string]string{"interface": "eth1"},
1560
- MetricType: "rate",
1561
- IsTable: true,
1562
- },
1563
- // Row 3 - both metrics but no tag
1564
- {
1565
- Name: "ifInOctets",
1566
- Value: 3000,
1567
- Tags: nil,
1568
- MetricType: "rate",
1569
- IsTable: true,
1570
- },
1571
- {
1572
- Name: "ifOutOctets",
1573
- Value: 1500,
1574
- Tags: nil,
1575
- MetricType: "rate",
1576
- IsTable: true,
1577
- },
1578
- },
1579
- },
1580
- },
1581
- expectedError: false,
1582
- },
1583
- "table with static tags": {
1584
- profiles: []*ddsnmp.Profile{
1585
- {
1586
- SourceFile: "test-profile.yaml",
1587
- Definition: &ddprofiledefinition.ProfileDefinition{
1588
- Metrics: []ddprofiledefinition.MetricsConfig{
1589
- {
1590
- Table: ddprofiledefinition.SymbolConfig{
1591
- OID: "1.3.6.1.2.1.2.2",
1592
- Name: "ifTable",
1593
- },
1594
- Symbols: []ddprofiledefinition.SymbolConfig{
1595
- {
1596
- OID: "1.3.6.1.2.1.2.2.1.10",
1597
- Name: "ifInOctets",
1598
- },
1599
- },
1600
- StaticTags: []string{
1601
- "source:interface",
1602
- "table:if",
1603
- },
1604
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1605
- {
1606
- Tag: "interface",
1607
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1608
- OID: "1.3.6.1.2.1.2.2.1.2",
1609
- Name: "ifDescr",
1610
- },
1611
- },
1612
- },
1613
- },
1614
- },
1615
- },
1616
- },
1617
- },
1618
- setupMock: func(m *snmpmock.MockHandler) {
1619
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1620
- m.EXPECT().Version().Return(gosnmp.Version2c)
1621
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1622
- []gosnmp.SnmpPDU{
1623
- {
1624
- Name: "1.3.6.1.2.1.2.2.1.10.1",
1625
- Type: gosnmp.Counter32,
1626
- Value: uint(1000),
1627
- },
1628
- {
1629
- Name: "1.3.6.1.2.1.2.2.1.2.1",
1630
- Type: gosnmp.OctetString,
1631
- Value: []byte("eth0"),
1632
- },
1633
- }, nil,
1634
- )
1635
- },
1636
- expectedResult: []*ddsnmp.ProfileMetrics{
1637
- {
1638
- Source: "test-profile.yaml",
1639
- DeviceMetadata: nil,
1640
- Metrics: []ddsnmp.Metric{
1641
- {
1642
- Name: "ifInOctets",
1643
- Value: 1000,
1644
- StaticTags: map[string]string{
1645
- "source": "interface",
1646
- "table": "if",
1647
- },
1648
- Tags: map[string]string{"interface": "eth0"},
1649
- MetricType: "rate",
1650
- IsTable: true,
1651
- },
1652
- },
1653
- },
1654
- },
1655
- expectedError: false,
1656
- },
1657
- "table with extract_value in tag": {
1658
- profiles: []*ddsnmp.Profile{
1659
- {
1660
- SourceFile: "test-profile.yaml",
1661
- Definition: &ddprofiledefinition.ProfileDefinition{
1662
- Metrics: []ddprofiledefinition.MetricsConfig{
1663
- {
1664
- Table: ddprofiledefinition.SymbolConfig{
1665
- OID: "1.3.6.1.2.1.2.2",
1666
- Name: "ifTable",
1667
- },
1668
- Symbols: []ddprofiledefinition.SymbolConfig{
1669
- {
1670
- OID: "1.3.6.1.2.1.2.2.1.14",
1671
- Name: "ifInErrors",
1672
- },
1673
- },
1674
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1675
- {
1676
- Tag: "interface",
1677
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1678
- OID: "1.3.6.1.2.1.2.2.1.2",
1679
- Name: "ifDescr",
1680
- ExtractValueCompiled: mustCompileRegex(`^(\S+)`), // Extract first word
1681
- },
1682
- },
1683
- },
1684
- },
1685
- },
1686
- },
1687
- },
1688
- },
1689
- setupMock: func(m *snmpmock.MockHandler) {
1690
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1691
- m.EXPECT().Version().Return(gosnmp.Version2c)
1692
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1693
- []gosnmp.SnmpPDU{
1694
- {
1695
- Name: "1.3.6.1.2.1.2.2.1.14.1",
1696
- Type: gosnmp.Counter32,
1697
- Value: uint(10),
1698
- },
1699
- {
1700
- Name: "1.3.6.1.2.1.2.2.1.2.1",
1701
- Type: gosnmp.OctetString,
1702
- Value: []byte("eth0 - Primary Network Interface"),
1703
- },
1704
- {
1705
- Name: "1.3.6.1.2.1.2.2.1.14.2",
1706
- Type: gosnmp.Counter32,
1707
- Value: uint(5),
1708
- },
1709
- {
1710
- Name: "1.3.6.1.2.1.2.2.1.2.2",
1711
- Type: gosnmp.OctetString,
1712
- Value: []byte("lo0 - Loopback"),
1713
- },
1714
- }, nil,
1715
- )
1716
- },
1717
- expectedResult: []*ddsnmp.ProfileMetrics{
1718
- {
1719
- Source: "test-profile.yaml",
1720
- DeviceMetadata: nil,
1721
- Metrics: []ddsnmp.Metric{
1722
- {
1723
- Name: "ifInErrors",
1724
- Value: 10,
1725
- Tags: map[string]string{"interface": "eth0"},
1726
- MetricType: "rate",
1727
- IsTable: true,
1728
- },
1729
- {
1730
- Name: "ifInErrors",
1731
- Value: 5,
1732
- Tags: map[string]string{"interface": "lo0"},
1733
- MetricType: "rate",
1734
- IsTable: true,
1735
- },
1736
- },
1737
- },
1738
- },
1739
- expectedError: false,
1740
- },
1741
- "table with multiple tag columns": {
1742
- profiles: []*ddsnmp.Profile{
1743
- {
1744
- SourceFile: "test-profile.yaml",
1745
- Definition: &ddprofiledefinition.ProfileDefinition{
1746
- Metrics: []ddprofiledefinition.MetricsConfig{
1747
- {
1748
- Table: ddprofiledefinition.SymbolConfig{
1749
- OID: "1.3.6.1.2.1.2.2",
1750
- Name: "ifTable",
1751
- },
1752
- Symbols: []ddprofiledefinition.SymbolConfig{
1753
- {
1754
- OID: "1.3.6.1.2.1.2.2.1.10",
1755
- Name: "ifInOctets",
1756
- },
1757
- },
1758
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1759
- {
1760
- Tag: "interface",
1761
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1762
- OID: "1.3.6.1.2.1.2.2.1.2",
1763
- Name: "ifDescr",
1764
- },
1765
- },
1766
- {
1767
- Tag: "if_type",
1768
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1769
- OID: "1.3.6.1.2.1.2.2.1.3",
1770
- Name: "ifType",
1771
- },
1772
- },
1773
- {
1774
- Tag: "admin_status",
1775
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1776
- OID: "1.3.6.1.2.1.2.2.1.7",
1777
- Name: "ifAdminStatus",
1778
- },
1779
- Mapping: map[string]string{
1780
- "1": "up",
1781
- "2": "down",
1782
- "3": "testing",
1783
- },
1784
- },
1785
- },
1786
- },
1787
- },
1788
- },
1789
- },
1790
- },
1791
- setupMock: func(m *snmpmock.MockHandler) {
1792
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1793
- m.EXPECT().Version().Return(gosnmp.Version2c)
1794
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1795
- []gosnmp.SnmpPDU{
1796
- {
1797
- Name: "1.3.6.1.2.1.2.2.1.10.1",
1798
- Type: gosnmp.Counter32,
1799
- Value: uint(1000),
1800
- },
1801
- {
1802
- Name: "1.3.6.1.2.1.2.2.1.2.1",
1803
- Type: gosnmp.OctetString,
1804
- Value: []byte("eth0"),
1805
- },
1806
- {
1807
- Name: "1.3.6.1.2.1.2.2.1.3.1",
1808
- Type: gosnmp.Integer,
1809
- Value: 6,
1810
- },
1811
- {
1812
- Name: "1.3.6.1.2.1.2.2.1.7.1",
1813
- Type: gosnmp.Integer,
1814
- Value: 1, // up
1815
- },
1816
- }, nil,
1817
- )
1818
- },
1819
- expectedResult: []*ddsnmp.ProfileMetrics{
1820
- {
1821
- Source: "test-profile.yaml",
1822
- DeviceMetadata: nil,
1823
- Metrics: []ddsnmp.Metric{
1824
- {
1825
- Name: "ifInOctets",
1826
- Value: 1000,
1827
- Tags: map[string]string{
1828
- "interface": "eth0",
1829
- "if_type": "6",
1830
- "admin_status": "up",
1831
- },
1832
- MetricType: "rate",
1833
- IsTable: true,
1834
- },
1835
- },
1836
- },
1837
- },
1838
- expectedError: false,
1839
- },
1840
- "empty table": {
1841
- profiles: []*ddsnmp.Profile{
1842
- {
1843
- SourceFile: "test-profile.yaml",
1844
- Definition: &ddprofiledefinition.ProfileDefinition{
1845
- Metrics: []ddprofiledefinition.MetricsConfig{
1846
- {
1847
- Table: ddprofiledefinition.SymbolConfig{
1848
- OID: "1.3.6.1.2.1.2.2",
1849
- Name: "ifTable",
1850
- },
1851
- Symbols: []ddprofiledefinition.SymbolConfig{
1852
- {
1853
- OID: "1.3.6.1.2.1.2.2.1.10",
1854
- Name: "ifInOctets",
1855
- },
1856
- },
1857
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1858
- {
1859
- Tag: "interface",
1860
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1861
- OID: "1.3.6.1.2.1.2.2.1.2",
1862
- Name: "ifDescr",
1863
- },
1864
- },
1865
- },
1866
- },
1867
- },
1868
- },
1869
- },
1870
- },
1871
- setupMock: func(m *snmpmock.MockHandler) {
1872
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1873
- m.EXPECT().Version().Return(gosnmp.Version2c)
1874
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1875
- []gosnmp.SnmpPDU{}, nil,
1876
- )
1877
- },
1878
- expectedResult: []*ddsnmp.ProfileMetrics{
1879
- {
1880
- Source: "test-profile.yaml",
1881
- DeviceMetadata: nil,
1882
- Metrics: []ddsnmp.Metric{},
1883
- },
1884
- },
1885
- expectedError: false,
1886
- },
1887
- "table walk error": {
1888
- profiles: []*ddsnmp.Profile{
1889
- {
1890
- SourceFile: "test-profile.yaml",
1891
- Definition: &ddprofiledefinition.ProfileDefinition{
1892
- Metrics: []ddprofiledefinition.MetricsConfig{
1893
- {
1894
- Table: ddprofiledefinition.SymbolConfig{
1895
- OID: "1.3.6.1.2.1.2.2",
1896
- Name: "ifTable",
1897
- },
1898
- Symbols: []ddprofiledefinition.SymbolConfig{
1899
- {
1900
- OID: "1.3.6.1.2.1.2.2.1.10",
1901
- Name: "ifInOctets",
1902
- },
1903
- },
1904
- },
1905
- },
1906
- },
1907
- },
1908
- },
1909
- setupMock: func(m *snmpmock.MockHandler) {
1910
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1911
- m.EXPECT().Version().Return(gosnmp.Version2c)
1912
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1913
- nil,
1914
- errors.New("timeout walking table"),
1915
- )
1916
- },
1917
- expectedResult: nil,
1918
- expectedError: true,
1919
- errorContains: "timeout walking table",
1920
- },
1921
- "table with scale factor": {
1922
- profiles: []*ddsnmp.Profile{
1923
- {
1924
- SourceFile: "test-profile.yaml",
1925
- Definition: &ddprofiledefinition.ProfileDefinition{
1926
- Metrics: []ddprofiledefinition.MetricsConfig{
1927
- {
1928
- Table: ddprofiledefinition.SymbolConfig{
1929
- OID: "1.3.6.1.4.1.9.9.109.1.1.1",
1930
- Name: "cpmCPUTotalTable",
1931
- },
1932
- Symbols: []ddprofiledefinition.SymbolConfig{
1933
- {
1934
- OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.12",
1935
- Name: "cpmCPUMemoryUsed",
1936
- ScaleFactor: 1024, // Convert KB to bytes
1937
- },
1938
- },
1939
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1940
- {
1941
- Tag: "cpu_id",
1942
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1943
- OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.2",
1944
- Name: "cpmCPUTotalPhysicalIndex",
1945
- },
1946
- },
1947
- },
1948
- },
1949
- },
1950
- },
1951
- },
1952
- },
1953
- setupMock: func(m *snmpmock.MockHandler) {
1954
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1955
- m.EXPECT().Version().Return(gosnmp.Version2c)
1956
- m.EXPECT().BulkWalkAll("1.3.6.1.4.1.9.9.109.1.1.1").Return(
1957
- []gosnmp.SnmpPDU{
1958
- {
1959
- Name: "1.3.6.1.4.1.9.9.109.1.1.1.1.12.1",
1960
- Type: gosnmp.Gauge32,
1961
- Value: uint(2048), // 2048 KB
1962
- },
1963
- {
1964
- Name: "1.3.6.1.4.1.9.9.109.1.1.1.1.2.1",
1965
- Type: gosnmp.Integer,
1966
- Value: 1,
1967
- },
1968
- }, nil,
1969
- )
1970
- },
1971
- expectedResult: []*ddsnmp.ProfileMetrics{
1972
- {
1973
- Source: "test-profile.yaml",
1974
- DeviceMetadata: nil,
1975
- Metrics: []ddsnmp.Metric{
1976
- {
1977
- Name: "cpmCPUMemoryUsed",
1978
- Value: 2097152, // 2048 * 1024
1979
- Tags: map[string]string{"cpu_id": "1"},
1980
- MetricType: "gauge",
1981
- IsTable: true,
1982
- },
1983
- },
1984
- },
1985
- },
1986
- expectedError: false,
1987
- },
1988
- "table with snmpv1 (using WalkAll instead of BulkWalkAll)": {
1989
- profiles: []*ddsnmp.Profile{
1990
- {
1991
- SourceFile: "test-profile.yaml",
1992
- Definition: &ddprofiledefinition.ProfileDefinition{
1993
- Metrics: []ddprofiledefinition.MetricsConfig{
1994
- {
1995
- Table: ddprofiledefinition.SymbolConfig{
1996
- OID: "1.3.6.1.2.1.2.2",
1997
- Name: "ifTable",
1998
- },
1999
- Symbols: []ddprofiledefinition.SymbolConfig{
2000
- {
2001
- OID: "1.3.6.1.2.1.2.2.1.10",
2002
- Name: "ifInOctets",
2003
- },
2004
- },
2005
- MetricTags: []ddprofiledefinition.MetricTagConfig{
2006
- {
2007
- Tag: "interface",
2008
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2009
- OID: "1.3.6.1.2.1.2.2.1.2",
2010
- Name: "ifDescr",
2011
- },
2012
- },
2013
- },
2014
- },
2015
- },
2016
- },
2017
- },
2018
- },
2019
- setupMock: func(m *snmpmock.MockHandler) {
2020
- m.EXPECT().MaxOids().Return(10).AnyTimes()
2021
- m.EXPECT().Version().Return(gosnmp.Version1)
2022
- m.EXPECT().WalkAll("1.3.6.1.2.1.2.2").Return(
2023
- []gosnmp.SnmpPDU{
2024
- {
2025
- Name: "1.3.6.1.2.1.2.2.1.10.1",
2026
- Type: gosnmp.Counter32,
2027
- Value: uint(1000),
2028
- },
2029
- {
2030
- Name: "1.3.6.1.2.1.2.2.1.2.1",
2031
- Type: gosnmp.OctetString,
2032
- Value: []byte("eth0"),
2033
- },
2034
- }, nil,
2035
- )
2036
- },
2037
- expectedResult: []*ddsnmp.ProfileMetrics{
2038
- {
2039
- Source: "test-profile.yaml",
2040
- DeviceMetadata: nil,
2041
- Metrics: []ddsnmp.Metric{
2042
- {
2043
- Name: "ifInOctets",
2044
- Value: 1000,
2045
- Tags: map[string]string{"interface": "eth0"},
2046
- MetricType: "rate",
2047
- IsTable: true,
2048
- },
2049
- },
2050
- },
2051
- },
2052
- expectedError: false,
2053
- },
2054
- }
2055
-
2056
- for name, tc := range tests {
2057
- t.Run(name, func(t *testing.T) {
2058
- ctrl := gomock.NewController(t)
2059
- defer ctrl.Finish()
2060
-
2061
- mockHandler := snmpmock.NewMockHandler(ctrl)
2062
- tc.setupMock(mockHandler)
2063
-
2064
- collector := New(mockHandler, tc.profiles, logger.New())
2065
- collector.DoTableMetrics = true
2066
-
2067
- // Configure cache based on test requirements
2068
- if tc.enableCache {
2069
- collector.tableCache.setTTL(30*time.Second, 0)
2070
- } else {
2071
- collector.tableCache.setTTL(0, 0) // Disable cache
2072
- }
2073
-
2074
- result, err := collector.Collect()
2075
-
2076
- // Clear circular references
2077
- for _, profile := range result {
2078
- for i := range profile.Metrics {
2079
- profile.Metrics[i].Profile = nil
2080
- }
2081
- }
2082
-
2083
- if tc.expectedError {
2084
- assert.Error(t, err)
2085
- if tc.errorContains != "" {
2086
- assert.Contains(t, err.Error(), tc.errorContains)
2087
- }
2088
- } else {
2089
- assert.NoError(t, err)
2090
- }
2091
-
2092
- if tc.expectedResult != nil {
2093
- require.Equal(t, len(tc.expectedResult), len(result))
2094
- for i := range tc.expectedResult {
2095
- assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
2096
- assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
2097
- assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
2098
- }
2099
- } else {
2100
- assert.Nil(t, result)
2101
- }
2102
- })
2103
- }
2104
-}
2105
-
2106
-func TestCollector_Collect_CrossTableTags(t *testing.T) {
2107
- tests := map[string]struct {
2108
- profiles []*ddsnmp.Profile
2109
- setupMock func(m *snmpmock.MockHandler)
2110
- expectedResult []*ddsnmp.ProfileMetrics
2111
- expectedError bool
2112
- errorContains string
2113
- enableCache bool
2114
- }{
2115
- "cross-table tag from ifXTable": {
2116
- profiles: []*ddsnmp.Profile{
2117
- {
2118
- SourceFile: "test-profile.yaml",
2119
- Definition: &ddprofiledefinition.ProfileDefinition{
2120
- Metrics: []ddprofiledefinition.MetricsConfig{
2121
- {
2122
- Table: ddprofiledefinition.SymbolConfig{
2123
- OID: "1.3.6.1.2.1.2.2",
2124
- Name: "ifTable",
2125
- },
2126
- Symbols: []ddprofiledefinition.SymbolConfig{
2127
- {
2128
- OID: "1.3.6.1.2.1.2.2.1.14",
2129
- Name: "ifInErrors",
2130
- },
2131
- },
2132
- MetricTags: []ddprofiledefinition.MetricTagConfig{
2133
- {
2134
- Tag: "interface",
2135
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2136
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2137
- Name: "ifName",
2138
- },
2139
- Table: "ifXTable",
2140
- },
2141
- },
2142
- },
2143
- {
2144
- Table: ddprofiledefinition.SymbolConfig{
2145
- OID: "1.3.6.1.2.1.31.1.1",
2146
- Name: "ifXTable",
2147
- },
2148
- Symbols: []ddprofiledefinition.SymbolConfig{
2149
- {
2150
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2151
- Name: "ifName",
2152
- },
2153
- },
2154
- },
2155
- },
2156
- },
2157
- },
2158
- },
2159
- setupMock: func(m *snmpmock.MockHandler) {
2160
- m.EXPECT().MaxOids().Return(10).AnyTimes()
2161
- // First walk for ifTable
2162
- m.EXPECT().Version().Return(gosnmp.Version2c)
2163
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2164
- []gosnmp.SnmpPDU{
2165
- {
2166
- Name: "1.3.6.1.2.1.2.2.1.14.1",
2167
- Type: gosnmp.Counter32,
2168
- Value: uint(10),
2169
- },
2170
- {
2171
- Name: "1.3.6.1.2.1.2.2.1.14.2",
2172
- Type: gosnmp.Counter32,
2173
- Value: uint(20),
2174
- },
2175
- }, nil,
2176
- )
2177
- // Second walk for ifXTable
2178
- m.EXPECT().Version().Return(gosnmp.Version2c)
2179
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2180
- []gosnmp.SnmpPDU{
2181
- {
2182
- Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2183
- Type: gosnmp.OctetString,
2184
- Value: []byte("GigabitEthernet0/0"),
2185
- },
2186
- {
2187
- Name: "1.3.6.1.2.1.31.1.1.1.1.2",
2188
- Type: gosnmp.OctetString,
2189
- Value: []byte("GigabitEthernet0/1"),
2190
- },
2191
- }, nil,
2192
- )
2193
- },
2194
- expectedResult: []*ddsnmp.ProfileMetrics{
2195
- {
2196
- Source: "test-profile.yaml",
2197
- DeviceMetadata: nil,
2198
- Metrics: []ddsnmp.Metric{
2199
- {
2200
- Name: "ifInErrors",
2201
- Value: 10,
2202
- Tags: map[string]string{"interface": "GigabitEthernet0/0"},
2203
- MetricType: "rate",
2204
- IsTable: true,
2205
- },
2206
- {
2207
- Name: "ifInErrors",
2208
- Value: 20,
2209
- Tags: map[string]string{"interface": "GigabitEthernet0/1"},
2210
- MetricType: "rate",
2211
- IsTable: true,
2212
- },
2213
- },
2214
- },
2215
- },
2216
- expectedError: false,
2217
- },
2218
- "cross-table tag with missing values": {
2219
- profiles: []*ddsnmp.Profile{
2220
- {
2221
- SourceFile: "test-profile.yaml",
2222
- Definition: &ddprofiledefinition.ProfileDefinition{
2223
- Metrics: []ddprofiledefinition.MetricsConfig{
2224
- {
2225
- Table: ddprofiledefinition.SymbolConfig{
2226
- OID: "1.3.6.1.2.1.2.2",
2227
- Name: "ifTable",
2228
- },
2229
- Symbols: []ddprofiledefinition.SymbolConfig{
2230
- {
2231
- OID: "1.3.6.1.2.1.2.2.1.14",
2232
- Name: "ifInErrors",
2233
- },
2234
- },
2235
- MetricTags: []ddprofiledefinition.MetricTagConfig{
2236
- {
2237
- Tag: "interface",
2238
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2239
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2240
- Name: "ifName",
2241
- },
2242
- Table: "ifXTable",
2243
- },
2244
- },
2245
- },
2246
- {
2247
- Table: ddprofiledefinition.SymbolConfig{
2248
- OID: "1.3.6.1.2.1.31.1.1",
2249
- Name: "ifXTable",
2250
- },
2251
- Symbols: []ddprofiledefinition.SymbolConfig{
2252
- {
2253
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2254
- Name: "ifName",
2255
- },
2256
- },
2257
- },
2258
- },
2259
- },
2260
- },
2261
- },
2262
- setupMock: func(m *snmpmock.MockHandler) {
2263
- m.EXPECT().MaxOids().Return(10).AnyTimes()
2264
- // Walk ifTable - has 3 interfaces
2265
- m.EXPECT().Version().Return(gosnmp.Version2c)
2266
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2267
- []gosnmp.SnmpPDU{
2268
- {
2269
- Name: "1.3.6.1.2.1.2.2.1.14.1",
2270
- Type: gosnmp.Counter32,
2271
- Value: uint(10),
2272
- },
2273
- {
2274
- Name: "1.3.6.1.2.1.2.2.1.14.2",
2275
- Type: gosnmp.Counter32,
2276
- Value: uint(20),
2277
- },
2278
- {
2279
- Name: "1.3.6.1.2.1.2.2.1.14.3",
2280
- Type: gosnmp.Counter32,
2281
- Value: uint(30),
2282
- },
2283
- }, nil,
2284
- )
2285
- // Walk ifXTable - only has 2 interfaces (missing index 3)
2286
- m.EXPECT().Version().Return(gosnmp.Version2c)
2287
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2288
- []gosnmp.SnmpPDU{
2289
- {
2290
- Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2291
- Type: gosnmp.OctetString,
2292
- Value: []byte("GigabitEthernet0/0"),
2293
- },
2294
- {
2295
- Name: "1.3.6.1.2.1.31.1.1.1.1.2",
2296
- Type: gosnmp.OctetString,
2297
- Value: []byte("GigabitEthernet0/1"),
2298
- },
2299
- }, nil,
2300
- )
2301
- },
2302
- expectedResult: []*ddsnmp.ProfileMetrics{
2303
- {
2304
- Source: "test-profile.yaml",
2305
- DeviceMetadata: nil,
2306
- Metrics: []ddsnmp.Metric{
2307
- {
2308
- Name: "ifInErrors",
2309
- Value: 10,
2310
- Tags: map[string]string{"interface": "GigabitEthernet0/0"},
2311
- MetricType: "rate",
2312
- IsTable: true,
2313
- },
2314
- {
2315
- Name: "ifInErrors",
2316
- Value: 20,
2317
- Tags: map[string]string{"interface": "GigabitEthernet0/1"},
2318
- MetricType: "rate",
2319
- IsTable: true,
2320
- },
2321
- {
2322
- Name: "ifInErrors",
2323
- Value: 30,
2324
- Tags: nil, // No cross-table tag found
2325
- MetricType: "rate",
2326
- IsTable: true,
2327
- },
2328
- },
2329
- },
2330
- },
2331
- expectedError: false,
2332
- },
2333
- "multiple cross-table tags": {
2334
- profiles: []*ddsnmp.Profile{
2335
- {
2336
- SourceFile: "test-profile.yaml",
2337
- Definition: &ddprofiledefinition.ProfileDefinition{
2338
- Metrics: []ddprofiledefinition.MetricsConfig{
2339
- {
2340
- Table: ddprofiledefinition.SymbolConfig{
2341
- OID: "1.3.6.1.2.1.2.2",
2342
- Name: "ifTable",
2343
- },
2344
- Symbols: []ddprofiledefinition.SymbolConfig{
2345
- {
2346
- OID: "1.3.6.1.2.1.2.2.1.14",
2347
- Name: "ifInErrors",
2348
- },
2349
- },
2350
- MetricTags: []ddprofiledefinition.MetricTagConfig{
2351
- {
2352
- Tag: "interface_desc",
2353
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2354
- OID: "1.3.6.1.2.1.2.2.1.2",
2355
- Name: "ifDescr",
2356
- },
2357
- },
2358
- {
2359
- Tag: "interface_name",
2360
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2361
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2362
- Name: "ifName",
2363
- },
2364
- Table: "ifXTable",
2365
- },
2366
- {
2367
- Tag: "interface_alias",
2368
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2369
- OID: "1.3.6.1.2.1.31.1.1.1.18",
2370
- Name: "ifAlias",
2371
- },
2372
- Table: "ifXTable",
2373
- },
2374
- },
2375
- },
2376
- {
2377
- Table: ddprofiledefinition.SymbolConfig{
2378
- OID: "1.3.6.1.2.1.31.1.1",
2379
- Name: "ifXTable",
2380
- },
2381
- Symbols: []ddprofiledefinition.SymbolConfig{
2382
- {
2383
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2384
- Name: "ifName",
2385
- },
2386
- {
2387
- OID: "1.3.6.1.2.1.31.1.1.1.18",
2388
- Name: "ifAlias",
2389
- },
2390
- },
2391
- },
2392
- },
2393
- },
2394
- },
2395
- },
2396
- setupMock: func(m *snmpmock.MockHandler) {
2397
- m.EXPECT().MaxOids().Return(10).AnyTimes()
2398
- // Walk ifTable
2399
- m.EXPECT().Version().Return(gosnmp.Version2c)
2400
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2401
- []gosnmp.SnmpPDU{
2402
- {
2403
- Name: "1.3.6.1.2.1.2.2.1.14.1",
2404
- Type: gosnmp.Counter32,
2405
- Value: uint(10),
2406
- },
2407
- {
2408
- Name: "1.3.6.1.2.1.2.2.1.2.1",
2409
- Type: gosnmp.OctetString,
2410
- Value: []byte("eth0"),
2411
- },
2412
- }, nil,
2413
- )
2414
- // Walk ifXTable
2415
- m.EXPECT().Version().Return(gosnmp.Version2c)
2416
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2417
- []gosnmp.SnmpPDU{
2418
- {
2419
- Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2420
- Type: gosnmp.OctetString,
2421
- Value: []byte("GigabitEthernet0/0"),
2422
- },
2423
- {
2424
- Name: "1.3.6.1.2.1.31.1.1.1.18.1",
2425
- Type: gosnmp.OctetString,
2426
- Value: []byte("Uplink to Core"),
2427
- },
2428
- }, nil,
2429
- )
2430
- },
2431
- expectedResult: []*ddsnmp.ProfileMetrics{
2432
- {
2433
- Source: "test-profile.yaml",
2434
- DeviceMetadata: nil,
2435
- Metrics: []ddsnmp.Metric{
2436
- {
2437
- Name: "ifInErrors",
2438
- Value: 10,
2439
- Tags: map[string]string{
2440
- "interface_desc": "eth0",
2441
- "interface_name": "GigabitEthernet0/0",
2442
- "interface_alias": "Uplink to Core",
2443
- },
2444
- MetricType: "rate",
2445
- IsTable: true,
2446
- },
2447
- },
2448
- },
2449
- },
2450
- expectedError: false,
2451
- },
2452
- "cross-table tag with mapping": {
2453
- profiles: []*ddsnmp.Profile{
2454
- {
2455
- SourceFile: "test-profile.yaml",
2456
- Definition: &ddprofiledefinition.ProfileDefinition{
2457
- Metrics: []ddprofiledefinition.MetricsConfig{
2458
- {
2459
- Table: ddprofiledefinition.SymbolConfig{
2460
- OID: "1.3.6.1.4.1.9.9.276.1.1.2",
2461
- Name: "cieIfInterfaceTable",
2462
- },
2463
- Symbols: []ddprofiledefinition.SymbolConfig{
2464
- {
2465
- OID: "1.3.6.1.4.1.9.9.276.1.1.2.1.1",
2466
- Name: "cieIfResetCount",
2467
- },
2468
- },
2469
- MetricTags: []ddprofiledefinition.MetricTagConfig{
2470
- {
2471
- Tag: "interface",
2472
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2473
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2474
- Name: "ifName",
2475
- },
2476
- Table: "ifXTable",
2477
- },
2478
- {
2479
- Tag: "oper_status",
2480
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2481
- OID: "1.3.6.1.2.1.2.2.1.8",
2482
- Name: "ifOperStatus",
2483
- },
2484
- Table: "ifTable",
2485
- Mapping: map[string]string{
2486
- "1": "up",
2487
- "2": "down",
2488
- "3": "testing",
2489
- },
2490
- },
2491
- },
2492
- },
2493
- {
2494
- Table: ddprofiledefinition.SymbolConfig{
2495
- OID: "1.3.6.1.2.1.31.1.1",
2496
- Name: "ifXTable",
2497
- },
2498
- Symbols: []ddprofiledefinition.SymbolConfig{
2499
- {
2500
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2501
- Name: "ifName",
2502
- },
2503
- },
2504
- },
2505
- {
2506
- Table: ddprofiledefinition.SymbolConfig{
2507
- OID: "1.3.6.1.2.1.2.2",
2508
- Name: "ifTable",
2509
- },
2510
- Symbols: []ddprofiledefinition.SymbolConfig{
2511
- {
2512
- OID: "1.3.6.1.2.1.2.2.1.8",
2513
- Name: "ifOperStatus",
2514
- },
2515
- },
2516
- },
2517
- },
2518
- },
2519
- },
2520
- },
2521
- setupMock: func(m *snmpmock.MockHandler) {
2522
- m.EXPECT().MaxOids().Return(10).AnyTimes()
2523
- // Walk cieIfInterfaceTable
2524
- m.EXPECT().Version().Return(gosnmp.Version2c)
2525
- m.EXPECT().BulkWalkAll("1.3.6.1.4.1.9.9.276.1.1.2").Return(
2526
- []gosnmp.SnmpPDU{
2527
- {
2528
- Name: "1.3.6.1.4.1.9.9.276.1.1.2.1.1.1",
2529
- Type: gosnmp.Counter32,
2530
- Value: uint(5),
2531
- },
2532
- {
2533
- Name: "1.3.6.1.4.1.9.9.276.1.1.2.1.1.2",
2534
- Type: gosnmp.Counter32,
2535
- Value: uint(3),
2536
- },
2537
- }, nil,
2538
- )
2539
- // Walk ifXTable
2540
- m.EXPECT().Version().Return(gosnmp.Version2c)
2541
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2542
- []gosnmp.SnmpPDU{
2543
- {
2544
- Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2545
- Type: gosnmp.OctetString,
2546
- Value: []byte("GigabitEthernet0/0"),
2547
- },
2548
- {
2549
- Name: "1.3.6.1.2.1.31.1.1.1.1.2",
2550
- Type: gosnmp.OctetString,
2551
- Value: []byte("GigabitEthernet0/1"),
2552
- },
2553
- }, nil,
2554
- )
2555
- // Walk ifTable
2556
- m.EXPECT().Version().Return(gosnmp.Version2c)
2557
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2558
- []gosnmp.SnmpPDU{
2559
- {
2560
- Name: "1.3.6.1.2.1.2.2.1.8.1",
2561
- Type: gosnmp.Integer,
2562
- Value: 1, // up
2563
- },
2564
- {
2565
- Name: "1.3.6.1.2.1.2.2.1.8.2",
2566
- Type: gosnmp.Integer,
2567
- Value: 2, // down
2568
- },
2569
- }, nil,
2570
- )
2571
- },
2572
- expectedResult: []*ddsnmp.ProfileMetrics{
2573
- {
2574
- Source: "test-profile.yaml",
2575
- DeviceMetadata: nil,
2576
- Metrics: []ddsnmp.Metric{
2577
- {
2578
- Name: "cieIfResetCount",
2579
- Value: 5,
2580
- Tags: map[string]string{
2581
- "interface": "GigabitEthernet0/0",
2582
- "oper_status": "up",
2583
- },
2584
- MetricType: "rate",
2585
- IsTable: true,
2586
- },
2587
- {
2588
- Name: "cieIfResetCount",
2589
- Value: 3,
2590
- Tags: map[string]string{
2591
- "interface": "GigabitEthernet0/1",
2592
- "oper_status": "down",
2593
- },
2594
- MetricType: "rate",
2595
- IsTable: true,
2596
- },
2597
- {
2598
- Name: "ifOperStatus",
2599
- Value: 1,
2600
- MetricType: "gauge",
2601
- IsTable: true,
2602
- },
2603
- {
2604
- Name: "ifOperStatus",
2605
- Value: 2,
2606
- MetricType: "gauge",
2607
- IsTable: true,
2608
- },
2609
- },
2610
- },
2611
- },
2612
- expectedError: false,
2613
- },
2614
- "cross-table tag when referenced table not walked": {
2615
- profiles: []*ddsnmp.Profile{
2616
- {
2617
- SourceFile: "test-profile.yaml",
2618
- Definition: &ddprofiledefinition.ProfileDefinition{
2619
- Metrics: []ddprofiledefinition.MetricsConfig{
2620
- {
2621
- Table: ddprofiledefinition.SymbolConfig{
2622
- OID: "1.3.6.1.2.1.2.2",
2623
- Name: "ifTable",
2624
- },
2625
- Symbols: []ddprofiledefinition.SymbolConfig{
2626
- {
2627
- OID: "1.3.6.1.2.1.2.2.1.14",
2628
- Name: "ifInErrors",
2629
- },
2630
- },
2631
- MetricTags: []ddprofiledefinition.MetricTagConfig{
2632
- {
2633
- Tag: "interface",
2634
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2635
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2636
- Name: "ifName",
2637
- },
2638
- Table: "ifXTable",
2639
- },
2640
- },
2641
- },
2642
- // Note: ifXTable is NOT in the metrics list
2643
- },
2644
- },
2645
- },
2646
- },
2647
- setupMock: func(m *snmpmock.MockHandler) {
2648
- m.EXPECT().MaxOids().Return(10).AnyTimes()
2649
- // Only walk ifTable
2650
- m.EXPECT().Version().Return(gosnmp.Version2c)
2651
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2652
- []gosnmp.SnmpPDU{
2653
- {
2654
- Name: "1.3.6.1.2.1.2.2.1.14.1",
2655
- Type: gosnmp.Counter32,
2656
- Value: uint(10),
2657
- },
2658
- {
2659
- Name: "1.3.6.1.2.1.2.2.1.14.2",
2660
- Type: gosnmp.Counter32,
2661
- Value: uint(20),
2662
- },
2663
- }, nil,
2664
- )
2665
- },
2666
- expectedResult: []*ddsnmp.ProfileMetrics{
2667
- {
2668
- Source: "test-profile.yaml",
2669
- DeviceMetadata: nil,
2670
- Metrics: []ddsnmp.Metric{
2671
- {
2672
- Name: "ifInErrors",
2673
- Value: 10,
2674
- Tags: nil, // No cross-table tag because ifXTable wasn't walked
2675
- MetricType: "rate",
2676
- IsTable: true,
2677
- },
2678
- {
2679
- Name: "ifInErrors",
2680
- Value: 20,
2681
- Tags: nil,
2682
- MetricType: "rate",
2683
- IsTable: true,
2684
- },
2685
- },
2686
- },
2687
- },
2688
- expectedError: false,
2689
- },
2690
- "cross-table tag with extract_value": {
2691
- profiles: []*ddsnmp.Profile{
2692
- {
2693
- SourceFile: "test-profile.yaml",
2694
- Definition: &ddprofiledefinition.ProfileDefinition{
2695
- Metrics: []ddprofiledefinition.MetricsConfig{
2696
- {
2697
- Table: ddprofiledefinition.SymbolConfig{
2698
- OID: "1.3.6.1.2.1.2.2",
2699
- Name: "ifTable",
2700
- },
2701
- Symbols: []ddprofiledefinition.SymbolConfig{
2702
- {
2703
- OID: "1.3.6.1.2.1.2.2.1.10",
2704
- Name: "ifInOctets",
2705
- },
2706
- },
2707
- MetricTags: []ddprofiledefinition.MetricTagConfig{
2708
- {
2709
- Tag: "interface",
2710
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2711
- OID: "1.3.6.1.2.1.31.1.1.1.18",
2712
- Name: "ifAlias",
2713
- ExtractValueCompiled: mustCompileRegex(`\[(\w+)\]`), // Extract value in brackets
2714
- },
2715
- Table: "ifXTable",
2716
- },
2717
- },
2718
- },
2719
- {
2720
- Table: ddprofiledefinition.SymbolConfig{
2721
- OID: "1.3.6.1.2.1.31.1.1",
2722
- Name: "ifXTable",
2723
- },
2724
- Symbols: []ddprofiledefinition.SymbolConfig{
2725
- {
2726
- OID: "1.3.6.1.2.1.31.1.1.1.18",
2727
- Name: "ifAlias",
2728
- },
2729
- },
2730
- },
2731
- },
2732
- },
2733
- },
2734
- },
2735
- setupMock: func(m *snmpmock.MockHandler) {
2736
- m.EXPECT().MaxOids().Return(10).AnyTimes()
2737
- // Walk ifTable
2738
- m.EXPECT().Version().Return(gosnmp.Version2c)
2739
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2740
- []gosnmp.SnmpPDU{
2741
- {
2742
- Name: "1.3.6.1.2.1.2.2.1.10.1",
2743
- Type: gosnmp.Counter32,
2744
- Value: uint(1000),
2745
- },
2746
- }, nil,
2747
- )
2748
- // Walk ifXTable
2749
- m.EXPECT().Version().Return(gosnmp.Version2c)
2750
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2751
- []gosnmp.SnmpPDU{
2752
- {
2753
- Name: "1.3.6.1.2.1.31.1.1.1.18.1",
2754
- Type: gosnmp.OctetString,
2755
- Value: []byte("Connection to [CORE-SW1] port 24"),
2756
- },
2757
- }, nil,
2758
- )
2759
- },
2760
- expectedResult: []*ddsnmp.ProfileMetrics{
2761
- {
2762
- Source: "test-profile.yaml",
2763
- DeviceMetadata: nil,
2764
- Metrics: []ddsnmp.Metric{
2765
- {
2766
- Name: "ifInOctets",
2767
- Value: 1000,
2768
- Tags: nil,
2769
- MetricType: "rate",
2770
- IsTable: true,
2771
- },
2772
- },
2773
- },
2774
- },
2775
- expectedError: false,
2776
- },
2777
- "same and cross-table tags combined": {
2778
- profiles: []*ddsnmp.Profile{
2779
- {
2780
- SourceFile: "test-profile.yaml",
2781
- Definition: &ddprofiledefinition.ProfileDefinition{
2782
- Metrics: []ddprofiledefinition.MetricsConfig{
2783
- {
2784
- Table: ddprofiledefinition.SymbolConfig{
2785
- OID: "1.3.6.1.2.1.2.2",
2786
- Name: "ifTable",
2787
- },
2788
- Symbols: []ddprofiledefinition.SymbolConfig{
2789
- {
2790
- OID: "1.3.6.1.2.1.2.2.1.14",
2791
- Name: "ifInErrors",
2792
- },
2793
- },
2794
- MetricTags: []ddprofiledefinition.MetricTagConfig{
2795
- {
2796
- Tag: "if_desc",
2797
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2798
- OID: "1.3.6.1.2.1.2.2.1.2",
2799
- Name: "ifDescr",
2800
- },
2801
- // No Table field = same table
2802
- },
2803
- {
2804
- Tag: "if_name",
2805
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2806
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2807
- Name: "ifName",
2808
- },
2809
- Table: "ifXTable",
2810
- },
2811
- {
2812
- Tag: "if_type",
2813
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2814
- OID: "1.3.6.1.2.1.2.2.1.3",
2815
- Name: "ifType",
2816
- },
2817
- // No Table field = same table
2818
- Mapping: map[string]string{
2819
- "6": "ethernet",
2820
- "24": "loopback",
2821
- },
2822
- },
2823
- },
2824
- },
2825
- {
2826
- Table: ddprofiledefinition.SymbolConfig{
2827
- OID: "1.3.6.1.2.1.31.1.1",
2828
- Name: "ifXTable",
2829
- },
2830
- Symbols: []ddprofiledefinition.SymbolConfig{
2831
- {
2832
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2833
- Name: "ifName",
2834
- },
2835
- },
2836
- },
2837
- },
2838
- },
2839
- },
2840
- },
2841
- setupMock: func(m *snmpmock.MockHandler) {
2842
- m.EXPECT().MaxOids().Return(10).AnyTimes()
2843
- // Walk ifTable
2844
- m.EXPECT().Version().Return(gosnmp.Version2c)
2845
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2846
- []gosnmp.SnmpPDU{
2847
- {
2848
- Name: "1.3.6.1.2.1.2.2.1.14.1",
2849
- Type: gosnmp.Counter32,
2850
- Value: uint(10),
2851
- },
2852
- {
2853
- Name: "1.3.6.1.2.1.2.2.1.2.1",
2854
- Type: gosnmp.OctetString,
2855
- Value: []byte("Ethernet Interface"),
2856
- },
2857
- {
2858
- Name: "1.3.6.1.2.1.2.2.1.3.1",
2859
- Type: gosnmp.Integer,
2860
- Value: 6,
2861
- },
2862
- }, nil,
2863
- )
2864
- // Walk ifXTable
2865
- m.EXPECT().Version().Return(gosnmp.Version2c)
2866
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2867
- []gosnmp.SnmpPDU{
2868
- {
2869
- Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2870
- Type: gosnmp.OctetString,
2871
- Value: []byte("GigabitEthernet0/0"),
2872
- },
2873
- }, nil,
2874
- )
2875
- },
2876
- expectedResult: []*ddsnmp.ProfileMetrics{
2877
- {
2878
- Source: "test-profile.yaml",
2879
- DeviceMetadata: nil,
2880
- Metrics: []ddsnmp.Metric{
2881
- {
2882
- Name: "ifInErrors",
2883
- Value: 10,
2884
- Tags: map[string]string{
2885
- "if_desc": "Ethernet Interface",
2886
- "if_name": "GigabitEthernet0/0",
2887
- "if_type": "ethernet",
2888
- },
2889
- MetricType: "rate",
2890
- IsTable: true,
2891
- },
2892
- },
2893
- },
2894
- },
2895
- expectedError: false,
2896
- },
2897
- "cross-table with caching disabled": {
2898
- profiles: []*ddsnmp.Profile{
2899
- {
2900
- SourceFile: "test-profile.yaml",
2901
- Definition: &ddprofiledefinition.ProfileDefinition{
2902
- Metrics: []ddprofiledefinition.MetricsConfig{
2903
- {
2904
- Table: ddprofiledefinition.SymbolConfig{
2905
- OID: "1.3.6.1.2.1.2.2",
2906
- Name: "ifTable",
2907
- },
2908
- Symbols: []ddprofiledefinition.SymbolConfig{
2909
- {
2910
- OID: "1.3.6.1.2.1.2.2.1.14",
2911
- Name: "ifInErrors",
2912
- },
2913
- },
2914
- MetricTags: []ddprofiledefinition.MetricTagConfig{
2915
- {
2916
- Tag: "interface",
2917
- Symbol: ddprofiledefinition.SymbolConfigCompat{
2918
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2919
- Name: "ifName",
2920
- },
2921
- Table: "ifXTable",
2922
- },
2923
- },
2924
- },
2925
- {
2926
- Table: ddprofiledefinition.SymbolConfig{
2927
- OID: "1.3.6.1.2.1.31.1.1",
2928
- Name: "ifXTable",
2929
- },
2930
- Symbols: []ddprofiledefinition.SymbolConfig{
2931
- {
2932
- OID: "1.3.6.1.2.1.31.1.1.1.1",
2933
- Name: "ifName",
2934
- },
2935
- },
2936
- },
2937
- },
2938
- },
2939
- },
2940
- },
2941
- setupMock: func(m *snmpmock.MockHandler) {
2942
- m.EXPECT().MaxOids().Return(10).AnyTimes()
2943
- // Walk ifTable
2944
- m.EXPECT().Version().Return(gosnmp.Version2c)
2945
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2946
- []gosnmp.SnmpPDU{
2947
- {
2948
- Name: "1.3.6.1.2.1.2.2.1.14.1",
2949
- Type: gosnmp.Counter32,
2950
- Value: uint(10),
2951
- },
2952
- }, nil,
2953
- )
2954
- // Walk ifXTable
2955
- m.EXPECT().Version().Return(gosnmp.Version2c)
2956
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2957
- []gosnmp.SnmpPDU{
2958
- {
2959
- Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2960
- Type: gosnmp.OctetString,
2961
- Value: []byte("GigabitEthernet0/0"),
2962
- },
2963
- }, nil,
2964
- )
2965
- },
2966
- expectedResult: []*ddsnmp.ProfileMetrics{
2967
- {
2968
- Source: "test-profile.yaml",
2969
- DeviceMetadata: nil,
2970
- Metrics: []ddsnmp.Metric{
2971
- {
2972
- Name: "ifInErrors",
2973
- Value: 10,
2974
- Tags: map[string]string{"interface": "GigabitEthernet0/0"},
2975
- MetricType: "rate",
2976
- IsTable: true,
2977
- },
2978
- },
2979
- },
2980
- },
2981
- expectedError: false,
2982
- enableCache: false, // Explicitly disable cache
2983
- },
2984
- }
2985
-
2986
- for name, tc := range tests {
2987
- t.Run(name, func(t *testing.T) {
2988
- ctrl := gomock.NewController(t)
2989
- defer ctrl.Finish()
2990
-
2991
- mockHandler := snmpmock.NewMockHandler(ctrl)
2992
- tc.setupMock(mockHandler)
2993
-
2994
- collector := New(mockHandler, tc.profiles, logger.New())
2995
- collector.DoTableMetrics = true
2996
-
2997
- // Configure cache based on test requirements
2998
- if tc.enableCache {
2999
- collector.tableCache.setTTL(30*time.Second, 0)
3000
- } else {
3001
- collector.tableCache.setTTL(0, 0) // Disable cache
3002
- }
3003
-
3004
- result, err := collector.Collect()
3005
-
3006
- // Clear circular references
3007
- for _, profile := range result {
3008
- for i := range profile.Metrics {
3009
- profile.Metrics[i].Profile = nil
3010
- }
3011
- }
3012
-
3013
- if tc.expectedError {
3014
- assert.Error(t, err)
3015
- if tc.errorContains != "" {
3016
- assert.Contains(t, err.Error(), tc.errorContains)
3017
- }
3018
- } else {
3019
- assert.NoError(t, err)
3020
- }
3021
-
3022
- if tc.expectedResult != nil {
3023
- require.Equal(t, len(tc.expectedResult), len(result))
3024
- for i := range tc.expectedResult {
3025
- assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
3026
- assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
3027
- assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
3028
- }
3029
- } else {
3030
- assert.Nil(t, result)
3031
- }
3032
- })
3033
- }
3034
-}
3035
-
3036
-func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3037
- tests := map[string]struct {
3038
- profiles []*ddsnmp.Profile
3039
- setupMock func(m *snmpmock.MockHandler)
3040
- expectedResult []*ddsnmp.ProfileMetrics
3041
- expectedError bool
3042
- errorContains string
3043
- enableCache bool
3044
- }{
3045
- // Index-based Tags Tests
3046
- "basic index tag": {
3047
- profiles: []*ddsnmp.Profile{
3048
- {
3049
- SourceFile: "test-profile.yaml",
3050
- Definition: &ddprofiledefinition.ProfileDefinition{
3051
- Metrics: []ddprofiledefinition.MetricsConfig{
3052
- {
3053
- Table: ddprofiledefinition.SymbolConfig{
3054
- OID: "1.3.6.1.2.1.4.31.1",
3055
- Name: "ipSystemStatsTable",
3056
- },
3057
- Symbols: []ddprofiledefinition.SymbolConfig{
3058
- {
3059
- OID: "1.3.6.1.2.1.4.31.1.1.4",
3060
- Name: "ipSystemStatsHCInReceives",
3061
- },
3062
- },
3063
- MetricTags: []ddprofiledefinition.MetricTagConfig{
3064
- {
3065
- Index: 1,
3066
- Tag: "ip_version",
3067
- },
3068
- },
3069
- },
3070
- },
3071
- },
3072
- },
3073
- },
3074
- setupMock: func(m *snmpmock.MockHandler) {
3075
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3076
- m.EXPECT().Version().Return(gosnmp.Version2c)
3077
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.4.31.1").Return(
3078
- []gosnmp.SnmpPDU{
3079
- {
3080
- Name: "1.3.6.1.2.1.4.31.1.1.4.1", // IPv4
3081
- Type: gosnmp.Counter64,
3082
- Value: uint64(1000),
3083
- },
3084
- {
3085
- Name: "1.3.6.1.2.1.4.31.1.1.4.2", // IPv6
3086
- Type: gosnmp.Counter64,
3087
- Value: uint64(2000),
3088
- },
3089
- }, nil,
3090
- )
3091
- },
3092
- expectedResult: []*ddsnmp.ProfileMetrics{
3093
- {
3094
- Source: "test-profile.yaml",
3095
- DeviceMetadata: nil,
3096
- Metrics: []ddsnmp.Metric{
3097
- {
3098
- Name: "ipSystemStatsHCInReceives",
3099
- Value: 1000,
3100
- Tags: map[string]string{"ip_version": "1"},
3101
- MetricType: "rate",
3102
- IsTable: true,
3103
- },
3104
- {
3105
- Name: "ipSystemStatsHCInReceives",
3106
- Value: 2000,
3107
- Tags: map[string]string{"ip_version": "2"},
3108
- MetricType: "rate",
3109
- IsTable: true,
3110
- },
3111
- },
3112
- },
3113
- },
3114
- expectedError: false,
3115
- },
3116
- "multiple index positions": {
3117
- profiles: []*ddsnmp.Profile{
3118
- {
3119
- SourceFile: "test-profile.yaml",
3120
- Definition: &ddprofiledefinition.ProfileDefinition{
3121
- Metrics: []ddprofiledefinition.MetricsConfig{
3122
- {
3123
- Table: ddprofiledefinition.SymbolConfig{
3124
- OID: "1.3.6.1.4.1.9.9.147.1.2.2.2",
3125
- Name: "cfwConnectionStatTable",
3126
- },
3127
- Symbols: []ddprofiledefinition.SymbolConfig{
3128
- {
3129
- OID: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5",
3130
- Name: "cfwConnectionStatValue",
3131
- },
3132
- },
3133
- MetricTags: []ddprofiledefinition.MetricTagConfig{
3134
- {
3135
- Index: 1,
3136
- Tag: "service_type",
3137
- },
3138
- {
3139
- Index: 2,
3140
- Tag: "stat_type",
3141
- },
3142
- },
3143
- },
3144
- },
3145
- },
3146
- },
3147
- },
3148
- setupMock: func(m *snmpmock.MockHandler) {
3149
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3150
- m.EXPECT().Version().Return(gosnmp.Version2c)
3151
- m.EXPECT().BulkWalkAll("1.3.6.1.4.1.9.9.147.1.2.2.2").Return(
3152
- []gosnmp.SnmpPDU{
3153
- {
3154
- Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.20.2", // service_type=20, stat_type=2
3155
- Type: gosnmp.Counter64,
3156
- Value: uint64(100),
3157
- },
3158
- {
3159
- Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.21.3", // service_type=21, stat_type=3
3160
- Type: gosnmp.Counter64,
3161
- Value: uint64(200),
3162
- },
3163
- {
3164
- Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.20.3", // service_type=20, stat_type=3
3165
- Type: gosnmp.Counter64,
3166
- Value: uint64(300),
3167
- },
3168
- }, nil,
3169
- )
3170
- },
3171
- expectedResult: []*ddsnmp.ProfileMetrics{
3172
- {
3173
- Source: "test-profile.yaml",
3174
- DeviceMetadata: nil,
3175
- Metrics: []ddsnmp.Metric{
3176
- {
3177
- Name: "cfwConnectionStatValue",
3178
- Value: 100,
3179
- Tags: map[string]string{"service_type": "20", "stat_type": "2"},
3180
- MetricType: "rate",
3181
- IsTable: true,
3182
- },
3183
- {
3184
- Name: "cfwConnectionStatValue",
3185
- Value: 200,
3186
- Tags: map[string]string{"service_type": "21", "stat_type": "3"},
3187
- MetricType: "rate",
3188
- IsTable: true,
3189
- },
3190
- {
3191
- Name: "cfwConnectionStatValue",
3192
- Value: 300,
3193
- Tags: map[string]string{"service_type": "20", "stat_type": "3"},
3194
- MetricType: "rate",
3195
- IsTable: true,
3196
- },
3197
- },
3198
- },
3199
- },
3200
- expectedError: false,
3201
- },
3202
- "index tag with mapping": {
3203
- profiles: []*ddsnmp.Profile{
3204
- {
3205
- SourceFile: "test-profile.yaml",
3206
- Definition: &ddprofiledefinition.ProfileDefinition{
3207
- Metrics: []ddprofiledefinition.MetricsConfig{
3208
- {
3209
- Table: ddprofiledefinition.SymbolConfig{
3210
- OID: "1.3.6.1.2.1.4.31.1",
3211
- Name: "ipSystemStatsTable",
3212
- },
3213
- Symbols: []ddprofiledefinition.SymbolConfig{
3214
- {
3215
- OID: "1.3.6.1.2.1.4.31.1.1.4",
3216
- Name: "ipSystemStatsHCInReceives",
3217
- },
3218
- },
3219
- MetricTags: []ddprofiledefinition.MetricTagConfig{
3220
- {
3221
- Index: 1,
3222
- Tag: "ip_version",
3223
- Mapping: map[string]string{
3224
- "0": "unknown",
3225
- "1": "ipv4",
3226
- "2": "ipv6",
3227
- "3": "ipv4z",
3228
- "4": "ipv6z",
3229
- },
3230
- },
3231
- },
3232
- },
3233
- },
3234
- },
3235
- },
3236
- },
3237
- setupMock: func(m *snmpmock.MockHandler) {
3238
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3239
- m.EXPECT().Version().Return(gosnmp.Version2c)
3240
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.4.31.1").Return(
3241
- []gosnmp.SnmpPDU{
3242
- {
3243
- Name: "1.3.6.1.2.1.4.31.1.1.4.1", // IPv4
3244
- Type: gosnmp.Counter64,
3245
- Value: uint64(1000),
3246
- },
3247
- {
3248
- Name: "1.3.6.1.2.1.4.31.1.1.4.2", // IPv6
3249
- Type: gosnmp.Counter64,
3250
- Value: uint64(2000),
3251
- },
3252
- {
3253
- Name: "1.3.6.1.2.1.4.31.1.1.4.0", // Unknown
3254
- Type: gosnmp.Counter64,
3255
- Value: uint64(10),
3256
- },
3257
- }, nil,
3258
- )
3259
- },
3260
- expectedResult: []*ddsnmp.ProfileMetrics{
3261
- {
3262
- Source: "test-profile.yaml",
3263
- DeviceMetadata: nil,
3264
- Metrics: []ddsnmp.Metric{
3265
- {
3266
- Name: "ipSystemStatsHCInReceives",
3267
- Value: 1000,
3268
- Tags: map[string]string{"ip_version": "ipv4"},
3269
- MetricType: "rate",
3270
- IsTable: true,
3271
- },
3272
- {
3273
- Name: "ipSystemStatsHCInReceives",
3274
- Value: 2000,
3275
- Tags: map[string]string{"ip_version": "ipv6"},
3276
- MetricType: "rate",
3277
- IsTable: true,
3278
- },
3279
- {
3280
- Name: "ipSystemStatsHCInReceives",
3281
- Value: 10,
3282
- Tags: map[string]string{"ip_version": "unknown"},
3283
- MetricType: "rate",
3284
- IsTable: true,
3285
- },
3286
- },
3287
- },
3288
- },
3289
- expectedError: false,
3290
- },
3291
- "index tag with missing position": {
3292
- profiles: []*ddsnmp.Profile{
3293
- {
3294
- SourceFile: "test-profile.yaml",
3295
- Definition: &ddprofiledefinition.ProfileDefinition{
3296
- Metrics: []ddprofiledefinition.MetricsConfig{
3297
- {
3298
- Table: ddprofiledefinition.SymbolConfig{
3299
- OID: "1.3.6.1.2.1.2.2",
3300
- Name: "ifTable",
3301
- },
3302
- Symbols: []ddprofiledefinition.SymbolConfig{
3303
- {
3304
- OID: "1.3.6.1.2.1.2.2.1.10",
3305
- Name: "ifInOctets",
3306
- },
3307
- },
3308
- MetricTags: []ddprofiledefinition.MetricTagConfig{
3309
- {
3310
- Index: 1,
3311
- Tag: "interface_index",
3312
- },
3313
- {
3314
- Index: 2,
3315
- Tag: "sub_interface", // Won't exist for single index
3316
- },
3317
- },
3318
- },
3319
- },
3320
- },
3321
- },
3322
- },
3323
- setupMock: func(m *snmpmock.MockHandler) {
3324
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3325
- m.EXPECT().Version().Return(gosnmp.Version2c)
3326
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3327
- []gosnmp.SnmpPDU{
3328
- {
3329
- Name: "1.3.6.1.2.1.2.2.1.10.1", // Single index
3330
- Type: gosnmp.Counter32,
3331
- Value: uint(1000),
3332
- },
3333
- {
3334
- Name: "1.3.6.1.2.1.2.2.1.10.2.3", // Multi-part index
3335
- Type: gosnmp.Counter32,
3336
- Value: uint(2000),
3337
- },
3338
- }, nil,
3339
- )
3340
- },
3341
- expectedResult: []*ddsnmp.ProfileMetrics{
3342
- {
3343
- Source: "test-profile.yaml",
3344
- DeviceMetadata: nil,
3345
- Metrics: []ddsnmp.Metric{
3346
- {
3347
- Name: "ifInOctets",
3348
- Value: 1000,
3349
- Tags: map[string]string{"interface_index": "1"}, // No sub_interface tag
3350
- MetricType: "rate",
3351
- IsTable: true,
3352
- },
3353
- {
3354
- Name: "ifInOctets",
3355
- Value: 2000,
3356
- Tags: map[string]string{"interface_index": "2", "sub_interface": "3"},
3357
- MetricType: "rate",
3358
- IsTable: true,
3359
- },
3360
- },
3361
- },
3362
- },
3363
- expectedError: false,
3364
- },
3365
-
3366
- // Index Transformation Tests
3367
- "basic index transform": {
3368
- profiles: []*ddsnmp.Profile{
3369
- {
3370
- SourceFile: "test-profile.yaml",
3371
- Definition: &ddprofiledefinition.ProfileDefinition{
3372
- Metrics: []ddprofiledefinition.MetricsConfig{
3373
- {
3374
- Table: ddprofiledefinition.SymbolConfig{
3375
- OID: "1.3.6.1.4.1.30932.1.10.1.3.110",
3376
- Name: "cpiPduBranchTable",
3377
- },
3378
- Symbols: []ddprofiledefinition.SymbolConfig{
3379
- {
3380
- OID: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3",
3381
- Name: "cpiPduBranchCurrent",
3382
- },
3383
- },
3384
- MetricTags: []ddprofiledefinition.MetricTagConfig{
3385
- {
3386
- Symbol: ddprofiledefinition.SymbolConfigCompat{
3387
- OID: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3",
3388
- Name: "cpiPduName",
3389
- },
3390
- Table: "cpiPduTable",
3391
- Tag: "pdu_name",
3392
- IndexTransform: []ddprofiledefinition.MetricIndexTransform{
3393
- {Start: 1, End: 7}, // Extract MAC address portion
3394
- },
3395
- },
3396
- },
3397
- },
3398
- {
3399
- Table: ddprofiledefinition.SymbolConfig{
3400
- OID: "1.3.6.1.4.1.30932.1.10.1.2.10",
3401
- Name: "cpiPduTable",
3402
- },
3403
- Symbols: []ddprofiledefinition.SymbolConfig{
3404
- {
3405
- OID: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3",
3406
- Name: "cpiPduName",
3407
- },
3408
- },
3409
- },
3410
- },
3411
- },
3412
- },
3413
- },
3414
- setupMock: func(m *snmpmock.MockHandler) {
3415
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3416
- // Walk cpiPduBranchTable
3417
- m.EXPECT().Version().Return(gosnmp.Version2c)
3418
- m.EXPECT().BulkWalkAll("1.3.6.1.4.1.30932.1.10.1.3.110").Return(
3419
- []gosnmp.SnmpPDU{
3420
- {
3421
- Name: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3.1.6.0.36.155.53.3.246", // Branch ID + MAC
3422
- Type: gosnmp.Gauge32,
3423
- Value: uint(150),
3424
- },
3425
- }, nil,
3426
- )
3427
- // Walk cpiPduTable
3428
- m.EXPECT().Version().Return(gosnmp.Version2c)
3429
- m.EXPECT().BulkWalkAll("1.3.6.1.4.1.30932.1.10.1.2.10").Return(
3430
- []gosnmp.SnmpPDU{
3431
- {
3432
- Name: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3.6.0.36.155.53.3.246", // Just MAC
3433
- Type: gosnmp.OctetString,
3434
- Value: []byte("PDU-01"),
3435
- },
3436
- }, nil,
3437
- )
3438
- },
3439
- expectedResult: []*ddsnmp.ProfileMetrics{
3440
- {
3441
- Source: "test-profile.yaml",
3442
- DeviceMetadata: nil,
3443
- Metrics: []ddsnmp.Metric{
3444
- {
3445
- Name: "cpiPduBranchCurrent",
3446
- Value: 150,
3447
- Tags: map[string]string{"pdu_name": "PDU-01"},
3448
- MetricType: "gauge",
3449
- IsTable: true,
3450
- },
3451
- },
3452
- },
3453
- },
3454
- expectedError: false,
3455
- },
3456
- "multiple index transform ranges": {
3457
- profiles: []*ddsnmp.Profile{
3458
- {
3459
- SourceFile: "test-profile.yaml",
3460
- Definition: &ddprofiledefinition.ProfileDefinition{
3461
- Metrics: []ddprofiledefinition.MetricsConfig{
3462
- {
3463
- Table: ddprofiledefinition.SymbolConfig{
3464
- OID: "1.3.6.1.4.1.12345.1.1",
3465
- Name: "customTable",
3466
- },
3467
- Symbols: []ddprofiledefinition.SymbolConfig{
3468
- {
3469
- OID: "1.3.6.1.4.1.12345.1.1.1.1",
3470
- Name: "customMetric",
3471
- },
3472
- },
3473
- MetricTags: []ddprofiledefinition.MetricTagConfig{
3474
- {
3475
- Symbol: ddprofiledefinition.SymbolConfigCompat{
3476
- OID: "1.3.6.1.4.1.12345.2.1.1.1",
3477
- Name: "customTag",
3478
- },
3479
- Table: "customRefTable",
3480
- Tag: "custom_ref",
3481
- IndexTransform: []ddprofiledefinition.MetricIndexTransform{
3482
- {Start: 1, End: 2}, // First two positions
3483
- {Start: 4, End: 6}, // Positions 5-7
3484
- },
3485
- },
3486
- },
3487
- },
3488
- {
3489
- Table: ddprofiledefinition.SymbolConfig{
3490
- OID: "1.3.6.1.4.1.12345.2.1",
3491
- Name: "customRefTable",
3492
- },
3493
- Symbols: []ddprofiledefinition.SymbolConfig{
3494
- {
3495
- OID: "1.3.6.1.4.1.12345.2.1.1.1",
3496
- Name: "customTag",
3497
- },
3498
- },
3499
- },
3500
- },
3501
- },
3502
- },
3503
- },
3504
- setupMock: func(m *snmpmock.MockHandler) {
3505
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3506
- // Walk customTable
3507
- m.EXPECT().Version().Return(gosnmp.Version2c)
3508
- m.EXPECT().BulkWalkAll("1.3.6.1.4.1.12345.1.1").Return(
3509
- []gosnmp.SnmpPDU{
3510
- {
3511
- Name: "1.3.6.1.4.1.12345.1.1.1.1.1.2.3.4.5.6.7", // Index: 1.2.3.4.5.6.7
3512
- Type: gosnmp.Counter32,
3513
- Value: uint(500),
3514
- },
3515
- }, nil,
3516
- )
3517
- // Walk customRefTable
3518
- m.EXPECT().Version().Return(gosnmp.Version2c)
3519
- m.EXPECT().BulkWalkAll("1.3.6.1.4.1.12345.2.1").Return(
3520
- []gosnmp.SnmpPDU{
3521
- {
3522
- Name: "1.3.6.1.4.1.12345.2.1.1.1.2.3.5.6.7", // Index: 2.3.5.6.7 (matches transform)
3523
- Type: gosnmp.OctetString,
3524
- Value: []byte("CustomValue"),
3525
- },
3526
- }, nil,
3527
- )
3528
- },
3529
- expectedResult: []*ddsnmp.ProfileMetrics{
3530
- {
3531
- Source: "test-profile.yaml",
3532
- DeviceMetadata: nil,
3533
- Metrics: []ddsnmp.Metric{
3534
- {
3535
- Name: "customMetric",
3536
- Value: 500,
3537
- Tags: map[string]string{"custom_ref": "CustomValue"},
3538
- MetricType: "rate",
3539
- IsTable: true,
3540
- },
3541
- },
3542
- },
3543
- },
3544
- expectedError: false,
3545
- },
3546
- "index transform with out of bounds": {
3547
- profiles: []*ddsnmp.Profile{
3548
- {
3549
- SourceFile: "test-profile.yaml",
3550
- Definition: &ddprofiledefinition.ProfileDefinition{
3551
- Metrics: []ddprofiledefinition.MetricsConfig{
3552
- {
3553
- Table: ddprofiledefinition.SymbolConfig{
3554
- OID: "1.3.6.1.2.1.2.2",
3555
- Name: "ifTable",
3556
- },
3557
- Symbols: []ddprofiledefinition.SymbolConfig{
3558
- {
3559
- OID: "1.3.6.1.2.1.2.2.1.10",
3560
- Name: "ifInOctets",
3561
- },
3562
- },
3563
- MetricTags: []ddprofiledefinition.MetricTagConfig{
3564
- {
3565
- Symbol: ddprofiledefinition.SymbolConfigCompat{
3566
- OID: "1.3.6.1.2.1.31.1.1.1.1",
3567
- Name: "ifName",
3568
- },
3569
- Table: "ifXTable",
3570
- Tag: "interface",
3571
- IndexTransform: []ddprofiledefinition.MetricIndexTransform{
3572
- {Start: 0, End: 5}, // Trying to extract 6 positions from single index
3573
- },
3574
- },
3575
- },
3576
- },
3577
- {
3578
- Table: ddprofiledefinition.SymbolConfig{
3579
- OID: "1.3.6.1.2.1.31.1.1",
3580
- Name: "ifXTable",
3581
- },
3582
- Symbols: []ddprofiledefinition.SymbolConfig{
3583
- {
3584
- OID: "1.3.6.1.2.1.31.1.1.1.1",
3585
- Name: "ifName",
3586
- },
3587
- },
3588
- },
3589
- },
3590
- },
3591
- },
3592
- },
3593
- setupMock: func(m *snmpmock.MockHandler) {
3594
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3595
- // Walk ifTable
3596
- m.EXPECT().Version().Return(gosnmp.Version2c)
3597
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3598
- []gosnmp.SnmpPDU{
3599
- {
3600
- Name: "1.3.6.1.2.1.2.2.1.10.1", // Single index
3601
- Type: gosnmp.Counter32,
3602
- Value: uint(1000),
3603
- },
3604
- {
3605
- Name: "1.3.6.1.2.1.2.2.1.10.1.2.3.4.5.6", // Multi-part index
3606
- Type: gosnmp.Counter32,
3607
- Value: uint(2000),
3608
- },
3609
- }, nil,
3610
- )
3611
- // Walk ifXTable
3612
- m.EXPECT().Version().Return(gosnmp.Version2c)
3613
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
3614
- []gosnmp.SnmpPDU{
3615
- {
3616
- Name: "1.3.6.1.2.1.31.1.1.1.1.1.2.3.4.5.6", // Matches second row
3617
- Type: gosnmp.OctetString,
3618
- Value: []byte("GigabitEthernet0/0"),
3619
- },
3620
- }, nil,
3621
- )
3622
- },
3623
- expectedResult: []*ddsnmp.ProfileMetrics{
3624
- {
3625
- Source: "test-profile.yaml",
3626
- DeviceMetadata: nil,
3627
- Metrics: []ddsnmp.Metric{
3628
- {
3629
- Name: "ifInOctets",
3630
- Value: 1000,
3631
- Tags: nil, // Transform fails - index too short
3632
- MetricType: "rate",
3633
- IsTable: true,
3634
- },
3635
- {
3636
- Name: "ifInOctets",
3637
- Value: 2000,
3638
- Tags: map[string]string{"interface": "GigabitEthernet0/0"},
3639
- MetricType: "rate",
3640
- IsTable: true,
3641
- },
3642
- },
3643
- },
3644
- },
3645
- expectedError: false,
3646
- },
3647
- }
3648
-
3649
- for name, tc := range tests {
3650
- t.Run(name, func(t *testing.T) {
3651
- ctrl := gomock.NewController(t)
3652
- defer ctrl.Finish()
3653
-
3654
- mockHandler := snmpmock.NewMockHandler(ctrl)
3655
- tc.setupMock(mockHandler)
3656
-
3657
- collector := New(mockHandler, tc.profiles, logger.New())
3658
- collector.DoTableMetrics = true
3659
-
3660
- // Configure cache based on test requirements
3661
- if tc.enableCache {
3662
- collector.tableCache.setTTL(30*time.Second, 0)
3663
- } else {
3664
- collector.tableCache.setTTL(0, 0) // Disable cache
3665
- }
3666
-
3667
- result, err := collector.Collect()
3668
-
3669
- // Clear circular references
3670
- for _, profile := range result {
3671
- for i := range profile.Metrics {
3672
- profile.Metrics[i].Profile = nil
3673
- }
3674
- }
3675
-
3676
- if tc.expectedError {
3677
- assert.Error(t, err)
3678
- if tc.errorContains != "" {
3679
- assert.Contains(t, err.Error(), tc.errorContains)
3680
- }
3681
- } else {
3682
- assert.NoError(t, err)
3683
- }
3684
-
3685
- if tc.expectedResult != nil {
3686
- require.Equal(t, len(tc.expectedResult), len(result))
3687
- for i := range tc.expectedResult {
3688
- assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
3689
- assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
3690
- assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
3691
- }
3692
- } else {
3693
- assert.Nil(t, result)
3694
- }
3695
- })
3696
- }
3697
-}
3698
-
3699
-func TestCollector_Collect_OpaqueTypes(t *testing.T) {
3700
- tests := map[string]struct {
3701
- profiles []*ddsnmp.Profile
3702
- setupMock func(m *snmpmock.MockHandler)
3703
- expectedResult []*ddsnmp.ProfileMetrics
3704
- expectedError bool
3705
- errorContains string
3706
- enableCache bool
3707
- }{
3708
- "opaque float metric": {
3709
- profiles: []*ddsnmp.Profile{
3710
- {
3711
- SourceFile: "test-profile.yaml",
3712
- Definition: &ddprofiledefinition.ProfileDefinition{
3713
- Metrics: []ddprofiledefinition.MetricsConfig{
3714
- {
3715
- Symbol: ddprofiledefinition.SymbolConfig{
3716
- OID: "1.3.6.1.4.1.9.9.305.1.1.1.0",
3717
- Name: "cpmCPULoadAvg1min",
3718
- },
3719
- },
3720
- {
3721
- Symbol: ddprofiledefinition.SymbolConfig{
3722
- OID: "1.3.6.1.4.1.9.9.305.1.1.2.0",
3723
- Name: "cpmCPULoadAvg5min",
3724
- },
3725
- },
3726
- {
3727
- Symbol: ddprofiledefinition.SymbolConfig{
3728
- OID: "1.3.6.1.4.1.9.9.305.1.1.3.0",
3729
- Name: "cpmCPULoadAvg15min",
3730
- },
3731
- },
3732
- },
3733
- },
3734
- },
3735
- },
3736
- setupMock: func(m *snmpmock.MockHandler) {
3737
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3738
- m.EXPECT().Get(gomock.InAnyOrder([]string{
3739
- "1.3.6.1.4.1.9.9.305.1.1.1.0",
3740
- "1.3.6.1.4.1.9.9.305.1.1.2.0",
3741
- "1.3.6.1.4.1.9.9.305.1.1.3.0",
3742
- })).Return(
3743
- &gosnmp.SnmpPacket{
3744
- Variables: []gosnmp.SnmpPDU{
3745
- {
3746
- Name: "1.3.6.1.4.1.9.9.305.1.1.1.0",
3747
- Type: gosnmp.OpaqueFloat,
3748
- Value: float32(0.75),
3749
- },
3750
- {
3751
- Name: "1.3.6.1.4.1.9.9.305.1.1.2.0",
3752
- Type: gosnmp.OpaqueFloat,
3753
- Value: float32(1.23),
3754
- },
3755
- {
3756
- Name: "1.3.6.1.4.1.9.9.305.1.1.3.0",
3757
- Type: gosnmp.OpaqueFloat,
3758
- Value: float32(2.45),
3759
- },
3760
- },
3761
- }, nil,
3762
- )
3763
- },
3764
- expectedResult: []*ddsnmp.ProfileMetrics{
3765
- {
3766
- Source: "test-profile.yaml",
3767
- DeviceMetadata: nil,
3768
- Metrics: []ddsnmp.Metric{
3769
- {
3770
- Name: "cpmCPULoadAvg1min",
3771
- Value: 0, // 0.75 truncated to int64
3772
- MetricType: "gauge",
3773
- },
3774
- {
3775
- Name: "cpmCPULoadAvg5min",
3776
- Value: 1, // 1.23 truncated to int64
3777
- MetricType: "gauge",
3778
- },
3779
- {
3780
- Name: "cpmCPULoadAvg15min",
3781
- Value: 2, // 2.45 truncated to int64
3782
- MetricType: "gauge",
3783
- },
3784
- },
3785
- },
3786
- },
3787
- expectedError: false,
3788
- },
3789
- "opaque double metric": {
3790
- profiles: []*ddsnmp.Profile{
3791
- {
3792
- SourceFile: "test-profile.yaml",
3793
- Definition: &ddprofiledefinition.ProfileDefinition{
3794
- Metrics: []ddprofiledefinition.MetricsConfig{
3795
- {
3796
- Symbol: ddprofiledefinition.SymbolConfig{
3797
- OID: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.1",
3798
- Name: "cpqHeSysBatteryVoltage",
3799
- },
3800
- },
3801
- {
3802
- Symbol: ddprofiledefinition.SymbolConfig{
3803
- OID: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.2",
3804
- Name: "cpqHeSysBatteryCurrent",
3805
- },
3806
- },
3807
- {
3808
- Symbol: ddprofiledefinition.SymbolConfig{
3809
- OID: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.3",
3810
- Name: "cpqHeSysBatteryCapacity",
3811
- },
3812
- },
3813
- },
3814
- },
3815
- },
3816
- },
3817
- setupMock: func(m *snmpmock.MockHandler) {
3818
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3819
- m.EXPECT().Get(gomock.InAnyOrder([]string{
3820
- "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.1",
3821
- "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.2",
3822
- "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.3",
3823
- })).Return(
3824
- &gosnmp.SnmpPacket{
3825
- Variables: []gosnmp.SnmpPDU{
3826
- {
3827
- Name: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.1",
3828
- Type: gosnmp.OpaqueDouble,
3829
- Value: 12.6,
3830
- },
3831
- {
3832
- Name: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.2",
3833
- Type: gosnmp.OpaqueDouble,
3834
- Value: 2.4,
3835
- },
3836
- {
3837
- Name: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.3",
3838
- Type: gosnmp.OpaqueDouble,
3839
- Value: 98.5,
3840
- },
3841
- },
3842
- }, nil,
3843
- )
3844
- },
3845
- expectedResult: []*ddsnmp.ProfileMetrics{
3846
- {
3847
- Source: "test-profile.yaml",
3848
- DeviceMetadata: nil,
3849
- Metrics: []ddsnmp.Metric{
3850
- {
3851
- Name: "cpqHeSysBatteryVoltage",
3852
- Value: 12, // 12.6 truncated to int64
3853
- MetricType: "gauge",
3854
- },
3855
- {
3856
- Name: "cpqHeSysBatteryCurrent",
3857
- Value: 2, // 2.4 truncated to int64
3858
- MetricType: "gauge",
3859
- },
3860
- {
3861
- Name: "cpqHeSysBatteryCapacity",
3862
- Value: 98, // 98.5 truncated to int64
3863
- MetricType: "gauge",
3864
- },
3865
- },
3866
- },
3867
- },
3868
- expectedError: false,
3869
- },
3870
- }
3871
-
3872
- for name, tc := range tests {
3873
- t.Run(name, func(t *testing.T) {
3874
- ctrl := gomock.NewController(t)
3875
- defer ctrl.Finish()
3876
-
3877
- mockHandler := snmpmock.NewMockHandler(ctrl)
3878
- tc.setupMock(mockHandler)
3879
-
3880
- collector := New(mockHandler, tc.profiles, logger.New())
3881
- collector.DoTableMetrics = true
3882
-
3883
- // Configure cache based on test requirements
3884
- if tc.enableCache {
3885
- collector.tableCache.setTTL(30*time.Second, 0)
3886
- } else {
3887
- collector.tableCache.setTTL(0, 0) // Disable cache
3888
- }
3889
-
3890
- result, err := collector.Collect()
3891
-
3892
- // Clear circular references
3893
- for _, profile := range result {
3894
- for i := range profile.Metrics {
3895
- profile.Metrics[i].Profile = nil
3896
- }
3897
- }
3898
-
3899
- if tc.expectedError {
3900
- assert.Error(t, err)
3901
- if tc.errorContains != "" {
3902
- assert.Contains(t, err.Error(), tc.errorContains)
3903
- }
3904
- } else {
3905
- assert.NoError(t, err)
3906
- }
3907
-
3908
- if tc.expectedResult != nil {
3909
- require.Equal(t, len(tc.expectedResult), len(result))
3910
- for i := range tc.expectedResult {
3911
- assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
3912
- assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
3913
- assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
3914
- }
3915
- } else {
3916
- assert.Nil(t, result)
3917
- }
3918
- })
3919
- }
3920
-}
3921
-
3922
-func TestCollector_Collect_TableCaching(t *testing.T) {
3923
- tests := map[string]struct {
3924
- profiles []*ddsnmp.Profile
3925
- setupMock func(m *snmpmock.MockHandler)
3926
- expectedResult []*ddsnmp.ProfileMetrics
3927
- expectedError bool
3928
- errorContains string
3929
- enableCache bool
3930
- cacheTTL time.Duration
3931
- collectCount int // Number of times to call Collect()
3932
- sleepBetween time.Duration
3933
- }{
3934
- "table cache basic operation": {
3935
- profiles: []*ddsnmp.Profile{
3936
- {
3937
- SourceFile: "test-profile.yaml",
3938
- Definition: &ddprofiledefinition.ProfileDefinition{
3939
- Metrics: []ddprofiledefinition.MetricsConfig{
3940
- {
3941
- Table: ddprofiledefinition.SymbolConfig{
3942
- OID: "1.3.6.1.2.1.2.2",
3943
- Name: "ifTable",
3944
- },
3945
- Symbols: []ddprofiledefinition.SymbolConfig{
3946
- {
3947
- OID: "1.3.6.1.2.1.2.2.1.10",
3948
- Name: "ifInOctets",
3949
- },
3950
- {
3951
- OID: "1.3.6.1.2.1.2.2.1.16",
3952
- Name: "ifOutOctets",
3953
- },
3954
- },
3955
- MetricTags: []ddprofiledefinition.MetricTagConfig{
3956
- {
3957
- Tag: "interface",
3958
- Symbol: ddprofiledefinition.SymbolConfigCompat{
3959
- OID: "1.3.6.1.2.1.2.2.1.2",
3960
- Name: "ifDescr",
3961
- },
3962
- },
3963
- },
3964
- },
3965
- },
3966
- },
3967
- },
3968
- },
3969
- setupMock: func(m *snmpmock.MockHandler) {
3970
- m.EXPECT().MaxOids().Return(10).AnyTimes()
3971
-
3972
- // First collection: Full walk
3973
- m.EXPECT().Version().Return(gosnmp.Version2c).Times(1)
3974
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3975
- []gosnmp.SnmpPDU{
3976
- {
3977
- Name: "1.3.6.1.2.1.2.2.1.10.1",
3978
- Type: gosnmp.Counter32,
3979
- Value: uint(1000),
3980
- },
3981
- {
3982
- Name: "1.3.6.1.2.1.2.2.1.16.1",
3983
- Type: gosnmp.Counter32,
3984
- Value: uint(500),
3985
- },
3986
- {
3987
- Name: "1.3.6.1.2.1.2.2.1.2.1",
3988
- Type: gosnmp.OctetString,
3989
- Value: []byte("eth0"),
3990
- },
3991
- {
3992
- Name: "1.3.6.1.2.1.2.2.1.10.2",
3993
- Type: gosnmp.Counter32,
3994
- Value: uint(2000),
3995
- },
3996
- {
3997
- Name: "1.3.6.1.2.1.2.2.1.16.2",
3998
- Type: gosnmp.Counter32,
3999
- Value: uint(1500),
4000
- },
4001
- {
4002
- Name: "1.3.6.1.2.1.2.2.1.2.2",
4003
- Type: gosnmp.OctetString,
4004
- Value: []byte("eth1"),
4005
- },
4006
- }, nil,
4007
- ).Times(1)
4008
-
4009
- // Second collection: Only GET metrics (not tags)
4010
- m.EXPECT().Get(gomock.InAnyOrder([]string{
4011
- "1.3.6.1.2.1.2.2.1.10.1",
4012
- "1.3.6.1.2.1.2.2.1.16.1",
4013
- "1.3.6.1.2.1.2.2.1.10.2",
4014
- "1.3.6.1.2.1.2.2.1.16.2",
4015
- })).Return(
4016
- &gosnmp.SnmpPacket{
4017
- Variables: []gosnmp.SnmpPDU{
4018
- {
4019
- Name: "1.3.6.1.2.1.2.2.1.10.1",
4020
- Type: gosnmp.Counter32,
4021
- Value: uint(1100), // Values changed
4022
- },
4023
- {
4024
- Name: "1.3.6.1.2.1.2.2.1.16.1",
4025
- Type: gosnmp.Counter32,
4026
- Value: uint(600),
4027
- },
4028
- {
4029
- Name: "1.3.6.1.2.1.2.2.1.10.2",
4030
- Type: gosnmp.Counter32,
4031
- Value: uint(2200),
4032
- },
4033
- {
4034
- Name: "1.3.6.1.2.1.2.2.1.16.2",
4035
- Type: gosnmp.Counter32,
4036
- Value: uint(1700),
4037
- },
4038
- },
4039
- }, nil,
4040
- ).Times(1)
4041
-
4042
- // Third collection: Still using cache
4043
- m.EXPECT().Get(gomock.InAnyOrder([]string{
4044
- "1.3.6.1.2.1.2.2.1.10.1",
4045
- "1.3.6.1.2.1.2.2.1.16.1",
4046
- "1.3.6.1.2.1.2.2.1.10.2",
4047
- "1.3.6.1.2.1.2.2.1.16.2",
4048
- })).Return(
4049
- &gosnmp.SnmpPacket{
4050
- Variables: []gosnmp.SnmpPDU{
4051
- {
4052
- Name: "1.3.6.1.2.1.2.2.1.10.1",
4053
- Type: gosnmp.Counter32,
4054
- Value: uint(1200),
4055
- },
4056
- {
4057
- Name: "1.3.6.1.2.1.2.2.1.16.1",
4058
- Type: gosnmp.Counter32,
4059
- Value: uint(700),
4060
- },
4061
- {
4062
- Name: "1.3.6.1.2.1.2.2.1.10.2",
4063
- Type: gosnmp.Counter32,
4064
- Value: uint(2400),
4065
- },
4066
- {
4067
- Name: "1.3.6.1.2.1.2.2.1.16.2",
4068
- Type: gosnmp.Counter32,
4069
- Value: uint(1900),
4070
- },
4071
- },
4072
- }, nil,
4073
- ).Times(1)
4074
- },
4075
- expectedResult: []*ddsnmp.ProfileMetrics{
4076
- {
4077
- Source: "test-profile.yaml",
4078
- DeviceMetadata: nil,
4079
- Metrics: []ddsnmp.Metric{
4080
- {
4081
- Name: "ifInOctets",
4082
- Value: 1200, // Latest value
4083
- Tags: map[string]string{"interface": "eth0"},
4084
- MetricType: "rate",
4085
- IsTable: true,
4086
- },
4087
- {
4088
- Name: "ifOutOctets",
4089
- Value: 700,
4090
- Tags: map[string]string{"interface": "eth0"},
4091
- MetricType: "rate",
4092
- IsTable: true,
4093
- },
4094
- {
4095
- Name: "ifInOctets",
4096
- Value: 2400,
4097
- Tags: map[string]string{"interface": "eth1"},
4098
- MetricType: "rate",
4099
- IsTable: true,
4100
- },
4101
- {
4102
- Name: "ifOutOctets",
4103
- Value: 1900,
4104
- Tags: map[string]string{"interface": "eth1"},
4105
- MetricType: "rate",
4106
- IsTable: true,
4107
- },
4108
- },
4109
- },
4110
- },
4111
- expectedError: false,
4112
- enableCache: true,
4113
- cacheTTL: 30 * time.Second,
4114
- collectCount: 3,
4115
- sleepBetween: 10 * time.Millisecond,
4116
- },
4117
- "table cache expiration": {
4118
- profiles: []*ddsnmp.Profile{
4119
- {
4120
- SourceFile: "test-profile.yaml",
4121
- Definition: &ddprofiledefinition.ProfileDefinition{
4122
- Metrics: []ddprofiledefinition.MetricsConfig{
4123
- {
4124
- Table: ddprofiledefinition.SymbolConfig{
4125
- OID: "1.3.6.1.2.1.2.2",
4126
- Name: "ifTable",
4127
- },
4128
- Symbols: []ddprofiledefinition.SymbolConfig{
4129
- {
4130
- OID: "1.3.6.1.2.1.2.2.1.10",
4131
- Name: "ifInOctets",
4132
- },
4133
- },
4134
- MetricTags: []ddprofiledefinition.MetricTagConfig{
4135
- {
4136
- Tag: "interface",
4137
- Symbol: ddprofiledefinition.SymbolConfigCompat{
4138
- OID: "1.3.6.1.2.1.2.2.1.2",
4139
- Name: "ifDescr",
4140
- },
4141
- },
4142
- },
4143
- },
4144
- },
4145
- },
4146
- },
4147
- },
4148
- setupMock: func(m *snmpmock.MockHandler) {
4149
- m.EXPECT().MaxOids().Return(10).AnyTimes()
4150
- m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
4151
-
4152
- // First collection: Full walk
4153
- gomock.InOrder(
4154
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
4155
- []gosnmp.SnmpPDU{
4156
- {
4157
- Name: "1.3.6.1.2.1.2.2.1.10.1",
4158
- Type: gosnmp.Counter32,
4159
- Value: uint(1000),
4160
- },
4161
- {
4162
- Name: "1.3.6.1.2.1.2.2.1.2.1",
4163
- Type: gosnmp.OctetString,
4164
- Value: []byte("eth0"),
4165
- },
4166
- }, nil,
4167
- ),
4168
-
4169
- // Second collection: Using cache
4170
- m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.10.1"}).Return(
4171
- &gosnmp.SnmpPacket{
4172
- Variables: []gosnmp.SnmpPDU{
4173
- {
4174
- Name: "1.3.6.1.2.1.2.2.1.10.1",
4175
- Type: gosnmp.Counter32,
4176
- Value: uint(1100),
4177
- },
4178
- },
4179
- }, nil,
4180
- ),
4181
-
4182
- // Third collection: After expiration, full walk again
4183
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
4184
- []gosnmp.SnmpPDU{
4185
- {
4186
- Name: "1.3.6.1.2.1.2.2.1.10.1",
4187
- Type: gosnmp.Counter32,
4188
- Value: uint(1200),
4189
- },
4190
- {
4191
- Name: "1.3.6.1.2.1.2.2.1.2.1",
4192
- Type: gosnmp.OctetString,
4193
- Value: []byte("eth0"),
4194
- },
4195
- {
4196
- Name: "1.3.6.1.2.1.2.2.1.10.2",
4197
- Type: gosnmp.Counter32,
4198
- Value: uint(2000),
4199
- },
4200
- {
4201
- Name: "1.3.6.1.2.1.2.2.1.2.2",
4202
- Type: gosnmp.OctetString,
4203
- Value: []byte("eth1"),
4204
- },
4205
- }, nil,
4206
- ),
4207
- )
4208
- },
4209
- expectedResult: []*ddsnmp.ProfileMetrics{
4210
- {
4211
- Source: "test-profile.yaml",
4212
- DeviceMetadata: nil,
4213
- Metrics: []ddsnmp.Metric{
4214
- {
4215
- Name: "ifInOctets",
4216
- Value: 1200,
4217
- Tags: map[string]string{"interface": "eth0"},
4218
- MetricType: "rate",
4219
- IsTable: true,
4220
- },
4221
- {
4222
- Name: "ifInOctets",
4223
- Value: 2000,
4224
- Tags: map[string]string{"interface": "eth1"},
4225
- MetricType: "rate",
4226
- IsTable: true,
4227
- },
4228
- },
4229
- },
4230
- },
4231
- expectedError: false,
4232
- enableCache: true,
4233
- cacheTTL: 100 * time.Millisecond,
4234
- collectCount: 3,
4235
- sleepBetween: 60 * time.Millisecond, // Sleep less than TTL for second collection, but total > TTL for third
4236
- },
4237
- "table cache with multiple configs same table": {
4238
- profiles: []*ddsnmp.Profile{
4239
- {
4240
- SourceFile: "test-profile.yaml",
4241
- Definition: &ddprofiledefinition.ProfileDefinition{
4242
- Metrics: []ddprofiledefinition.MetricsConfig{
4243
- {
4244
- Table: ddprofiledefinition.SymbolConfig{
4245
- OID: "1.3.6.1.2.1.2.2",
4246
- Name: "ifTable",
4247
- },
4248
- Symbols: []ddprofiledefinition.SymbolConfig{
4249
- {
4250
- OID: "1.3.6.1.2.1.2.2.1.10",
4251
- Name: "ifInOctets",
4252
- },
4253
- {
4254
- OID: "1.3.6.1.2.1.2.2.1.16",
4255
- Name: "ifOutOctets",
4256
- },
4257
- },
4258
- MetricTags: []ddprofiledefinition.MetricTagConfig{
4259
- {
4260
- Tag: "interface",
4261
- Symbol: ddprofiledefinition.SymbolConfigCompat{
4262
- OID: "1.3.6.1.2.1.2.2.1.2",
4263
- Name: "ifDescr",
4264
- },
4265
- },
4266
- },
4267
- },
4268
- {
4269
- Table: ddprofiledefinition.SymbolConfig{
4270
- OID: "1.3.6.1.2.1.2.2",
4271
- Name: "ifTable",
4272
- },
4273
- Symbols: []ddprofiledefinition.SymbolConfig{
4274
- {
4275
- OID: "1.3.6.1.2.1.2.2.1.14",
4276
- Name: "ifInErrors",
4277
- },
4278
- {
4279
- OID: "1.3.6.1.2.1.2.2.1.20",
4280
- Name: "ifOutErrors",
4281
- },
4282
- },
4283
- MetricTags: []ddprofiledefinition.MetricTagConfig{
4284
- {
4285
- Tag: "interface",
4286
- Symbol: ddprofiledefinition.SymbolConfigCompat{
4287
- OID: "1.3.6.1.2.1.2.2.1.2",
4288
- Name: "ifDescr",
4289
- },
4290
- },
4291
- },
4292
- },
4293
- },
4294
- },
4295
- },
4296
- },
4297
- setupMock: func(m *snmpmock.MockHandler) {
4298
- m.EXPECT().MaxOids().Return(10).AnyTimes()
4299
- m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
4300
-
4301
- // First collection: Walk table once
4302
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
4303
- []gosnmp.SnmpPDU{
4304
- // Interface 1
4305
- {
4306
- Name: "1.3.6.1.2.1.2.2.1.2.1",
4307
- Type: gosnmp.OctetString,
4308
- Value: []byte("eth0"),
4309
- },
4310
- {
4311
- Name: "1.3.6.1.2.1.2.2.1.10.1",
4312
- Type: gosnmp.Counter32,
4313
- Value: uint(1000),
4314
- },
4315
- {
4316
- Name: "1.3.6.1.2.1.2.2.1.16.1",
4317
- Type: gosnmp.Counter32,
4318
- Value: uint(500),
4319
- },
4320
- {
4321
- Name: "1.3.6.1.2.1.2.2.1.14.1",
4322
- Type: gosnmp.Counter32,
4323
- Value: uint(10),
4324
- },
4325
- {
4326
- Name: "1.3.6.1.2.1.2.2.1.20.1",
4327
- Type: gosnmp.Counter32,
4328
- Value: uint(5),
4329
- },
4330
- }, nil,
4331
- ).Times(1)
4332
-
4333
- // Second collection: Each config uses cache separately
4334
- // First config GET
4335
- m.EXPECT().Get(gomock.InAnyOrder([]string{
4336
- "1.3.6.1.2.1.2.2.1.10.1",
4337
- "1.3.6.1.2.1.2.2.1.16.1",
4338
- })).Return(
4339
- &gosnmp.SnmpPacket{
4340
- Variables: []gosnmp.SnmpPDU{
4341
- {
4342
- Name: "1.3.6.1.2.1.2.2.1.10.1",
4343
- Type: gosnmp.Counter32,
4344
- Value: uint(1100),
4345
- },
4346
- {
4347
- Name: "1.3.6.1.2.1.2.2.1.16.1",
4348
- Type: gosnmp.Counter32,
4349
- Value: uint(600),
4350
- },
4351
- },
4352
- }, nil,
4353
- ).Times(1)
4354
-
4355
- // Second config GET
4356
- m.EXPECT().Get(gomock.InAnyOrder([]string{
4357
- "1.3.6.1.2.1.2.2.1.14.1",
4358
- "1.3.6.1.2.1.2.2.1.20.1",
4359
- })).Return(
4360
- &gosnmp.SnmpPacket{
4361
- Variables: []gosnmp.SnmpPDU{
4362
- {
4363
- Name: "1.3.6.1.2.1.2.2.1.14.1",
4364
- Type: gosnmp.Counter32,
4365
- Value: uint(12),
4366
- },
4367
- {
4368
- Name: "1.3.6.1.2.1.2.2.1.20.1",
4369
- Type: gosnmp.Counter32,
4370
- Value: uint(6),
4371
- },
4372
- },
4373
- }, nil,
4374
- ).Times(1)
4375
- },
4376
- expectedResult: []*ddsnmp.ProfileMetrics{
4377
- {
4378
- Source: "test-profile.yaml",
4379
- DeviceMetadata: nil,
4380
- Metrics: []ddsnmp.Metric{
4381
- // From first config
4382
- {
4383
- Name: "ifInOctets",
4384
- Value: 1100,
4385
- Tags: map[string]string{"interface": "eth0"},
4386
- MetricType: "rate",
4387
- IsTable: true,
4388
- },
4389
- {
4390
- Name: "ifOutOctets",
4391
- Value: 600,
4392
- Tags: map[string]string{"interface": "eth0"},
4393
- MetricType: "rate",
4394
- IsTable: true,
4395
- },
4396
- // From second config
4397
- {
4398
- Name: "ifInErrors",
4399
- Value: 12,
4400
- Tags: map[string]string{"interface": "eth0"},
4401
- MetricType: "rate",
4402
- IsTable: true,
4403
- },
4404
- {
4405
- Name: "ifOutErrors",
4406
- Value: 6,
4407
- Tags: map[string]string{"interface": "eth0"},
4408
- MetricType: "rate",
4409
- IsTable: true,
4410
- },
4411
- },
4412
- },
4413
- },
4414
- expectedError: false,
4415
- enableCache: true,
4416
- cacheTTL: 30 * time.Second,
4417
- collectCount: 2,
4418
- sleepBetween: 10 * time.Millisecond,
4419
- },
4420
- }
4421
-
4422
- for name, tc := range tests {
4423
- t.Run(name, func(t *testing.T) {
4424
- ctrl := gomock.NewController(t)
4425
- defer ctrl.Finish()
4426
-
4427
- mockHandler := snmpmock.NewMockHandler(ctrl)
4428
- tc.setupMock(mockHandler)
4429
-
4430
- collector := New(mockHandler, tc.profiles, logger.New())
4431
- collector.DoTableMetrics = true
4432
-
4433
- // Configure cache based on test requirements
4434
- if tc.enableCache {
4435
- collector.tableCache.setTTL(tc.cacheTTL, 0)
4436
- } else {
4437
- collector.tableCache.setTTL(0, 0) // Disable cache
4438
- }
4439
-
4440
- var result []*ddsnmp.ProfileMetrics
4441
- var err error
4442
-
4443
- // Perform multiple collections to test caching behavior
4444
- for i := 0; i < tc.collectCount; i++ {
4445
- if i > 0 && tc.sleepBetween > 0 {
4446
- time.Sleep(tc.sleepBetween)
4447
- }
4448
-
4449
- result, err = collector.Collect()
4450
-
4451
- // For intermediate collections, just verify no error
4452
- if i < tc.collectCount-1 {
4453
- assert.NoError(t, err)
4454
- }
4455
- }
4456
-
4457
- // Clear circular references in final result
4458
- for _, profile := range result {
4459
- for i := range profile.Metrics {
4460
- profile.Metrics[i].Profile = nil
4461
- }
4462
- }
4463
-
4464
- if tc.expectedError {
4465
- assert.Error(t, err)
4466
- if tc.errorContains != "" {
4467
- assert.Contains(t, err.Error(), tc.errorContains)
4468
- }
4469
- } else {
4470
- assert.NoError(t, err)
4471
- }
4472
-
4473
- if tc.expectedResult != nil {
4474
- require.Equal(t, len(tc.expectedResult), len(result))
4475
- for i := range tc.expectedResult {
4476
- assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
4477
- assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
4478
- assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
4479
- }
4480
- } else {
4481
- assert.Nil(t, result)
4482
- }
4483
- })
4484
- }
4485
-}
4486
-
4487
-func TestCollector_Collect_MetricTransforms(t *testing.T) {
4488
- tests := map[string]struct {
4489
- profiles []*ddsnmp.Profile
4490
- setupMock func(m *snmpmock.MockHandler)
4491
- expectedResult []*ddsnmp.ProfileMetrics
4492
- expectedError bool
4493
- errorContains string
4494
- }{
4495
- "basic metric name transformation": {
4496
- profiles: []*ddsnmp.Profile{
4497
- {
4498
- SourceFile: "test-profile.yaml",
4499
- Definition: &ddprofiledefinition.ProfileDefinition{
4500
- Metrics: []ddprofiledefinition.MetricsConfig{
4501
- {
4502
- Symbol: ddprofiledefinition.SymbolConfig{
4503
- OID: "1.3.6.1.4.1.12345.1.1",
4504
- Name: "sensorValue",
4505
- Transform: `{{- setName .Metric "customName" -}}`,
4506
- },
4507
- },
4508
- },
4509
- },
4510
- },
4511
- },
4512
- setupMock: func(m *snmpmock.MockHandler) {
4513
- m.EXPECT().MaxOids().Return(10).AnyTimes()
4514
- m.EXPECT().Get([]string{"1.3.6.1.4.1.12345.1.1"}).Return(
4515
- &gosnmp.SnmpPacket{
4516
- Variables: []gosnmp.SnmpPDU{
4517
- {
4518
- Name: "1.3.6.1.4.1.12345.1.1",
4519
- Type: gosnmp.Gauge32,
4520
- Value: uint(100),
4521
- },
4522
- },
4523
- }, nil,
4524
- )
4525
- },
4526
- expectedResult: []*ddsnmp.ProfileMetrics{
4527
- {
4528
- Source: "test-profile.yaml",
4529
- DeviceMetadata: nil,
4530
- Metrics: []ddsnmp.Metric{
4531
- {
4532
- Name: "customName",
4533
- Value: 100,
4534
- MetricType: "gauge",
4535
- },
4536
- },
4537
- },
4538
- },
4539
- expectedError: false,
4540
- },
4541
- "table metric with sensor type transformation": {
4542
- profiles: []*ddsnmp.Profile{
4543
- {
4544
- SourceFile: "test-profile.yaml",
4545
- Definition: &ddprofiledefinition.ProfileDefinition{
4546
- Metrics: []ddprofiledefinition.MetricsConfig{
4547
- {
4548
- Table: ddprofiledefinition.SymbolConfig{
4549
- OID: "1.3.6.1.4.1.14988.1.1.3.100",
4550
- Name: "mtxrHlTable",
4551
- },
4552
- Symbols: []ddprofiledefinition.SymbolConfig{
4553
- {
4554
- OID: "1.3.6.1.4.1.14988.1.1.3.100.1.3",
4555
- Name: "mtxrHlSensorValue",
4556
- Transform: `
4557
-{{- $sensorType := index .Metric.Tags "sensor_type" | default "" -}}
4558
-{{- if eq $sensorType "1" -}}
4559
- {{- setName .Metric (printf "%s_temperature" .Metric.Name) -}}
4560
- {{- setUnit .Metric "celsius" -}}
4561
- {{- setFamily .Metric "Health/Temperature" -}}
4562
-{{- else if eq $sensorType "3" -}}
4563
- {{- setName .Metric (printf "%s_voltage" .Metric.Name) -}}
4564
- {{- setUnit .Metric "volts" -}}
4565
- {{- setValue .Metric (int64 (div (float64 .Metric.Value) 10.0)) -}}
4566
- {{- setFamily .Metric "Health/Power" -}}
4567
-{{- end -}}
4568
-{{- deleteTag .Metric "sensor_type" -}}`,
4569
- },
4570
- },
4571
- MetricTags: []ddprofiledefinition.MetricTagConfig{
4572
- {
4573
- Tag: "sensor_name",
4574
- Symbol: ddprofiledefinition.SymbolConfigCompat{
4575
- OID: "1.3.6.1.4.1.14988.1.1.3.100.1.2",
4576
- Name: "mtxrHlSensorName",
4577
- },
4578
- },
4579
- {
4580
- Tag: "sensor_type",
4581
- Symbol: ddprofiledefinition.SymbolConfigCompat{
4582
- OID: "1.3.6.1.4.1.14988.1.1.3.100.1.4",
4583
- Name: "mtxrHlSensorType",
4584
- },
4585
- },
4586
- },
4587
- },
4588
- },
4589
- },
4590
- },
4591
- },
4592
- setupMock: func(m *snmpmock.MockHandler) {
4593
- m.EXPECT().MaxOids().Return(10).AnyTimes()
4594
- m.EXPECT().Version().Return(gosnmp.Version2c)
4595
- m.EXPECT().BulkWalkAll("1.3.6.1.4.1.14988.1.1.3.100").Return(
4596
- []gosnmp.SnmpPDU{
4597
- // Temperature sensor (type 1)
4598
- {
4599
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.1",
4600
- Type: gosnmp.Integer,
4601
- Value: 25,
4602
- },
4603
- {
4604
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.1",
4605
- Type: gosnmp.OctetString,
4606
- Value: []byte("cpu-temperature"),
4607
- },
4608
- {
4609
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.1",
4610
- Type: gosnmp.Integer,
4611
- Value: 1,
4612
- },
4613
- // Voltage sensor (type 3)
4614
- {
4615
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.2",
4616
- Type: gosnmp.Integer,
4617
- Value: 120, // 12.0V in decivolts
4618
- },
4619
- {
4620
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.2",
4621
- Type: gosnmp.OctetString,
4622
- Value: []byte("input-voltage"),
4623
- },
4624
- {
4625
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.2",
4626
- Type: gosnmp.Integer,
4627
- Value: 3,
4628
- },
4629
- }, nil,
4630
- )
4631
- },
4632
- expectedResult: []*ddsnmp.ProfileMetrics{
4633
- {
4634
- Source: "test-profile.yaml",
4635
- DeviceMetadata: nil,
4636
- Metrics: []ddsnmp.Metric{
4637
- {
4638
- Name: "mtxrHlSensorValue_temperature",
4639
- Value: 25,
4640
- Tags: map[string]string{"sensor_name": "cpu-temperature"},
4641
- Unit: "celsius",
4642
- Family: "Health/Temperature",
4643
- MetricType: "gauge",
4644
- IsTable: true,
4645
- },
4646
- {
4647
- Name: "mtxrHlSensorValue_voltage",
4648
- Value: 12, // Converted from decivolts
4649
- Tags: map[string]string{"sensor_name": "input-voltage"},
4650
- Unit: "volts",
4651
- Family: "Health/Power",
4652
- MetricType: "gauge",
4653
- IsTable: true,
4654
- },
4655
- },
4656
- },
4657
- },
4658
- expectedError: false,
4659
- },
4660
- "transformation with value mappings": {
4661
- profiles: []*ddsnmp.Profile{
4662
- {
4663
- SourceFile: "test-profile.yaml",
4664
- Definition: &ddprofiledefinition.ProfileDefinition{
4665
- Metrics: []ddprofiledefinition.MetricsConfig{
4666
- {
4667
- Symbol: ddprofiledefinition.SymbolConfig{
4668
- OID: "1.3.6.1.4.1.12345.1.1",
4669
- Name: "deviceStatus",
4670
- Transform: `
4671
-{{- setMappings .Metric (i64map 0 "down" 1 "up" 2 "testing") -}}`,
4672
- },
4673
- },
4674
- },
4675
- },
4676
- },
4677
- },
4678
- setupMock: func(m *snmpmock.MockHandler) {
4679
- m.EXPECT().MaxOids().Return(10).AnyTimes()
4680
- m.EXPECT().Get([]string{"1.3.6.1.4.1.12345.1.1"}).Return(
4681
- &gosnmp.SnmpPacket{
4682
- Variables: []gosnmp.SnmpPDU{
4683
- {
4684
- Name: "1.3.6.1.4.1.12345.1.1",
4685
- Type: gosnmp.Integer,
4686
- Value: 1,
4687
- },
4688
- },
4689
- }, nil,
4690
- )
4691
- },
4692
- expectedResult: []*ddsnmp.ProfileMetrics{
4693
- {
4694
- Source: "test-profile.yaml",
4695
- DeviceMetadata: nil,
4696
- Metrics: []ddsnmp.Metric{
4697
- {
4698
- Name: "deviceStatus",
4699
- Value: 1,
4700
- Mappings: map[int64]string{
4701
- 0: "down",
4702
- 1: "up",
4703
- 2: "testing",
4704
- },
4705
- MetricType: "gauge",
4706
- },
4707
- },
4708
- },
4709
- },
4710
- expectedError: false,
4711
- },
4712
- "complex transformation with sprig functions": {
4713
- profiles: []*ddsnmp.Profile{
4714
- {
4715
- SourceFile: "test-profile.yaml",
4716
- Definition: &ddprofiledefinition.ProfileDefinition{
4717
- Metrics: []ddprofiledefinition.MetricsConfig{
4718
- {
4719
- Table: ddprofiledefinition.SymbolConfig{
4720
- OID: "1.3.6.1.2.1.2.2",
4721
- Name: "ifTable",
4722
- },
4723
- Symbols: []ddprofiledefinition.SymbolConfig{
4724
- {
4725
- OID: "1.3.6.1.2.1.2.2.1.10",
4726
- Name: "ifInOctets",
4727
- Transform: `
4728
-{{- $ifName := index .Metric.Tags "interface" | lower -}}
4729
-{{- if contains "eth" $ifName -}}
4730
- {{- setFamily .Metric "Interfaces/Ethernet" -}}
4731
-{{- else if contains "lo" $ifName -}}
4732
- {{- setFamily .Metric "Interfaces/Loopback" -}}
4733
-{{- end -}}
4734
-{{- setDesc .Metric (printf "Traffic on %s interface" (index .Metric.Tags "interface")) -}}`,
4735
- },
4736
- },
4737
- MetricTags: []ddprofiledefinition.MetricTagConfig{
4738
- {
4739
- Tag: "interface",
4740
- Symbol: ddprofiledefinition.SymbolConfigCompat{
4741
- OID: "1.3.6.1.2.1.2.2.1.2",
4742
- Name: "ifDescr",
4743
- },
4744
- },
4745
- },
4746
- },
4747
- },
4748
- },
4749
- },
4750
- },
4751
- setupMock: func(m *snmpmock.MockHandler) {
4752
- m.EXPECT().MaxOids().Return(10).AnyTimes()
4753
- m.EXPECT().Version().Return(gosnmp.Version2c)
4754
- m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
4755
- []gosnmp.SnmpPDU{
4756
- {
4757
- Name: "1.3.6.1.2.1.2.2.1.10.1",
4758
- Type: gosnmp.Counter32,
4759
- Value: uint(1000),
4760
- },
4761
- {
4762
- Name: "1.3.6.1.2.1.2.2.1.2.1",
4763
- Type: gosnmp.OctetString,
4764
- Value: []byte("eth0"),
4765
- },
4766
- {
4767
- Name: "1.3.6.1.2.1.2.2.1.10.2",
4768
- Type: gosnmp.Counter32,
4769
- Value: uint(2000),
4770
- },
4771
- {
4772
- Name: "1.3.6.1.2.1.2.2.1.2.2",
4773
- Type: gosnmp.OctetString,
4774
- Value: []byte("lo0"),
4775
- },
4776
- }, nil,
4777
- )
4778
- },
4779
- expectedResult: []*ddsnmp.ProfileMetrics{
4780
- {
4781
- Source: "test-profile.yaml",
4782
- DeviceMetadata: nil,
4783
- Metrics: []ddsnmp.Metric{
4784
- {
4785
- Name: "ifInOctets",
4786
- Value: 1000,
4787
- Tags: map[string]string{"interface": "eth0"},
4788
- Family: "Interfaces/Ethernet",
4789
- Description: "Traffic on eth0 interface",
4790
- MetricType: "rate",
4791
- IsTable: true,
4792
- },
4793
- {
4794
- Name: "ifInOctets",
4795
- Value: 2000,
4796
- Tags: map[string]string{"interface": "lo0"},
4797
- Family: "Interfaces/Loopback",
4798
- Description: "Traffic on lo0 interface",
4799
- MetricType: "rate",
4800
- IsTable: true,
4801
- },
4802
- },
4803
- },
4804
- },
4805
- expectedError: false,
4806
- },
4807
- "transformation with invalid template": {
4808
- profiles: []*ddsnmp.Profile{
4809
- {
4810
- SourceFile: "test-profile.yaml",
4811
- Definition: &ddprofiledefinition.ProfileDefinition{
4812
- Metrics: []ddprofiledefinition.MetricsConfig{
4813
- {
4814
- Symbol: ddprofiledefinition.SymbolConfig{
4815
- OID: "1.3.6.1.4.1.12345.1.1",
4816
- Name: "testMetric",
4817
- Transform: `{{- invalid template syntax {{`,
4818
- TransformCompiled: nil, // Will fail compilation
4819
- },
4820
- },
4821
- },
4822
- },
4823
- },
4824
- },
4825
- setupMock: func(m *snmpmock.MockHandler) {
4826
- // This test should fail during profile validation, not during collection
4827
- },
4828
- expectedResult: nil,
4829
- expectedError: true,
4830
- errorContains: "template",
4831
- },
4832
- "mikrotik sensor table real-world example": {
4833
- profiles: []*ddsnmp.Profile{
4834
- {
4835
- SourceFile: "mikrotik-profile.yaml",
4836
- Definition: &ddprofiledefinition.ProfileDefinition{
4837
- Metrics: []ddprofiledefinition.MetricsConfig{
4838
- {
4839
- Table: ddprofiledefinition.SymbolConfig{
4840
- OID: "1.3.6.1.4.1.14988.1.1.3.100",
4841
- Name: "mtxrHlTable",
4842
- },
4843
- Symbols: []ddprofiledefinition.SymbolConfig{
4844
- {
4845
- OID: "1.3.6.1.4.1.14988.1.1.3.100.1.3",
4846
- Name: "mtxrHlSensorValue",
4847
- Transform: `
4848
-{{- $config := get (dict
4849
- "1" (dict "name" "temperature" "unit" "celsius" "family" "Health/Temperature")
4850
- "2" (dict "name" "fan_speed" "unit" "rpm" "family" "Health/Cooling")
4851
- "3" (dict "name" "voltage" "unit" "volts" "family" "Health/Power" "divisor" 10.0)
4852
- "6" (dict "name" "sensor_status" "family" "Health/Status"
4853
- "mapping" (i64map 0 "not_ok" 1 "ok"))
4854
-) (index .Metric.Tags "sensor_type" | default "") -}}
4855
-
4856
-{{- if $config -}}
4857
- {{- setName .Metric (printf "%s_%s" .Metric.Name (get $config "name")) -}}
4858
- {{- setFamily .Metric (get $config "family") -}}
4859
- {{- with get $config "unit" -}}{{- setUnit $.Metric . -}}{{- end -}}
4860
- {{- with get $config "divisor" -}}{{- setValue $.Metric (int64 (div (float64 $.Metric.Value) .)) -}}{{- end -}}
4861
- {{- with get $config "mapping" -}}{{- setMappings $.Metric . -}}{{- end -}}
4862
-{{- end -}}
4863
-
4864
-{{- deleteTag .Metric "sensor_type" -}}`,
4865
- },
4866
- },
4867
- MetricTags: []ddprofiledefinition.MetricTagConfig{
4868
- {
4869
- Tag: "sensor_name",
4870
- Symbol: ddprofiledefinition.SymbolConfigCompat{
4871
- OID: "1.3.6.1.4.1.14988.1.1.3.100.1.2",
4872
- Name: "mtxrHlSensorName",
4873
- },
4874
- },
4875
- {
4876
- Tag: "sensor_type",
4877
- Symbol: ddprofiledefinition.SymbolConfigCompat{
4878
- OID: "1.3.6.1.4.1.14988.1.1.3.100.1.4",
4879
- Name: "mtxrHlSensorType",
4880
- },
4881
- },
4882
- },
4883
- },
4884
- },
4885
- },
4886
- },
4887
- },
4888
- setupMock: func(m *snmpmock.MockHandler) {
4889
- m.EXPECT().MaxOids().Return(10).AnyTimes()
4890
- m.EXPECT().Version().Return(gosnmp.Version2c)
4891
- m.EXPECT().BulkWalkAll("1.3.6.1.4.1.14988.1.1.3.100").Return(
4892
- []gosnmp.SnmpPDU{
4893
- // Temperature sensor
4894
- {
4895
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.1",
4896
- Type: gosnmp.Integer,
4897
- Value: 45,
4898
- },
4899
- {
4900
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.1",
4901
- Type: gosnmp.OctetString,
4902
- Value: []byte("cpu-temperature"),
4903
- },
4904
- {
4905
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.1",
4906
- Type: gosnmp.Integer,
4907
- Value: 1,
4908
- },
4909
- // PSU status sensor
4910
- {
4911
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.2",
4912
- Type: gosnmp.Integer,
4913
- Value: 1,
4914
- },
4915
- {
4916
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.2",
4917
- Type: gosnmp.OctetString,
4918
- Value: []byte("psu1-state"),
4919
- },
4920
- {
4921
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.2",
4922
- Type: gosnmp.Integer,
4923
- Value: 6,
4924
- },
4925
- // Voltage sensor
4926
- {
4927
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.3",
4928
- Type: gosnmp.Integer,
4929
- Value: 240, // 24.0V
4930
- },
4931
- {
4932
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.3",
4933
- Type: gosnmp.OctetString,
4934
- Value: []byte("psu-voltage"),
4935
- },
4936
- {
4937
- Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.3",
4938
- Type: gosnmp.Integer,
4939
- Value: 3,
4940
- },
4941
- }, nil,
4942
- )
4943
- },
4944
- expectedResult: []*ddsnmp.ProfileMetrics{
4945
- {
4946
- Source: "mikrotik-profile.yaml",
4947
- DeviceMetadata: nil,
4948
- Metrics: []ddsnmp.Metric{
4949
- {
4950
- Name: "mtxrHlSensorValue_temperature",
4951
- Value: 45,
4952
- Tags: map[string]string{"sensor_name": "cpu-temperature"},
4953
- Unit: "celsius",
4954
- Family: "Health/Temperature",
4955
- MetricType: "gauge",
4956
- IsTable: true,
4957
- },
4958
- {
4959
- Name: "mtxrHlSensorValue_sensor_status",
4960
- Value: 1,
4961
- Tags: map[string]string{"sensor_name": "psu1-state"},
4962
- Family: "Health/Status",
4963
- Mappings: map[int64]string{
4964
- 0: "not_ok",
4965
- 1: "ok",
4966
- },
4967
- MetricType: "gauge",
4968
- IsTable: true,
4969
- },
4970
- {
4971
- Name: "mtxrHlSensorValue_voltage",
4972
- Value: 24,
4973
- Tags: map[string]string{"sensor_name": "psu-voltage"},
4974
- Unit: "volts",
4975
- Family: "Health/Power",
4976
- MetricType: "gauge",
4977
- IsTable: true,
4978
- },
4979
- },
4980
- },
4981
- },
4982
- expectedError: false,
4983
- },
4984
- }
4985
-
4986
- for name, tc := range tests {
4987
- t.Run(name, func(t *testing.T) {
4988
- ctrl := gomock.NewController(t)
4989
- defer ctrl.Finish()
4990
-
4991
- mockHandler := snmpmock.NewMockHandler(ctrl)
4992
- tc.setupMock(mockHandler)
4993
-
4994
- // Compile transforms for profiles
4995
- for _, profile := range tc.profiles {
4996
- if err := ddsnmp.CompileTransforms(profile); err != nil {
4997
- if tc.expectedError && tc.errorContains != "" && strings.Contains(err.Error(), tc.errorContains) {
4998
- return // Expected error during compilation
4999
- }
This file is too large to show in full.
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collectors_meta.go
new
+316
@@ -0,0 +1,316 @@
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/logger"
13
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
+)
16
+
17
+// globalTagsCollector handles collection of profile-wide tags
18
+type globalTagsCollector struct {
19
+ snmpClient gosnmp.Handler
20
+ missingOIDs map[string]bool
21
+ log *logger.Logger
22
+ tagProc *globalTagProcessor
23
+}
24
+
25
+func newGlobalTagsCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, log *logger.Logger) *globalTagsCollector {
26
+ return &globalTagsCollector{
27
+ snmpClient: snmpClient,
28
+ missingOIDs: missingOIDs,
29
+ log: log,
30
+ tagProc: newGlobalTagProcessor(),
31
+ }
32
+}
33
+
34
+// Collect gathers all global tags from the profile
35
+func (gc *globalTagsCollector) Collect(prof *ddsnmp.Profile) (map[string]string, error) {
36
+ if len(prof.Definition.MetricTags) == 0 && len(prof.Definition.StaticTags) == 0 {
37
+ return nil, nil
38
+ }
39
+
40
+ tags := make(map[string]string)
41
+
42
+ gc.processStaticTags(prof.Definition.StaticTags, tags)
43
+
44
+ if err := gc.processDynamicTags(prof.Definition.MetricTags, tags); err != nil {
45
+ return ternary(len(tags) > 0, tags, nil), err
46
+ }
47
+
48
+ return tags, nil
49
+}
50
+
51
+func (gc *globalTagsCollector) processStaticTags(staticTags []string, globalTags map[string]string) {
52
+ mergeTagsWithEmptyFallback(globalTags, parseStaticTags(staticTags))
53
+}
54
+
55
+// processDynamicTags processes tags that require SNMP fetching
56
+func (gc *globalTagsCollector) processDynamicTags(metricTags []ddprofiledefinition.MetricTagConfig, globalTags map[string]string) error {
57
+ // Identify OIDs to collect
58
+ oids, missingOIDs := gc.identifyTagOIDs(metricTags)
59
+
60
+ if len(missingOIDs) > 0 {
61
+ gc.log.Debugf("global tags missing OIDs: %v", missingOIDs)
62
+ }
63
+
64
+ if len(oids) == 0 {
65
+ return nil
66
+ }
67
+
68
+ pdus, err := gc.fetchTagValues(oids)
69
+ if err != nil {
70
+ return fmt.Errorf("failed to fetch global tag values: %w", err)
71
+ }
72
+
73
+ // Process each tag configuration
74
+ var errs []error
75
+ for _, tagCfg := range metricTags {
76
+ if tagCfg.Symbol.OID == "" {
77
+ continue
78
+ }
79
+
80
+ ta := tagAdder{tags: globalTags}
81
+
82
+ if err := gc.tagProc.processTag(tagCfg, pdus, ta); err != nil {
83
+ errs = append(errs, fmt.Errorf("failed to process tag value for '%s/%s': %w",
84
+ tagCfg.Tag, tagCfg.Symbol.Name, err))
85
+ continue
86
+ }
87
+ }
88
+
89
+ if len(errs) > 0 && len(globalTags) == 0 {
90
+ return fmt.Errorf("failed to process any global tags: %w", errors.Join(errs...))
91
+ }
92
+
93
+ return nil
94
+}
95
+
96
+func (gc *globalTagsCollector) identifyTagOIDs(metricTags []ddprofiledefinition.MetricTagConfig) ([]string, []string) {
97
+ var oids []string
98
+ var missingOIDs []string
99
+
100
+ for _, tagCfg := range metricTags {
101
+ if tagCfg.Symbol.OID == "" {
102
+ continue
103
+ }
104
+
105
+ oid := trimOID(tagCfg.Symbol.OID)
106
+ if gc.missingOIDs[oid] {
107
+ missingOIDs = append(missingOIDs, tagCfg.Symbol.OID)
108
+ continue
109
+ }
110
+
111
+ oids = append(oids, tagCfg.Symbol.OID)
112
+ }
113
+
114
+ // Sort and deduplicate
115
+ slices.Sort(oids)
116
+ oids = slices.Compact(oids)
117
+
118
+ return oids, missingOIDs
119
+}
120
+
121
+func (gc *globalTagsCollector) fetchTagValues(oids []string) (map[string]gosnmp.SnmpPDU, error) {
122
+ pdus := make(map[string]gosnmp.SnmpPDU)
123
+ maxOids := gc.snmpClient.MaxOids()
124
+
125
+ for chunk := range slices.Chunk(oids, maxOids) {
126
+ result, err := gc.snmpClient.Get(chunk)
127
+ if err != nil {
128
+ return nil, err
129
+ }
130
+
131
+ for _, pdu := range result.Variables {
132
+ if !isPduWithData(pdu) {
133
+ gc.missingOIDs[trimOID(pdu.Name)] = true
134
+ continue
135
+ }
136
+ pdus[trimOID(pdu.Name)] = pdu
137
+ }
138
+ }
139
+
140
+ return pdus, nil
141
+}
142
+
143
+// deviceMetadataCollector handles collection of device metadata
144
+type deviceMetadataCollector struct {
145
+ snmpClient gosnmp.Handler
146
+ missingOIDs map[string]bool
147
+ log *logger.Logger
148
+}
149
+
150
+func newDeviceMetadataCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, log *logger.Logger) *deviceMetadataCollector {
151
+ return &deviceMetadataCollector{
152
+ snmpClient: snmpClient,
153
+ missingOIDs: missingOIDs,
154
+ log: log,
155
+ }
156
+}
157
+
158
+func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]string, error) {
159
+ if len(prof.Definition.Metadata) == 0 {
160
+ return nil, nil
161
+ }
162
+
163
+ meta := make(map[string]string)
164
+
165
+ for resName, cfg := range prof.Definition.Metadata {
166
+ if !ddprofiledefinition.IsMetadataResourceWithScalarOids(resName) {
167
+ continue
168
+ }
169
+
170
+ if err := dc.processResource(cfg, meta); err != nil {
171
+ return ternary(len(meta) > 0, meta, nil), fmt.Errorf("failed to process metadata resource '%s': %w", resName, err)
172
+ }
173
+ }
174
+
175
+ return meta, nil
176
+}
177
+
178
+// processResource processes a single metadata resource
179
+func (dc *deviceMetadataCollector) processResource(cfg ddprofiledefinition.MetadataResourceConfig, metadata map[string]string) error {
180
+ staticValues := make(map[string]string)
181
+ oids := dc.collectStaticAndIdentifyOIDs(cfg, staticValues)
182
+
183
+ for k, v := range staticValues {
184
+ metadata[k] = v
185
+ }
186
+
187
+ if len(oids) == 0 {
188
+ return nil
189
+ }
190
+
191
+ pdus, err := dc.fetchMetadataValues(oids)
192
+ if err != nil {
193
+ return fmt.Errorf("failed to fetch metadata values: %w", err)
194
+ }
195
+
196
+ return dc.processDynamicFields(cfg, pdus, metadata)
197
+}
198
+
199
+// collectStaticAndIdentifyOIDs collects static values and returns OIDs to fetch
200
+func (dc *deviceMetadataCollector) collectStaticAndIdentifyOIDs(cfg ddprofiledefinition.MetadataResourceConfig, staticValues map[string]string) []string {
201
+ var oids []string
202
+
203
+ for name, field := range cfg.Fields {
204
+ switch {
205
+ case field.Value != "":
206
+ staticValues[name] = field.Value
207
+ case field.Symbol.OID != "":
208
+ if !dc.missingOIDs[trimOID(field.Symbol.OID)] {
209
+ oids = append(oids, field.Symbol.OID)
210
+ }
211
+ case len(field.Symbols) > 0:
212
+ for _, sym := range field.Symbols {
213
+ if sym.OID != "" && !dc.missingOIDs[trimOID(sym.OID)] {
214
+ oids = append(oids, sym.OID)
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // Sort and deduplicate
221
+ slices.Sort(oids)
222
+ oids = slices.Compact(oids)
223
+
224
+ return oids
225
+}
226
+
227
+func (dc *deviceMetadataCollector) fetchMetadataValues(oids []string) (map[string]gosnmp.SnmpPDU, error) {
228
+ pdus := make(map[string]gosnmp.SnmpPDU)
229
+ maxOids := dc.snmpClient.MaxOids()
230
+
231
+ for chunk := range slices.Chunk(oids, maxOids) {
232
+ result, err := dc.snmpClient.Get(chunk)
233
+ if err != nil {
234
+ return nil, err
235
+ }
236
+
237
+ for _, pdu := range result.Variables {
238
+ if !isPduWithData(pdu) {
239
+ dc.missingOIDs[trimOID(pdu.Name)] = true
240
+ continue
241
+ }
242
+ pdus[trimOID(pdu.Name)] = pdu
243
+ }
244
+ }
245
+
246
+ return pdus, nil
247
+}
248
+
249
+func (dc *deviceMetadataCollector) processDynamicFields(cfg ddprofiledefinition.MetadataResourceConfig, pdus map[string]gosnmp.SnmpPDU, metadata map[string]string) error {
250
+ var errs []error
251
+
252
+ for name, field := range cfg.Fields {
253
+ switch {
254
+ case field.Symbol.OID != "":
255
+ // Single symbol
256
+ v, err := dc.processSymbolValue(field.Symbol, pdus)
257
+ if err != nil {
258
+ errs = append(errs, fmt.Errorf("failed to process metadata field '%s': %w", name, err))
259
+ continue
260
+ }
261
+ if v != "" {
262
+ mergeTagsWithEmptyFallback(metadata, map[string]string{name: v})
263
+ }
264
+ case len(field.Symbols) > 0:
265
+ // Multiple symbols - try each until one succeeds
266
+ for _, sym := range field.Symbols {
267
+ v, err := dc.processSymbolValue(sym, pdus)
268
+ if err != nil {
269
+ errs = append(errs, fmt.Errorf("failed to process metadata field '%s' symbol '%s': %w",
270
+ name, sym.Name, err))
271
+ continue
272
+ }
273
+ if v != "" {
274
+ mergeTagsWithEmptyFallback(metadata, map[string]string{name: v})
275
+ break // Use first successful value
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ if len(errs) > 0 && len(metadata) == 0 {
282
+ return fmt.Errorf("failed to process any metadata fields: %w", errors.Join(errs...))
283
+ }
284
+
285
+ return nil
286
+}
287
+
288
+func (dc *deviceMetadataCollector) processSymbolValue(cfg ddprofiledefinition.SymbolConfig, pdus map[string]gosnmp.SnmpPDU) (string, error) {
289
+ pdu, ok := pdus[trimOID(cfg.OID)]
290
+ if !ok {
291
+ return "", nil
292
+ }
293
+
294
+ val, err := convPduToStringf(pdu, cfg.Format)
295
+ if err != nil {
296
+ return "", err
297
+ }
298
+
299
+ if cfg.ExtractValueCompiled != nil {
300
+ if sm := cfg.ExtractValueCompiled.FindStringSubmatch(val); len(sm) > 1 {
301
+ val = sm[1]
302
+ }
303
+ }
304
+
305
+ if cfg.MatchPatternCompiled != nil {
306
+ if sm := cfg.MatchPatternCompiled.FindStringSubmatch(val); len(sm) > 0 {
307
+ val = replaceSubmatches(cfg.MatchValue, sm)
308
+ }
309
+ }
310
+
311
+ if v, ok := cfg.Mapping[val]; ok {
312
+ val = v
313
+ }
314
+
315
+ return val, nil
316
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collectors_meta_test.go
new
+919
@@ -0,0 +1,919 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "testing"
8
+
9
+ "github.com/golang/mock/gomock"
10
+ "github.com/gosnmp/gosnmp"
11
+ "github.com/stretchr/testify/assert"
12
+
13
+ snmpmock "github.com/gosnmp/gosnmp/mocks"
14
+
15
+ "github.com/netdata/netdata/go/plugins/logger"
16
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
18
+)
19
+
20
+func TestGlobalTagsCollector_Collect(t *testing.T) {
21
+ tests := map[string]struct {
22
+ profile *ddsnmp.Profile
23
+ setupMock func(m *snmpmock.MockHandler)
24
+ expectedResult map[string]string
25
+ expectedError bool
26
+ errorContains string
27
+ }{
28
+ "no tags configured": {
29
+ profile: &ddsnmp.Profile{
30
+ Definition: &ddprofiledefinition.ProfileDefinition{
31
+ MetricTags: []ddprofiledefinition.MetricTagConfig{},
32
+ StaticTags: []string{},
33
+ },
34
+ },
35
+ setupMock: func(m *snmpmock.MockHandler) {},
36
+ expectedResult: nil,
37
+ expectedError: false,
38
+ },
39
+ "static tags only": {
40
+ profile: &ddsnmp.Profile{
41
+ Definition: &ddprofiledefinition.ProfileDefinition{
42
+ StaticTags: []string{
43
+ "environment:production",
44
+ "region:us-east-1",
45
+ "service:network",
46
+ },
47
+ },
48
+ },
49
+ setupMock: func(m *snmpmock.MockHandler) {},
50
+ expectedResult: map[string]string{
51
+ "environment": "production",
52
+ "region": "us-east-1",
53
+ "service": "network",
54
+ },
55
+ expectedError: false,
56
+ },
57
+ "dynamic tags only": {
58
+ profile: &ddsnmp.Profile{
59
+ Definition: &ddprofiledefinition.ProfileDefinition{
60
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
61
+ {
62
+ Tag: "device_vendor",
63
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
64
+ OID: "1.3.6.1.2.1.1.1.0",
65
+ Name: "sysDescr",
66
+ },
67
+ },
68
+ {
69
+ Tag: "location",
70
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
71
+ OID: "1.3.6.1.2.1.1.6.0",
72
+ Name: "sysLocation",
73
+ },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ setupMock: func(m *snmpmock.MockHandler) {
79
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
80
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
81
+ "1.3.6.1.2.1.1.1.0",
82
+ "1.3.6.1.2.1.1.6.0",
83
+ })).Return(
84
+ &gosnmp.SnmpPacket{
85
+ Variables: []gosnmp.SnmpPDU{
86
+ {
87
+ Name: "1.3.6.1.2.1.1.1.0",
88
+ Type: gosnmp.OctetString,
89
+ Value: []byte("Cisco IOS Software"),
90
+ },
91
+ {
92
+ Name: "1.3.6.1.2.1.1.6.0",
93
+ Type: gosnmp.OctetString,
94
+ Value: []byte("DataCenter-1"),
95
+ },
96
+ },
97
+ }, nil,
98
+ )
99
+ },
100
+ expectedResult: map[string]string{
101
+ "device_vendor": "Cisco IOS Software",
102
+ "location": "DataCenter-1",
103
+ },
104
+ expectedError: false,
105
+ },
106
+ "mixed static and dynamic tags": {
107
+ profile: &ddsnmp.Profile{
108
+ Definition: &ddprofiledefinition.ProfileDefinition{
109
+ StaticTags: []string{
110
+ "environment:production",
111
+ "managed:true",
112
+ },
113
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
114
+ {
115
+ Tag: "hostname",
116
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
117
+ OID: "1.3.6.1.2.1.1.5.0",
118
+ Name: "sysName",
119
+ },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ setupMock: func(m *snmpmock.MockHandler) {
125
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
126
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
127
+ &gosnmp.SnmpPacket{
128
+ Variables: []gosnmp.SnmpPDU{
129
+ {
130
+ Name: "1.3.6.1.2.1.1.5.0",
131
+ Type: gosnmp.OctetString,
132
+ Value: []byte("router-01"),
133
+ },
134
+ },
135
+ }, nil,
136
+ )
137
+ },
138
+ expectedResult: map[string]string{
139
+ "environment": "production",
140
+ "managed": "true",
141
+ "hostname": "router-01",
142
+ },
143
+ expectedError: false,
144
+ },
145
+ "tag with mapping": {
146
+ profile: &ddsnmp.Profile{
147
+ Definition: &ddprofiledefinition.ProfileDefinition{
148
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
149
+ {
150
+ Tag: "device_type",
151
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
152
+ OID: "1.3.6.1.2.1.1.2.0",
153
+ Name: "sysObjectID",
154
+ },
155
+ Mapping: map[string]string{
156
+ "1.3.6.1.4.1.9.1.1": "router",
157
+ "1.3.6.1.4.1.9.1.2": "switch",
158
+ "1.3.6.1.4.1.9.1.3": "firewall",
159
+ },
160
+ },
161
+ },
162
+ },
163
+ },
164
+ setupMock: func(m *snmpmock.MockHandler) {
165
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
166
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.2.0"}).Return(
167
+ &gosnmp.SnmpPacket{
168
+ Variables: []gosnmp.SnmpPDU{
169
+ {
170
+ Name: "1.3.6.1.2.1.1.2.0",
171
+ Type: gosnmp.ObjectIdentifier,
172
+ Value: "1.3.6.1.4.1.9.1.2",
173
+ },
174
+ },
175
+ }, nil,
176
+ )
177
+ },
178
+ expectedResult: map[string]string{
179
+ "device_type": "switch",
180
+ },
181
+ expectedError: false,
182
+ },
183
+ "tag with pattern matching": {
184
+ profile: &ddsnmp.Profile{
185
+ Definition: &ddprofiledefinition.ProfileDefinition{
186
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
187
+ {
188
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
189
+ OID: "1.3.6.1.2.1.1.5.0",
190
+ Name: "sysName",
191
+ },
192
+ Pattern: mustCompileRegex(`(.*)-(.*)-(.*)`),
193
+ Tags: map[string]string{
194
+ "site": "$1",
195
+ "role": "$2",
196
+ "unit_num": "$3",
197
+ },
198
+ },
199
+ },
200
+ },
201
+ },
202
+ setupMock: func(m *snmpmock.MockHandler) {
203
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
204
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
205
+ &gosnmp.SnmpPacket{
206
+ Variables: []gosnmp.SnmpPDU{
207
+ {
208
+ Name: "1.3.6.1.2.1.1.5.0",
209
+ Type: gosnmp.OctetString,
210
+ Value: []byte("NYC-CORE-01"),
211
+ },
212
+ },
213
+ }, nil,
214
+ )
215
+ },
216
+ expectedResult: map[string]string{
217
+ "site": "NYC",
218
+ "role": "CORE",
219
+ "unit_num": "01",
220
+ },
221
+ expectedError: false,
222
+ },
223
+ "missing OID": {
224
+ profile: &ddsnmp.Profile{
225
+ Definition: &ddprofiledefinition.ProfileDefinition{
226
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
227
+ {
228
+ Tag: "hostname",
229
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
230
+ OID: "1.3.6.1.2.1.1.5.0",
231
+ Name: "sysName",
232
+ },
233
+ },
234
+ },
235
+ },
236
+ },
237
+ setupMock: func(m *snmpmock.MockHandler) {
238
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
239
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
240
+ &gosnmp.SnmpPacket{
241
+ Variables: []gosnmp.SnmpPDU{
242
+ {
243
+ Name: "1.3.6.1.2.1.1.5.0",
244
+ Type: gosnmp.NoSuchObject,
245
+ Value: nil,
246
+ },
247
+ },
248
+ }, nil,
249
+ )
250
+ },
251
+ expectedResult: map[string]string{},
252
+ expectedError: false,
253
+ },
254
+ "SNMP error": {
255
+ profile: &ddsnmp.Profile{
256
+ Definition: &ddprofiledefinition.ProfileDefinition{
257
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
258
+ {
259
+ Tag: "hostname",
260
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
261
+ OID: "1.3.6.1.2.1.1.5.0",
262
+ Name: "sysName",
263
+ },
264
+ },
265
+ },
266
+ },
267
+ },
268
+ setupMock: func(m *snmpmock.MockHandler) {
269
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
270
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
271
+ nil,
272
+ errors.New("SNMP timeout"),
273
+ )
274
+ },
275
+ expectedResult: nil,
276
+ expectedError: true,
277
+ errorContains: "failed to fetch global tag values",
278
+ },
279
+ "empty tag name falls back to symbol name": {
280
+ profile: &ddsnmp.Profile{
281
+ Definition: &ddprofiledefinition.ProfileDefinition{
282
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
283
+ {
284
+ Tag: "", // Empty tag name
285
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
286
+ OID: "1.3.6.1.2.1.1.5.0",
287
+ Name: "sysName",
288
+ },
289
+ },
290
+ },
291
+ },
292
+ },
293
+ setupMock: func(m *snmpmock.MockHandler) {
294
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
295
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
296
+ &gosnmp.SnmpPacket{
297
+ Variables: []gosnmp.SnmpPDU{
298
+ {
299
+ Name: "1.3.6.1.2.1.1.5.0",
300
+ Type: gosnmp.OctetString,
301
+ Value: []byte("router-01"),
302
+ },
303
+ },
304
+ }, nil,
305
+ )
306
+ },
307
+ expectedResult: map[string]string{
308
+ "sysName": "router-01",
309
+ },
310
+ expectedError: false,
311
+ },
312
+ "chunked requests": {
313
+ profile: &ddsnmp.Profile{
314
+ Definition: &ddprofiledefinition.ProfileDefinition{
315
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
316
+ {
317
+ Tag: "tag1",
318
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
319
+ OID: "1.3.6.1.2.1.1.1.0",
320
+ Name: "oid1",
321
+ },
322
+ },
323
+ {
324
+ Tag: "tag2",
325
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
326
+ OID: "1.3.6.1.2.1.1.2.0",
327
+ Name: "oid2",
328
+ },
329
+ },
330
+ {
331
+ Tag: "tag3",
332
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
333
+ OID: "1.3.6.1.2.1.1.3.0",
334
+ Name: "oid3",
335
+ },
336
+ },
337
+ },
338
+ },
339
+ },
340
+ setupMock: func(m *snmpmock.MockHandler) {
341
+ m.EXPECT().MaxOids().Return(2).AnyTimes() // Force chunking
342
+ // First chunk
343
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"}).Return(
344
+ &gosnmp.SnmpPacket{
345
+ Variables: []gosnmp.SnmpPDU{
346
+ {
347
+ Name: "1.3.6.1.2.1.1.1.0",
348
+ Type: gosnmp.OctetString,
349
+ Value: []byte("value1"),
350
+ },
351
+ {
352
+ Name: "1.3.6.1.2.1.1.2.0",
353
+ Type: gosnmp.OctetString,
354
+ Value: []byte("value2"),
355
+ },
356
+ },
357
+ }, nil,
358
+ )
359
+ // Second chunk
360
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
361
+ &gosnmp.SnmpPacket{
362
+ Variables: []gosnmp.SnmpPDU{
363
+ {
364
+ Name: "1.3.6.1.2.1.1.3.0",
365
+ Type: gosnmp.OctetString,
366
+ Value: []byte("value3"),
367
+ },
368
+ },
369
+ }, nil,
370
+ )
371
+ },
372
+ expectedResult: map[string]string{
373
+ "tag1": "value1",
374
+ "tag2": "value2",
375
+ "tag3": "value3",
376
+ },
377
+ expectedError: false,
378
+ },
379
+ }
380
+
381
+ for name, tc := range tests {
382
+ t.Run(name, func(t *testing.T) {
383
+ ctrl := gomock.NewController(t)
384
+ defer ctrl.Finish()
385
+
386
+ mockHandler := snmpmock.NewMockHandler(ctrl)
387
+ tc.setupMock(mockHandler)
388
+
389
+ missingOIDs := make(map[string]bool)
390
+ collector := newGlobalTagsCollector(mockHandler, missingOIDs, logger.New())
391
+
392
+ result, err := collector.Collect(tc.profile)
393
+
394
+ if tc.expectedError {
395
+ assert.Error(t, err)
396
+ if tc.errorContains != "" {
397
+ assert.Contains(t, err.Error(), tc.errorContains)
398
+ }
399
+ } else {
400
+ assert.NoError(t, err)
401
+ }
402
+
403
+ assert.Equal(t, tc.expectedResult, result)
404
+ })
405
+ }
406
+}
407
+
408
+func TestDeviceMetadataCollector_Collect(t *testing.T) {
409
+ tests := map[string]struct {
410
+ profile *ddsnmp.Profile
411
+ setupMock func(m *snmpmock.MockHandler)
412
+ expectedResult map[string]string
413
+ expectedError bool
414
+ errorContains string
415
+ }{
416
+ "no metadata configured": {
417
+ profile: &ddsnmp.Profile{
418
+ Definition: &ddprofiledefinition.ProfileDefinition{
419
+ Metadata: ddprofiledefinition.MetadataConfig{},
420
+ },
421
+ },
422
+ setupMock: func(m *snmpmock.MockHandler) {},
423
+ expectedResult: nil,
424
+ expectedError: false,
425
+ },
426
+ "static values only": {
427
+ profile: &ddsnmp.Profile{
428
+ Definition: &ddprofiledefinition.ProfileDefinition{
429
+ Metadata: ddprofiledefinition.MetadataConfig{
430
+ "device": ddprofiledefinition.MetadataResourceConfig{
431
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
432
+ "vendor": {
433
+ Value: "dell",
434
+ },
435
+ "type": {
436
+ Value: "router",
437
+ },
438
+ "model": {
439
+ Value: "PowerEdge R740",
440
+ },
441
+ },
442
+ },
443
+ },
444
+ },
445
+ },
446
+ setupMock: func(m *snmpmock.MockHandler) {},
447
+ expectedResult: map[string]string{
448
+ "vendor": "dell",
449
+ "type": "router",
450
+ "model": "PowerEdge R740",
451
+ },
452
+ expectedError: false,
453
+ },
454
+ "dynamic values with single symbol": {
455
+ profile: &ddsnmp.Profile{
456
+ Definition: &ddprofiledefinition.ProfileDefinition{
457
+ Metadata: ddprofiledefinition.MetadataConfig{
458
+ "device": ddprofiledefinition.MetadataResourceConfig{
459
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
460
+ "vendor": {
461
+ Value: "dell",
462
+ },
463
+ "serial_number": {
464
+ Symbol: ddprofiledefinition.SymbolConfig{
465
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
466
+ Name: "chassisSerialNumber",
467
+ },
468
+ },
469
+ "version": {
470
+ Symbol: ddprofiledefinition.SymbolConfig{
471
+ OID: "1.3.6.1.2.1.1.1.0",
472
+ Name: "sysDescr",
473
+ },
474
+ },
475
+ },
476
+ },
477
+ },
478
+ },
479
+ },
480
+ setupMock: func(m *snmpmock.MockHandler) {
481
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
482
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
483
+ "1.3.6.1.2.1.1.1.0",
484
+ "1.3.6.1.4.1.674.10892.5.1.3.2.0",
485
+ })).Return(
486
+ &gosnmp.SnmpPacket{
487
+ Variables: []gosnmp.SnmpPDU{
488
+ {
489
+ Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
490
+ Type: gosnmp.OctetString,
491
+ Value: []byte("ABC123XYZ"),
492
+ },
493
+ {
494
+ Name: "1.3.6.1.2.1.1.1.0",
495
+ Type: gosnmp.OctetString,
496
+ Value: []byte("Dell EMC Networking OS10"),
497
+ },
498
+ },
499
+ }, nil,
500
+ )
501
+ },
502
+ expectedResult: map[string]string{
503
+ "vendor": "dell",
504
+ "serial_number": "ABC123XYZ",
505
+ "version": "Dell EMC Networking OS10",
506
+ },
507
+ expectedError: false,
508
+ },
509
+ "multiple symbols fallback": {
510
+ profile: &ddsnmp.Profile{
511
+ Definition: &ddprofiledefinition.ProfileDefinition{
512
+ Metadata: ddprofiledefinition.MetadataConfig{
513
+ "device": ddprofiledefinition.MetadataResourceConfig{
514
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
515
+ "serial_number": {
516
+ Symbols: []ddprofiledefinition.SymbolConfig{
517
+ {
518
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
519
+ Name: "chassisSerialNumber",
520
+ },
521
+ {
522
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.3.0",
523
+ Name: "backupSerialNumber",
524
+ },
525
+ },
526
+ },
527
+ },
528
+ },
529
+ },
530
+ },
531
+ },
532
+ setupMock: func(m *snmpmock.MockHandler) {
533
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
534
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
535
+ "1.3.6.1.4.1.674.10892.5.1.3.2.0",
536
+ "1.3.6.1.4.1.674.10892.5.1.3.3.0",
537
+ })).Return(
538
+ &gosnmp.SnmpPacket{
539
+ Variables: []gosnmp.SnmpPDU{
540
+ {
541
+ Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
542
+ Type: gosnmp.NoSuchObject,
543
+ Value: nil,
544
+ },
545
+ {
546
+ Name: "1.3.6.1.4.1.674.10892.5.1.3.3.0",
547
+ Type: gosnmp.OctetString,
548
+ Value: []byte("BACKUP123"),
549
+ },
550
+ },
551
+ }, nil,
552
+ )
553
+ },
554
+ expectedResult: map[string]string{
555
+ "serial_number": "BACKUP123",
556
+ },
557
+ expectedError: false,
558
+ },
559
+ "value with match_pattern": {
560
+ profile: &ddsnmp.Profile{
561
+ Definition: &ddprofiledefinition.ProfileDefinition{
562
+ Metadata: ddprofiledefinition.MetadataConfig{
563
+ "device": ddprofiledefinition.MetadataResourceConfig{
564
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
565
+ "version": {
566
+ Symbol: ddprofiledefinition.SymbolConfig{
567
+ OID: "1.3.6.1.2.1.1.1.0",
568
+ Name: "sysDescr",
569
+ MatchPatternCompiled: mustCompileRegex(`Isilon OneFS v(\S+)`),
570
+ MatchValue: "$1",
571
+ },
572
+ },
573
+ },
574
+ },
575
+ },
576
+ },
577
+ },
578
+ setupMock: func(m *snmpmock.MockHandler) {
579
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
580
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
581
+ &gosnmp.SnmpPacket{
582
+ Variables: []gosnmp.SnmpPDU{
583
+ {
584
+ Name: "1.3.6.1.2.1.1.1.0",
585
+ Type: gosnmp.OctetString,
586
+ Value: []byte("device-name-3 263829375 Isilon OneFS v8.2.0.0"),
587
+ },
588
+ },
589
+ }, nil,
590
+ )
591
+ },
592
+ expectedResult: map[string]string{
593
+ "version": "8.2.0.0",
594
+ },
595
+ expectedError: false,
596
+ },
597
+ "value with extract_value": {
598
+ profile: &ddsnmp.Profile{
599
+ Definition: &ddprofiledefinition.ProfileDefinition{
600
+ Metadata: ddprofiledefinition.MetadataConfig{
601
+ "device": ddprofiledefinition.MetadataResourceConfig{
602
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
603
+ "temperature": {
604
+ Symbol: ddprofiledefinition.SymbolConfig{
605
+ OID: "1.3.6.1.4.1.674.10892.5.4.200.10.1.2.1.3.1",
606
+ Name: "temperatureProbeReading",
607
+ ExtractValueCompiled: mustCompileRegex(`(\d+)C`),
608
+ },
609
+ },
610
+ },
611
+ },
612
+ },
613
+ },
614
+ },
615
+ setupMock: func(m *snmpmock.MockHandler) {
616
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
617
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.4.200.10.1.2.1.3.1"}).Return(
618
+ &gosnmp.SnmpPacket{
619
+ Variables: []gosnmp.SnmpPDU{
620
+ {
621
+ Name: "1.3.6.1.4.1.674.10892.5.4.200.10.1.2.1.3.1",
622
+ Type: gosnmp.OctetString,
623
+ Value: []byte("25C"),
624
+ },
625
+ },
626
+ }, nil,
627
+ )
628
+ },
629
+ expectedResult: map[string]string{
630
+ "temperature": "25",
631
+ },
632
+ expectedError: false,
633
+ },
634
+ "value with mapping": {
635
+ profile: &ddsnmp.Profile{
636
+ Definition: &ddprofiledefinition.ProfileDefinition{
637
+ Metadata: ddprofiledefinition.MetadataConfig{
638
+ "device": ddprofiledefinition.MetadataResourceConfig{
639
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
640
+ "status": {
641
+ Symbol: ddprofiledefinition.SymbolConfig{
642
+ OID: "1.3.6.1.4.1.674.10892.5.2.1.0",
643
+ Name: "globalSystemStatus",
644
+ Mapping: map[string]string{
645
+ "1": "other",
646
+ "2": "unknown",
647
+ "3": "ok",
648
+ "4": "nonCritical",
649
+ "5": "critical",
650
+ "6": "nonRecoverable",
651
+ },
652
+ },
653
+ },
654
+ },
655
+ },
656
+ },
657
+ },
658
+ },
659
+ setupMock: func(m *snmpmock.MockHandler) {
660
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
661
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.2.1.0"}).Return(
662
+ &gosnmp.SnmpPacket{
663
+ Variables: []gosnmp.SnmpPDU{
664
+ {
665
+ Name: "1.3.6.1.4.1.674.10892.5.2.1.0",
666
+ Type: gosnmp.Integer,
667
+ Value: 3,
668
+ },
669
+ },
670
+ }, nil,
671
+ )
672
+ },
673
+ expectedResult: map[string]string{
674
+ "status": "ok",
675
+ },
676
+ expectedError: false,
677
+ },
678
+ "format mac_address": {
679
+ profile: &ddsnmp.Profile{
680
+ Definition: &ddprofiledefinition.ProfileDefinition{
681
+ Metadata: ddprofiledefinition.MetadataConfig{
682
+ "device": ddprofiledefinition.MetadataResourceConfig{
683
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
684
+ "mac_address": {
685
+ Symbol: ddprofiledefinition.SymbolConfig{
686
+ OID: "1.3.6.1.2.1.2.2.1.6.1",
687
+ Name: "ifPhysAddress",
688
+ Format: "mac_address",
689
+ },
690
+ },
691
+ },
692
+ },
693
+ },
694
+ },
695
+ },
696
+ setupMock: func(m *snmpmock.MockHandler) {
697
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
698
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.6.1"}).Return(
699
+ &gosnmp.SnmpPacket{
700
+ Variables: []gosnmp.SnmpPDU{
701
+ {
702
+ Name: "1.3.6.1.2.1.2.2.1.6.1",
703
+ Type: gosnmp.OctetString,
704
+ Value: []byte{0x00, 0x50, 0x56, 0xAB, 0xCD, 0xEF},
705
+ },
706
+ },
707
+ }, nil,
708
+ )
709
+ },
710
+ expectedResult: map[string]string{
711
+ "mac_address": "00:50:56:AB:CD:EF",
712
+ },
713
+ expectedError: false,
714
+ },
715
+ "non-device resource ignored": {
716
+ profile: &ddsnmp.Profile{
717
+ Definition: &ddprofiledefinition.ProfileDefinition{
718
+ Metadata: ddprofiledefinition.MetadataConfig{
719
+ "interface": ddprofiledefinition.MetadataResourceConfig{
720
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
721
+ "name": {
722
+ Value: "eth0",
723
+ },
724
+ },
725
+ },
726
+ "device": ddprofiledefinition.MetadataResourceConfig{
727
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
728
+ "vendor": {
729
+ Value: "cisco",
730
+ },
731
+ },
732
+ },
733
+ },
734
+ },
735
+ },
736
+ setupMock: func(m *snmpmock.MockHandler) {},
737
+ expectedResult: map[string]string{
738
+ "vendor": "cisco",
739
+ },
740
+ expectedError: false,
741
+ },
742
+ "SNMP error": {
743
+ profile: &ddsnmp.Profile{
744
+ Definition: &ddprofiledefinition.ProfileDefinition{
745
+ Metadata: ddprofiledefinition.MetadataConfig{
746
+ "device": ddprofiledefinition.MetadataResourceConfig{
747
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
748
+ "serial_number": {
749
+ Symbol: ddprofiledefinition.SymbolConfig{
750
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
751
+ Name: "chassisSerialNumber",
752
+ },
753
+ },
754
+ },
755
+ },
756
+ },
757
+ },
758
+ },
759
+ setupMock: func(m *snmpmock.MockHandler) {
760
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
761
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.1.3.2.0"}).Return(
762
+ nil,
763
+ errors.New("SNMP timeout"),
764
+ )
765
+ },
766
+ expectedResult: nil,
767
+ expectedError: true,
768
+ errorContains: "failed to fetch metadata values",
769
+ },
770
+ "missing OID continues with other fields": {
771
+ profile: &ddsnmp.Profile{
772
+ Definition: &ddprofiledefinition.ProfileDefinition{
773
+ Metadata: ddprofiledefinition.MetadataConfig{
774
+ "device": ddprofiledefinition.MetadataResourceConfig{
775
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
776
+ "vendor": {
777
+ Value: "dell",
778
+ },
779
+ "serial_number": {
780
+ Symbol: ddprofiledefinition.SymbolConfig{
781
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
782
+ Name: "chassisSerialNumber",
783
+ },
784
+ },
785
+ "model": {
786
+ Symbol: ddprofiledefinition.SymbolConfig{
787
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.1.0",
788
+ Name: "chassisModelName",
789
+ },
790
+ },
791
+ },
792
+ },
793
+ },
794
+ },
795
+ },
796
+ setupMock: func(m *snmpmock.MockHandler) {
797
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
798
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
799
+ "1.3.6.1.4.1.674.10892.5.1.3.1.0",
800
+ "1.3.6.1.4.1.674.10892.5.1.3.2.0",
801
+ })).Return(
802
+ &gosnmp.SnmpPacket{
803
+ Variables: []gosnmp.SnmpPDU{
804
+ {
805
+ Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
806
+ Type: gosnmp.NoSuchObject,
807
+ Value: nil,
808
+ },
809
+ {
810
+ Name: "1.3.6.1.4.1.674.10892.5.1.3.1.0",
811
+ Type: gosnmp.OctetString,
812
+ Value: []byte("PowerEdge R740"),
813
+ },
814
+ },
815
+ }, nil,
816
+ )
817
+ },
818
+ expectedResult: map[string]string{
819
+ "vendor": "dell",
820
+ "model": "PowerEdge R740",
821
+ },
822
+ expectedError: false,
823
+ },
824
+ "chunked requests": {
825
+ profile: &ddsnmp.Profile{
826
+ Definition: &ddprofiledefinition.ProfileDefinition{
827
+ Metadata: ddprofiledefinition.MetadataConfig{
828
+ "device": ddprofiledefinition.MetadataResourceConfig{
829
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
830
+ "field1": {
831
+ Symbol: ddprofiledefinition.SymbolConfig{
832
+ OID: "1.3.6.1.2.1.1.1.0",
833
+ Name: "oid1",
834
+ },
835
+ },
836
+ "field2": {
837
+ Symbol: ddprofiledefinition.SymbolConfig{
838
+ OID: "1.3.6.1.2.1.1.2.0",
839
+ Name: "oid2",
840
+ },
841
+ },
842
+ "field3": {
843
+ Symbol: ddprofiledefinition.SymbolConfig{
844
+ OID: "1.3.6.1.2.1.1.3.0",
845
+ Name: "oid3",
846
+ },
847
+ },
848
+ },
849
+ },
850
+ },
851
+ },
852
+ },
853
+ setupMock: func(m *snmpmock.MockHandler) {
854
+ m.EXPECT().MaxOids().Return(2).AnyTimes() // Force chunking
855
+ // First chunk
856
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"}).Return(
857
+ &gosnmp.SnmpPacket{
858
+ Variables: []gosnmp.SnmpPDU{
859
+ {
860
+ Name: "1.3.6.1.2.1.1.1.0",
861
+ Type: gosnmp.OctetString,
862
+ Value: []byte("value1"),
863
+ },
864
+ {
865
+ Name: "1.3.6.1.2.1.1.2.0",
866
+ Type: gosnmp.OctetString,
867
+ Value: []byte("value2"),
868
+ },
869
+ },
870
+ }, nil,
871
+ )
872
+ // Second chunk
873
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
874
+ &gosnmp.SnmpPacket{
875
+ Variables: []gosnmp.SnmpPDU{
876
+ {
877
+ Name: "1.3.6.1.2.1.1.3.0",
878
+ Type: gosnmp.OctetString,
879
+ Value: []byte("value3"),
880
+ },
881
+ },
882
+ }, nil,
883
+ )
884
+ },
885
+ expectedResult: map[string]string{
886
+ "field1": "value1",
887
+ "field2": "value2",
888
+ "field3": "value3",
889
+ },
890
+ expectedError: false,
891
+ },
892
+ }
893
+
894
+ for name, tc := range tests {
895
+ t.Run(name, func(t *testing.T) {
896
+ ctrl := gomock.NewController(t)
897
+ defer ctrl.Finish()
898
+
899
+ mockHandler := snmpmock.NewMockHandler(ctrl)
900
+ tc.setupMock(mockHandler)
901
+
902
+ missingOIDs := make(map[string]bool)
903
+ collector := newDeviceMetadataCollector(mockHandler, missingOIDs, logger.New())
904
+
905
+ result, err := collector.Collect(tc.profile)
906
+
907
+ if tc.expectedError {
908
+ assert.Error(t, err)
909
+ if tc.errorContains != "" {
910
+ assert.Contains(t, err.Error(), tc.errorContains)
911
+ }
912
+ } else {
913
+ assert.NoError(t, err)
914
+ }
915
+
916
+ assert.Equal(t, tc.expectedResult, result)
917
+ })
918
+ }
919
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/common_test.go
new
+144
@@ -0,0 +1,144 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "regexp"
7
+ "testing"
8
+
9
+ "github.com/golang/mock/gomock"
10
+ "github.com/gosnmp/gosnmp"
11
+ "github.com/stretchr/testify/assert"
12
+ "github.com/stretchr/testify/require"
13
+
14
+ snmpmock "github.com/gosnmp/gosnmp/mocks"
15
+
16
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
18
+)
19
+
20
+func mustCompileRegex(pattern string) *regexp.Regexp {
21
+ re, err := regexp.Compile(pattern)
22
+ if err != nil {
23
+ panic(err)
24
+ }
25
+ return re
26
+}
27
+
28
+func setupMockHandler(t *testing.T) (*gomock.Controller, *snmpmock.MockHandler) {
29
+ ctrl := gomock.NewController(t)
30
+ mockHandler := snmpmock.NewMockHandler(ctrl)
31
+ mockHandler.EXPECT().MaxOids().Return(10).AnyTimes()
32
+ return ctrl, mockHandler
33
+}
34
+
35
+func createTestProfile(sourceFile string, metrics []ddprofiledefinition.MetricsConfig) *ddsnmp.Profile {
36
+ return &ddsnmp.Profile{
37
+ SourceFile: sourceFile,
38
+ Definition: &ddprofiledefinition.ProfileDefinition{
39
+ Metrics: metrics,
40
+ },
41
+ }
42
+}
43
+
44
+func createScalarMetric(oid, name string) ddprofiledefinition.MetricsConfig {
45
+ return ddprofiledefinition.MetricsConfig{
46
+ Symbol: ddprofiledefinition.SymbolConfig{
47
+ OID: oid,
48
+ Name: name,
49
+ },
50
+ }
51
+}
52
+
53
+func expectSNMPGet(mockHandler *snmpmock.MockHandler, oids []string, pdus []gosnmp.SnmpPDU) {
54
+ mockHandler.EXPECT().Get(gomock.InAnyOrder(oids)).Return(
55
+ &gosnmp.SnmpPacket{Variables: pdus}, nil,
56
+ )
57
+}
58
+
59
+func expectSNMPGetError(mockHandler *snmpmock.MockHandler, oids []string, err error) {
60
+ mockHandler.EXPECT().Get(gomock.InAnyOrder(oids)).Return(nil, err)
61
+}
62
+
63
+func expectSNMPWalk(mockHandler *snmpmock.MockHandler, version gosnmp.SnmpVersion, oid string, pdus []gosnmp.SnmpPDU) {
64
+ mockHandler.EXPECT().Version().Return(version)
65
+ if version == gosnmp.Version1 {
66
+ mockHandler.EXPECT().WalkAll(oid).Return(pdus, nil)
67
+ } else {
68
+ mockHandler.EXPECT().BulkWalkAll(oid).Return(pdus, nil)
69
+ }
70
+}
71
+
72
+func expectSNMPWalkError(mockHandler *snmpmock.MockHandler, version gosnmp.SnmpVersion, oid string, err error) {
73
+ mockHandler.EXPECT().Version().Return(version)
74
+ if version == gosnmp.Version1 {
75
+ mockHandler.EXPECT().WalkAll(oid).Return(nil, err)
76
+ } else {
77
+ mockHandler.EXPECT().BulkWalkAll(oid).Return(nil, err)
78
+ }
79
+}
80
+
81
+func createPDU(name string, pduType gosnmp.Asn1BER, value interface{}) gosnmp.SnmpPDU {
82
+ return gosnmp.SnmpPDU{
83
+ Name: name,
84
+ Type: pduType,
85
+ Value: value,
86
+ }
87
+}
88
+
89
+func createStringPDU(name string, value string) gosnmp.SnmpPDU {
90
+ return createPDU(name, gosnmp.OctetString, []byte(value))
91
+}
92
+
93
+func createIntegerPDU(name string, value int) gosnmp.SnmpPDU {
94
+ return createPDU(name, gosnmp.Integer, value)
95
+}
96
+
97
+func createCounter32PDU(name string, value uint) gosnmp.SnmpPDU {
98
+ return createPDU(name, gosnmp.Counter32, value)
99
+}
100
+
101
+func createCounter64PDU(name string, value uint64) gosnmp.SnmpPDU {
102
+ return createPDU(name, gosnmp.Counter64, value)
103
+}
104
+
105
+func createGauge32PDU(name string, value uint) gosnmp.SnmpPDU {
106
+ return createPDU(name, gosnmp.Gauge32, value)
107
+}
108
+
109
+func createTimeTicksPDU(name string, value uint32) gosnmp.SnmpPDU {
110
+ return createPDU(name, gosnmp.TimeTicks, value)
111
+}
112
+
113
+func createNoSuchObjectPDU(name string) gosnmp.SnmpPDU {
114
+ return createPDU(name, gosnmp.NoSuchObject, nil)
115
+}
116
+
117
+func assertMetricsEqual(t *testing.T, expected, actual []ddsnmp.Metric) {
118
+ t.Helper()
119
+ assert.Equal(t, len(expected), len(actual))
120
+ require.Equal(t, len(expected), len(actual), "number of metrics")
121
+
122
+ // Sort metrics for consistent comparison
123
+
124
+ for i := range expected {
125
+ assert.Equal(t, expected[i].Name, actual[i].Name, "metric name")
126
+ assert.Equal(t, expected[i].Value, actual[i].Value, "metric value")
127
+ assert.Equal(t, expected[i].MetricType, actual[i].MetricType, "metric type")
128
+ assert.Equal(t, expected[i].Tags, actual[i].Tags, "metric tags")
129
+ assert.Equal(t, expected[i].StaticTags, actual[i].StaticTags, "metric static tags")
130
+ assert.Equal(t, expected[i].IsTable, actual[i].IsTable, "metric is table")
131
+ assert.Equal(t, expected[i].Unit, actual[i].Unit, "metric unit")
132
+ assert.Equal(t, expected[i].Family, actual[i].Family, "metric family")
133
+ assert.Equal(t, expected[i].Description, actual[i].Description, "metric description")
134
+ assert.Equal(t, expected[i].Mappings, actual[i].Mappings, "metric mappings")
135
+ }
136
+}
137
+
138
+func assertTableMetricsEqual(t *testing.T, expected, actual []ddsnmp.Metric) {
139
+ t.Helper()
140
+ assert.Equal(t, len(expected), len(actual), "number of metrics")
141
+
142
+ // Use ElementsMatch for unordered comparison
143
+ assert.ElementsMatch(t, expected, actual, "table metrics should match (unordered)")
144
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/metric_builder.go
new
+139
@@ -0,0 +1,139 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "bytes"
7
+ "fmt"
8
+ "maps"
9
+ "strconv"
10
+
11
+ "github.com/gosnmp/gosnmp"
12
+
13
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
+)
16
+
17
+type metricBuilder struct {
18
+ metric ddsnmp.Metric
19
+}
20
+
21
+func newMetricBuilder(name string, value int64) *metricBuilder {
22
+ return &metricBuilder{
23
+ metric: ddsnmp.Metric{
24
+ Name: name,
25
+ Value: value,
26
+ },
27
+ }
28
+}
29
+
30
+func (mb *metricBuilder) withTags(tags map[string]string) *metricBuilder {
31
+ if len(tags) > 0 {
32
+ mb.metric.Tags = maps.Clone(tags)
33
+ }
34
+ return mb
35
+}
36
+
37
+func (mb *metricBuilder) withStaticTags(tags map[string]string) *metricBuilder {
38
+ if len(tags) > 0 {
39
+ mb.metric.StaticTags = maps.Clone(tags)
40
+ }
41
+ return mb
42
+}
43
+
44
+func (mb *metricBuilder) asTableMetric() *metricBuilder {
45
+ mb.metric.IsTable = true
46
+ return mb
47
+}
48
+
49
+func (mb *metricBuilder) fromSymbol(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) *metricBuilder {
50
+ mb.metric.Unit = sym.Unit
51
+ mb.metric.Description = sym.Description
52
+ mb.metric.Family = sym.Family
53
+ mb.metric.MetricType = ternary(sym.MetricType != "", sym.MetricType, getMetricTypeFromPDUType(pdu))
54
+ mb.metric.Mappings = convMappingToNumeric(sym)
55
+ return mb
56
+}
57
+
58
+func (mb *metricBuilder) build() ddsnmp.Metric {
59
+ return mb.metric
60
+}
61
+
62
+func buildScalarMetric(cfg ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU, value int64, staticTags map[string]string) (*ddsnmp.Metric, error) {
63
+ metric := newMetricBuilder(cfg.Name, value).
64
+ withStaticTags(staticTags).
65
+ fromSymbol(cfg, pdu).
66
+ build()
67
+
68
+ if cfg.TransformCompiled != nil {
69
+ if err := applyTransform(&metric, cfg); err != nil {
70
+ return nil, err
71
+ }
72
+ }
73
+
74
+ return &metric, nil
75
+}
76
+
77
+func buildTableMetric(cfg ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU, value int64, tags, staticTags map[string]string) (*ddsnmp.Metric, error) {
78
+ metric := newMetricBuilder(cfg.Name, value).
79
+ withTags(tags).
80
+ withStaticTags(staticTags).
81
+ fromSymbol(cfg, pdu).
82
+ asTableMetric().
83
+ build()
84
+
85
+ if cfg.TransformCompiled != nil {
86
+ if err := applyTransform(&metric, cfg); err != nil {
87
+ return nil, err
88
+ }
89
+ }
90
+
91
+ return &metric, nil
92
+}
93
+
94
+func convMappingToNumeric(cfg ddprofiledefinition.SymbolConfig) map[int64]string {
95
+ if len(cfg.Mapping) == 0 {
96
+ return nil
97
+ }
98
+
99
+ isMappingKeysNumeric := func() bool {
100
+ for k := range cfg.Mapping {
101
+ if !isInt(k) {
102
+ return false
103
+ }
104
+ }
105
+ return true
106
+ }()
107
+
108
+ mappings := make(map[int64]string)
109
+
110
+ if isMappingKeysNumeric {
111
+ for k, v := range cfg.Mapping {
112
+ intKey, _ := strconv.ParseInt(k, 10, 64)
113
+ mappings[intKey] = v
114
+ }
115
+ } else {
116
+ for k, v := range cfg.Mapping {
117
+ if intVal, err := strconv.ParseInt(v, 10, 64); err == nil {
118
+ mappings[intVal] = k
119
+ }
120
+ }
121
+ }
122
+
123
+ return mappings
124
+}
125
+
126
+func applyTransform(metric *ddsnmp.Metric, sym ddprofiledefinition.SymbolConfig) error {
127
+ if sym.TransformCompiled == nil {
128
+ return nil
129
+ }
130
+
131
+ ctx := struct{ Metric *ddsnmp.Metric }{Metric: metric}
132
+
133
+ var buf bytes.Buffer
134
+ if err := sym.TransformCompiled.Execute(&buf, ctx); err != nil {
135
+ return fmt.Errorf("failed to execute transform template: %w", err)
136
+ }
137
+
138
+ return nil
139
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/table_cache.go
+15
-2
@@ -187,12 +187,25 @@ func (tc *tableCache) clearExpired() []string {
187
}
188
189
// Second pass: cascade expiration to dependent tables
190
- for tableOID := range expiredTables {
190
+ visited := make(map[string]bool)
191
+ var cascadeExpiration func(tableOID string)
192
+ cascadeExpiration = func(tableOID string) {
193
+ if visited[tableOID] {
194
+ return
195
+ }
196
+ visited[tableOID] = true
197
+ expiredTables[tableOID] = true
198
+
199
for dep := range tc.tableDeps[tableOID] {
192
- expiredTables[dep] = true
200
+ cascadeExpiration(dep)
201
}
202
}
203
204
+ // Start cascade from naturally expired tables
205
+ for tableOID := range expiredTables {
206
+ cascadeExpiration(tableOID)
207
+ }
208
+
209
// Clear all expired tables
210
for tableOID := range expiredTables {
211
delete(tc.tables, tableOID)
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/table_row_processor.go
new
+305
@@ -0,0 +1,305 @@
1
+package ddsnmpcollector
2
+
3
+import (
4
+ "fmt"
5
+ "strings"
6
+
7
+ "github.com/gosnmp/gosnmp"
8
+
9
+ "github.com/netdata/netdata/go/plugins/logger"
10
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
12
+)
13
+
14
+type (
15
+ // tableRowProcessor processes individual rows from SNMP tables
16
+ tableRowProcessor struct {
17
+ log *logger.Logger
18
+ crossTableResolver *crossTableResolver
19
+ valProc *valueProcessor
20
+ tagProc *tableTagProcessor
21
+ }
22
+ // tableRowData represents data for a single table row
23
+ tableRowData struct {
24
+ index string
25
+ pdus map[string]gosnmp.SnmpPDU
26
+ tags map[string]string
27
+ staticTags map[string]string
28
+ }
29
+ // tableRowProcessingContext contains context needed for processing a row
30
+ tableRowProcessingContext struct {
31
+ config ddprofiledefinition.MetricsConfig
32
+ columnOIDs map[string]ddprofiledefinition.SymbolConfig
33
+ sameTableTagOIDs map[string][]ddprofiledefinition.MetricTagConfig
34
+ crossTableCtx *crossTableContext
35
+ }
36
+)
37
+
38
+func newTableRowProcessor(log *logger.Logger) *tableRowProcessor {
39
+ return &tableRowProcessor{
40
+ log: log,
41
+ valProc: newValueProcessor(),
42
+ tagProc: newTableTagProcessor(),
43
+ crossTableResolver: newCrossTableResolver(log),
44
+ }
45
+}
46
+
47
+func (p *tableRowProcessor) processRow(row *tableRowData, ctx *tableRowProcessingContext) ([]ddsnmp.Metric, error) {
48
+ if err := p.processRowTags(row, ctx); err != nil {
49
+ p.log.Debugf("Error processing tags for row %s: %v", row.index, err)
50
+ }
51
+
52
+ return p.processRowMetrics(row, ctx)
53
+}
54
+
55
+func (p *tableRowProcessor) processRowTags(row *tableRowData, ctx *tableRowProcessingContext) error {
56
+ p.processSameTableTags(row, ctx.sameTableTagOIDs)
57
+
58
+ if ctx.crossTableCtx != nil {
59
+ p.processCrossTableTags(row, ctx)
60
+ }
61
+
62
+ p.processIndexBasedTags(row, ctx.config.MetricTags)
63
+
64
+ return nil
65
+}
66
+
67
+func (p *tableRowProcessor) processSameTableTags(row *tableRowData, tagColumnOIDs map[string][]ddprofiledefinition.MetricTagConfig) {
68
+ for columnOID, tagConfigs := range tagColumnOIDs {
69
+ pdu, ok := row.pdus[columnOID]
70
+ if !ok {
71
+ continue
72
+ }
73
+
74
+ ta := tagAdder{tags: row.tags}
75
+
76
+ for _, tagCfg := range tagConfigs {
77
+ if err := p.tagProc.processTag(tagCfg, pdu, ta); err != nil {
78
+ p.log.Debugf("Error processing tag %s: %v", tagCfg.Tag, err)
79
+ continue
80
+ }
81
+ }
82
+ }
83
+}
84
+
85
+func (p *tableRowProcessor) processCrossTableTags(row *tableRowData, ctx *tableRowProcessingContext) {
86
+ for _, tagCfg := range ctx.config.MetricTags {
87
+ if !p.crossTableResolver.isCrossTable(tagCfg, ctx.config.Table.Name) {
88
+ continue
89
+ }
90
+
91
+ if err := p.crossTableResolver.resolveCrossTableTag(tagCfg, row.index, ctx.crossTableCtx); err != nil {
92
+ p.log.Debugf("Error resolving cross-table tag %s: %v", tagCfg.Tag, err)
93
+ continue
94
+ }
95
+ }
96
+}
97
+
98
+func (p *tableRowProcessor) processIndexBasedTags(row *tableRowData, metricTags []ddprofiledefinition.MetricTagConfig) {
99
+ for _, tagCfg := range metricTags {
100
+ if tagCfg.Index == 0 {
101
+ continue
102
+ }
103
+
104
+ tagName, indexValue, ok := p.processIndexTag(tagCfg, row.index)
105
+ if !ok {
106
+ p.log.Debugf("Cannot extract position %d from index %s", tagCfg.Index, row.index)
107
+ continue
108
+ }
109
+
110
+ row.tags[tagName] = indexValue
111
+ }
112
+}
113
+
114
+func (p *tableRowProcessor) processIndexTag(cfg ddprofiledefinition.MetricTagConfig, index string) (string, string, bool) {
115
+ indexValue, ok := p.extractIndexPosition(index, cfg.Index)
116
+ if !ok {
117
+ return "", "", false
118
+ }
119
+
120
+ tagName := ternary(cfg.Tag != "", cfg.Tag, fmt.Sprintf("index%d", cfg.Index))
121
+
122
+ if v, ok := cfg.Mapping[indexValue]; ok {
123
+ indexValue = v
124
+ }
125
+
126
+ return tagName, indexValue, true
127
+}
128
+
129
+// extractPosition extracts a specific position from an index
130
+// Position uses 1-based indexing as per the profile format
131
+// Example: index "7.8.9", position 2 → "8"
132
+func (p *tableRowProcessor) extractIndexPosition(index string, position uint) (string, bool) {
133
+ if position == 0 {
134
+ return "", false
135
+ }
136
+
137
+ var n uint
138
+ for {
139
+ n++
140
+ i := strings.IndexByte(index, '.')
141
+ if i == -1 {
142
+ break
143
+ }
144
+ if n == position {
145
+ return index[:i], true
146
+ }
147
+ index = index[i+1:]
148
+ }
149
+
150
+ return index, n == position && index != ""
151
+}
152
+
153
+func (p *tableRowProcessor) processRowMetrics(row *tableRowData, ctx *tableRowProcessingContext) ([]ddsnmp.Metric, error) {
154
+ metrics := make([]ddsnmp.Metric, 0, len(ctx.columnOIDs))
155
+
156
+ for columnOID, sym := range ctx.columnOIDs {
157
+ pdu, ok := row.pdus[columnOID]
158
+ if !ok {
159
+ continue
160
+ }
161
+
162
+ metric, err := p.createMetric(sym, pdu, row)
163
+ if err != nil {
164
+ p.log.Debugf("Error creating metric %s: %v", sym.Name, err)
165
+ continue
166
+ }
167
+
168
+ metrics = append(metrics, *metric)
169
+ }
170
+
171
+ return metrics, nil
172
+}
173
+
174
+func (p *tableRowProcessor) createMetric(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU, row *tableRowData) (*ddsnmp.Metric, error) {
175
+ value, err := p.valProc.processValue(sym, pdu)
176
+ if err != nil {
177
+ return nil, fmt.Errorf("error processing value: %w", err)
178
+ }
179
+
180
+ return buildTableMetric(sym, pdu, value, row.tags, row.staticTags)
181
+}
182
+
183
+type (
184
+ // crossTableResolver handles resolving tags from other tables
185
+ crossTableResolver struct {
186
+ log *logger.Logger
187
+ tagProcessor *tableTagProcessor
188
+ }
189
+ // crossTableContext contains all data needed for cross-table resolution
190
+ crossTableContext struct {
191
+ walkedData map[string]map[string]gosnmp.SnmpPDU // tableOID -> PDUs
192
+ tableNameToOID map[string]string // tableName -> tableOID
193
+ rowTags map[string]string
194
+ }
195
+)
196
+
197
+func newCrossTableResolver(log *logger.Logger) *crossTableResolver {
198
+ return &crossTableResolver{
199
+ log: log,
200
+ tagProcessor: newTableTagProcessor(),
201
+ }
202
+}
203
+
204
+// resolveCrossTableTag resolves a tag value from another table
205
+func (r *crossTableResolver) resolveCrossTableTag(tagCfg ddprofiledefinition.MetricTagConfig, index string, ctx *crossTableContext) error {
206
+ refTableOID, err := r.findReferencedTableOID(tagCfg.Table, ctx.tableNameToOID)
207
+ if err != nil {
208
+ return err
209
+ }
210
+
211
+ refTablePDUs, err := r.getReferencedTableData(tagCfg.Table, refTableOID, ctx.walkedData)
212
+ if err != nil {
213
+ return err
214
+ }
215
+
216
+ lookupIndex, err := r.transformIndex(index, tagCfg.IndexTransform)
217
+ if err != nil {
218
+ return err
219
+ }
220
+
221
+ pdu, err := r.lookupValue(tagCfg, lookupIndex, refTablePDUs)
222
+ if err != nil {
223
+ return err
224
+ }
225
+
226
+ ta := tagAdder{tags: ctx.rowTags}
227
+
228
+ return r.tagProcessor.processTag(tagCfg, pdu, ta)
229
+}
230
+
231
+func (r *crossTableResolver) findReferencedTableOID(tableName string, tableNameToOID map[string]string) (string, error) {
232
+ tableOID, ok := tableNameToOID[tableName]
233
+ if !ok {
234
+ r.log.Debugf("Cannot find table OID for referenced table %s", tableName)
235
+ return "", fmt.Errorf("table %s not found", tableName)
236
+ }
237
+ return tableOID, nil
238
+}
239
+
240
+func (r *crossTableResolver) getReferencedTableData(tableName string, tableOID string, walkedData map[string]map[string]gosnmp.SnmpPDU) (map[string]gosnmp.SnmpPDU, error) {
241
+ pdus, ok := walkedData[tableOID]
242
+ if !ok {
243
+ r.log.Debugf("No walked data for referenced table %s (OID: %s)", tableName, tableOID)
244
+ return nil, fmt.Errorf("no data for table %s", tableName)
245
+ }
246
+ return pdus, nil
247
+}
248
+
249
+// transformIndex applies index transformation if configured
250
+func (r *crossTableResolver) transformIndex(index string, transforms []ddprofiledefinition.MetricIndexTransform) (string, error) {
251
+ if len(transforms) == 0 {
252
+ return index, nil
253
+ }
254
+
255
+ transformedIndex := r.applyIndexTransform(index, transforms)
256
+ if transformedIndex == "" {
257
+ r.log.Debugf("Index transformation failed for index %s with transforms %v", index, transforms)
258
+ return "", fmt.Errorf("index transformation failed")
259
+ }
260
+
261
+ return transformedIndex, nil
262
+}
263
+
264
+func (r *crossTableResolver) lookupValue(tagCfg ddprofiledefinition.MetricTagConfig, lookupIndex string, refTablePDUs map[string]gosnmp.SnmpPDU) (gosnmp.SnmpPDU, error) {
265
+ refColumnOID := trimOID(tagCfg.Symbol.OID)
266
+ refFullOID := refColumnOID + "." + lookupIndex
267
+
268
+ pdu, ok := refTablePDUs[refFullOID]
269
+ if !ok {
270
+ r.log.Debugf("Cannot find cross-table tag value at OID %s for table %s (lookup index: %s)",
271
+ refFullOID, tagCfg.Table, lookupIndex)
272
+ return gosnmp.SnmpPDU{}, fmt.Errorf("value not found at OID %s", refFullOID)
273
+ }
274
+
275
+ return pdu, nil
276
+}
277
+
278
+// applyTransform applies index transformation rules to extract a subset of the index
279
+// Example: index "1.6.0.36.155.53.3.246", transform [{start: 1, end: 7}] → "6.0.36.155.53.3.246"
280
+func (r *crossTableResolver) applyIndexTransform(index string, transforms []ddprofiledefinition.MetricIndexTransform) string {
281
+ if len(transforms) == 0 {
282
+ return index
283
+ }
284
+
285
+ parts := strings.Split(index, ".")
286
+ var result []string
287
+
288
+ for _, transform := range transforms {
289
+ start, end := transform.Start, transform.End
290
+
291
+ if int(start) >= len(parts) || end < start || int(end) >= len(parts) {
292
+ return ""
293
+ }
294
+
295
+ // Extract the range (inclusive)
296
+ extracted := parts[start : end+1]
297
+ result = append(result, extracted...)
298
+ }
299
+
300
+ return strings.Join(result, ".")
301
+}
302
+
303
+func (r *crossTableResolver) isCrossTable(tagCfg ddprofiledefinition.MetricTagConfig, currentTableName string) bool {
304
+ return tagCfg.Table != "" && tagCfg.Table != currentTableName && tagCfg.Index == 0
305
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/tag_processor.go
new
+81
@@ -0,0 +1,81 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "github.com/gosnmp/gosnmp"
7
+
8
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
9
+)
10
+
11
+type tagAdder struct {
12
+ tags map[string]string
13
+}
14
+
15
+func (m *tagAdder) addTag(key, value string) {
16
+ if existing, ok := m.tags[key]; !ok || existing == "" {
17
+ m.tags[key] = value
18
+ }
19
+}
20
+
21
+type globalTagProcessor struct {
22
+ tp *tableTagProcessor
23
+}
24
+
25
+func newGlobalTagProcessor() *globalTagProcessor {
26
+ return &globalTagProcessor{
27
+ tp: newTableTagProcessor(),
28
+ }
29
+}
30
+
31
+func (p *globalTagProcessor) processTag(cfg ddprofiledefinition.MetricTagConfig, pdus map[string]gosnmp.SnmpPDU, ta tagAdder) error {
32
+ pdu, ok := pdus[trimOID(cfg.Symbol.OID)]
33
+ if !ok {
34
+ return nil
35
+ }
36
+ return p.tp.processTag(cfg, pdu, ta)
37
+}
38
+
39
+type tableTagProcessor struct{}
40
+
41
+func newTableTagProcessor() *tableTagProcessor {
42
+ return &tableTagProcessor{}
43
+}
44
+
45
+func (p *tableTagProcessor) processTag(cfg ddprofiledefinition.MetricTagConfig, pdu gosnmp.SnmpPDU, ta tagAdder) error {
46
+ tagName := ternary(cfg.Tag != "", cfg.Tag, cfg.Symbol.Name)
47
+ if tagName == "" {
48
+ return nil
49
+ }
50
+
51
+ val, err := convPduToStringf(pdu, cfg.Symbol.Format)
52
+ if err != nil {
53
+ return err
54
+ }
55
+
56
+ switch {
57
+ case len(cfg.Mapping) > 0:
58
+ if v, ok := cfg.Mapping[val]; ok {
59
+ val = v
60
+ }
61
+ ta.addTag(tagName, val)
62
+ case cfg.Pattern != nil:
63
+ if sm := cfg.Pattern.FindStringSubmatch(val); len(sm) > 0 {
64
+ for name, tmpl := range cfg.Tags {
65
+ ta.addTag(name, replaceSubmatches(tmpl, sm))
66
+ }
67
+ }
68
+ case cfg.Symbol.ExtractValueCompiled != nil:
69
+ if sm := cfg.Symbol.ExtractValueCompiled.FindStringSubmatch(val); len(sm) > 1 {
70
+ ta.addTag(tagName, sm[1])
71
+ }
72
+ case cfg.Symbol.MatchPatternCompiled != nil:
73
+ if sm := cfg.Symbol.MatchPatternCompiled.FindStringSubmatch(val); len(sm) > 0 {
74
+ ta.addTag(tagName, replaceSubmatches(cfg.Symbol.MatchValue, sm))
75
+ }
76
+ default:
77
+ ta.addTag(tagName, val)
78
+ }
79
+
80
+ return nil
81
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/utils.go
-58
@@ -3,7 +3,6 @@
3
package ddsnmpcollector
4
5
import (
6
- "bytes"
6
"encoding/hex"
7
"fmt"
8
"regexp"
@@ -13,40 +12,9 @@ import (
12
13
"github.com/gosnmp/gosnmp"
14
16
- "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
20
-func convSymMappingToNumeric(cfg ddprofiledefinition.SymbolConfig) map[int64]string {
21
- if len(cfg.Mapping) == 0 {
22
- return nil
23
- }
24
-
25
- mappings := make(map[int64]string)
26
-
27
- if isMappingKeysNumeric(cfg.Mapping) {
28
- for k, v := range cfg.Mapping {
29
- intKey, _ := strconv.ParseInt(k, 10, 64)
30
- mappings[intKey] = v
31
- }
32
- } else {
33
- for k, v := range cfg.Mapping {
34
- if intVal, err := strconv.ParseInt(v, 10, 64); err == nil {
35
- mappings[intVal] = k
36
- }
37
- }
38
- }
39
-
40
- return mappings
41
-}
42
-
43
-func getMetricType(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) ddprofiledefinition.ProfileMetricType {
44
- if sym.MetricType != "" {
45
- return sym.MetricType
46
- }
47
- return getMetricTypeFromPDUType(pdu)
48
-}
49
-
18
func getMetricTypeFromPDUType(pdu gosnmp.SnmpPDU) ddprofiledefinition.ProfileMetricType {
19
switch pdu.Type {
20
case gosnmp.Counter32, gosnmp.Counter64:
@@ -246,15 +214,6 @@ func isInt(s string) bool {
214
return err == nil
215
}
216
249
-func isMappingKeysNumeric(mapping map[string]string) bool {
250
- for k := range mapping {
251
- if !isInt(k) {
252
- return false
253
- }
254
- }
255
- return true
256
-}
257
-
217
func mergeTagsWithEmptyFallback(dest, src map[string]string) {
218
for k, v := range src {
219
if existing, ok := dest[k]; !ok || existing == "" {
@@ -262,20 +221,3 @@ func mergeTagsWithEmptyFallback(dest, src map[string]string) {
221
}
222
}
223
}
265
-
266
-func applyTransform(metric *ddsnmp.Metric, sym ddprofiledefinition.SymbolConfig) error {
267
- if sym.TransformCompiled == nil {
268
- return nil
269
- }
270
-
271
- type transformCtx struct{ Metric *ddsnmp.Metric }
272
-
273
- ctx := &transformCtx{Metric: metric}
274
-
275
- var buf bytes.Buffer
276
- if err := sym.TransformCompiled.Execute(&buf, ctx); err != nil {
277
- return fmt.Errorf("failed to execute transform template: %w", err)
278
- }
279
-
280
- return nil
281
-}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/value_processor.go
new
+124
@@ -0,0 +1,124 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "fmt"
7
+ "strconv"
8
+
9
+ "github.com/gosnmp/gosnmp"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
12
+)
13
+
14
+// valueProcessorFactory selects the appropriate processor based on PDU type
15
+type valueProcessor struct {
16
+ numericProcessor *numericValueProcessor
17
+ stringProcessor *stringValueProcessor
18
+}
19
+
20
+func newValueProcessor() *valueProcessor {
21
+ return &valueProcessor{
22
+ numericProcessor: &numericValueProcessor{},
23
+ stringProcessor: &stringValueProcessor{},
24
+ }
25
+}
26
+
27
+func (p *valueProcessor) processValue(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) (int64, error) {
28
+ if isPduNumericType(pdu) {
29
+ return p.numericProcessor.processValue(sym, pdu)
30
+ }
31
+ return p.stringProcessor.processValue(sym, pdu)
32
+}
33
+
34
+// numericValueProcessor handles numeric PDU types
35
+type numericValueProcessor struct{}
36
+
37
+func (p *numericValueProcessor) processValue(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) (int64, error) {
38
+ switch pdu.Type {
39
+ case gosnmp.OpaqueFloat:
40
+ return p.processOpaqueFloat(sym, pdu)
41
+ case gosnmp.OpaqueDouble:
42
+ return p.processOpaqueDouble(sym, pdu)
43
+ default:
44
+ return p.processInteger(sym, pdu)
45
+ }
46
+}
47
+
48
+func (p *numericValueProcessor) processOpaqueFloat(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) (int64, error) {
49
+ floatVal, ok := pdu.Value.(float32)
50
+ if !ok {
51
+ return 0, fmt.Errorf("OpaqueFloat has unexpected type %T", pdu.Value)
52
+ }
53
+
54
+ if sym.ScaleFactor != 0 {
55
+ return int64(float64(floatVal) * sym.ScaleFactor), nil
56
+ }
57
+ return int64(floatVal), nil
58
+}
59
+
60
+func (p *numericValueProcessor) processOpaqueDouble(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) (int64, error) {
61
+ floatVal, ok := pdu.Value.(float64)
62
+ if !ok {
63
+ return 0, fmt.Errorf("OpaqueDouble has unexpected type %T", pdu.Value)
64
+ }
65
+
66
+ if sym.ScaleFactor != 0 {
67
+ return int64(floatVal * sym.ScaleFactor), nil
68
+ }
69
+ return int64(floatVal), nil
70
+}
71
+
72
+func (p *numericValueProcessor) processInteger(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) (int64, error) {
73
+ value := gosnmp.ToBigInt(pdu.Value).Int64()
74
+
75
+ if len(sym.Mapping) > 0 {
76
+ s := strconv.FormatInt(value, 10)
77
+ if v, ok := sym.Mapping[s]; ok && isInt(v) {
78
+ value, _ = strconv.ParseInt(v, 10, 64)
79
+ }
80
+ }
81
+
82
+ if sym.ScaleFactor != 0 {
83
+ value = int64(float64(value) * sym.ScaleFactor)
84
+ }
85
+
86
+ return value, nil
87
+}
88
+
89
+// stringValueProcessor handles string-based PDU types
90
+type stringValueProcessor struct{}
91
+
92
+func (p *stringValueProcessor) processValue(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) (int64, error) {
93
+ s, err := convPduToStringf(pdu, sym.Format)
94
+ if err != nil {
95
+ return 0, err
96
+ }
97
+
98
+ if sym.ExtractValueCompiled != nil {
99
+ if sm := sym.ExtractValueCompiled.FindStringSubmatch(s); len(sm) > 1 {
100
+ s = sm[1]
101
+ }
102
+ }
103
+
104
+ if sym.MatchPatternCompiled != nil {
105
+ if sm := sym.MatchPatternCompiled.FindStringSubmatch(s); len(sm) > 0 {
106
+ s = replaceSubmatches(sym.MatchValue, sm)
107
+ }
108
+ }
109
+
110
+ if v, ok := sym.Mapping[s]; ok && isInt(v) {
111
+ s = v
112
+ }
113
+
114
+ value, err := strconv.ParseInt(s, 10, 64)
115
+ if err != nil {
116
+ return 0, fmt.Errorf("cannot convert '%s' to int64: %w", s, err)
117
+ }
118
+
119
+ if sym.ScaleFactor != 0 {
120
+ value = int64(float64(value) * sym.ScaleFactor)
121
+ }
122
+
123
+ return value, nil
124
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/transform.go
+7
@@ -89,6 +89,13 @@ func newMetricTransformFuncMap() template.FuncMap {
89
m.Value = value
90
return ""
91
},
92
+ "setTag": func(m *Metric, key, value string) string {
93
+ if m.Tags == nil {
94
+ m.Tags = make(map[string]string)
95
+ }
96
+ m.Tags[key] = value
97
+ return ""
98
+ },
99
"setMappings": func(m *Metric, mappings map[int64]string) string {
100
m.Mappings = mappings
101
return ""