@cryptotaxi247 / netdata / commits / 055d6a3f9

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