@cryptotaxi247 / netdata / commits / 0ff31d55e

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 ""