chore(go.d/ddsnmp): support extracting table index components as tags (#20482)
Ilya Mashchenko committed
Jun 15, 2025 at 17:06 UTC
055d6a3f9ed7c5c4544c61341fb73e3550dc95e9
4 files changed
+1622
-77
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_scalar.go
+22
-5
@@ -108,11 +108,28 @@ func processSymbolValue(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU
108
var value int64
109
110
if isPduNumericType(pdu) {
111
- value = gosnmp.ToBigInt(pdu.Value).Int64()
112
- if len(sym.Mapping) > 0 {
113
- s := strconv.FormatInt(value, 10)
114
- if v, ok := sym.Mapping[s]; ok && isInt(v) {
115
- value, _ = strconv.ParseInt(v, 10, 64)
111
+ switch pdu.Type {
112
+ case gosnmp.OpaqueFloat:
113
+ floatVal, ok := pdu.Value.(float32)
114
+ if !ok {
115
+ return 0, fmt.Errorf("OpaqueFloat has unexpected type %T", pdu.Value)
116
+ }
117
+ value = ternary(sym.ScaleFactor != 0, int64(float64(floatVal)*sym.ScaleFactor), int64(floatVal))
118
+ return value, nil
119
+ case gosnmp.OpaqueDouble:
120
+ floatVal, ok := pdu.Value.(float64)
121
+ if !ok {
122
+ return 0, fmt.Errorf("OpaqueFloat has unexpected type %T", pdu.Value)
123
+ }
124
+ value = ternary(sym.ScaleFactor != 0, int64(floatVal*sym.ScaleFactor), int64(floatVal))
125
+ return value, nil
126
+ default:
127
+ value = gosnmp.ToBigInt(pdu.Value).Int64()
128
+ if len(sym.Mapping) > 0 {
129
+ s := strconv.FormatInt(value, 10)
130
+ if v, ok := sym.Mapping[s]; ok && isInt(v) {
131
+ value, _ = strconv.ParseInt(v, 10, 64)
132
+ }
133
}
134
}
135
} else {
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_table.go
+91
-22
@@ -52,20 +52,6 @@ func (c *Collector) walkAllTables(prof *ddsnmp.Profile) ([]tableWalkResult, erro
52
53
doneOids[cfg.Table.OID] = true
54
55
- // Check if we should skip this table (only skip for index transforms now)
56
- skipTable := false
57
- for _, tagCfg := range cfg.MetricTags {
58
- if len(tagCfg.IndexTransform) > 0 {
59
- c.log.Debugf("Skipping table %s: has index transformation", cfg.Table.Name)
60
- skipTable = true
61
- break
62
- }
63
- }
64
-
65
- if skipTable {
66
- continue
67
- }
68
-
55
// Walk the table
56
pdus, err := c.snmpWalk(cfg.Table.OID)
57
if err != nil {
@@ -213,8 +199,8 @@ func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus
199
continue
200
}
201
216
- // Skip if has index transformation (not supported yet)
217
- if len(tagCfg.IndexTransform) > 0 {
202
+ // Skip if it's an index-based tag (handled separately)
203
+ if tagCfg.Index != 0 {
204
continue
205
}
206
@@ -232,13 +218,22 @@ func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus
218
continue
219
}
220
221
+ lookupIndex := index
222
+ if len(tagCfg.IndexTransform) > 0 {
223
+ lookupIndex = applyIndexTransform(index, tagCfg.IndexTransform)
224
+ if lookupIndex == "" {
225
+ c.log.Debugf("Index transformation failed for index %s with transforms %v", index, tagCfg.IndexTransform)
226
+ continue
227
+ }
228
+ }
229
+
230
// Look up the value from the referenced table using the same index
231
refColumnOID := trimOID(tagCfg.Symbol.OID)
237
- refFullOID := refColumnOID + "." + index
232
+ refFullOID := refColumnOID + "." + lookupIndex
233
234
pdu, ok := refTablePDUs[refFullOID]
235
if !ok {
241
- c.log.Debugf("Cannot find cross-table tag value at OID %s for table %s", refFullOID, tagCfg.Table)
236
+ c.log.Debugf("Cannot find cross-table tag value at OID %s for table %s (lookup index: %s)", refFullOID, tagCfg.Table, lookupIndex)
237
continue
238
}
239
@@ -255,6 +250,29 @@ func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus
250
}
251
}
252
253
+ // Process index-based tags
254
+ for _, tagCfg := range cfg.MetricTags {
255
+ // Skip if not an index-based tag
256
+ if tagCfg.Index == 0 {
257
+ continue
258
+ }
259
+
260
+ indexValue, ok := getIndexPosition(index, tagCfg.Index)
261
+ if !ok {
262
+ c.log.Debugf("Cannot extract position %d from index %s", tagCfg.Index, index)
263
+ continue
264
+ }
265
+
266
+ tagName := ternary(tagCfg.Tag != "", tagCfg.Tag, fmt.Sprintf("index%d", tagCfg.Index))
267
+
268
+ if v, ok := tagCfg.Mapping[indexValue]; ok {
269
+ indexValue = v
270
+ }
271
+
272
+ rowTags[tagName] = indexValue
273
+ tagCache[index][tagName] = indexValue
274
+ }
275
+
276
// Process metrics for this row
277
for columnOID, sym := range columnOIDs {
278
pdu, ok := rowPDUs[columnOID]
@@ -449,12 +467,63 @@ func (c *Collector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error) {
467
}
468
469
for _, pdu := range resp {
452
- if !isPduWithData(pdu) {
453
- c.missingOIDs[trimOID(pdu.Name)] = true
454
- continue
470
+ if isPduWithData(pdu) {
471
+ pdus[trimOID(pdu.Name)] = pdu
472
}
456
- pdus[trimOID(pdu.Name)] = pdu
473
+ }
474
+
475
+ if len(pdus) == 0 {
476
+ c.missingOIDs[trimOID(oid)] = true
477
}
478
479
return pdus, nil
480
}
481
+
482
+// getIndexPosition extracts a specific position from an index
483
+// Position uses 1-based indexing as per the profile format
484
+// Example: index "7.8.9", position 2 → "8"
485
+func getIndexPosition(index string, position uint) (string, bool) {
486
+ if position == 0 {
487
+ return "", false
488
+ }
489
+
490
+ var n uint
491
+ for {
492
+ n++
493
+ i := strings.IndexByte(index, '.')
494
+ if i == -1 {
495
+ break
496
+ }
497
+ if n == position {
498
+ return index[:i], true
499
+ }
500
+ index = index[i+1:]
501
+ }
502
+
503
+ return index, n == position && index != ""
504
+}
505
+
506
+// applyIndexTransform applies index transformation rules to extract a subset of the index
507
+// Example: index "1.6.0.36.155.53.3.246", transform [{start: 1, end: 7}] → "6.0.36.155.53.3.246"
508
+func applyIndexTransform(index string, transforms []ddprofiledefinition.MetricIndexTransform) string {
509
+ if len(transforms) == 0 {
510
+ return index
511
+ }
512
+
513
+ parts := strings.Split(index, ".")
514
+ var result []string
515
+
516
+ for _, transform := range transforms {
517
+ start := transform.Start
518
+ end := transform.End
519
+
520
+ if int(start) >= len(parts) || end < start || int(end) >= len(parts) {
521
+ continue
522
+ }
523
+
524
+ // Extract the range (inclusive)
525
+ result = append(result, parts[start:end+1]...)
526
+ }
527
+
528
+ return strings.Join(result, ".")
529
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
+1506
-49
@@ -1708,55 +1708,6 @@ func TestCollector_Collect(t *testing.T) {
1708
},
1709
expectedError: false,
1710
},
1711
- "cross-table tags with index transformation should be skipped": {
1712
- profiles: []*ddsnmp.Profile{
1713
- {
1714
- SourceFile: "test-profile.yaml",
1715
- Definition: &ddprofiledefinition.ProfileDefinition{
1716
- Metrics: []ddprofiledefinition.MetricsConfig{
1717
- {
1718
- MIB: "MY-MIB",
1719
- Table: ddprofiledefinition.SymbolConfig{
1720
- OID: "1.3.6.1.4.1.1000.1",
1721
- Name: "myTable",
1722
- },
1723
- Symbols: []ddprofiledefinition.SymbolConfig{
1724
- {
1725
- OID: "1.3.6.1.4.1.1000.1.1.1",
1726
- Name: "myMetric",
1727
- },
1728
- },
1729
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1730
- {
1731
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1732
- OID: "1.3.6.1.2.1.31.1.1.1.1",
1733
- Name: "ifName",
1734
- },
1735
- Table: "ifXTable",
1736
- Tag: "interface",
1737
- IndexTransform: []ddprofiledefinition.MetricIndexTransform{
1738
- {Start: 1, End: 3},
1739
- },
1740
- },
1741
- },
1742
- },
1743
- },
1744
- },
1745
- },
1746
- },
1747
- setupMock: func(m *snmpmock.MockHandler) {
1748
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1749
- // Table should be skipped due to index transformation
1750
- },
1751
- expectedResult: []*ProfileMetrics{
1752
- {
1753
- Source: "test-profile.yaml",
1754
- DeviceMetadata: nil,
1755
- Metrics: []Metric{},
1756
- },
1757
- },
1758
- expectedError: false,
1759
- },
1711
"multiple cross-table tags from different tables": {
1712
profiles: []*ddsnmp.Profile{
1713
{
@@ -1875,6 +1826,1512 @@ func TestCollector_Collect(t *testing.T) {
1826
},
1827
expectedError: false,
1828
},
1829
+
1830
+ "index-based tags with single position": {
1831
+ profiles: []*ddsnmp.Profile{
1832
+ {
1833
+ SourceFile: "test-profile.yaml",
1834
+ Definition: &ddprofiledefinition.ProfileDefinition{
1835
+ Metrics: []ddprofiledefinition.MetricsConfig{
1836
+ {
1837
+ MIB: "IP-MIB",
1838
+ Table: ddprofiledefinition.SymbolConfig{
1839
+ OID: "1.3.6.1.2.1.4.31.1",
1840
+ Name: "ipSystemStatsTable",
1841
+ },
1842
+ Symbols: []ddprofiledefinition.SymbolConfig{
1843
+ {
1844
+ OID: "1.3.6.1.2.1.4.31.1.1.4",
1845
+ Name: "ipSystemStatsHCInReceives",
1846
+ },
1847
+ },
1848
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1849
+ {
1850
+ Index: 1,
1851
+ Tag: "ipversion",
1852
+ Mapping: map[string]string{
1853
+ "0": "unknown",
1854
+ "1": "ipv4",
1855
+ "2": "ipv6",
1856
+ "3": "ipv4z",
1857
+ "4": "ipv6z",
1858
+ "16": "dns",
1859
+ },
1860
+ },
1861
+ },
1862
+ },
1863
+ },
1864
+ },
1865
+ },
1866
+ },
1867
+ setupMock: func(m *snmpmock.MockHandler) {
1868
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1869
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1870
+
1871
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.4.31.1").Return(
1872
+ []gosnmp.SnmpPDU{
1873
+ // IPv4 row
1874
+ {
1875
+ Name: "1.3.6.1.2.1.4.31.1.1.4.1",
1876
+ Type: gosnmp.Counter64,
1877
+ Value: uint64(1000),
1878
+ },
1879
+ // IPv6 row
1880
+ {
1881
+ Name: "1.3.6.1.2.1.4.31.1.1.4.2",
1882
+ Type: gosnmp.Counter64,
1883
+ Value: uint64(2000),
1884
+ },
1885
+ }, nil,
1886
+ )
1887
+ },
1888
+ expectedResult: []*ProfileMetrics{
1889
+ {
1890
+ Source: "test-profile.yaml",
1891
+ DeviceMetadata: nil,
1892
+ Metrics: []Metric{
1893
+ {
1894
+ Name: "ipSystemStatsHCInReceives",
1895
+ Value: 1000,
1896
+ Tags: map[string]string{"ipversion": "ipv4"},
1897
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1898
+ IsTable: true,
1899
+ },
1900
+ {
1901
+ Name: "ipSystemStatsHCInReceives",
1902
+ Value: 2000,
1903
+ Tags: map[string]string{"ipversion": "ipv6"},
1904
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1905
+ IsTable: true,
1906
+ },
1907
+ },
1908
+ },
1909
+ },
1910
+ expectedError: false,
1911
+ },
1912
+ "index-based tags with multiple positions": {
1913
+ profiles: []*ddsnmp.Profile{
1914
+ {
1915
+ SourceFile: "test-profile.yaml",
1916
+ Definition: &ddprofiledefinition.ProfileDefinition{
1917
+ Metrics: []ddprofiledefinition.MetricsConfig{
1918
+ {
1919
+ MIB: "CISCO-FIREWALL-MIB",
1920
+ Table: ddprofiledefinition.SymbolConfig{
1921
+ OID: "1.3.6.1.4.1.9.9.147.1.2.2.2",
1922
+ Name: "cfwConnectionStatTable",
1923
+ },
1924
+ Symbols: []ddprofiledefinition.SymbolConfig{
1925
+ {
1926
+ OID: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5",
1927
+ Name: "cfwConnectionStatValue",
1928
+ },
1929
+ },
1930
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1931
+ {
1932
+ Index: 1,
1933
+ Tag: "service_type",
1934
+ },
1935
+ {
1936
+ Index: 2,
1937
+ Tag: "stat_type",
1938
+ },
1939
+ },
1940
+ },
1941
+ },
1942
+ },
1943
+ },
1944
+ },
1945
+ setupMock: func(m *snmpmock.MockHandler) {
1946
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1947
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1948
+
1949
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.9.9.147.1.2.2.2").Return(
1950
+ []gosnmp.SnmpPDU{
1951
+ // Index: 20.2 (service_type=20, stat_type=2)
1952
+ {
1953
+ Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.20.2",
1954
+ Type: gosnmp.Counter64,
1955
+ Value: uint64(4087850099),
1956
+ },
1957
+ // Index: 21.3 (service_type=21, stat_type=3)
1958
+ {
1959
+ Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.21.3",
1960
+ Type: gosnmp.Counter64,
1961
+ Value: uint64(5000000),
1962
+ },
1963
+ }, nil,
1964
+ )
1965
+ },
1966
+ expectedResult: []*ProfileMetrics{
1967
+ {
1968
+ Source: "test-profile.yaml",
1969
+ DeviceMetadata: nil,
1970
+ Metrics: []Metric{
1971
+ {
1972
+ Name: "cfwConnectionStatValue",
1973
+ Value: 4087850099,
1974
+ Tags: map[string]string{
1975
+ "service_type": "20",
1976
+ "stat_type": "2",
1977
+ },
1978
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1979
+ IsTable: true,
1980
+ },
1981
+ {
1982
+ Name: "cfwConnectionStatValue",
1983
+ Value: 5000000,
1984
+ Tags: map[string]string{
1985
+ "service_type": "21",
1986
+ "stat_type": "3",
1987
+ },
1988
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1989
+ IsTable: true,
1990
+ },
1991
+ },
1992
+ },
1993
+ },
1994
+ expectedError: false,
1995
+ },
1996
+ "index-based tags with missing mapping value": {
1997
+ profiles: []*ddsnmp.Profile{
1998
+ {
1999
+ SourceFile: "test-profile.yaml",
2000
+ Definition: &ddprofiledefinition.ProfileDefinition{
2001
+ Metrics: []ddprofiledefinition.MetricsConfig{
2002
+ {
2003
+ MIB: "IP-MIB",
2004
+ Table: ddprofiledefinition.SymbolConfig{
2005
+ OID: "1.3.6.1.2.1.4.31.1",
2006
+ Name: "ipSystemStatsTable",
2007
+ },
2008
+ Symbols: []ddprofiledefinition.SymbolConfig{
2009
+ {
2010
+ OID: "1.3.6.1.2.1.4.31.1.1.4",
2011
+ Name: "ipSystemStatsHCInReceives",
2012
+ },
2013
+ },
2014
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2015
+ {
2016
+ Index: 1,
2017
+ Tag: "ipversion",
2018
+ Mapping: map[string]string{
2019
+ "1": "ipv4",
2020
+ "2": "ipv6",
2021
+ // Missing mapping for "99"
2022
+ },
2023
+ },
2024
+ },
2025
+ },
2026
+ },
2027
+ },
2028
+ },
2029
+ },
2030
+ setupMock: func(m *snmpmock.MockHandler) {
2031
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2032
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2033
+
2034
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.4.31.1").Return(
2035
+ []gosnmp.SnmpPDU{
2036
+ // Index 99 - no mapping defined
2037
+ {
2038
+ Name: "1.3.6.1.2.1.4.31.1.1.4.99",
2039
+ Type: gosnmp.Counter64,
2040
+ Value: uint64(3000),
2041
+ },
2042
+ }, nil,
2043
+ )
2044
+ },
2045
+ expectedResult: []*ProfileMetrics{
2046
+ {
2047
+ Source: "test-profile.yaml",
2048
+ DeviceMetadata: nil,
2049
+ Metrics: []Metric{
2050
+ {
2051
+ Name: "ipSystemStatsHCInReceives",
2052
+ Value: 3000,
2053
+ Tags: map[string]string{"ipversion": "99"}, // Raw value when no mapping exists
2054
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
2055
+ IsTable: true,
2056
+ },
2057
+ },
2058
+ },
2059
+ },
2060
+ expectedError: false,
2061
+ },
2062
+ "index-based tags with complex multi-part index": {
2063
+ profiles: []*ddsnmp.Profile{
2064
+ {
2065
+ SourceFile: "test-profile.yaml",
2066
+ Definition: &ddprofiledefinition.ProfileDefinition{
2067
+ Metrics: []ddprofiledefinition.MetricsConfig{
2068
+ {
2069
+ MIB: "MY-MIB",
2070
+ Table: ddprofiledefinition.SymbolConfig{
2071
+ OID: "1.3.6.1.4.1.1000.1",
2072
+ Name: "myComplexTable",
2073
+ },
2074
+ Symbols: []ddprofiledefinition.SymbolConfig{
2075
+ {
2076
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2077
+ Name: "myMetric",
2078
+ },
2079
+ },
2080
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2081
+ {
2082
+ Index: 1,
2083
+ Tag: "first",
2084
+ },
2085
+ {
2086
+ Index: 3,
2087
+ Tag: "third",
2088
+ },
2089
+ {
2090
+ Index: 5,
2091
+ Tag: "fifth",
2092
+ },
2093
+ },
2094
+ },
2095
+ },
2096
+ },
2097
+ },
2098
+ },
2099
+ setupMock: func(m *snmpmock.MockHandler) {
2100
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2101
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2102
+
2103
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2104
+ []gosnmp.SnmpPDU{
2105
+ // Complex index: 10.20.30.40.50
2106
+ {
2107
+ Name: "1.3.6.1.4.1.1000.1.1.1.10.20.30.40.50",
2108
+ Type: gosnmp.Gauge32,
2109
+ Value: uint(100),
2110
+ },
2111
+ }, nil,
2112
+ )
2113
+ },
2114
+ expectedResult: []*ProfileMetrics{
2115
+ {
2116
+ Source: "test-profile.yaml",
2117
+ DeviceMetadata: nil,
2118
+ Metrics: []Metric{
2119
+ {
2120
+ Name: "myMetric",
2121
+ Value: 100,
2122
+ Tags: map[string]string{
2123
+ "first": "10",
2124
+ "third": "30",
2125
+ "fifth": "50",
2126
+ },
2127
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2128
+ IsTable: true,
2129
+ },
2130
+ },
2131
+ },
2132
+ },
2133
+ expectedError: false,
2134
+ },
2135
+ "index-based tags with out-of-range position": {
2136
+ profiles: []*ddsnmp.Profile{
2137
+ {
2138
+ SourceFile: "test-profile.yaml",
2139
+ Definition: &ddprofiledefinition.ProfileDefinition{
2140
+ Metrics: []ddprofiledefinition.MetricsConfig{
2141
+ {
2142
+ MIB: "MY-MIB",
2143
+ Table: ddprofiledefinition.SymbolConfig{
2144
+ OID: "1.3.6.1.4.1.1000.1",
2145
+ Name: "myTable",
2146
+ },
2147
+ Symbols: []ddprofiledefinition.SymbolConfig{
2148
+ {
2149
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2150
+ Name: "myMetric",
2151
+ },
2152
+ },
2153
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2154
+ {
2155
+ Index: 1,
2156
+ Tag: "first",
2157
+ },
2158
+ {
2159
+ Index: 5, // This position doesn't exist in index "1.2"
2160
+ Tag: "fifth",
2161
+ },
2162
+ },
2163
+ },
2164
+ },
2165
+ },
2166
+ },
2167
+ },
2168
+ setupMock: func(m *snmpmock.MockHandler) {
2169
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2170
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2171
+
2172
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2173
+ []gosnmp.SnmpPDU{
2174
+ // Simple index: 1.2
2175
+ {
2176
+ Name: "1.3.6.1.4.1.1000.1.1.1.1.2",
2177
+ Type: gosnmp.Gauge32,
2178
+ Value: uint(100),
2179
+ },
2180
+ }, nil,
2181
+ )
2182
+ },
2183
+ expectedResult: []*ProfileMetrics{
2184
+ {
2185
+ Source: "test-profile.yaml",
2186
+ DeviceMetadata: nil,
2187
+ Metrics: []Metric{
2188
+ {
2189
+ Name: "myMetric",
2190
+ Value: 100,
2191
+ Tags: map[string]string{
2192
+ "first": "1",
2193
+ // "fifth" tag is not present because position 5 doesn't exist
2194
+ },
2195
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2196
+ IsTable: true,
2197
+ },
2198
+ },
2199
+ },
2200
+ },
2201
+ expectedError: false,
2202
+ },
2203
+ "index-based tags combined with symbol tags": {
2204
+ profiles: []*ddsnmp.Profile{
2205
+ {
2206
+ SourceFile: "test-profile.yaml",
2207
+ Definition: &ddprofiledefinition.ProfileDefinition{
2208
+ Metrics: []ddprofiledefinition.MetricsConfig{
2209
+ {
2210
+ MIB: "MY-MIB",
2211
+ Table: ddprofiledefinition.SymbolConfig{
2212
+ OID: "1.3.6.1.4.1.1000.1",
2213
+ Name: "myTable",
2214
+ },
2215
+ Symbols: []ddprofiledefinition.SymbolConfig{
2216
+ {
2217
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2218
+ Name: "myMetric",
2219
+ },
2220
+ },
2221
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2222
+ {
2223
+ Index: 1,
2224
+ Tag: "index_tag",
2225
+ },
2226
+ {
2227
+ Tag: "name_tag",
2228
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2229
+ OID: "1.3.6.1.4.1.1000.1.1.2",
2230
+ Name: "myName",
2231
+ },
2232
+ },
2233
+ },
2234
+ },
2235
+ },
2236
+ },
2237
+ },
2238
+ },
2239
+ setupMock: func(m *snmpmock.MockHandler) {
2240
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2241
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2242
+
2243
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2244
+ []gosnmp.SnmpPDU{
2245
+ {
2246
+ Name: "1.3.6.1.4.1.1000.1.1.1.5",
2247
+ Type: gosnmp.Gauge32,
2248
+ Value: uint(100),
2249
+ },
2250
+ {
2251
+ Name: "1.3.6.1.4.1.1000.1.1.2.5",
2252
+ Type: gosnmp.OctetString,
2253
+ Value: []byte("device-5"),
2254
+ },
2255
+ }, nil,
2256
+ )
2257
+ },
2258
+ expectedResult: []*ProfileMetrics{
2259
+ {
2260
+ Source: "test-profile.yaml",
2261
+ DeviceMetadata: nil,
2262
+ Metrics: []Metric{
2263
+ {
2264
+ Name: "myMetric",
2265
+ Value: 100,
2266
+ Tags: map[string]string{
2267
+ "index_tag": "5",
2268
+ "name_tag": "device-5",
2269
+ },
2270
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2271
+ IsTable: true,
2272
+ },
2273
+ },
2274
+ },
2275
+ },
2276
+ expectedError: false,
2277
+ },
2278
+ "index-based tags with default tag name": {
2279
+ profiles: []*ddsnmp.Profile{
2280
+ {
2281
+ SourceFile: "test-profile.yaml",
2282
+ Definition: &ddprofiledefinition.ProfileDefinition{
2283
+ Metrics: []ddprofiledefinition.MetricsConfig{
2284
+ {
2285
+ MIB: "MY-MIB",
2286
+ Table: ddprofiledefinition.SymbolConfig{
2287
+ OID: "1.3.6.1.4.1.1000.1",
2288
+ Name: "myTable",
2289
+ },
2290
+ Symbols: []ddprofiledefinition.SymbolConfig{
2291
+ {
2292
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2293
+ Name: "myMetric",
2294
+ },
2295
+ },
2296
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2297
+ {
2298
+ Index: 2,
2299
+ // No tag name specified, should default to "index2"
2300
+ },
2301
+ },
2302
+ },
2303
+ },
2304
+ },
2305
+ },
2306
+ },
2307
+ setupMock: func(m *snmpmock.MockHandler) {
2308
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2309
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2310
+
2311
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2312
+ []gosnmp.SnmpPDU{
2313
+ {
2314
+ Name: "1.3.6.1.4.1.1000.1.1.1.10.20",
2315
+ Type: gosnmp.Gauge32,
2316
+ Value: uint(100),
2317
+ },
2318
+ }, nil,
2319
+ )
2320
+ },
2321
+ expectedResult: []*ProfileMetrics{
2322
+ {
2323
+ Source: "test-profile.yaml",
2324
+ DeviceMetadata: nil,
2325
+ Metrics: []Metric{
2326
+ {
2327
+ Name: "myMetric",
2328
+ Value: 100,
2329
+ Tags: map[string]string{"index2": "20"},
2330
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2331
+ IsTable: true,
2332
+ },
2333
+ },
2334
+ },
2335
+ },
2336
+ expectedError: false,
2337
+ },
2338
+ "index-based tags with single component index": {
2339
+ profiles: []*ddsnmp.Profile{
2340
+ {
2341
+ SourceFile: "test-profile.yaml",
2342
+ Definition: &ddprofiledefinition.ProfileDefinition{
2343
+ Metrics: []ddprofiledefinition.MetricsConfig{
2344
+ {
2345
+ MIB: "MY-MIB",
2346
+ Table: ddprofiledefinition.SymbolConfig{
2347
+ OID: "1.3.6.1.4.1.1000.1",
2348
+ Name: "myTable",
2349
+ },
2350
+ Symbols: []ddprofiledefinition.SymbolConfig{
2351
+ {
2352
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2353
+ Name: "myMetric",
2354
+ },
2355
+ },
2356
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2357
+ {
2358
+ Index: 1,
2359
+ Tag: "id",
2360
+ },
2361
+ },
2362
+ },
2363
+ },
2364
+ },
2365
+ },
2366
+ },
2367
+ setupMock: func(m *snmpmock.MockHandler) {
2368
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2369
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2370
+
2371
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2372
+ []gosnmp.SnmpPDU{
2373
+ // Single component index: just "42"
2374
+ {
2375
+ Name: "1.3.6.1.4.1.1000.1.1.1.42",
2376
+ Type: gosnmp.Gauge32,
2377
+ Value: uint(100),
2378
+ },
2379
+ }, nil,
2380
+ )
2381
+ },
2382
+ expectedResult: []*ProfileMetrics{
2383
+ {
2384
+ Source: "test-profile.yaml",
2385
+ DeviceMetadata: nil,
2386
+ Metrics: []Metric{
2387
+ {
2388
+ Name: "myMetric",
2389
+ Value: 100,
2390
+ Tags: map[string]string{"id": "42"},
2391
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2392
+ IsTable: true,
2393
+ },
2394
+ },
2395
+ },
2396
+ },
2397
+ expectedError: false,
2398
+ },
2399
+
2400
+ "cross-table tags with index transformation": {
2401
+ profiles: []*ddsnmp.Profile{
2402
+ {
2403
+ SourceFile: "test-profile.yaml",
2404
+ Definition: &ddprofiledefinition.ProfileDefinition{
2405
+ Metrics: []ddprofiledefinition.MetricsConfig{
2406
+ {
2407
+ MIB: "CPI-UNITY-MIB",
2408
+ Table: ddprofiledefinition.SymbolConfig{
2409
+ OID: "1.3.6.1.4.1.30932.1.10.1.3.110",
2410
+ Name: "cpiPduBranchTable",
2411
+ },
2412
+ Symbols: []ddprofiledefinition.SymbolConfig{
2413
+ {
2414
+ OID: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3",
2415
+ Name: "cpiPduBranchCurrent",
2416
+ },
2417
+ },
2418
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2419
+ {
2420
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2421
+ OID: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3",
2422
+ Name: "cpiPduName",
2423
+ },
2424
+ Table: "cpiPduTable",
2425
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2426
+ {Start: 1, End: 7},
2427
+ },
2428
+ Tag: "pdu_name",
2429
+ },
2430
+ },
2431
+ },
2432
+ {
2433
+ MIB: "CPI-UNITY-MIB",
2434
+ Table: ddprofiledefinition.SymbolConfig{
2435
+ OID: "1.3.6.1.4.1.30932.1.10.1.2.10",
2436
+ Name: "cpiPduTable",
2437
+ },
2438
+ Symbols: []ddprofiledefinition.SymbolConfig{
2439
+ // No symbols needed - only used for cross-table reference
2440
+ },
2441
+ },
2442
+ },
2443
+ },
2444
+ },
2445
+ },
2446
+ setupMock: func(m *snmpmock.MockHandler) {
2447
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2448
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2449
+
2450
+ // Walk cpiPduBranchTable
2451
+ // Index structure: <branch_id>.<mac_address>
2452
+ // Example: 1.6.0.36.155.53.3.246
2453
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.30932.1.10.1.3.110").Return(
2454
+ []gosnmp.SnmpPDU{
2455
+ {
2456
+ Name: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3.1.6.0.36.155.53.3.246",
2457
+ Type: gosnmp.Gauge32,
2458
+ Value: uint(150), // 1.5 Amps
2459
+ },
2460
+ {
2461
+ Name: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3.2.6.0.36.155.53.3.247",
2462
+ Type: gosnmp.Gauge32,
2463
+ Value: uint(200), // 2.0 Amps
2464
+ },
2465
+ }, nil,
2466
+ )
2467
+
2468
+ // Walk cpiPduTable
2469
+ // Index structure: <mac_address> only
2470
+ // Example: 6.0.36.155.53.3.246
2471
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.30932.1.10.1.2.10").Return(
2472
+ []gosnmp.SnmpPDU{
2473
+ {
2474
+ Name: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3.6.0.36.155.53.3.246",
2475
+ Type: gosnmp.OctetString,
2476
+ Value: []byte("PDU-A"),
2477
+ },
2478
+ {
2479
+ Name: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3.6.0.36.155.53.3.247",
2480
+ Type: gosnmp.OctetString,
2481
+ Value: []byte("PDU-B"),
2482
+ },
2483
+ }, nil,
2484
+ )
2485
+ },
2486
+ expectedResult: []*ProfileMetrics{
2487
+ {
2488
+ Source: "test-profile.yaml",
2489
+ DeviceMetadata: nil,
2490
+ Metrics: []Metric{
2491
+ {
2492
+ Name: "cpiPduBranchCurrent",
2493
+ Value: 150,
2494
+ Tags: map[string]string{"pdu_name": "PDU-A"},
2495
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2496
+ IsTable: true,
2497
+ },
2498
+ {
2499
+ Name: "cpiPduBranchCurrent",
2500
+ Value: 200,
2501
+ Tags: map[string]string{"pdu_name": "PDU-B"},
2502
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2503
+ IsTable: true,
2504
+ },
2505
+ },
2506
+ },
2507
+ },
2508
+ expectedError: false,
2509
+ },
2510
+ "cross-table tags with multiple index transformations": {
2511
+ profiles: []*ddsnmp.Profile{
2512
+ {
2513
+ SourceFile: "test-profile.yaml",
2514
+ Definition: &ddprofiledefinition.ProfileDefinition{
2515
+ Metrics: []ddprofiledefinition.MetricsConfig{
2516
+ {
2517
+ MIB: "MY-MIB",
2518
+ Table: ddprofiledefinition.SymbolConfig{
2519
+ OID: "1.3.6.1.4.1.1000.1",
2520
+ Name: "myComplexTable",
2521
+ },
2522
+ Symbols: []ddprofiledefinition.SymbolConfig{
2523
+ {
2524
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2525
+ Name: "myMetric",
2526
+ },
2527
+ },
2528
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2529
+ {
2530
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2531
+ OID: "1.3.6.1.4.1.1000.2.1.1",
2532
+ Name: "refName",
2533
+ },
2534
+ Table: "refTable",
2535
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2536
+ {Start: 0, End: 1},
2537
+ {Start: 3, End: 5},
2538
+ },
2539
+ Tag: "ref_name",
2540
+ },
2541
+ },
2542
+ },
2543
+ {
2544
+ MIB: "MY-MIB",
2545
+ Table: ddprofiledefinition.SymbolConfig{
2546
+ OID: "1.3.6.1.4.1.1000.2",
2547
+ Name: "refTable",
2548
+ },
2549
+ Symbols: []ddprofiledefinition.SymbolConfig{
2550
+ // No symbols needed
2551
+ },
2552
+ },
2553
+ },
2554
+ },
2555
+ },
2556
+ },
2557
+ setupMock: func(m *snmpmock.MockHandler) {
2558
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2559
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2560
+
2561
+ // Walk myComplexTable
2562
+ // Index: 1.2.3.4.5.6.7
2563
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2564
+ []gosnmp.SnmpPDU{
2565
+ {
2566
+ Name: "1.3.6.1.4.1.1000.1.1.1.1.2.3.4.5.6.7",
2567
+ Type: gosnmp.Gauge32,
2568
+ Value: uint(100),
2569
+ },
2570
+ }, nil,
2571
+ )
2572
+
2573
+ // Walk refTable
2574
+ // Expected transformed index: 1.2.4.5.6 (positions 1-2 and 4-6)
2575
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2576
+ []gosnmp.SnmpPDU{
2577
+ {
2578
+ Name: "1.3.6.1.4.1.1000.2.1.1.1.2.4.5.6",
2579
+ Type: gosnmp.OctetString,
2580
+ Value: []byte("Complex-Ref"),
2581
+ },
2582
+ }, nil,
2583
+ )
2584
+ },
2585
+ expectedResult: []*ProfileMetrics{
2586
+ {
2587
+ Source: "test-profile.yaml",
2588
+ DeviceMetadata: nil,
2589
+ Metrics: []Metric{
2590
+ {
2591
+ Name: "myMetric",
2592
+ Value: 100,
2593
+ Tags: map[string]string{"ref_name": "Complex-Ref"},
2594
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2595
+ IsTable: true,
2596
+ },
2597
+ },
2598
+ },
2599
+ },
2600
+ expectedError: false,
2601
+ },
2602
+ "cross-table tags with index transformation no match": {
2603
+ profiles: []*ddsnmp.Profile{
2604
+ {
2605
+ SourceFile: "test-profile.yaml",
2606
+ Definition: &ddprofiledefinition.ProfileDefinition{
2607
+ Metrics: []ddprofiledefinition.MetricsConfig{
2608
+ {
2609
+ MIB: "MY-MIB",
2610
+ Table: ddprofiledefinition.SymbolConfig{
2611
+ OID: "1.3.6.1.4.1.1000.1",
2612
+ Name: "myTable",
2613
+ },
2614
+ Symbols: []ddprofiledefinition.SymbolConfig{
2615
+ {
2616
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2617
+ Name: "myMetric",
2618
+ },
2619
+ },
2620
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2621
+ {
2622
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2623
+ OID: "1.3.6.1.4.1.1000.2.1.1",
2624
+ Name: "refName",
2625
+ },
2626
+ Table: "refTable",
2627
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2628
+ {Start: 2, End: 4},
2629
+ },
2630
+ Tag: "ref_name",
2631
+ },
2632
+ },
2633
+ },
2634
+ {
2635
+ MIB: "MY-MIB",
2636
+ Table: ddprofiledefinition.SymbolConfig{
2637
+ OID: "1.3.6.1.4.1.1000.2",
2638
+ Name: "refTable",
2639
+ },
2640
+ Symbols: []ddprofiledefinition.SymbolConfig{
2641
+ // No symbols needed
2642
+ },
2643
+ },
2644
+ },
2645
+ },
2646
+ },
2647
+ },
2648
+ setupMock: func(m *snmpmock.MockHandler) {
2649
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2650
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2651
+
2652
+ // Walk myTable
2653
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2654
+ []gosnmp.SnmpPDU{
2655
+ {
2656
+ Name: "1.3.6.1.4.1.1000.1.1.1.1.2.3",
2657
+ Type: gosnmp.Gauge32,
2658
+ Value: uint(100),
2659
+ },
2660
+ }, nil,
2661
+ )
2662
+
2663
+ // Walk refTable - but it doesn't have the transformed index
2664
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2665
+ []gosnmp.SnmpPDU{
2666
+ {
2667
+ Name: "1.3.6.1.4.1.1000.2.1.1.9.9.9", // Different index
2668
+ Type: gosnmp.OctetString,
2669
+ Value: []byte("Other-Ref"),
2670
+ },
2671
+ }, nil,
2672
+ )
2673
+ },
2674
+ expectedResult: []*ProfileMetrics{
2675
+ {
2676
+ Source: "test-profile.yaml",
2677
+ DeviceMetadata: nil,
2678
+ Metrics: []Metric{
2679
+ {
2680
+ Name: "myMetric",
2681
+ Value: 100,
2682
+ Tags: nil, // No tag because transformed index not found
2683
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2684
+ IsTable: true,
2685
+ },
2686
+ },
2687
+ },
2688
+ },
2689
+ expectedError: false,
2690
+ },
2691
+ "cross-table tags with invalid index transformation": {
2692
+ profiles: []*ddsnmp.Profile{
2693
+ {
2694
+ SourceFile: "test-profile.yaml",
2695
+ Definition: &ddprofiledefinition.ProfileDefinition{
2696
+ Metrics: []ddprofiledefinition.MetricsConfig{
2697
+ {
2698
+ MIB: "MY-MIB",
2699
+ Table: ddprofiledefinition.SymbolConfig{
2700
+ OID: "1.3.6.1.4.1.1000.1",
2701
+ Name: "myTable",
2702
+ },
2703
+ Symbols: []ddprofiledefinition.SymbolConfig{
2704
+ {
2705
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2706
+ Name: "myMetric",
2707
+ },
2708
+ },
2709
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2710
+ {
2711
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2712
+ OID: "1.3.6.1.4.1.1000.2.1.1",
2713
+ Name: "refName",
2714
+ },
2715
+ Table: "refTable",
2716
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2717
+ {Start: 5, End: 10}, // Out of bounds
2718
+ },
2719
+ Tag: "ref_name",
2720
+ },
2721
+ },
2722
+ },
2723
+ {
2724
+ MIB: "MY-MIB",
2725
+ Table: ddprofiledefinition.SymbolConfig{
2726
+ OID: "1.3.6.1.4.1.1000.2",
2727
+ Name: "refTable",
2728
+ },
2729
+ Symbols: []ddprofiledefinition.SymbolConfig{
2730
+ // No symbols needed
2731
+ },
2732
+ },
2733
+ },
2734
+ },
2735
+ },
2736
+ },
2737
+ setupMock: func(m *snmpmock.MockHandler) {
2738
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2739
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2740
+
2741
+ // Walk myTable with short index
2742
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2743
+ []gosnmp.SnmpPDU{
2744
+ {
2745
+ Name: "1.3.6.1.4.1.1000.1.1.1.1.2",
2746
+ Type: gosnmp.Gauge32,
2747
+ Value: uint(100),
2748
+ },
2749
+ }, nil,
2750
+ )
2751
+
2752
+ // Walk refTable
2753
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2754
+ []gosnmp.SnmpPDU{
2755
+ {
2756
+ Name: "1.3.6.1.4.1.1000.2.1.1.1.2",
2757
+ Type: gosnmp.OctetString,
2758
+ Value: []byte("Ref"),
2759
+ },
2760
+ }, nil,
2761
+ )
2762
+ },
2763
+ expectedResult: []*ProfileMetrics{
2764
+ {
2765
+ Source: "test-profile.yaml",
2766
+ DeviceMetadata: nil,
2767
+ Metrics: []Metric{
2768
+ {
2769
+ Name: "myMetric",
2770
+ Value: 100,
2771
+ Tags: nil, // No tag because transformation failed
2772
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2773
+ IsTable: true,
2774
+ },
2775
+ },
2776
+ },
2777
+ },
2778
+ expectedError: false,
2779
+ },
2780
+ "cross-table tags with transformation and mapping": {
2781
+ profiles: []*ddsnmp.Profile{
2782
+ {
2783
+ SourceFile: "test-profile.yaml",
2784
+ Definition: &ddprofiledefinition.ProfileDefinition{
2785
+ Metrics: []ddprofiledefinition.MetricsConfig{
2786
+ {
2787
+ MIB: "MY-MIB",
2788
+ Table: ddprofiledefinition.SymbolConfig{
2789
+ OID: "1.3.6.1.4.1.1000.1",
2790
+ Name: "myTable",
2791
+ },
2792
+ Symbols: []ddprofiledefinition.SymbolConfig{
2793
+ {
2794
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2795
+ Name: "myMetric",
2796
+ },
2797
+ },
2798
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2799
+ {
2800
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2801
+ OID: "1.3.6.1.4.1.1000.2.1.1",
2802
+ Name: "refType",
2803
+ },
2804
+ Table: "refTable",
2805
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2806
+ {Start: 1, End: 2},
2807
+ },
2808
+ Tag: "ref_type",
2809
+ Mapping: map[string]string{
2810
+ "1": "primary",
2811
+ "2": "secondary",
2812
+ "3": "backup",
2813
+ },
2814
+ },
2815
+ },
2816
+ },
2817
+ {
2818
+ MIB: "MY-MIB",
2819
+ Table: ddprofiledefinition.SymbolConfig{
2820
+ OID: "1.3.6.1.4.1.1000.2",
2821
+ Name: "refTable",
2822
+ },
2823
+ Symbols: []ddprofiledefinition.SymbolConfig{
2824
+ // No symbols needed
2825
+ },
2826
+ },
2827
+ },
2828
+ },
2829
+ },
2830
+ },
2831
+ setupMock: func(m *snmpmock.MockHandler) {
2832
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2833
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2834
+
2835
+ // Walk myTable
2836
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2837
+ []gosnmp.SnmpPDU{
2838
+ {
2839
+ Name: "1.3.6.1.4.1.1000.1.1.1.5.10.20",
2840
+ Type: gosnmp.Gauge32,
2841
+ Value: uint(100),
2842
+ },
2843
+ {
2844
+ Name: "1.3.6.1.4.1.1000.1.1.1.6.20.30",
2845
+ Type: gosnmp.Gauge32,
2846
+ Value: uint(200),
2847
+ },
2848
+ }, nil,
2849
+ )
2850
+
2851
+ // Walk refTable with transformed indexes
2852
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2853
+ []gosnmp.SnmpPDU{
2854
+ {
2855
+ Name: "1.3.6.1.4.1.1000.2.1.1.10.20", // Matches first row (positions 2-3)
2856
+ Type: gosnmp.Integer,
2857
+ Value: 1, // Will map to "primary"
2858
+ },
2859
+ {
2860
+ Name: "1.3.6.1.4.1.1000.2.1.1.20.30", // Matches second row (positions 2-3)
2861
+ Type: gosnmp.Integer,
2862
+ Value: 3, // Will map to "backup"
2863
+ },
2864
+ }, nil,
2865
+ )
2866
+ },
2867
+ expectedResult: []*ProfileMetrics{
2868
+ {
2869
+ Source: "test-profile.yaml",
2870
+ DeviceMetadata: nil,
2871
+ Metrics: []Metric{
2872
+ {
2873
+ Name: "myMetric",
2874
+ Value: 100,
2875
+ Tags: map[string]string{"ref_type": "primary"},
2876
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2877
+ IsTable: true,
2878
+ },
2879
+ {
2880
+ Name: "myMetric",
2881
+ Value: 200,
2882
+ Tags: map[string]string{"ref_type": "backup"},
2883
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2884
+ IsTable: true,
2885
+ },
2886
+ },
2887
+ },
2888
+ },
2889
+ expectedError: false,
2890
+ },
2891
+ "cross-table tags mixed with index-based tags": {
2892
+ profiles: []*ddsnmp.Profile{
2893
+ {
2894
+ SourceFile: "test-profile.yaml",
2895
+ Definition: &ddprofiledefinition.ProfileDefinition{
2896
+ Metrics: []ddprofiledefinition.MetricsConfig{
2897
+ {
2898
+ MIB: "MY-MIB",
2899
+ Table: ddprofiledefinition.SymbolConfig{
2900
+ OID: "1.3.6.1.4.1.1000.1",
2901
+ Name: "myTable",
2902
+ },
2903
+ Symbols: []ddprofiledefinition.SymbolConfig{
2904
+ {
2905
+ OID: "1.3.6.1.4.1.1000.1.1.1",
2906
+ Name: "myMetric",
2907
+ },
2908
+ },
2909
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2910
+ {
2911
+ Index: 1,
2912
+ Tag: "branch_id",
2913
+ },
2914
+ {
2915
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2916
+ OID: "1.3.6.1.4.1.1000.2.1.1",
2917
+ Name: "pduName",
2918
+ },
2919
+ Table: "pduTable",
2920
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2921
+ {Start: 1, End: 7},
2922
+ },
2923
+ Tag: "pdu_name",
2924
+ },
2925
+ },
2926
+ },
2927
+ {
2928
+ MIB: "MY-MIB",
2929
+ Table: ddprofiledefinition.SymbolConfig{
2930
+ OID: "1.3.6.1.4.1.1000.2",
2931
+ Name: "pduTable",
2932
+ },
2933
+ Symbols: []ddprofiledefinition.SymbolConfig{
2934
+ // No symbols needed
2935
+ },
2936
+ },
2937
+ },
2938
+ },
2939
+ },
2940
+ },
2941
+ setupMock: func(m *snmpmock.MockHandler) {
2942
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
2943
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2944
+
2945
+ // Walk myTable
2946
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2947
+ []gosnmp.SnmpPDU{
2948
+ {
2949
+ Name: "1.3.6.1.4.1.1000.1.1.1.1.6.0.36.155.53.3.246",
2950
+ Type: gosnmp.Gauge32,
2951
+ Value: uint(100),
2952
+ },
2953
+ }, nil,
2954
+ )
2955
+
2956
+ // Walk pduTable
2957
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2958
+ []gosnmp.SnmpPDU{
2959
+ {
2960
+ Name: "1.3.6.1.4.1.1000.2.1.1.6.0.36.155.53.3.246",
2961
+ Type: gosnmp.OctetString,
2962
+ Value: []byte("Main-PDU"),
2963
+ },
2964
+ }, nil,
2965
+ )
2966
+ },
2967
+ expectedResult: []*ProfileMetrics{
2968
+ {
2969
+ Source: "test-profile.yaml",
2970
+ DeviceMetadata: nil,
2971
+ Metrics: []Metric{
2972
+ {
2973
+ Name: "myMetric",
2974
+ Value: 100,
2975
+ Tags: map[string]string{
2976
+ "branch_id": "1",
2977
+ "pdu_name": "Main-PDU",
2978
+ },
2979
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2980
+ IsTable: true,
2981
+ },
2982
+ },
2983
+ },
2984
+ },
2985
+ expectedError: false,
2986
+ },
2987
+
2988
+ "scalar metric with OpaqueFloat": {
2989
+ profiles: []*ddsnmp.Profile{
2990
+ {
2991
+ SourceFile: "test-profile.yaml",
2992
+ Definition: &ddprofiledefinition.ProfileDefinition{
2993
+ Metrics: []ddprofiledefinition.MetricsConfig{
2994
+ {
2995
+ Symbol: ddprofiledefinition.SymbolConfig{
2996
+ OID: "1.3.6.1.4.1.6574.4.2.12.1.0",
2997
+ Name: "temperature",
2998
+ },
2999
+ },
3000
+ },
3001
+ },
3002
+ },
3003
+ },
3004
+ setupMock: func(m *snmpmock.MockHandler) {
3005
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3006
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.2.12.1.0"}).Return(
3007
+ &gosnmp.SnmpPacket{
3008
+ Variables: []gosnmp.SnmpPDU{
3009
+ {
3010
+ Name: "1.3.6.1.4.1.6574.4.2.12.1.0",
3011
+ Type: gosnmp.OpaqueFloat,
3012
+ Value: float32(29.5),
3013
+ },
3014
+ },
3015
+ }, nil,
3016
+ )
3017
+ },
3018
+ expectedResult: []*ProfileMetrics{
3019
+ {
3020
+ Source: "test-profile.yaml",
3021
+ DeviceMetadata: nil,
3022
+ Metrics: []Metric{
3023
+ {
3024
+ Name: "temperature",
3025
+ Value: 29, // Truncated from 29.5
3026
+ MetricType: "gauge",
3027
+ },
3028
+ },
3029
+ },
3030
+ },
3031
+ expectedError: false,
3032
+ },
3033
+ "scalar metric with OpaqueDouble": {
3034
+ profiles: []*ddsnmp.Profile{
3035
+ {
3036
+ SourceFile: "test-profile.yaml",
3037
+ Definition: &ddprofiledefinition.ProfileDefinition{
3038
+ Metrics: []ddprofiledefinition.MetricsConfig{
3039
+ {
3040
+ Symbol: ddprofiledefinition.SymbolConfig{
3041
+ OID: "1.3.6.1.4.1.6574.4.4.1.1.0",
3042
+ Name: "voltage",
3043
+ },
3044
+ },
3045
+ },
3046
+ },
3047
+ },
3048
+ },
3049
+ setupMock: func(m *snmpmock.MockHandler) {
3050
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3051
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.4.1.1.0"}).Return(
3052
+ &gosnmp.SnmpPacket{
3053
+ Variables: []gosnmp.SnmpPDU{
3054
+ {
3055
+ Name: "1.3.6.1.4.1.6574.4.4.1.1.0",
3056
+ Type: gosnmp.OpaqueDouble,
3057
+ Value: float64(232.75),
3058
+ },
3059
+ },
3060
+ }, nil,
3061
+ )
3062
+ },
3063
+ expectedResult: []*ProfileMetrics{
3064
+ {
3065
+ Source: "test-profile.yaml",
3066
+ DeviceMetadata: nil,
3067
+ Metrics: []Metric{
3068
+ {
3069
+ Name: "voltage",
3070
+ Value: 232, // Truncated from 232.75
3071
+ MetricType: "gauge",
3072
+ },
3073
+ },
3074
+ },
3075
+ },
3076
+ expectedError: false,
3077
+ },
3078
+ "scalar metric with OpaqueFloat and scale factor": {
3079
+ profiles: []*ddsnmp.Profile{
3080
+ {
3081
+ SourceFile: "test-profile.yaml",
3082
+ Definition: &ddprofiledefinition.ProfileDefinition{
3083
+ Metrics: []ddprofiledefinition.MetricsConfig{
3084
+ {
3085
+ Symbol: ddprofiledefinition.SymbolConfig{
3086
+ OID: "1.3.6.1.4.1.6574.4.2.12.1.0",
3087
+ Name: "temperatureMilliDegrees",
3088
+ ScaleFactor: 1000, // Convert to milli-degrees to preserve precision
3089
+ },
3090
+ },
3091
+ },
3092
+ },
3093
+ },
3094
+ },
3095
+ setupMock: func(m *snmpmock.MockHandler) {
3096
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3097
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.2.12.1.0"}).Return(
3098
+ &gosnmp.SnmpPacket{
3099
+ Variables: []gosnmp.SnmpPDU{
3100
+ {
3101
+ Name: "1.3.6.1.4.1.6574.4.2.12.1.0",
3102
+ Type: gosnmp.OpaqueFloat,
3103
+ Value: float32(29.567),
3104
+ },
3105
+ },
3106
+ }, nil,
3107
+ )
3108
+ },
3109
+ expectedResult: []*ProfileMetrics{
3110
+ {
3111
+ Source: "test-profile.yaml",
3112
+ DeviceMetadata: nil,
3113
+ Metrics: []Metric{
3114
+ {
3115
+ Name: "temperatureMilliDegrees",
3116
+ Value: 29566, // 29.567 * 1000
3117
+ MetricType: "gauge",
3118
+ },
3119
+ },
3120
+ },
3121
+ },
3122
+ expectedError: false,
3123
+ },
3124
+ "table metric with OpaqueFloat": {
3125
+ profiles: []*ddsnmp.Profile{
3126
+ {
3127
+ SourceFile: "test-profile.yaml",
3128
+ Definition: &ddprofiledefinition.ProfileDefinition{
3129
+ Metrics: []ddprofiledefinition.MetricsConfig{
3130
+ {
3131
+ MIB: "SYNOLOGY-SYSTEM-MIB",
3132
+ Table: ddprofiledefinition.SymbolConfig{
3133
+ OID: "1.3.6.1.4.1.6574.1",
3134
+ Name: "temperatureTable",
3135
+ },
3136
+ Symbols: []ddprofiledefinition.SymbolConfig{
3137
+ {
3138
+ OID: "1.3.6.1.4.1.6574.1.2",
3139
+ Name: "temperature",
3140
+ },
3141
+ },
3142
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3143
+ {
3144
+ Tag: "sensor",
3145
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3146
+ OID: "1.3.6.1.4.1.6574.1.1",
3147
+ Name: "temperatureIndex",
3148
+ },
3149
+ },
3150
+ },
3151
+ },
3152
+ },
3153
+ },
3154
+ },
3155
+ },
3156
+ setupMock: func(m *snmpmock.MockHandler) {
3157
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3158
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
3159
+
3160
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.6574.1").Return(
3161
+ []gosnmp.SnmpPDU{
3162
+ // Row 1
3163
+ {
3164
+ Name: "1.3.6.1.4.1.6574.1.1.1",
3165
+ Type: gosnmp.Integer,
3166
+ Value: 1,
3167
+ },
3168
+ {
3169
+ Name: "1.3.6.1.4.1.6574.1.2.1",
3170
+ Type: gosnmp.OpaqueFloat,
3171
+ Value: float32(65.5),
3172
+ },
3173
+ // Row 2
3174
+ {
3175
+ Name: "1.3.6.1.4.1.6574.1.1.2",
3176
+ Type: gosnmp.Integer,
3177
+ Value: 2,
3178
+ },
3179
+ {
3180
+ Name: "1.3.6.1.4.1.6574.1.2.2",
3181
+ Type: gosnmp.OpaqueFloat,
3182
+ Value: float32(71.25),
3183
+ },
3184
+ }, nil,
3185
+ )
3186
+ },
3187
+ expectedResult: []*ProfileMetrics{
3188
+ {
3189
+ Source: "test-profile.yaml",
3190
+ DeviceMetadata: nil,
3191
+ Metrics: []Metric{
3192
+ {
3193
+ Name: "temperature",
3194
+ Value: 65, // Truncated from 65.5
3195
+ Tags: map[string]string{"sensor": "1"},
3196
+ MetricType: "gauge",
3197
+ IsTable: true,
3198
+ },
3199
+ {
3200
+ Name: "temperature",
3201
+ Value: 71, // Truncated from 71.25
3202
+ Tags: map[string]string{"sensor": "2"},
3203
+ MetricType: "gauge",
3204
+ IsTable: true,
3205
+ },
3206
+ },
3207
+ },
3208
+ },
3209
+ expectedError: false,
3210
+ },
3211
+ "OpaqueFloat with unexpected type": {
3212
+ profiles: []*ddsnmp.Profile{
3213
+ {
3214
+ SourceFile: "test-profile.yaml",
3215
+ Definition: &ddprofiledefinition.ProfileDefinition{
3216
+ Metrics: []ddprofiledefinition.MetricsConfig{
3217
+ {
3218
+ Symbol: ddprofiledefinition.SymbolConfig{
3219
+ OID: "1.3.6.1.4.1.6574.4.2.12.1.0",
3220
+ Name: "temperature",
3221
+ },
3222
+ },
3223
+ },
3224
+ },
3225
+ },
3226
+ },
3227
+ setupMock: func(m *snmpmock.MockHandler) {
3228
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3229
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.2.12.1.0"}).Return(
3230
+ &gosnmp.SnmpPacket{
3231
+ Variables: []gosnmp.SnmpPDU{
3232
+ {
3233
+ Name: "1.3.6.1.4.1.6574.4.2.12.1.0",
3234
+ Type: gosnmp.OpaqueFloat,
3235
+ Value: "29.5", // Wrong type - should be float32
3236
+ },
3237
+ },
3238
+ }, nil,
3239
+ )
3240
+ },
3241
+ expectedResult: nil,
3242
+ expectedError: true,
3243
+ errorContains: "OpaqueFloat has unexpected type",
3244
+ },
3245
+ "OpaqueFloat with negative value": {
3246
+ profiles: []*ddsnmp.Profile{
3247
+ {
3248
+ SourceFile: "test-profile.yaml",
3249
+ Definition: &ddprofiledefinition.ProfileDefinition{
3250
+ Metrics: []ddprofiledefinition.MetricsConfig{
3251
+ {
3252
+ Symbol: ddprofiledefinition.SymbolConfig{
3253
+ OID: "1.3.6.1.4.1.6574.4.2.12.1.0",
3254
+ Name: "temperature",
3255
+ },
3256
+ },
3257
+ },
3258
+ },
3259
+ },
3260
+ },
3261
+ setupMock: func(m *snmpmock.MockHandler) {
3262
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3263
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.2.12.1.0"}).Return(
3264
+ &gosnmp.SnmpPacket{
3265
+ Variables: []gosnmp.SnmpPDU{
3266
+ {
3267
+ Name: "1.3.6.1.4.1.6574.4.2.12.1.0",
3268
+ Type: gosnmp.OpaqueFloat,
3269
+ Value: float32(-15.5),
3270
+ },
3271
+ },
3272
+ }, nil,
3273
+ )
3274
+ },
3275
+ expectedResult: []*ProfileMetrics{
3276
+ {
3277
+ Source: "test-profile.yaml",
3278
+ DeviceMetadata: nil,
3279
+ Metrics: []Metric{
3280
+ {
3281
+ Name: "temperature",
3282
+ Value: -15, // Truncated from -15.5
3283
+ MetricType: "gauge",
3284
+ },
3285
+ },
3286
+ },
3287
+ },
3288
+ expectedError: false,
3289
+ },
3290
+ "OpaqueFloat with very large value": {
3291
+ profiles: []*ddsnmp.Profile{
3292
+ {
3293
+ SourceFile: "test-profile.yaml",
3294
+ Definition: &ddprofiledefinition.ProfileDefinition{
3295
+ Metrics: []ddprofiledefinition.MetricsConfig{
3296
+ {
3297
+ Symbol: ddprofiledefinition.SymbolConfig{
3298
+ OID: "1.3.6.1.4.1.6574.4.4.2.2.0",
3299
+ Name: "power",
3300
+ },
3301
+ },
3302
+ },
3303
+ },
3304
+ },
3305
+ },
3306
+ setupMock: func(m *snmpmock.MockHandler) {
3307
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3308
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.4.2.2.0"}).Return(
3309
+ &gosnmp.SnmpPacket{
3310
+ Variables: []gosnmp.SnmpPDU{
3311
+ {
3312
+ Name: "1.3.6.1.4.1.6574.4.4.2.2.0",
3313
+ Type: gosnmp.OpaqueFloat,
3314
+ Value: float32(1234567.89),
3315
+ },
3316
+ },
3317
+ }, nil,
3318
+ )
3319
+ },
3320
+ expectedResult: []*ProfileMetrics{
3321
+ {
3322
+ Source: "test-profile.yaml",
3323
+ DeviceMetadata: nil,
3324
+ Metrics: []Metric{
3325
+ {
3326
+ Name: "power",
3327
+ Value: 1234567, // Truncated from 1234567.89
3328
+ MetricType: "gauge",
3329
+ },
3330
+ },
3331
+ },
3332
+ },
3333
+ expectedError: false,
3334
+ },
3335
}
3336
3337
for name, tc := range tests {
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/utils.go
+3
-1
@@ -201,7 +201,9 @@ func isPduNumericType(pdu gosnmp.SnmpPDU) bool {
201
gosnmp.Integer,
202
gosnmp.Gauge32,
203
gosnmp.Uinteger32,
204
- gosnmp.TimeTicks:
204
+ gosnmp.TimeTicks,
205
+ gosnmp.OpaqueFloat,
206
+ gosnmp.OpaqueDouble:
207
return true
208
default:
209
return false