@cryptotaxi247 / netdata / commits / 2ef4b8752

test(go.d/ddsnmp): add more tests for table metrics (#20510)

Ilya Mashchenko committed Jun 18, 2025 at 09:26 UTC 2ef4b8752c6159ddf4076e1de5cbf88fd5a06c53
1 file changed +3375 -2
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
+3375 -2
@@ -20,7 +20,7 @@ import (
20 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
21 )
22
23 -func TestCollector_Collect_Group1_ScalarMetrics(t *testing.T) {
23 +func TestCollector_Collect_ScalarMetrics(t *testing.T) {
24 tests := map[string]struct {
25 profiles []*ddsnmp.Profile
26 setupMock func(m *snmpmock.MockHandler)
@@ -595,7 +595,7 @@ func TestCollector_Collect_Group1_ScalarMetrics(t *testing.T) {
595 }
596 }
597
598 -func TestCollector_Collect_Group2_ValueMappings(t *testing.T) {
598 +func TestCollector_Collect_ValueMappings(t *testing.T) {
599 tests := map[string]struct {
600 profiles []*ddsnmp.Profile
601 setupMock func(m *snmpmock.MockHandler)
@@ -1110,6 +1110,3379 @@ func TestCollector_Collect_Group2_ValueMappings(t *testing.T) {
1110 }
1111 }
1112
1113 +func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1114 + tests := map[string]struct {
1115 + profiles []*ddsnmp.Profile
1116 + setupMock func(m *snmpmock.MockHandler)
1117 + expectedResult []*ProfileMetrics
1118 + expectedError bool
1119 + errorContains string
1120 + enableCache bool
1121 + }{
1122 + "basic table with single metric": {
1123 + profiles: []*ddsnmp.Profile{
1124 + {
1125 + SourceFile: "test-profile.yaml",
1126 + Definition: &ddprofiledefinition.ProfileDefinition{
1127 + Metrics: []ddprofiledefinition.MetricsConfig{
1128 + {
1129 + Table: ddprofiledefinition.SymbolConfig{
1130 + OID: "1.3.6.1.2.1.2.2",
1131 + Name: "ifTable",
1132 + },
1133 + Symbols: []ddprofiledefinition.SymbolConfig{
1134 + {
1135 + OID: "1.3.6.1.2.1.2.2.1.10",
1136 + Name: "ifInOctets",
1137 + },
1138 + },
1139 + MetricTags: []ddprofiledefinition.MetricTagConfig{
1140 + {
1141 + Tag: "interface",
1142 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1143 + OID: "1.3.6.1.2.1.2.2.1.2",
1144 + Name: "ifDescr",
1145 + },
1146 + },
1147 + },
1148 + },
1149 + },
1150 + },
1151 + },
1152 + },
1153 + setupMock: func(m *snmpmock.MockHandler) {
1154 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1155 + m.EXPECT().Version().Return(gosnmp.Version2c)
1156 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1157 + []gosnmp.SnmpPDU{
1158 + {
1159 + Name: "1.3.6.1.2.1.2.2.1.10.1",
1160 + Type: gosnmp.Counter32,
1161 + Value: uint(1000),
1162 + },
1163 + {
1164 + Name: "1.3.6.1.2.1.2.2.1.10.2",
1165 + Type: gosnmp.Counter32,
1166 + Value: uint(2000),
1167 + },
1168 + {
1169 + Name: "1.3.6.1.2.1.2.2.1.2.1",
1170 + Type: gosnmp.OctetString,
1171 + Value: []byte("eth0"),
1172 + },
1173 + {
1174 + Name: "1.3.6.1.2.1.2.2.1.2.2",
1175 + Type: gosnmp.OctetString,
1176 + Value: []byte("eth1"),
1177 + },
1178 + }, nil,
1179 + )
1180 + },
1181 + expectedResult: []*ProfileMetrics{
1182 + {
1183 + Source: "test-profile.yaml",
1184 + DeviceMetadata: nil,
1185 + Metrics: []Metric{
1186 + {
1187 + Name: "ifInOctets",
1188 + Value: 1000,
1189 + Tags: map[string]string{"interface": "eth0"},
1190 + MetricType: "rate",
1191 + IsTable: true,
1192 + },
1193 + {
1194 + Name: "ifInOctets",
1195 + Value: 2000,
1196 + Tags: map[string]string{"interface": "eth1"},
1197 + MetricType: "rate",
1198 + IsTable: true,
1199 + },
1200 + },
1201 + },
1202 + },
1203 + expectedError: false,
1204 + },
1205 + "table with multiple metrics": {
1206 + profiles: []*ddsnmp.Profile{
1207 + {
1208 + SourceFile: "test-profile.yaml",
1209 + Definition: &ddprofiledefinition.ProfileDefinition{
1210 + Metrics: []ddprofiledefinition.MetricsConfig{
1211 + {
1212 + Table: ddprofiledefinition.SymbolConfig{
1213 + OID: "1.3.6.1.2.1.2.2",
1214 + Name: "ifTable",
1215 + },
1216 + Symbols: []ddprofiledefinition.SymbolConfig{
1217 + {
1218 + OID: "1.3.6.1.2.1.2.2.1.10",
1219 + Name: "ifInOctets",
1220 + },
1221 + {
1222 + OID: "1.3.6.1.2.1.2.2.1.16",
1223 + Name: "ifOutOctets",
1224 + },
1225 + {
1226 + OID: "1.3.6.1.2.1.2.2.1.8",
1227 + Name: "ifOperStatus",
1228 + },
1229 + },
1230 + MetricTags: []ddprofiledefinition.MetricTagConfig{
1231 + {
1232 + Tag: "interface",
1233 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1234 + OID: "1.3.6.1.2.1.2.2.1.2",
1235 + Name: "ifDescr",
1236 + },
1237 + },
1238 + },
1239 + },
1240 + },
1241 + },
1242 + },
1243 + },
1244 + setupMock: func(m *snmpmock.MockHandler) {
1245 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1246 + m.EXPECT().Version().Return(gosnmp.Version2c)
1247 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1248 + []gosnmp.SnmpPDU{
1249 + // Row 1
1250 + {
1251 + Name: "1.3.6.1.2.1.2.2.1.10.1",
1252 + Type: gosnmp.Counter32,
1253 + Value: uint(1000),
1254 + },
1255 + {
1256 + Name: "1.3.6.1.2.1.2.2.1.16.1",
1257 + Type: gosnmp.Counter32,
1258 + Value: uint(500),
1259 + },
1260 + {
1261 + Name: "1.3.6.1.2.1.2.2.1.8.1",
1262 + Type: gosnmp.Integer,
1263 + Value: 1, // up
1264 + },
1265 + {
1266 + Name: "1.3.6.1.2.1.2.2.1.2.1",
1267 + Type: gosnmp.OctetString,
1268 + Value: []byte("eth0"),
1269 + },
1270 + // Row 2
1271 + {
1272 + Name: "1.3.6.1.2.1.2.2.1.10.2",
1273 + Type: gosnmp.Counter32,
1274 + Value: uint(2000),
1275 + },
1276 + {
1277 + Name: "1.3.6.1.2.1.2.2.1.16.2",
1278 + Type: gosnmp.Counter32,
1279 + Value: uint(1500),
1280 + },
1281 + {
1282 + Name: "1.3.6.1.2.1.2.2.1.8.2",
1283 + Type: gosnmp.Integer,
1284 + Value: 2, // down
1285 + },
1286 + {
1287 + Name: "1.3.6.1.2.1.2.2.1.2.2",
1288 + Type: gosnmp.OctetString,
1289 + Value: []byte("eth1"),
1290 + },
1291 + }, nil,
1292 + )
1293 + },
1294 + expectedResult: []*ProfileMetrics{
1295 + {
1296 + Source: "test-profile.yaml",
1297 + DeviceMetadata: nil,
1298 + Metrics: []Metric{
1299 + // Row 1
1300 + {
1301 + Name: "ifInOctets",
1302 + Value: 1000,
1303 + Tags: map[string]string{"interface": "eth0"},
1304 + MetricType: "rate",
1305 + IsTable: true,
1306 + },
1307 + {
1308 + Name: "ifOutOctets",
1309 + Value: 500,
1310 + Tags: map[string]string{"interface": "eth0"},
1311 + MetricType: "rate",
1312 + IsTable: true,
1313 + },
1314 + {
1315 + Name: "ifOperStatus",
1316 + Value: 1,
1317 + Tags: map[string]string{"interface": "eth0"},
1318 + MetricType: "gauge",
1319 + IsTable: true,
1320 + },
1321 + // Row 2
1322 + {
1323 + Name: "ifInOctets",
1324 + Value: 2000,
1325 + Tags: map[string]string{"interface": "eth1"},
1326 + MetricType: "rate",
1327 + IsTable: true,
1328 + },
1329 + {
1330 + Name: "ifOutOctets",
1331 + Value: 1500,
1332 + Tags: map[string]string{"interface": "eth1"},
1333 + MetricType: "rate",
1334 + IsTable: true,
1335 + },
1336 + {
1337 + Name: "ifOperStatus",
1338 + Value: 2,
1339 + Tags: map[string]string{"interface": "eth1"},
1340 + MetricType: "gauge",
1341 + IsTable: true,
1342 + },
1343 + },
1344 + },
1345 + },
1346 + expectedError: false,
1347 + },
1348 + "table with tag mapping": {
1349 + profiles: []*ddsnmp.Profile{
1350 + {
1351 + SourceFile: "test-profile.yaml",
1352 + Definition: &ddprofiledefinition.ProfileDefinition{
1353 + Metrics: []ddprofiledefinition.MetricsConfig{
1354 + {
1355 + Table: ddprofiledefinition.SymbolConfig{
1356 + OID: "1.3.6.1.2.1.2.2",
1357 + Name: "ifTable",
1358 + },
1359 + Symbols: []ddprofiledefinition.SymbolConfig{
1360 + {
1361 + OID: "1.3.6.1.2.1.2.2.1.14",
1362 + Name: "ifInErrors",
1363 + },
1364 + },
1365 + MetricTags: []ddprofiledefinition.MetricTagConfig{
1366 + {
1367 + Tag: "interface",
1368 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1369 + OID: "1.3.6.1.2.1.2.2.1.2",
1370 + Name: "ifDescr",
1371 + },
1372 + },
1373 + {
1374 + Tag: "if_type",
1375 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1376 + OID: "1.3.6.1.2.1.2.2.1.3",
1377 + Name: "ifType",
1378 + },
1379 + Mapping: map[string]string{
1380 + "1": "other",
1381 + "6": "ethernetCsmacd",
1382 + "24": "softwareLoopback",
1383 + },
1384 + },
1385 + },
1386 + },
1387 + },
1388 + },
1389 + },
1390 + },
1391 + setupMock: func(m *snmpmock.MockHandler) {
1392 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1393 + m.EXPECT().Version().Return(gosnmp.Version2c)
1394 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1395 + []gosnmp.SnmpPDU{
1396 + {
1397 + Name: "1.3.6.1.2.1.2.2.1.14.1",
1398 + Type: gosnmp.Counter32,
1399 + Value: uint(10),
1400 + },
1401 + {
1402 + Name: "1.3.6.1.2.1.2.2.1.2.1",
1403 + Type: gosnmp.OctetString,
1404 + Value: []byte("lo0"),
1405 + },
1406 + {
1407 + Name: "1.3.6.1.2.1.2.2.1.3.1",
1408 + Type: gosnmp.Integer,
1409 + Value: 24, // softwareLoopback
1410 + },
1411 + {
1412 + Name: "1.3.6.1.2.1.2.2.1.14.2",
1413 + Type: gosnmp.Counter32,
1414 + Value: uint(5),
1415 + },
1416 + {
1417 + Name: "1.3.6.1.2.1.2.2.1.2.2",
1418 + Type: gosnmp.OctetString,
1419 + Value: []byte("eth0"),
1420 + },
1421 + {
1422 + Name: "1.3.6.1.2.1.2.2.1.3.2",
1423 + Type: gosnmp.Integer,
1424 + Value: 6, // ethernetCsmacd
1425 + },
1426 + }, nil,
1427 + )
1428 + },
1429 + expectedResult: []*ProfileMetrics{
1430 + {
1431 + Source: "test-profile.yaml",
1432 + DeviceMetadata: nil,
1433 + Metrics: []Metric{
1434 + {
1435 + Name: "ifInErrors",
1436 + Value: 10,
1437 + Tags: map[string]string{"interface": "lo0", "if_type": "softwareLoopback"},
1438 + MetricType: "rate",
1439 + IsTable: true,
1440 + },
1441 + {
1442 + Name: "ifInErrors",
1443 + Value: 5,
1444 + Tags: map[string]string{"interface": "eth0", "if_type": "ethernetCsmacd"},
1445 + MetricType: "rate",
1446 + IsTable: true,
1447 + },
1448 + },
1449 + },
1450 + },
1451 + expectedError: false,
1452 + },
1453 + "table with missing rows": {
1454 + profiles: []*ddsnmp.Profile{
1455 + {
1456 + SourceFile: "test-profile.yaml",
1457 + Definition: &ddprofiledefinition.ProfileDefinition{
1458 + Metrics: []ddprofiledefinition.MetricsConfig{
1459 + {
1460 + Table: ddprofiledefinition.SymbolConfig{
1461 + OID: "1.3.6.1.2.1.2.2",
1462 + Name: "ifTable",
1463 + },
1464 + Symbols: []ddprofiledefinition.SymbolConfig{
1465 + {
1466 + OID: "1.3.6.1.2.1.2.2.1.10",
1467 + Name: "ifInOctets",
1468 + },
1469 + {
1470 + OID: "1.3.6.1.2.1.2.2.1.16",
1471 + Name: "ifOutOctets",
1472 + },
1473 + },
1474 + MetricTags: []ddprofiledefinition.MetricTagConfig{
1475 + {
1476 + Tag: "interface",
1477 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1478 + OID: "1.3.6.1.2.1.2.2.1.2",
1479 + Name: "ifDescr",
1480 + },
1481 + },
1482 + },
1483 + },
1484 + },
1485 + },
1486 + },
1487 + },
1488 + setupMock: func(m *snmpmock.MockHandler) {
1489 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1490 + m.EXPECT().Version().Return(gosnmp.Version2c)
1491 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1492 + []gosnmp.SnmpPDU{
1493 + // Row 1 - complete
1494 + {
1495 + Name: "1.3.6.1.2.1.2.2.1.10.1",
1496 + Type: gosnmp.Counter32,
1497 + Value: uint(1000),
1498 + },
1499 + {
1500 + Name: "1.3.6.1.2.1.2.2.1.16.1",
1501 + Type: gosnmp.Counter32,
1502 + Value: uint(500),
1503 + },
1504 + {
1505 + Name: "1.3.6.1.2.1.2.2.1.2.1",
1506 + Type: gosnmp.OctetString,
1507 + Value: []byte("eth0"),
1508 + },
1509 + // Row 2 - missing ifOutOctets
1510 + {
1511 + Name: "1.3.6.1.2.1.2.2.1.10.2",
1512 + Type: gosnmp.Counter32,
1513 + Value: uint(2000),
1514 + },
1515 + {
1516 + Name: "1.3.6.1.2.1.2.2.1.2.2",
1517 + Type: gosnmp.OctetString,
1518 + Value: []byte("eth1"),
1519 + },
1520 + // Row 3 - missing tag (ifDescr)
1521 + {
1522 + Name: "1.3.6.1.2.1.2.2.1.10.3",
1523 + Type: gosnmp.Counter32,
1524 + Value: uint(3000),
1525 + },
1526 + {
1527 + Name: "1.3.6.1.2.1.2.2.1.16.3",
1528 + Type: gosnmp.Counter32,
1529 + Value: uint(1500),
1530 + },
1531 + }, nil,
1532 + )
1533 + },
1534 + expectedResult: []*ProfileMetrics{
1535 + {
1536 + Source: "test-profile.yaml",
1537 + DeviceMetadata: nil,
1538 + Metrics: []Metric{
1539 + // Row 1 - complete
1540 + {
1541 + Name: "ifInOctets",
1542 + Value: 1000,
1543 + Tags: map[string]string{"interface": "eth0"},
1544 + MetricType: "rate",
1545 + IsTable: true,
1546 + },
1547 + {
1548 + Name: "ifOutOctets",
1549 + Value: 500,
1550 + Tags: map[string]string{"interface": "eth0"},
1551 + MetricType: "rate",
1552 + IsTable: true,
1553 + },
1554 + // Row 2 - only ifInOctets
1555 + {
1556 + Name: "ifInOctets",
1557 + Value: 2000,
1558 + Tags: map[string]string{"interface": "eth1"},
1559 + MetricType: "rate",
1560 + IsTable: true,
1561 + },
1562 + // Row 3 - both metrics but no tag
1563 + {
1564 + Name: "ifInOctets",
1565 + Value: 3000,
1566 + Tags: nil,
1567 + MetricType: "rate",
1568 + IsTable: true,
1569 + },
1570 + {
1571 + Name: "ifOutOctets",
1572 + Value: 1500,
1573 + Tags: nil,
1574 + MetricType: "rate",
1575 + IsTable: true,
1576 + },
1577 + },
1578 + },
1579 + },
1580 + expectedError: false,
1581 + },
1582 + "table with static tags": {
1583 + profiles: []*ddsnmp.Profile{
1584 + {
1585 + SourceFile: "test-profile.yaml",
1586 + Definition: &ddprofiledefinition.ProfileDefinition{
1587 + Metrics: []ddprofiledefinition.MetricsConfig{
1588 + {
1589 + Table: ddprofiledefinition.SymbolConfig{
1590 + OID: "1.3.6.1.2.1.2.2",
1591 + Name: "ifTable",
1592 + },
1593 + Symbols: []ddprofiledefinition.SymbolConfig{
1594 + {
1595 + OID: "1.3.6.1.2.1.2.2.1.10",
1596 + Name: "ifInOctets",
1597 + },
1598 + },
1599 + StaticTags: []string{
1600 + "source:interface",
1601 + "table:if",
1602 + },
1603 + MetricTags: []ddprofiledefinition.MetricTagConfig{
1604 + {
1605 + Tag: "interface",
1606 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1607 + OID: "1.3.6.1.2.1.2.2.1.2",
1608 + Name: "ifDescr",
1609 + },
1610 + },
1611 + },
1612 + },
1613 + },
1614 + },
1615 + },
1616 + },
1617 + setupMock: func(m *snmpmock.MockHandler) {
1618 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1619 + m.EXPECT().Version().Return(gosnmp.Version2c)
1620 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1621 + []gosnmp.SnmpPDU{
1622 + {
1623 + Name: "1.3.6.1.2.1.2.2.1.10.1",
1624 + Type: gosnmp.Counter32,
1625 + Value: uint(1000),
1626 + },
1627 + {
1628 + Name: "1.3.6.1.2.1.2.2.1.2.1",
1629 + Type: gosnmp.OctetString,
1630 + Value: []byte("eth0"),
1631 + },
1632 + }, nil,
1633 + )
1634 + },
1635 + expectedResult: []*ProfileMetrics{
1636 + {
1637 + Source: "test-profile.yaml",
1638 + DeviceMetadata: nil,
1639 + Metrics: []Metric{
1640 + {
1641 + Name: "ifInOctets",
1642 + Value: 1000,
1643 + StaticTags: map[string]string{
1644 + "source": "interface",
1645 + "table": "if",
1646 + },
1647 + Tags: map[string]string{"interface": "eth0"},
1648 + MetricType: "rate",
1649 + IsTable: true,
1650 + },
1651 + },
1652 + },
1653 + },
1654 + expectedError: false,
1655 + },
1656 + "table with extract_value in tag": {
1657 + profiles: []*ddsnmp.Profile{
1658 + {
1659 + SourceFile: "test-profile.yaml",
1660 + Definition: &ddprofiledefinition.ProfileDefinition{
1661 + Metrics: []ddprofiledefinition.MetricsConfig{
1662 + {
1663 + Table: ddprofiledefinition.SymbolConfig{
1664 + OID: "1.3.6.1.2.1.2.2",
1665 + Name: "ifTable",
1666 + },
1667 + Symbols: []ddprofiledefinition.SymbolConfig{
1668 + {
1669 + OID: "1.3.6.1.2.1.2.2.1.14",
1670 + Name: "ifInErrors",
1671 + },
1672 + },
1673 + MetricTags: []ddprofiledefinition.MetricTagConfig{
1674 + {
1675 + Tag: "interface",
1676 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1677 + OID: "1.3.6.1.2.1.2.2.1.2",
1678 + Name: "ifDescr",
1679 + ExtractValueCompiled: mustCompileRegex(`^(\S+)`), // Extract first word
1680 + },
1681 + },
1682 + },
1683 + },
1684 + },
1685 + },
1686 + },
1687 + },
1688 + setupMock: func(m *snmpmock.MockHandler) {
1689 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1690 + m.EXPECT().Version().Return(gosnmp.Version2c)
1691 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1692 + []gosnmp.SnmpPDU{
1693 + {
1694 + Name: "1.3.6.1.2.1.2.2.1.14.1",
1695 + Type: gosnmp.Counter32,
1696 + Value: uint(10),
1697 + },
1698 + {
1699 + Name: "1.3.6.1.2.1.2.2.1.2.1",
1700 + Type: gosnmp.OctetString,
1701 + Value: []byte("eth0 - Primary Network Interface"),
1702 + },
1703 + {
1704 + Name: "1.3.6.1.2.1.2.2.1.14.2",
1705 + Type: gosnmp.Counter32,
1706 + Value: uint(5),
1707 + },
1708 + {
1709 + Name: "1.3.6.1.2.1.2.2.1.2.2",
1710 + Type: gosnmp.OctetString,
1711 + Value: []byte("lo0 - Loopback"),
1712 + },
1713 + }, nil,
1714 + )
1715 + },
1716 + expectedResult: []*ProfileMetrics{
1717 + {
1718 + Source: "test-profile.yaml",
1719 + DeviceMetadata: nil,
1720 + Metrics: []Metric{
1721 + {
1722 + Name: "ifInErrors",
1723 + Value: 10,
1724 + Tags: map[string]string{"interface": "eth0"},
1725 + MetricType: "rate",
1726 + IsTable: true,
1727 + },
1728 + {
1729 + Name: "ifInErrors",
1730 + Value: 5,
1731 + Tags: map[string]string{"interface": "lo0"},
1732 + MetricType: "rate",
1733 + IsTable: true,
1734 + },
1735 + },
1736 + },
1737 + },
1738 + expectedError: false,
1739 + },
1740 + "table with multiple tag columns": {
1741 + profiles: []*ddsnmp.Profile{
1742 + {
1743 + SourceFile: "test-profile.yaml",
1744 + Definition: &ddprofiledefinition.ProfileDefinition{
1745 + Metrics: []ddprofiledefinition.MetricsConfig{
1746 + {
1747 + Table: ddprofiledefinition.SymbolConfig{
1748 + OID: "1.3.6.1.2.1.2.2",
1749 + Name: "ifTable",
1750 + },
1751 + Symbols: []ddprofiledefinition.SymbolConfig{
1752 + {
1753 + OID: "1.3.6.1.2.1.2.2.1.10",
1754 + Name: "ifInOctets",
1755 + },
1756 + },
1757 + MetricTags: []ddprofiledefinition.MetricTagConfig{
1758 + {
1759 + Tag: "interface",
1760 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1761 + OID: "1.3.6.1.2.1.2.2.1.2",
1762 + Name: "ifDescr",
1763 + },
1764 + },
1765 + {
1766 + Tag: "if_type",
1767 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1768 + OID: "1.3.6.1.2.1.2.2.1.3",
1769 + Name: "ifType",
1770 + },
1771 + },
1772 + {
1773 + Tag: "admin_status",
1774 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1775 + OID: "1.3.6.1.2.1.2.2.1.7",
1776 + Name: "ifAdminStatus",
1777 + },
1778 + Mapping: map[string]string{
1779 + "1": "up",
1780 + "2": "down",
1781 + "3": "testing",
1782 + },
1783 + },
1784 + },
1785 + },
1786 + },
1787 + },
1788 + },
1789 + },
1790 + setupMock: func(m *snmpmock.MockHandler) {
1791 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1792 + m.EXPECT().Version().Return(gosnmp.Version2c)
1793 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1794 + []gosnmp.SnmpPDU{
1795 + {
1796 + Name: "1.3.6.1.2.1.2.2.1.10.1",
1797 + Type: gosnmp.Counter32,
1798 + Value: uint(1000),
1799 + },
1800 + {
1801 + Name: "1.3.6.1.2.1.2.2.1.2.1",
1802 + Type: gosnmp.OctetString,
1803 + Value: []byte("eth0"),
1804 + },
1805 + {
1806 + Name: "1.3.6.1.2.1.2.2.1.3.1",
1807 + Type: gosnmp.Integer,
1808 + Value: 6,
1809 + },
1810 + {
1811 + Name: "1.3.6.1.2.1.2.2.1.7.1",
1812 + Type: gosnmp.Integer,
1813 + Value: 1, // up
1814 + },
1815 + }, nil,
1816 + )
1817 + },
1818 + expectedResult: []*ProfileMetrics{
1819 + {
1820 + Source: "test-profile.yaml",
1821 + DeviceMetadata: nil,
1822 + Metrics: []Metric{
1823 + {
1824 + Name: "ifInOctets",
1825 + Value: 1000,
1826 + Tags: map[string]string{
1827 + "interface": "eth0",
1828 + "if_type": "6",
1829 + "admin_status": "up",
1830 + },
1831 + MetricType: "rate",
1832 + IsTable: true,
1833 + },
1834 + },
1835 + },
1836 + },
1837 + expectedError: false,
1838 + },
1839 + "empty table": {
1840 + profiles: []*ddsnmp.Profile{
1841 + {
1842 + SourceFile: "test-profile.yaml",
1843 + Definition: &ddprofiledefinition.ProfileDefinition{
1844 + Metrics: []ddprofiledefinition.MetricsConfig{
1845 + {
1846 + Table: ddprofiledefinition.SymbolConfig{
1847 + OID: "1.3.6.1.2.1.2.2",
1848 + Name: "ifTable",
1849 + },
1850 + Symbols: []ddprofiledefinition.SymbolConfig{
1851 + {
1852 + OID: "1.3.6.1.2.1.2.2.1.10",
1853 + Name: "ifInOctets",
1854 + },
1855 + },
1856 + MetricTags: []ddprofiledefinition.MetricTagConfig{
1857 + {
1858 + Tag: "interface",
1859 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1860 + OID: "1.3.6.1.2.1.2.2.1.2",
1861 + Name: "ifDescr",
1862 + },
1863 + },
1864 + },
1865 + },
1866 + },
1867 + },
1868 + },
1869 + },
1870 + setupMock: func(m *snmpmock.MockHandler) {
1871 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1872 + m.EXPECT().Version().Return(gosnmp.Version2c)
1873 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1874 + []gosnmp.SnmpPDU{}, nil,
1875 + )
1876 + },
1877 + expectedResult: []*ProfileMetrics{
1878 + {
1879 + Source: "test-profile.yaml",
1880 + DeviceMetadata: nil,
1881 + Metrics: []Metric{},
1882 + },
1883 + },
1884 + expectedError: false,
1885 + },
1886 + "table walk error": {
1887 + profiles: []*ddsnmp.Profile{
1888 + {
1889 + SourceFile: "test-profile.yaml",
1890 + Definition: &ddprofiledefinition.ProfileDefinition{
1891 + Metrics: []ddprofiledefinition.MetricsConfig{
1892 + {
1893 + Table: ddprofiledefinition.SymbolConfig{
1894 + OID: "1.3.6.1.2.1.2.2",
1895 + Name: "ifTable",
1896 + },
1897 + Symbols: []ddprofiledefinition.SymbolConfig{
1898 + {
1899 + OID: "1.3.6.1.2.1.2.2.1.10",
1900 + Name: "ifInOctets",
1901 + },
1902 + },
1903 + },
1904 + },
1905 + },
1906 + },
1907 + },
1908 + setupMock: func(m *snmpmock.MockHandler) {
1909 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1910 + m.EXPECT().Version().Return(gosnmp.Version2c)
1911 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1912 + nil,
1913 + errors.New("timeout walking table"),
1914 + )
1915 + },
1916 + expectedResult: nil,
1917 + expectedError: true,
1918 + errorContains: "timeout walking table",
1919 + },
1920 + "table with scale factor": {
1921 + profiles: []*ddsnmp.Profile{
1922 + {
1923 + SourceFile: "test-profile.yaml",
1924 + Definition: &ddprofiledefinition.ProfileDefinition{
1925 + Metrics: []ddprofiledefinition.MetricsConfig{
1926 + {
1927 + Table: ddprofiledefinition.SymbolConfig{
1928 + OID: "1.3.6.1.4.1.9.9.109.1.1.1",
1929 + Name: "cpmCPUTotalTable",
1930 + },
1931 + Symbols: []ddprofiledefinition.SymbolConfig{
1932 + {
1933 + OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.12",
1934 + Name: "cpmCPUMemoryUsed",
1935 + ScaleFactor: 1024, // Convert KB to bytes
1936 + },
1937 + },
1938 + MetricTags: []ddprofiledefinition.MetricTagConfig{
1939 + {
1940 + Tag: "cpu_id",
1941 + Symbol: ddprofiledefinition.SymbolConfigCompat{
1942 + OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.2",
1943 + Name: "cpmCPUTotalPhysicalIndex",
1944 + },
1945 + },
1946 + },
1947 + },
1948 + },
1949 + },
1950 + },
1951 + },
1952 + setupMock: func(m *snmpmock.MockHandler) {
1953 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1954 + m.EXPECT().Version().Return(gosnmp.Version2c)
1955 + m.EXPECT().BulkWalkAll("1.3.6.1.4.1.9.9.109.1.1.1").Return(
1956 + []gosnmp.SnmpPDU{
1957 + {
1958 + Name: "1.3.6.1.4.1.9.9.109.1.1.1.1.12.1",
1959 + Type: gosnmp.Gauge32,
1960 + Value: uint(2048), // 2048 KB
1961 + },
1962 + {
1963 + Name: "1.3.6.1.4.1.9.9.109.1.1.1.1.2.1",
1964 + Type: gosnmp.Integer,
1965 + Value: 1,
1966 + },
1967 + }, nil,
1968 + )
1969 + },
1970 + expectedResult: []*ProfileMetrics{
1971 + {
1972 + Source: "test-profile.yaml",
1973 + DeviceMetadata: nil,
1974 + Metrics: []Metric{
1975 + {
1976 + Name: "cpmCPUMemoryUsed",
1977 + Value: 2097152, // 2048 * 1024
1978 + Tags: map[string]string{"cpu_id": "1"},
1979 + MetricType: "gauge",
1980 + IsTable: true,
1981 + },
1982 + },
1983 + },
1984 + },
1985 + expectedError: false,
1986 + },
1987 + "table with snmpv1 (using WalkAll instead of BulkWalkAll)": {
1988 + profiles: []*ddsnmp.Profile{
1989 + {
1990 + SourceFile: "test-profile.yaml",
1991 + Definition: &ddprofiledefinition.ProfileDefinition{
1992 + Metrics: []ddprofiledefinition.MetricsConfig{
1993 + {
1994 + Table: ddprofiledefinition.SymbolConfig{
1995 + OID: "1.3.6.1.2.1.2.2",
1996 + Name: "ifTable",
1997 + },
1998 + Symbols: []ddprofiledefinition.SymbolConfig{
1999 + {
2000 + OID: "1.3.6.1.2.1.2.2.1.10",
2001 + Name: "ifInOctets",
2002 + },
2003 + },
2004 + MetricTags: []ddprofiledefinition.MetricTagConfig{
2005 + {
2006 + Tag: "interface",
2007 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2008 + OID: "1.3.6.1.2.1.2.2.1.2",
2009 + Name: "ifDescr",
2010 + },
2011 + },
2012 + },
2013 + },
2014 + },
2015 + },
2016 + },
2017 + },
2018 + setupMock: func(m *snmpmock.MockHandler) {
2019 + m.EXPECT().MaxOids().Return(10).AnyTimes()
2020 + m.EXPECT().Version().Return(gosnmp.Version1)
2021 + m.EXPECT().WalkAll("1.3.6.1.2.1.2.2").Return(
2022 + []gosnmp.SnmpPDU{
2023 + {
2024 + Name: "1.3.6.1.2.1.2.2.1.10.1",
2025 + Type: gosnmp.Counter32,
2026 + Value: uint(1000),
2027 + },
2028 + {
2029 + Name: "1.3.6.1.2.1.2.2.1.2.1",
2030 + Type: gosnmp.OctetString,
2031 + Value: []byte("eth0"),
2032 + },
2033 + }, nil,
2034 + )
2035 + },
2036 + expectedResult: []*ProfileMetrics{
2037 + {
2038 + Source: "test-profile.yaml",
2039 + DeviceMetadata: nil,
2040 + Metrics: []Metric{
2041 + {
2042 + Name: "ifInOctets",
2043 + Value: 1000,
2044 + Tags: map[string]string{"interface": "eth0"},
2045 + MetricType: "rate",
2046 + IsTable: true,
2047 + },
2048 + },
2049 + },
2050 + },
2051 + expectedError: false,
2052 + },
2053 + }
2054 +
2055 + for name, tc := range tests {
2056 + t.Run(name, func(t *testing.T) {
2057 + ctrl := gomock.NewController(t)
2058 + defer ctrl.Finish()
2059 +
2060 + mockHandler := snmpmock.NewMockHandler(ctrl)
2061 + tc.setupMock(mockHandler)
2062 +
2063 + collector := New(mockHandler, tc.profiles, logger.New())
2064 + collector.DoTableMetrics = true
2065 +
2066 + // Configure cache based on test requirements
2067 + if tc.enableCache {
2068 + collector.tableCache.setTTL(30*time.Second, 0)
2069 + } else {
2070 + collector.tableCache.setTTL(0, 0) // Disable cache
2071 + }
2072 +
2073 + result, err := collector.Collect()
2074 +
2075 + // Clear circular references
2076 + for _, profile := range result {
2077 + for i := range profile.Metrics {
2078 + profile.Metrics[i].Profile = nil
2079 + }
2080 + }
2081 +
2082 + if tc.expectedError {
2083 + assert.Error(t, err)
2084 + if tc.errorContains != "" {
2085 + assert.Contains(t, err.Error(), tc.errorContains)
2086 + }
2087 + } else {
2088 + assert.NoError(t, err)
2089 + }
2090 +
2091 + if tc.expectedResult != nil {
2092 + require.Equal(t, len(tc.expectedResult), len(result))
2093 + for i := range tc.expectedResult {
2094 + assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
2095 + assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
2096 + assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
2097 + }
2098 + } else {
2099 + assert.Nil(t, result)
2100 + }
2101 + })
2102 + }
2103 +}
2104 +
2105 +func TestCollector_Collect_CrossTableTags(t *testing.T) {
2106 + tests := map[string]struct {
2107 + profiles []*ddsnmp.Profile
2108 + setupMock func(m *snmpmock.MockHandler)
2109 + expectedResult []*ProfileMetrics
2110 + expectedError bool
2111 + errorContains string
2112 + enableCache bool
2113 + }{
2114 + "cross-table tag from ifXTable": {
2115 + profiles: []*ddsnmp.Profile{
2116 + {
2117 + SourceFile: "test-profile.yaml",
2118 + Definition: &ddprofiledefinition.ProfileDefinition{
2119 + Metrics: []ddprofiledefinition.MetricsConfig{
2120 + {
2121 + Table: ddprofiledefinition.SymbolConfig{
2122 + OID: "1.3.6.1.2.1.2.2",
2123 + Name: "ifTable",
2124 + },
2125 + Symbols: []ddprofiledefinition.SymbolConfig{
2126 + {
2127 + OID: "1.3.6.1.2.1.2.2.1.14",
2128 + Name: "ifInErrors",
2129 + },
2130 + },
2131 + MetricTags: []ddprofiledefinition.MetricTagConfig{
2132 + {
2133 + Tag: "interface",
2134 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2135 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2136 + Name: "ifName",
2137 + },
2138 + Table: "ifXTable",
2139 + },
2140 + },
2141 + },
2142 + {
2143 + Table: ddprofiledefinition.SymbolConfig{
2144 + OID: "1.3.6.1.2.1.31.1.1",
2145 + Name: "ifXTable",
2146 + },
2147 + Symbols: []ddprofiledefinition.SymbolConfig{
2148 + {
2149 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2150 + Name: "ifName",
2151 + },
2152 + },
2153 + },
2154 + },
2155 + },
2156 + },
2157 + },
2158 + setupMock: func(m *snmpmock.MockHandler) {
2159 + m.EXPECT().MaxOids().Return(10).AnyTimes()
2160 + // First walk for ifTable
2161 + m.EXPECT().Version().Return(gosnmp.Version2c)
2162 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2163 + []gosnmp.SnmpPDU{
2164 + {
2165 + Name: "1.3.6.1.2.1.2.2.1.14.1",
2166 + Type: gosnmp.Counter32,
2167 + Value: uint(10),
2168 + },
2169 + {
2170 + Name: "1.3.6.1.2.1.2.2.1.14.2",
2171 + Type: gosnmp.Counter32,
2172 + Value: uint(20),
2173 + },
2174 + }, nil,
2175 + )
2176 + // Second walk for ifXTable
2177 + m.EXPECT().Version().Return(gosnmp.Version2c)
2178 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2179 + []gosnmp.SnmpPDU{
2180 + {
2181 + Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2182 + Type: gosnmp.OctetString,
2183 + Value: []byte("GigabitEthernet0/0"),
2184 + },
2185 + {
2186 + Name: "1.3.6.1.2.1.31.1.1.1.1.2",
2187 + Type: gosnmp.OctetString,
2188 + Value: []byte("GigabitEthernet0/1"),
2189 + },
2190 + }, nil,
2191 + )
2192 + },
2193 + expectedResult: []*ProfileMetrics{
2194 + {
2195 + Source: "test-profile.yaml",
2196 + DeviceMetadata: nil,
2197 + Metrics: []Metric{
2198 + {
2199 + Name: "ifInErrors",
2200 + Value: 10,
2201 + Tags: map[string]string{"interface": "GigabitEthernet0/0"},
2202 + MetricType: "rate",
2203 + IsTable: true,
2204 + },
2205 + {
2206 + Name: "ifInErrors",
2207 + Value: 20,
2208 + Tags: map[string]string{"interface": "GigabitEthernet0/1"},
2209 + MetricType: "rate",
2210 + IsTable: true,
2211 + },
2212 + },
2213 + },
2214 + },
2215 + expectedError: false,
2216 + },
2217 + "cross-table tag with missing values": {
2218 + profiles: []*ddsnmp.Profile{
2219 + {
2220 + SourceFile: "test-profile.yaml",
2221 + Definition: &ddprofiledefinition.ProfileDefinition{
2222 + Metrics: []ddprofiledefinition.MetricsConfig{
2223 + {
2224 + Table: ddprofiledefinition.SymbolConfig{
2225 + OID: "1.3.6.1.2.1.2.2",
2226 + Name: "ifTable",
2227 + },
2228 + Symbols: []ddprofiledefinition.SymbolConfig{
2229 + {
2230 + OID: "1.3.6.1.2.1.2.2.1.14",
2231 + Name: "ifInErrors",
2232 + },
2233 + },
2234 + MetricTags: []ddprofiledefinition.MetricTagConfig{
2235 + {
2236 + Tag: "interface",
2237 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2238 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2239 + Name: "ifName",
2240 + },
2241 + Table: "ifXTable",
2242 + },
2243 + },
2244 + },
2245 + {
2246 + Table: ddprofiledefinition.SymbolConfig{
2247 + OID: "1.3.6.1.2.1.31.1.1",
2248 + Name: "ifXTable",
2249 + },
2250 + Symbols: []ddprofiledefinition.SymbolConfig{
2251 + {
2252 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2253 + Name: "ifName",
2254 + },
2255 + },
2256 + },
2257 + },
2258 + },
2259 + },
2260 + },
2261 + setupMock: func(m *snmpmock.MockHandler) {
2262 + m.EXPECT().MaxOids().Return(10).AnyTimes()
2263 + // Walk ifTable - has 3 interfaces
2264 + m.EXPECT().Version().Return(gosnmp.Version2c)
2265 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2266 + []gosnmp.SnmpPDU{
2267 + {
2268 + Name: "1.3.6.1.2.1.2.2.1.14.1",
2269 + Type: gosnmp.Counter32,
2270 + Value: uint(10),
2271 + },
2272 + {
2273 + Name: "1.3.6.1.2.1.2.2.1.14.2",
2274 + Type: gosnmp.Counter32,
2275 + Value: uint(20),
2276 + },
2277 + {
2278 + Name: "1.3.6.1.2.1.2.2.1.14.3",
2279 + Type: gosnmp.Counter32,
2280 + Value: uint(30),
2281 + },
2282 + }, nil,
2283 + )
2284 + // Walk ifXTable - only has 2 interfaces (missing index 3)
2285 + m.EXPECT().Version().Return(gosnmp.Version2c)
2286 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2287 + []gosnmp.SnmpPDU{
2288 + {
2289 + Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2290 + Type: gosnmp.OctetString,
2291 + Value: []byte("GigabitEthernet0/0"),
2292 + },
2293 + {
2294 + Name: "1.3.6.1.2.1.31.1.1.1.1.2",
2295 + Type: gosnmp.OctetString,
2296 + Value: []byte("GigabitEthernet0/1"),
2297 + },
2298 + }, nil,
2299 + )
2300 + },
2301 + expectedResult: []*ProfileMetrics{
2302 + {
2303 + Source: "test-profile.yaml",
2304 + DeviceMetadata: nil,
2305 + Metrics: []Metric{
2306 + {
2307 + Name: "ifInErrors",
2308 + Value: 10,
2309 + Tags: map[string]string{"interface": "GigabitEthernet0/0"},
2310 + MetricType: "rate",
2311 + IsTable: true,
2312 + },
2313 + {
2314 + Name: "ifInErrors",
2315 + Value: 20,
2316 + Tags: map[string]string{"interface": "GigabitEthernet0/1"},
2317 + MetricType: "rate",
2318 + IsTable: true,
2319 + },
2320 + {
2321 + Name: "ifInErrors",
2322 + Value: 30,
2323 + Tags: nil, // No cross-table tag found
2324 + MetricType: "rate",
2325 + IsTable: true,
2326 + },
2327 + },
2328 + },
2329 + },
2330 + expectedError: false,
2331 + },
2332 + "multiple cross-table tags": {
2333 + profiles: []*ddsnmp.Profile{
2334 + {
2335 + SourceFile: "test-profile.yaml",
2336 + Definition: &ddprofiledefinition.ProfileDefinition{
2337 + Metrics: []ddprofiledefinition.MetricsConfig{
2338 + {
2339 + Table: ddprofiledefinition.SymbolConfig{
2340 + OID: "1.3.6.1.2.1.2.2",
2341 + Name: "ifTable",
2342 + },
2343 + Symbols: []ddprofiledefinition.SymbolConfig{
2344 + {
2345 + OID: "1.3.6.1.2.1.2.2.1.14",
2346 + Name: "ifInErrors",
2347 + },
2348 + },
2349 + MetricTags: []ddprofiledefinition.MetricTagConfig{
2350 + {
2351 + Tag: "interface_desc",
2352 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2353 + OID: "1.3.6.1.2.1.2.2.1.2",
2354 + Name: "ifDescr",
2355 + },
2356 + },
2357 + {
2358 + Tag: "interface_name",
2359 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2360 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2361 + Name: "ifName",
2362 + },
2363 + Table: "ifXTable",
2364 + },
2365 + {
2366 + Tag: "interface_alias",
2367 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2368 + OID: "1.3.6.1.2.1.31.1.1.1.18",
2369 + Name: "ifAlias",
2370 + },
2371 + Table: "ifXTable",
2372 + },
2373 + },
2374 + },
2375 + {
2376 + Table: ddprofiledefinition.SymbolConfig{
2377 + OID: "1.3.6.1.2.1.31.1.1",
2378 + Name: "ifXTable",
2379 + },
2380 + Symbols: []ddprofiledefinition.SymbolConfig{
2381 + {
2382 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2383 + Name: "ifName",
2384 + },
2385 + {
2386 + OID: "1.3.6.1.2.1.31.1.1.1.18",
2387 + Name: "ifAlias",
2388 + },
2389 + },
2390 + },
2391 + },
2392 + },
2393 + },
2394 + },
2395 + setupMock: func(m *snmpmock.MockHandler) {
2396 + m.EXPECT().MaxOids().Return(10).AnyTimes()
2397 + // Walk ifTable
2398 + m.EXPECT().Version().Return(gosnmp.Version2c)
2399 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2400 + []gosnmp.SnmpPDU{
2401 + {
2402 + Name: "1.3.6.1.2.1.2.2.1.14.1",
2403 + Type: gosnmp.Counter32,
2404 + Value: uint(10),
2405 + },
2406 + {
2407 + Name: "1.3.6.1.2.1.2.2.1.2.1",
2408 + Type: gosnmp.OctetString,
2409 + Value: []byte("eth0"),
2410 + },
2411 + }, nil,
2412 + )
2413 + // Walk ifXTable
2414 + m.EXPECT().Version().Return(gosnmp.Version2c)
2415 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2416 + []gosnmp.SnmpPDU{
2417 + {
2418 + Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2419 + Type: gosnmp.OctetString,
2420 + Value: []byte("GigabitEthernet0/0"),
2421 + },
2422 + {
2423 + Name: "1.3.6.1.2.1.31.1.1.1.18.1",
2424 + Type: gosnmp.OctetString,
2425 + Value: []byte("Uplink to Core"),
2426 + },
2427 + }, nil,
2428 + )
2429 + },
2430 + expectedResult: []*ProfileMetrics{
2431 + {
2432 + Source: "test-profile.yaml",
2433 + DeviceMetadata: nil,
2434 + Metrics: []Metric{
2435 + {
2436 + Name: "ifInErrors",
2437 + Value: 10,
2438 + Tags: map[string]string{
2439 + "interface_desc": "eth0",
2440 + "interface_name": "GigabitEthernet0/0",
2441 + "interface_alias": "Uplink to Core",
2442 + },
2443 + MetricType: "rate",
2444 + IsTable: true,
2445 + },
2446 + },
2447 + },
2448 + },
2449 + expectedError: false,
2450 + },
2451 + "cross-table tag with mapping": {
2452 + profiles: []*ddsnmp.Profile{
2453 + {
2454 + SourceFile: "test-profile.yaml",
2455 + Definition: &ddprofiledefinition.ProfileDefinition{
2456 + Metrics: []ddprofiledefinition.MetricsConfig{
2457 + {
2458 + Table: ddprofiledefinition.SymbolConfig{
2459 + OID: "1.3.6.1.4.1.9.9.276.1.1.2",
2460 + Name: "cieIfInterfaceTable",
2461 + },
2462 + Symbols: []ddprofiledefinition.SymbolConfig{
2463 + {
2464 + OID: "1.3.6.1.4.1.9.9.276.1.1.2.1.1",
2465 + Name: "cieIfResetCount",
2466 + },
2467 + },
2468 + MetricTags: []ddprofiledefinition.MetricTagConfig{
2469 + {
2470 + Tag: "interface",
2471 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2472 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2473 + Name: "ifName",
2474 + },
2475 + Table: "ifXTable",
2476 + },
2477 + {
2478 + Tag: "oper_status",
2479 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2480 + OID: "1.3.6.1.2.1.2.2.1.8",
2481 + Name: "ifOperStatus",
2482 + },
2483 + Table: "ifTable",
2484 + Mapping: map[string]string{
2485 + "1": "up",
2486 + "2": "down",
2487 + "3": "testing",
2488 + },
2489 + },
2490 + },
2491 + },
2492 + {
2493 + Table: ddprofiledefinition.SymbolConfig{
2494 + OID: "1.3.6.1.2.1.31.1.1",
2495 + Name: "ifXTable",
2496 + },
2497 + Symbols: []ddprofiledefinition.SymbolConfig{
2498 + {
2499 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2500 + Name: "ifName",
2501 + },
2502 + },
2503 + },
2504 + {
2505 + Table: ddprofiledefinition.SymbolConfig{
2506 + OID: "1.3.6.1.2.1.2.2",
2507 + Name: "ifTable",
2508 + },
2509 + Symbols: []ddprofiledefinition.SymbolConfig{
2510 + {
2511 + OID: "1.3.6.1.2.1.2.2.1.8",
2512 + Name: "ifOperStatus",
2513 + },
2514 + },
2515 + },
2516 + },
2517 + },
2518 + },
2519 + },
2520 + setupMock: func(m *snmpmock.MockHandler) {
2521 + m.EXPECT().MaxOids().Return(10).AnyTimes()
2522 + // Walk cieIfInterfaceTable
2523 + m.EXPECT().Version().Return(gosnmp.Version2c)
2524 + m.EXPECT().BulkWalkAll("1.3.6.1.4.1.9.9.276.1.1.2").Return(
2525 + []gosnmp.SnmpPDU{
2526 + {
2527 + Name: "1.3.6.1.4.1.9.9.276.1.1.2.1.1.1",
2528 + Type: gosnmp.Counter32,
2529 + Value: uint(5),
2530 + },
2531 + {
2532 + Name: "1.3.6.1.4.1.9.9.276.1.1.2.1.1.2",
2533 + Type: gosnmp.Counter32,
2534 + Value: uint(3),
2535 + },
2536 + }, nil,
2537 + )
2538 + // Walk ifXTable
2539 + m.EXPECT().Version().Return(gosnmp.Version2c)
2540 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2541 + []gosnmp.SnmpPDU{
2542 + {
2543 + Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2544 + Type: gosnmp.OctetString,
2545 + Value: []byte("GigabitEthernet0/0"),
2546 + },
2547 + {
2548 + Name: "1.3.6.1.2.1.31.1.1.1.1.2",
2549 + Type: gosnmp.OctetString,
2550 + Value: []byte("GigabitEthernet0/1"),
2551 + },
2552 + }, nil,
2553 + )
2554 + // Walk ifTable
2555 + m.EXPECT().Version().Return(gosnmp.Version2c)
2556 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2557 + []gosnmp.SnmpPDU{
2558 + {
2559 + Name: "1.3.6.1.2.1.2.2.1.8.1",
2560 + Type: gosnmp.Integer,
2561 + Value: 1, // up
2562 + },
2563 + {
2564 + Name: "1.3.6.1.2.1.2.2.1.8.2",
2565 + Type: gosnmp.Integer,
2566 + Value: 2, // down
2567 + },
2568 + }, nil,
2569 + )
2570 + },
2571 + expectedResult: []*ProfileMetrics{
2572 + {
2573 + Source: "test-profile.yaml",
2574 + DeviceMetadata: nil,
2575 + Metrics: []Metric{
2576 + {
2577 + Name: "cieIfResetCount",
2578 + Value: 5,
2579 + Tags: map[string]string{
2580 + "interface": "GigabitEthernet0/0",
2581 + "oper_status": "up",
2582 + },
2583 + MetricType: "rate",
2584 + IsTable: true,
2585 + },
2586 + {
2587 + Name: "cieIfResetCount",
2588 + Value: 3,
2589 + Tags: map[string]string{
2590 + "interface": "GigabitEthernet0/1",
2591 + "oper_status": "down",
2592 + },
2593 + MetricType: "rate",
2594 + IsTable: true,
2595 + },
2596 + {
2597 + Name: "ifOperStatus",
2598 + Value: 1,
2599 + MetricType: "gauge",
2600 + IsTable: true,
2601 + },
2602 + {
2603 + Name: "ifOperStatus",
2604 + Value: 2,
2605 + MetricType: "gauge",
2606 + IsTable: true,
2607 + },
2608 + },
2609 + },
2610 + },
2611 + expectedError: false,
2612 + },
2613 + "cross-table tag when referenced table not walked": {
2614 + profiles: []*ddsnmp.Profile{
2615 + {
2616 + SourceFile: "test-profile.yaml",
2617 + Definition: &ddprofiledefinition.ProfileDefinition{
2618 + Metrics: []ddprofiledefinition.MetricsConfig{
2619 + {
2620 + Table: ddprofiledefinition.SymbolConfig{
2621 + OID: "1.3.6.1.2.1.2.2",
2622 + Name: "ifTable",
2623 + },
2624 + Symbols: []ddprofiledefinition.SymbolConfig{
2625 + {
2626 + OID: "1.3.6.1.2.1.2.2.1.14",
2627 + Name: "ifInErrors",
2628 + },
2629 + },
2630 + MetricTags: []ddprofiledefinition.MetricTagConfig{
2631 + {
2632 + Tag: "interface",
2633 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2634 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2635 + Name: "ifName",
2636 + },
2637 + Table: "ifXTable",
2638 + },
2639 + },
2640 + },
2641 + // Note: ifXTable is NOT in the metrics list
2642 + },
2643 + },
2644 + },
2645 + },
2646 + setupMock: func(m *snmpmock.MockHandler) {
2647 + m.EXPECT().MaxOids().Return(10).AnyTimes()
2648 + // Only walk ifTable
2649 + m.EXPECT().Version().Return(gosnmp.Version2c)
2650 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2651 + []gosnmp.SnmpPDU{
2652 + {
2653 + Name: "1.3.6.1.2.1.2.2.1.14.1",
2654 + Type: gosnmp.Counter32,
2655 + Value: uint(10),
2656 + },
2657 + {
2658 + Name: "1.3.6.1.2.1.2.2.1.14.2",
2659 + Type: gosnmp.Counter32,
2660 + Value: uint(20),
2661 + },
2662 + }, nil,
2663 + )
2664 + },
2665 + expectedResult: []*ProfileMetrics{
2666 + {
2667 + Source: "test-profile.yaml",
2668 + DeviceMetadata: nil,
2669 + Metrics: []Metric{
2670 + {
2671 + Name: "ifInErrors",
2672 + Value: 10,
2673 + Tags: nil, // No cross-table tag because ifXTable wasn't walked
2674 + MetricType: "rate",
2675 + IsTable: true,
2676 + },
2677 + {
2678 + Name: "ifInErrors",
2679 + Value: 20,
2680 + Tags: nil,
2681 + MetricType: "rate",
2682 + IsTable: true,
2683 + },
2684 + },
2685 + },
2686 + },
2687 + expectedError: false,
2688 + },
2689 + "cross-table tag with extract_value": {
2690 + profiles: []*ddsnmp.Profile{
2691 + {
2692 + SourceFile: "test-profile.yaml",
2693 + Definition: &ddprofiledefinition.ProfileDefinition{
2694 + Metrics: []ddprofiledefinition.MetricsConfig{
2695 + {
2696 + Table: ddprofiledefinition.SymbolConfig{
2697 + OID: "1.3.6.1.2.1.2.2",
2698 + Name: "ifTable",
2699 + },
2700 + Symbols: []ddprofiledefinition.SymbolConfig{
2701 + {
2702 + OID: "1.3.6.1.2.1.2.2.1.10",
2703 + Name: "ifInOctets",
2704 + },
2705 + },
2706 + MetricTags: []ddprofiledefinition.MetricTagConfig{
2707 + {
2708 + Tag: "interface",
2709 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2710 + OID: "1.3.6.1.2.1.31.1.1.1.18",
2711 + Name: "ifAlias",
2712 + ExtractValueCompiled: mustCompileRegex(`\[(\w+)\]`), // Extract value in brackets
2713 + },
2714 + Table: "ifXTable",
2715 + },
2716 + },
2717 + },
2718 + {
2719 + Table: ddprofiledefinition.SymbolConfig{
2720 + OID: "1.3.6.1.2.1.31.1.1",
2721 + Name: "ifXTable",
2722 + },
2723 + Symbols: []ddprofiledefinition.SymbolConfig{
2724 + {
2725 + OID: "1.3.6.1.2.1.31.1.1.1.18",
2726 + Name: "ifAlias",
2727 + },
2728 + },
2729 + },
2730 + },
2731 + },
2732 + },
2733 + },
2734 + setupMock: func(m *snmpmock.MockHandler) {
2735 + m.EXPECT().MaxOids().Return(10).AnyTimes()
2736 + // Walk ifTable
2737 + m.EXPECT().Version().Return(gosnmp.Version2c)
2738 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2739 + []gosnmp.SnmpPDU{
2740 + {
2741 + Name: "1.3.6.1.2.1.2.2.1.10.1",
2742 + Type: gosnmp.Counter32,
2743 + Value: uint(1000),
2744 + },
2745 + }, nil,
2746 + )
2747 + // Walk ifXTable
2748 + m.EXPECT().Version().Return(gosnmp.Version2c)
2749 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2750 + []gosnmp.SnmpPDU{
2751 + {
2752 + Name: "1.3.6.1.2.1.31.1.1.1.18.1",
2753 + Type: gosnmp.OctetString,
2754 + Value: []byte("Connection to [CORE-SW1] port 24"),
2755 + },
2756 + }, nil,
2757 + )
2758 + },
2759 + expectedResult: []*ProfileMetrics{
2760 + {
2761 + Source: "test-profile.yaml",
2762 + DeviceMetadata: nil,
2763 + Metrics: []Metric{
2764 + {
2765 + Name: "ifInOctets",
2766 + Value: 1000,
2767 + Tags: nil,
2768 + MetricType: "rate",
2769 + IsTable: true,
2770 + },
2771 + },
2772 + },
2773 + },
2774 + expectedError: false,
2775 + },
2776 + "same and cross-table tags combined": {
2777 + profiles: []*ddsnmp.Profile{
2778 + {
2779 + SourceFile: "test-profile.yaml",
2780 + Definition: &ddprofiledefinition.ProfileDefinition{
2781 + Metrics: []ddprofiledefinition.MetricsConfig{
2782 + {
2783 + Table: ddprofiledefinition.SymbolConfig{
2784 + OID: "1.3.6.1.2.1.2.2",
2785 + Name: "ifTable",
2786 + },
2787 + Symbols: []ddprofiledefinition.SymbolConfig{
2788 + {
2789 + OID: "1.3.6.1.2.1.2.2.1.14",
2790 + Name: "ifInErrors",
2791 + },
2792 + },
2793 + MetricTags: []ddprofiledefinition.MetricTagConfig{
2794 + {
2795 + Tag: "if_desc",
2796 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2797 + OID: "1.3.6.1.2.1.2.2.1.2",
2798 + Name: "ifDescr",
2799 + },
2800 + // No Table field = same table
2801 + },
2802 + {
2803 + Tag: "if_name",
2804 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2805 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2806 + Name: "ifName",
2807 + },
2808 + Table: "ifXTable",
2809 + },
2810 + {
2811 + Tag: "if_type",
2812 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2813 + OID: "1.3.6.1.2.1.2.2.1.3",
2814 + Name: "ifType",
2815 + },
2816 + // No Table field = same table
2817 + Mapping: map[string]string{
2818 + "6": "ethernet",
2819 + "24": "loopback",
2820 + },
2821 + },
2822 + },
2823 + },
2824 + {
2825 + Table: ddprofiledefinition.SymbolConfig{
2826 + OID: "1.3.6.1.2.1.31.1.1",
2827 + Name: "ifXTable",
2828 + },
2829 + Symbols: []ddprofiledefinition.SymbolConfig{
2830 + {
2831 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2832 + Name: "ifName",
2833 + },
2834 + },
2835 + },
2836 + },
2837 + },
2838 + },
2839 + },
2840 + setupMock: func(m *snmpmock.MockHandler) {
2841 + m.EXPECT().MaxOids().Return(10).AnyTimes()
2842 + // Walk ifTable
2843 + m.EXPECT().Version().Return(gosnmp.Version2c)
2844 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2845 + []gosnmp.SnmpPDU{
2846 + {
2847 + Name: "1.3.6.1.2.1.2.2.1.14.1",
2848 + Type: gosnmp.Counter32,
2849 + Value: uint(10),
2850 + },
2851 + {
2852 + Name: "1.3.6.1.2.1.2.2.1.2.1",
2853 + Type: gosnmp.OctetString,
2854 + Value: []byte("Ethernet Interface"),
2855 + },
2856 + {
2857 + Name: "1.3.6.1.2.1.2.2.1.3.1",
2858 + Type: gosnmp.Integer,
2859 + Value: 6,
2860 + },
2861 + }, nil,
2862 + )
2863 + // Walk ifXTable
2864 + m.EXPECT().Version().Return(gosnmp.Version2c)
2865 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2866 + []gosnmp.SnmpPDU{
2867 + {
2868 + Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2869 + Type: gosnmp.OctetString,
2870 + Value: []byte("GigabitEthernet0/0"),
2871 + },
2872 + }, nil,
2873 + )
2874 + },
2875 + expectedResult: []*ProfileMetrics{
2876 + {
2877 + Source: "test-profile.yaml",
2878 + DeviceMetadata: nil,
2879 + Metrics: []Metric{
2880 + {
2881 + Name: "ifInErrors",
2882 + Value: 10,
2883 + Tags: map[string]string{
2884 + "if_desc": "Ethernet Interface",
2885 + "if_name": "GigabitEthernet0/0",
2886 + "if_type": "ethernet",
2887 + },
2888 + MetricType: "rate",
2889 + IsTable: true,
2890 + },
2891 + },
2892 + },
2893 + },
2894 + expectedError: false,
2895 + },
2896 + "cross-table with caching disabled": {
2897 + profiles: []*ddsnmp.Profile{
2898 + {
2899 + SourceFile: "test-profile.yaml",
2900 + Definition: &ddprofiledefinition.ProfileDefinition{
2901 + Metrics: []ddprofiledefinition.MetricsConfig{
2902 + {
2903 + Table: ddprofiledefinition.SymbolConfig{
2904 + OID: "1.3.6.1.2.1.2.2",
2905 + Name: "ifTable",
2906 + },
2907 + Symbols: []ddprofiledefinition.SymbolConfig{
2908 + {
2909 + OID: "1.3.6.1.2.1.2.2.1.14",
2910 + Name: "ifInErrors",
2911 + },
2912 + },
2913 + MetricTags: []ddprofiledefinition.MetricTagConfig{
2914 + {
2915 + Tag: "interface",
2916 + Symbol: ddprofiledefinition.SymbolConfigCompat{
2917 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2918 + Name: "ifName",
2919 + },
2920 + Table: "ifXTable",
2921 + },
2922 + },
2923 + },
2924 + {
2925 + Table: ddprofiledefinition.SymbolConfig{
2926 + OID: "1.3.6.1.2.1.31.1.1",
2927 + Name: "ifXTable",
2928 + },
2929 + Symbols: []ddprofiledefinition.SymbolConfig{
2930 + {
2931 + OID: "1.3.6.1.2.1.31.1.1.1.1",
2932 + Name: "ifName",
2933 + },
2934 + },
2935 + },
2936 + },
2937 + },
2938 + },
2939 + },
2940 + setupMock: func(m *snmpmock.MockHandler) {
2941 + m.EXPECT().MaxOids().Return(10).AnyTimes()
2942 + // Walk ifTable
2943 + m.EXPECT().Version().Return(gosnmp.Version2c)
2944 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
2945 + []gosnmp.SnmpPDU{
2946 + {
2947 + Name: "1.3.6.1.2.1.2.2.1.14.1",
2948 + Type: gosnmp.Counter32,
2949 + Value: uint(10),
2950 + },
2951 + }, nil,
2952 + )
2953 + // Walk ifXTable
2954 + m.EXPECT().Version().Return(gosnmp.Version2c)
2955 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
2956 + []gosnmp.SnmpPDU{
2957 + {
2958 + Name: "1.3.6.1.2.1.31.1.1.1.1.1",
2959 + Type: gosnmp.OctetString,
2960 + Value: []byte("GigabitEthernet0/0"),
2961 + },
2962 + }, nil,
2963 + )
2964 + },
2965 + expectedResult: []*ProfileMetrics{
2966 + {
2967 + Source: "test-profile.yaml",
2968 + DeviceMetadata: nil,
2969 + Metrics: []Metric{
2970 + {
2971 + Name: "ifInErrors",
2972 + Value: 10,
2973 + Tags: map[string]string{"interface": "GigabitEthernet0/0"},
2974 + MetricType: "rate",
2975 + IsTable: true,
2976 + },
2977 + },
2978 + },
2979 + },
2980 + expectedError: false,
2981 + enableCache: false, // Explicitly disable cache
2982 + },
2983 + }
2984 +
2985 + for name, tc := range tests {
2986 + t.Run(name, func(t *testing.T) {
2987 + ctrl := gomock.NewController(t)
2988 + defer ctrl.Finish()
2989 +
2990 + mockHandler := snmpmock.NewMockHandler(ctrl)
2991 + tc.setupMock(mockHandler)
2992 +
2993 + collector := New(mockHandler, tc.profiles, logger.New())
2994 + collector.DoTableMetrics = true
2995 +
2996 + // Configure cache based on test requirements
2997 + if tc.enableCache {
2998 + collector.tableCache.setTTL(30*time.Second, 0)
2999 + } else {
3000 + collector.tableCache.setTTL(0, 0) // Disable cache
3001 + }
3002 +
3003 + result, err := collector.Collect()
3004 +
3005 + // Clear circular references
3006 + for _, profile := range result {
3007 + for i := range profile.Metrics {
3008 + profile.Metrics[i].Profile = nil
3009 + }
3010 + }
3011 +
3012 + if tc.expectedError {
3013 + assert.Error(t, err)
3014 + if tc.errorContains != "" {
3015 + assert.Contains(t, err.Error(), tc.errorContains)
3016 + }
3017 + } else {
3018 + assert.NoError(t, err)
3019 + }
3020 +
3021 + if tc.expectedResult != nil {
3022 + require.Equal(t, len(tc.expectedResult), len(result))
3023 + for i := range tc.expectedResult {
3024 + assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
3025 + assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
3026 + assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
3027 + }
3028 + } else {
3029 + assert.Nil(t, result)
3030 + }
3031 + })
3032 + }
3033 +}
3034 +
3035 +func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3036 + tests := map[string]struct {
3037 + profiles []*ddsnmp.Profile
3038 + setupMock func(m *snmpmock.MockHandler)
3039 + expectedResult []*ProfileMetrics
3040 + expectedError bool
3041 + errorContains string
3042 + enableCache bool
3043 + }{
3044 + // Index-based Tags Tests
3045 + "basic index tag": {
3046 + profiles: []*ddsnmp.Profile{
3047 + {
3048 + SourceFile: "test-profile.yaml",
3049 + Definition: &ddprofiledefinition.ProfileDefinition{
3050 + Metrics: []ddprofiledefinition.MetricsConfig{
3051 + {
3052 + Table: ddprofiledefinition.SymbolConfig{
3053 + OID: "1.3.6.1.2.1.4.31.1",
3054 + Name: "ipSystemStatsTable",
3055 + },
3056 + Symbols: []ddprofiledefinition.SymbolConfig{
3057 + {
3058 + OID: "1.3.6.1.2.1.4.31.1.1.4",
3059 + Name: "ipSystemStatsHCInReceives",
3060 + },
3061 + },
3062 + MetricTags: []ddprofiledefinition.MetricTagConfig{
3063 + {
3064 + Index: 1,
3065 + Tag: "ip_version",
3066 + },
3067 + },
3068 + },
3069 + },
3070 + },
3071 + },
3072 + },
3073 + setupMock: func(m *snmpmock.MockHandler) {
3074 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3075 + m.EXPECT().Version().Return(gosnmp.Version2c)
3076 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.4.31.1").Return(
3077 + []gosnmp.SnmpPDU{
3078 + {
3079 + Name: "1.3.6.1.2.1.4.31.1.1.4.1", // IPv4
3080 + Type: gosnmp.Counter64,
3081 + Value: uint64(1000),
3082 + },
3083 + {
3084 + Name: "1.3.6.1.2.1.4.31.1.1.4.2", // IPv6
3085 + Type: gosnmp.Counter64,
3086 + Value: uint64(2000),
3087 + },
3088 + }, nil,
3089 + )
3090 + },
3091 + expectedResult: []*ProfileMetrics{
3092 + {
3093 + Source: "test-profile.yaml",
3094 + DeviceMetadata: nil,
3095 + Metrics: []Metric{
3096 + {
3097 + Name: "ipSystemStatsHCInReceives",
3098 + Value: 1000,
3099 + Tags: map[string]string{"ip_version": "1"},
3100 + MetricType: "rate",
3101 + IsTable: true,
3102 + },
3103 + {
3104 + Name: "ipSystemStatsHCInReceives",
3105 + Value: 2000,
3106 + Tags: map[string]string{"ip_version": "2"},
3107 + MetricType: "rate",
3108 + IsTable: true,
3109 + },
3110 + },
3111 + },
3112 + },
3113 + expectedError: false,
3114 + },
3115 + "multiple index positions": {
3116 + profiles: []*ddsnmp.Profile{
3117 + {
3118 + SourceFile: "test-profile.yaml",
3119 + Definition: &ddprofiledefinition.ProfileDefinition{
3120 + Metrics: []ddprofiledefinition.MetricsConfig{
3121 + {
3122 + Table: ddprofiledefinition.SymbolConfig{
3123 + OID: "1.3.6.1.4.1.9.9.147.1.2.2.2",
3124 + Name: "cfwConnectionStatTable",
3125 + },
3126 + Symbols: []ddprofiledefinition.SymbolConfig{
3127 + {
3128 + OID: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5",
3129 + Name: "cfwConnectionStatValue",
3130 + },
3131 + },
3132 + MetricTags: []ddprofiledefinition.MetricTagConfig{
3133 + {
3134 + Index: 1,
3135 + Tag: "service_type",
3136 + },
3137 + {
3138 + Index: 2,
3139 + Tag: "stat_type",
3140 + },
3141 + },
3142 + },
3143 + },
3144 + },
3145 + },
3146 + },
3147 + setupMock: func(m *snmpmock.MockHandler) {
3148 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3149 + m.EXPECT().Version().Return(gosnmp.Version2c)
3150 + m.EXPECT().BulkWalkAll("1.3.6.1.4.1.9.9.147.1.2.2.2").Return(
3151 + []gosnmp.SnmpPDU{
3152 + {
3153 + Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.20.2", // service_type=20, stat_type=2
3154 + Type: gosnmp.Counter64,
3155 + Value: uint64(100),
3156 + },
3157 + {
3158 + Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.21.3", // service_type=21, stat_type=3
3159 + Type: gosnmp.Counter64,
3160 + Value: uint64(200),
3161 + },
3162 + {
3163 + Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.20.3", // service_type=20, stat_type=3
3164 + Type: gosnmp.Counter64,
3165 + Value: uint64(300),
3166 + },
3167 + }, nil,
3168 + )
3169 + },
3170 + expectedResult: []*ProfileMetrics{
3171 + {
3172 + Source: "test-profile.yaml",
3173 + DeviceMetadata: nil,
3174 + Metrics: []Metric{
3175 + {
3176 + Name: "cfwConnectionStatValue",
3177 + Value: 100,
3178 + Tags: map[string]string{"service_type": "20", "stat_type": "2"},
3179 + MetricType: "rate",
3180 + IsTable: true,
3181 + },
3182 + {
3183 + Name: "cfwConnectionStatValue",
3184 + Value: 200,
3185 + Tags: map[string]string{"service_type": "21", "stat_type": "3"},
3186 + MetricType: "rate",
3187 + IsTable: true,
3188 + },
3189 + {
3190 + Name: "cfwConnectionStatValue",
3191 + Value: 300,
3192 + Tags: map[string]string{"service_type": "20", "stat_type": "3"},
3193 + MetricType: "rate",
3194 + IsTable: true,
3195 + },
3196 + },
3197 + },
3198 + },
3199 + expectedError: false,
3200 + },
3201 + "index tag with mapping": {
3202 + profiles: []*ddsnmp.Profile{
3203 + {
3204 + SourceFile: "test-profile.yaml",
3205 + Definition: &ddprofiledefinition.ProfileDefinition{
3206 + Metrics: []ddprofiledefinition.MetricsConfig{
3207 + {
3208 + Table: ddprofiledefinition.SymbolConfig{
3209 + OID: "1.3.6.1.2.1.4.31.1",
3210 + Name: "ipSystemStatsTable",
3211 + },
3212 + Symbols: []ddprofiledefinition.SymbolConfig{
3213 + {
3214 + OID: "1.3.6.1.2.1.4.31.1.1.4",
3215 + Name: "ipSystemStatsHCInReceives",
3216 + },
3217 + },
3218 + MetricTags: []ddprofiledefinition.MetricTagConfig{
3219 + {
3220 + Index: 1,
3221 + Tag: "ip_version",
3222 + Mapping: map[string]string{
3223 + "0": "unknown",
3224 + "1": "ipv4",
3225 + "2": "ipv6",
3226 + "3": "ipv4z",
3227 + "4": "ipv6z",
3228 + },
3229 + },
3230 + },
3231 + },
3232 + },
3233 + },
3234 + },
3235 + },
3236 + setupMock: func(m *snmpmock.MockHandler) {
3237 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3238 + m.EXPECT().Version().Return(gosnmp.Version2c)
3239 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.4.31.1").Return(
3240 + []gosnmp.SnmpPDU{
3241 + {
3242 + Name: "1.3.6.1.2.1.4.31.1.1.4.1", // IPv4
3243 + Type: gosnmp.Counter64,
3244 + Value: uint64(1000),
3245 + },
3246 + {
3247 + Name: "1.3.6.1.2.1.4.31.1.1.4.2", // IPv6
3248 + Type: gosnmp.Counter64,
3249 + Value: uint64(2000),
3250 + },
3251 + {
3252 + Name: "1.3.6.1.2.1.4.31.1.1.4.0", // Unknown
3253 + Type: gosnmp.Counter64,
3254 + Value: uint64(10),
3255 + },
3256 + }, nil,
3257 + )
3258 + },
3259 + expectedResult: []*ProfileMetrics{
3260 + {
3261 + Source: "test-profile.yaml",
3262 + DeviceMetadata: nil,
3263 + Metrics: []Metric{
3264 + {
3265 + Name: "ipSystemStatsHCInReceives",
3266 + Value: 1000,
3267 + Tags: map[string]string{"ip_version": "ipv4"},
3268 + MetricType: "rate",
3269 + IsTable: true,
3270 + },
3271 + {
3272 + Name: "ipSystemStatsHCInReceives",
3273 + Value: 2000,
3274 + Tags: map[string]string{"ip_version": "ipv6"},
3275 + MetricType: "rate",
3276 + IsTable: true,
3277 + },
3278 + {
3279 + Name: "ipSystemStatsHCInReceives",
3280 + Value: 10,
3281 + Tags: map[string]string{"ip_version": "unknown"},
3282 + MetricType: "rate",
3283 + IsTable: true,
3284 + },
3285 + },
3286 + },
3287 + },
3288 + expectedError: false,
3289 + },
3290 + "index tag with missing position": {
3291 + profiles: []*ddsnmp.Profile{
3292 + {
3293 + SourceFile: "test-profile.yaml",
3294 + Definition: &ddprofiledefinition.ProfileDefinition{
3295 + Metrics: []ddprofiledefinition.MetricsConfig{
3296 + {
3297 + Table: ddprofiledefinition.SymbolConfig{
3298 + OID: "1.3.6.1.2.1.2.2",
3299 + Name: "ifTable",
3300 + },
3301 + Symbols: []ddprofiledefinition.SymbolConfig{
3302 + {
3303 + OID: "1.3.6.1.2.1.2.2.1.10",
3304 + Name: "ifInOctets",
3305 + },
3306 + },
3307 + MetricTags: []ddprofiledefinition.MetricTagConfig{
3308 + {
3309 + Index: 1,
3310 + Tag: "interface_index",
3311 + },
3312 + {
3313 + Index: 2,
3314 + Tag: "sub_interface", // Won't exist for single index
3315 + },
3316 + },
3317 + },
3318 + },
3319 + },
3320 + },
3321 + },
3322 + setupMock: func(m *snmpmock.MockHandler) {
3323 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3324 + m.EXPECT().Version().Return(gosnmp.Version2c)
3325 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3326 + []gosnmp.SnmpPDU{
3327 + {
3328 + Name: "1.3.6.1.2.1.2.2.1.10.1", // Single index
3329 + Type: gosnmp.Counter32,
3330 + Value: uint(1000),
3331 + },
3332 + {
3333 + Name: "1.3.6.1.2.1.2.2.1.10.2.3", // Multi-part index
3334 + Type: gosnmp.Counter32,
3335 + Value: uint(2000),
3336 + },
3337 + }, nil,
3338 + )
3339 + },
3340 + expectedResult: []*ProfileMetrics{
3341 + {
3342 + Source: "test-profile.yaml",
3343 + DeviceMetadata: nil,
3344 + Metrics: []Metric{
3345 + {
3346 + Name: "ifInOctets",
3347 + Value: 1000,
3348 + Tags: map[string]string{"interface_index": "1"}, // No sub_interface tag
3349 + MetricType: "rate",
3350 + IsTable: true,
3351 + },
3352 + {
3353 + Name: "ifInOctets",
3354 + Value: 2000,
3355 + Tags: map[string]string{"interface_index": "2", "sub_interface": "3"},
3356 + MetricType: "rate",
3357 + IsTable: true,
3358 + },
3359 + },
3360 + },
3361 + },
3362 + expectedError: false,
3363 + },
3364 +
3365 + // Index Transformation Tests
3366 + "basic index transform": {
3367 + profiles: []*ddsnmp.Profile{
3368 + {
3369 + SourceFile: "test-profile.yaml",
3370 + Definition: &ddprofiledefinition.ProfileDefinition{
3371 + Metrics: []ddprofiledefinition.MetricsConfig{
3372 + {
3373 + Table: ddprofiledefinition.SymbolConfig{
3374 + OID: "1.3.6.1.4.1.30932.1.10.1.3.110",
3375 + Name: "cpiPduBranchTable",
3376 + },
3377 + Symbols: []ddprofiledefinition.SymbolConfig{
3378 + {
3379 + OID: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3",
3380 + Name: "cpiPduBranchCurrent",
3381 + },
3382 + },
3383 + MetricTags: []ddprofiledefinition.MetricTagConfig{
3384 + {
3385 + Symbol: ddprofiledefinition.SymbolConfigCompat{
3386 + OID: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3",
3387 + Name: "cpiPduName",
3388 + },
3389 + Table: "cpiPduTable",
3390 + Tag: "pdu_name",
3391 + IndexTransform: []ddprofiledefinition.MetricIndexTransform{
3392 + {Start: 1, End: 7}, // Extract MAC address portion
3393 + },
3394 + },
3395 + },
3396 + },
3397 + {
3398 + Table: ddprofiledefinition.SymbolConfig{
3399 + OID: "1.3.6.1.4.1.30932.1.10.1.2.10",
3400 + Name: "cpiPduTable",
3401 + },
3402 + Symbols: []ddprofiledefinition.SymbolConfig{
3403 + {
3404 + OID: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3",
3405 + Name: "cpiPduName",
3406 + },
3407 + },
3408 + },
3409 + },
3410 + },
3411 + },
3412 + },
3413 + setupMock: func(m *snmpmock.MockHandler) {
3414 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3415 + // Walk cpiPduBranchTable
3416 + m.EXPECT().Version().Return(gosnmp.Version2c)
3417 + m.EXPECT().BulkWalkAll("1.3.6.1.4.1.30932.1.10.1.3.110").Return(
3418 + []gosnmp.SnmpPDU{
3419 + {
3420 + Name: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3.1.6.0.36.155.53.3.246", // Branch ID + MAC
3421 + Type: gosnmp.Gauge32,
3422 + Value: uint(150),
3423 + },
3424 + }, nil,
3425 + )
3426 + // Walk cpiPduTable
3427 + m.EXPECT().Version().Return(gosnmp.Version2c)
3428 + m.EXPECT().BulkWalkAll("1.3.6.1.4.1.30932.1.10.1.2.10").Return(
3429 + []gosnmp.SnmpPDU{
3430 + {
3431 + Name: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3.6.0.36.155.53.3.246", // Just MAC
3432 + Type: gosnmp.OctetString,
3433 + Value: []byte("PDU-01"),
3434 + },
3435 + }, nil,
3436 + )
3437 + },
3438 + expectedResult: []*ProfileMetrics{
3439 + {
3440 + Source: "test-profile.yaml",
3441 + DeviceMetadata: nil,
3442 + Metrics: []Metric{
3443 + {
3444 + Name: "cpiPduBranchCurrent",
3445 + Value: 150,
3446 + Tags: map[string]string{"pdu_name": "PDU-01"},
3447 + MetricType: "gauge",
3448 + IsTable: true,
3449 + },
3450 + },
3451 + },
3452 + },
3453 + expectedError: false,
3454 + },
3455 + "multiple index transform ranges": {
3456 + profiles: []*ddsnmp.Profile{
3457 + {
3458 + SourceFile: "test-profile.yaml",
3459 + Definition: &ddprofiledefinition.ProfileDefinition{
3460 + Metrics: []ddprofiledefinition.MetricsConfig{
3461 + {
3462 + Table: ddprofiledefinition.SymbolConfig{
3463 + OID: "1.3.6.1.4.1.12345.1.1",
3464 + Name: "customTable",
3465 + },
3466 + Symbols: []ddprofiledefinition.SymbolConfig{
3467 + {
3468 + OID: "1.3.6.1.4.1.12345.1.1.1.1",
3469 + Name: "customMetric",
3470 + },
3471 + },
3472 + MetricTags: []ddprofiledefinition.MetricTagConfig{
3473 + {
3474 + Symbol: ddprofiledefinition.SymbolConfigCompat{
3475 + OID: "1.3.6.1.4.1.12345.2.1.1.1",
3476 + Name: "customTag",
3477 + },
3478 + Table: "customRefTable",
3479 + Tag: "custom_ref",
3480 + IndexTransform: []ddprofiledefinition.MetricIndexTransform{
3481 + {Start: 1, End: 2}, // First two positions
3482 + {Start: 4, End: 6}, // Positions 5-7
3483 + },
3484 + },
3485 + },
3486 + },
3487 + {
3488 + Table: ddprofiledefinition.SymbolConfig{
3489 + OID: "1.3.6.1.4.1.12345.2.1",
3490 + Name: "customRefTable",
3491 + },
3492 + Symbols: []ddprofiledefinition.SymbolConfig{
3493 + {
3494 + OID: "1.3.6.1.4.1.12345.2.1.1.1",
3495 + Name: "customTag",
3496 + },
3497 + },
3498 + },
3499 + },
3500 + },
3501 + },
3502 + },
3503 + setupMock: func(m *snmpmock.MockHandler) {
3504 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3505 + // Walk customTable
3506 + m.EXPECT().Version().Return(gosnmp.Version2c)
3507 + m.EXPECT().BulkWalkAll("1.3.6.1.4.1.12345.1.1").Return(
3508 + []gosnmp.SnmpPDU{
3509 + {
3510 + Name: "1.3.6.1.4.1.12345.1.1.1.1.1.2.3.4.5.6.7", // Index: 1.2.3.4.5.6.7
3511 + Type: gosnmp.Counter32,
3512 + Value: uint(500),
3513 + },
3514 + }, nil,
3515 + )
3516 + // Walk customRefTable
3517 + m.EXPECT().Version().Return(gosnmp.Version2c)
3518 + m.EXPECT().BulkWalkAll("1.3.6.1.4.1.12345.2.1").Return(
3519 + []gosnmp.SnmpPDU{
3520 + {
3521 + Name: "1.3.6.1.4.1.12345.2.1.1.1.2.3.5.6.7", // Index: 2.3.5.6.7 (matches transform)
3522 + Type: gosnmp.OctetString,
3523 + Value: []byte("CustomValue"),
3524 + },
3525 + }, nil,
3526 + )
3527 + },
3528 + expectedResult: []*ProfileMetrics{
3529 + {
3530 + Source: "test-profile.yaml",
3531 + DeviceMetadata: nil,
3532 + Metrics: []Metric{
3533 + {
3534 + Name: "customMetric",
3535 + Value: 500,
3536 + Tags: map[string]string{"custom_ref": "CustomValue"},
3537 + MetricType: "rate",
3538 + IsTable: true,
3539 + },
3540 + },
3541 + },
3542 + },
3543 + expectedError: false,
3544 + },
3545 + "index transform with out of bounds": {
3546 + profiles: []*ddsnmp.Profile{
3547 + {
3548 + SourceFile: "test-profile.yaml",
3549 + Definition: &ddprofiledefinition.ProfileDefinition{
3550 + Metrics: []ddprofiledefinition.MetricsConfig{
3551 + {
3552 + Table: ddprofiledefinition.SymbolConfig{
3553 + OID: "1.3.6.1.2.1.2.2",
3554 + Name: "ifTable",
3555 + },
3556 + Symbols: []ddprofiledefinition.SymbolConfig{
3557 + {
3558 + OID: "1.3.6.1.2.1.2.2.1.10",
3559 + Name: "ifInOctets",
3560 + },
3561 + },
3562 + MetricTags: []ddprofiledefinition.MetricTagConfig{
3563 + {
3564 + Symbol: ddprofiledefinition.SymbolConfigCompat{
3565 + OID: "1.3.6.1.2.1.31.1.1.1.1",
3566 + Name: "ifName",
3567 + },
3568 + Table: "ifXTable",
3569 + Tag: "interface",
3570 + IndexTransform: []ddprofiledefinition.MetricIndexTransform{
3571 + {Start: 0, End: 5}, // Trying to extract 6 positions from single index
3572 + },
3573 + },
3574 + },
3575 + },
3576 + {
3577 + Table: ddprofiledefinition.SymbolConfig{
3578 + OID: "1.3.6.1.2.1.31.1.1",
3579 + Name: "ifXTable",
3580 + },
3581 + Symbols: []ddprofiledefinition.SymbolConfig{
3582 + {
3583 + OID: "1.3.6.1.2.1.31.1.1.1.1",
3584 + Name: "ifName",
3585 + },
3586 + },
3587 + },
3588 + },
3589 + },
3590 + },
3591 + },
3592 + setupMock: func(m *snmpmock.MockHandler) {
3593 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3594 + // Walk ifTable
3595 + m.EXPECT().Version().Return(gosnmp.Version2c)
3596 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3597 + []gosnmp.SnmpPDU{
3598 + {
3599 + Name: "1.3.6.1.2.1.2.2.1.10.1", // Single index
3600 + Type: gosnmp.Counter32,
3601 + Value: uint(1000),
3602 + },
3603 + {
3604 + Name: "1.3.6.1.2.1.2.2.1.10.1.2.3.4.5.6", // Multi-part index
3605 + Type: gosnmp.Counter32,
3606 + Value: uint(2000),
3607 + },
3608 + }, nil,
3609 + )
3610 + // Walk ifXTable
3611 + m.EXPECT().Version().Return(gosnmp.Version2c)
3612 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
3613 + []gosnmp.SnmpPDU{
3614 + {
3615 + Name: "1.3.6.1.2.1.31.1.1.1.1.1.2.3.4.5.6", // Matches second row
3616 + Type: gosnmp.OctetString,
3617 + Value: []byte("GigabitEthernet0/0"),
3618 + },
3619 + }, nil,
3620 + )
3621 + },
3622 + expectedResult: []*ProfileMetrics{
3623 + {
3624 + Source: "test-profile.yaml",
3625 + DeviceMetadata: nil,
3626 + Metrics: []Metric{
3627 + {
3628 + Name: "ifInOctets",
3629 + Value: 1000,
3630 + Tags: nil, // Transform fails - index too short
3631 + MetricType: "rate",
3632 + IsTable: true,
3633 + },
3634 + {
3635 + Name: "ifInOctets",
3636 + Value: 2000,
3637 + Tags: map[string]string{"interface": "GigabitEthernet0/0"},
3638 + MetricType: "rate",
3639 + IsTable: true,
3640 + },
3641 + },
3642 + },
3643 + },
3644 + expectedError: false,
3645 + },
3646 + }
3647 +
3648 + for name, tc := range tests {
3649 + t.Run(name, func(t *testing.T) {
3650 + ctrl := gomock.NewController(t)
3651 + defer ctrl.Finish()
3652 +
3653 + mockHandler := snmpmock.NewMockHandler(ctrl)
3654 + tc.setupMock(mockHandler)
3655 +
3656 + collector := New(mockHandler, tc.profiles, logger.New())
3657 + collector.DoTableMetrics = true
3658 +
3659 + // Configure cache based on test requirements
3660 + if tc.enableCache {
3661 + collector.tableCache.setTTL(30*time.Second, 0)
3662 + } else {
3663 + collector.tableCache.setTTL(0, 0) // Disable cache
3664 + }
3665 +
3666 + result, err := collector.Collect()
3667 +
3668 + // Clear circular references
3669 + for _, profile := range result {
3670 + for i := range profile.Metrics {
3671 + profile.Metrics[i].Profile = nil
3672 + }
3673 + }
3674 +
3675 + if tc.expectedError {
3676 + assert.Error(t, err)
3677 + if tc.errorContains != "" {
3678 + assert.Contains(t, err.Error(), tc.errorContains)
3679 + }
3680 + } else {
3681 + assert.NoError(t, err)
3682 + }
3683 +
3684 + if tc.expectedResult != nil {
3685 + require.Equal(t, len(tc.expectedResult), len(result))
3686 + for i := range tc.expectedResult {
3687 + assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
3688 + assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
3689 + assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
3690 + }
3691 + } else {
3692 + assert.Nil(t, result)
3693 + }
3694 + })
3695 + }
3696 +}
3697 +
3698 +func TestCollector_Collect_OpaqueTypes(t *testing.T) {
3699 + tests := map[string]struct {
3700 + profiles []*ddsnmp.Profile
3701 + setupMock func(m *snmpmock.MockHandler)
3702 + expectedResult []*ProfileMetrics
3703 + expectedError bool
3704 + errorContains string
3705 + enableCache bool
3706 + }{
3707 + "opaque float metric": {
3708 + profiles: []*ddsnmp.Profile{
3709 + {
3710 + SourceFile: "test-profile.yaml",
3711 + Definition: &ddprofiledefinition.ProfileDefinition{
3712 + Metrics: []ddprofiledefinition.MetricsConfig{
3713 + {
3714 + Symbol: ddprofiledefinition.SymbolConfig{
3715 + OID: "1.3.6.1.4.1.9.9.305.1.1.1.0",
3716 + Name: "cpmCPULoadAvg1min",
3717 + },
3718 + },
3719 + {
3720 + Symbol: ddprofiledefinition.SymbolConfig{
3721 + OID: "1.3.6.1.4.1.9.9.305.1.1.2.0",
3722 + Name: "cpmCPULoadAvg5min",
3723 + },
3724 + },
3725 + {
3726 + Symbol: ddprofiledefinition.SymbolConfig{
3727 + OID: "1.3.6.1.4.1.9.9.305.1.1.3.0",
3728 + Name: "cpmCPULoadAvg15min",
3729 + },
3730 + },
3731 + },
3732 + },
3733 + },
3734 + },
3735 + setupMock: func(m *snmpmock.MockHandler) {
3736 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3737 + m.EXPECT().Get(gomock.InAnyOrder([]string{
3738 + "1.3.6.1.4.1.9.9.305.1.1.1.0",
3739 + "1.3.6.1.4.1.9.9.305.1.1.2.0",
3740 + "1.3.6.1.4.1.9.9.305.1.1.3.0",
3741 + })).Return(
3742 + &gosnmp.SnmpPacket{
3743 + Variables: []gosnmp.SnmpPDU{
3744 + {
3745 + Name: "1.3.6.1.4.1.9.9.305.1.1.1.0",
3746 + Type: gosnmp.OpaqueFloat,
3747 + Value: float32(0.75),
3748 + },
3749 + {
3750 + Name: "1.3.6.1.4.1.9.9.305.1.1.2.0",
3751 + Type: gosnmp.OpaqueFloat,
3752 + Value: float32(1.23),
3753 + },
3754 + {
3755 + Name: "1.3.6.1.4.1.9.9.305.1.1.3.0",
3756 + Type: gosnmp.OpaqueFloat,
3757 + Value: float32(2.45),
3758 + },
3759 + },
3760 + }, nil,
3761 + )
3762 + },
3763 + expectedResult: []*ProfileMetrics{
3764 + {
3765 + Source: "test-profile.yaml",
3766 + DeviceMetadata: nil,
3767 + Metrics: []Metric{
3768 + {
3769 + Name: "cpmCPULoadAvg1min",
3770 + Value: 0, // 0.75 truncated to int64
3771 + MetricType: "gauge",
3772 + },
3773 + {
3774 + Name: "cpmCPULoadAvg5min",
3775 + Value: 1, // 1.23 truncated to int64
3776 + MetricType: "gauge",
3777 + },
3778 + {
3779 + Name: "cpmCPULoadAvg15min",
3780 + Value: 2, // 2.45 truncated to int64
3781 + MetricType: "gauge",
3782 + },
3783 + },
3784 + },
3785 + },
3786 + expectedError: false,
3787 + },
3788 + "opaque double metric": {
3789 + profiles: []*ddsnmp.Profile{
3790 + {
3791 + SourceFile: "test-profile.yaml",
3792 + Definition: &ddprofiledefinition.ProfileDefinition{
3793 + Metrics: []ddprofiledefinition.MetricsConfig{
3794 + {
3795 + Symbol: ddprofiledefinition.SymbolConfig{
3796 + OID: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.1",
3797 + Name: "cpqHeSysBatteryVoltage",
3798 + },
3799 + },
3800 + {
3801 + Symbol: ddprofiledefinition.SymbolConfig{
3802 + OID: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.2",
3803 + Name: "cpqHeSysBatteryCurrent",
3804 + },
3805 + },
3806 + {
3807 + Symbol: ddprofiledefinition.SymbolConfig{
3808 + OID: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.3",
3809 + Name: "cpqHeSysBatteryCapacity",
3810 + },
3811 + },
3812 + },
3813 + },
3814 + },
3815 + },
3816 + setupMock: func(m *snmpmock.MockHandler) {
3817 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3818 + m.EXPECT().Get(gomock.InAnyOrder([]string{
3819 + "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.1",
3820 + "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.2",
3821 + "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.3",
3822 + })).Return(
3823 + &gosnmp.SnmpPacket{
3824 + Variables: []gosnmp.SnmpPDU{
3825 + {
3826 + Name: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.1",
3827 + Type: gosnmp.OpaqueDouble,
3828 + Value: 12.6,
3829 + },
3830 + {
3831 + Name: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.2",
3832 + Type: gosnmp.OpaqueDouble,
3833 + Value: 2.4,
3834 + },
3835 + {
3836 + Name: "1.3.6.1.4.1.232.6.2.6.8.1.4.1.1.6.3",
3837 + Type: gosnmp.OpaqueDouble,
3838 + Value: 98.5,
3839 + },
3840 + },
3841 + }, nil,
3842 + )
3843 + },
3844 + expectedResult: []*ProfileMetrics{
3845 + {
3846 + Source: "test-profile.yaml",
3847 + DeviceMetadata: nil,
3848 + Metrics: []Metric{
3849 + {
3850 + Name: "cpqHeSysBatteryVoltage",
3851 + Value: 12, // 12.6 truncated to int64
3852 + MetricType: "gauge",
3853 + },
3854 + {
3855 + Name: "cpqHeSysBatteryCurrent",
3856 + Value: 2, // 2.4 truncated to int64
3857 + MetricType: "gauge",
3858 + },
3859 + {
3860 + Name: "cpqHeSysBatteryCapacity",
3861 + Value: 98, // 98.5 truncated to int64
3862 + MetricType: "gauge",
3863 + },
3864 + },
3865 + },
3866 + },
3867 + expectedError: false,
3868 + },
3869 + }
3870 +
3871 + for name, tc := range tests {
3872 + t.Run(name, func(t *testing.T) {
3873 + ctrl := gomock.NewController(t)
3874 + defer ctrl.Finish()
3875 +
3876 + mockHandler := snmpmock.NewMockHandler(ctrl)
3877 + tc.setupMock(mockHandler)
3878 +
3879 + collector := New(mockHandler, tc.profiles, logger.New())
3880 + collector.DoTableMetrics = true
3881 +
3882 + // Configure cache based on test requirements
3883 + if tc.enableCache {
3884 + collector.tableCache.setTTL(30*time.Second, 0)
3885 + } else {
3886 + collector.tableCache.setTTL(0, 0) // Disable cache
3887 + }
3888 +
3889 + result, err := collector.Collect()
3890 +
3891 + // Clear circular references
3892 + for _, profile := range result {
3893 + for i := range profile.Metrics {
3894 + profile.Metrics[i].Profile = nil
3895 + }
3896 + }
3897 +
3898 + if tc.expectedError {
3899 + assert.Error(t, err)
3900 + if tc.errorContains != "" {
3901 + assert.Contains(t, err.Error(), tc.errorContains)
3902 + }
3903 + } else {
3904 + assert.NoError(t, err)
3905 + }
3906 +
3907 + if tc.expectedResult != nil {
3908 + require.Equal(t, len(tc.expectedResult), len(result))
3909 + for i := range tc.expectedResult {
3910 + assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
3911 + assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
3912 + assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
3913 + }
3914 + } else {
3915 + assert.Nil(t, result)
3916 + }
3917 + })
3918 + }
3919 +}
3920 +
3921 +func TestCollector_Collect_TableCaching(t *testing.T) {
3922 + tests := map[string]struct {
3923 + profiles []*ddsnmp.Profile
3924 + setupMock func(m *snmpmock.MockHandler)
3925 + expectedResult []*ProfileMetrics
3926 + expectedError bool
3927 + errorContains string
3928 + enableCache bool
3929 + cacheTTL time.Duration
3930 + collectCount int // Number of times to call Collect()
3931 + sleepBetween time.Duration
3932 + }{
3933 + "table cache basic operation": {
3934 + profiles: []*ddsnmp.Profile{
3935 + {
3936 + SourceFile: "test-profile.yaml",
3937 + Definition: &ddprofiledefinition.ProfileDefinition{
3938 + Metrics: []ddprofiledefinition.MetricsConfig{
3939 + {
3940 + Table: ddprofiledefinition.SymbolConfig{
3941 + OID: "1.3.6.1.2.1.2.2",
3942 + Name: "ifTable",
3943 + },
3944 + Symbols: []ddprofiledefinition.SymbolConfig{
3945 + {
3946 + OID: "1.3.6.1.2.1.2.2.1.10",
3947 + Name: "ifInOctets",
3948 + },
3949 + {
3950 + OID: "1.3.6.1.2.1.2.2.1.16",
3951 + Name: "ifOutOctets",
3952 + },
3953 + },
3954 + MetricTags: []ddprofiledefinition.MetricTagConfig{
3955 + {
3956 + Tag: "interface",
3957 + Symbol: ddprofiledefinition.SymbolConfigCompat{
3958 + OID: "1.3.6.1.2.1.2.2.1.2",
3959 + Name: "ifDescr",
3960 + },
3961 + },
3962 + },
3963 + },
3964 + },
3965 + },
3966 + },
3967 + },
3968 + setupMock: func(m *snmpmock.MockHandler) {
3969 + m.EXPECT().MaxOids().Return(10).AnyTimes()
3970 +
3971 + // First collection: Full walk
3972 + m.EXPECT().Version().Return(gosnmp.Version2c).Times(1)
3973 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
3974 + []gosnmp.SnmpPDU{
3975 + {
3976 + Name: "1.3.6.1.2.1.2.2.1.10.1",
3977 + Type: gosnmp.Counter32,
3978 + Value: uint(1000),
3979 + },
3980 + {
3981 + Name: "1.3.6.1.2.1.2.2.1.16.1",
3982 + Type: gosnmp.Counter32,
3983 + Value: uint(500),
3984 + },
3985 + {
3986 + Name: "1.3.6.1.2.1.2.2.1.2.1",
3987 + Type: gosnmp.OctetString,
3988 + Value: []byte("eth0"),
3989 + },
3990 + {
3991 + Name: "1.3.6.1.2.1.2.2.1.10.2",
3992 + Type: gosnmp.Counter32,
3993 + Value: uint(2000),
3994 + },
3995 + {
3996 + Name: "1.3.6.1.2.1.2.2.1.16.2",
3997 + Type: gosnmp.Counter32,
3998 + Value: uint(1500),
3999 + },
4000 + {
4001 + Name: "1.3.6.1.2.1.2.2.1.2.2",
4002 + Type: gosnmp.OctetString,
4003 + Value: []byte("eth1"),
4004 + },
4005 + }, nil,
4006 + ).Times(1)
4007 +
4008 + // Second collection: Only GET metrics (not tags)
4009 + m.EXPECT().Get(gomock.InAnyOrder([]string{
4010 + "1.3.6.1.2.1.2.2.1.10.1",
4011 + "1.3.6.1.2.1.2.2.1.16.1",
4012 + "1.3.6.1.2.1.2.2.1.10.2",
4013 + "1.3.6.1.2.1.2.2.1.16.2",
4014 + })).Return(
4015 + &gosnmp.SnmpPacket{
4016 + Variables: []gosnmp.SnmpPDU{
4017 + {
4018 + Name: "1.3.6.1.2.1.2.2.1.10.1",
4019 + Type: gosnmp.Counter32,
4020 + Value: uint(1100), // Values changed
4021 + },
4022 + {
4023 + Name: "1.3.6.1.2.1.2.2.1.16.1",
4024 + Type: gosnmp.Counter32,
4025 + Value: uint(600),
4026 + },
4027 + {
4028 + Name: "1.3.6.1.2.1.2.2.1.10.2",
4029 + Type: gosnmp.Counter32,
4030 + Value: uint(2200),
4031 + },
4032 + {
4033 + Name: "1.3.6.1.2.1.2.2.1.16.2",
4034 + Type: gosnmp.Counter32,
4035 + Value: uint(1700),
4036 + },
4037 + },
4038 + }, nil,
4039 + ).Times(1)
4040 +
4041 + // Third collection: Still using cache
4042 + m.EXPECT().Get(gomock.InAnyOrder([]string{
4043 + "1.3.6.1.2.1.2.2.1.10.1",
4044 + "1.3.6.1.2.1.2.2.1.16.1",
4045 + "1.3.6.1.2.1.2.2.1.10.2",
4046 + "1.3.6.1.2.1.2.2.1.16.2",
4047 + })).Return(
4048 + &gosnmp.SnmpPacket{
4049 + Variables: []gosnmp.SnmpPDU{
4050 + {
4051 + Name: "1.3.6.1.2.1.2.2.1.10.1",
4052 + Type: gosnmp.Counter32,
4053 + Value: uint(1200),
4054 + },
4055 + {
4056 + Name: "1.3.6.1.2.1.2.2.1.16.1",
4057 + Type: gosnmp.Counter32,
4058 + Value: uint(700),
4059 + },
4060 + {
4061 + Name: "1.3.6.1.2.1.2.2.1.10.2",
4062 + Type: gosnmp.Counter32,
4063 + Value: uint(2400),
4064 + },
4065 + {
4066 + Name: "1.3.6.1.2.1.2.2.1.16.2",
4067 + Type: gosnmp.Counter32,
4068 + Value: uint(1900),
4069 + },
4070 + },
4071 + }, nil,
4072 + ).Times(1)
4073 + },
4074 + expectedResult: []*ProfileMetrics{
4075 + {
4076 + Source: "test-profile.yaml",
4077 + DeviceMetadata: nil,
4078 + Metrics: []Metric{
4079 + {
4080 + Name: "ifInOctets",
4081 + Value: 1200, // Latest value
4082 + Tags: map[string]string{"interface": "eth0"},
4083 + MetricType: "rate",
4084 + IsTable: true,
4085 + },
4086 + {
4087 + Name: "ifOutOctets",
4088 + Value: 700,
4089 + Tags: map[string]string{"interface": "eth0"},
4090 + MetricType: "rate",
4091 + IsTable: true,
4092 + },
4093 + {
4094 + Name: "ifInOctets",
4095 + Value: 2400,
4096 + Tags: map[string]string{"interface": "eth1"},
4097 + MetricType: "rate",
4098 + IsTable: true,
4099 + },
4100 + {
4101 + Name: "ifOutOctets",
4102 + Value: 1900,
4103 + Tags: map[string]string{"interface": "eth1"},
4104 + MetricType: "rate",
4105 + IsTable: true,
4106 + },
4107 + },
4108 + },
4109 + },
4110 + expectedError: false,
4111 + enableCache: true,
4112 + cacheTTL: 30 * time.Second,
4113 + collectCount: 3,
4114 + sleepBetween: 10 * time.Millisecond,
4115 + },
4116 + "table cache expiration": {
4117 + profiles: []*ddsnmp.Profile{
4118 + {
4119 + SourceFile: "test-profile.yaml",
4120 + Definition: &ddprofiledefinition.ProfileDefinition{
4121 + Metrics: []ddprofiledefinition.MetricsConfig{
4122 + {
4123 + Table: ddprofiledefinition.SymbolConfig{
4124 + OID: "1.3.6.1.2.1.2.2",
4125 + Name: "ifTable",
4126 + },
4127 + Symbols: []ddprofiledefinition.SymbolConfig{
4128 + {
4129 + OID: "1.3.6.1.2.1.2.2.1.10",
4130 + Name: "ifInOctets",
4131 + },
4132 + },
4133 + MetricTags: []ddprofiledefinition.MetricTagConfig{
4134 + {
4135 + Tag: "interface",
4136 + Symbol: ddprofiledefinition.SymbolConfigCompat{
4137 + OID: "1.3.6.1.2.1.2.2.1.2",
4138 + Name: "ifDescr",
4139 + },
4140 + },
4141 + },
4142 + },
4143 + },
4144 + },
4145 + },
4146 + },
4147 + setupMock: func(m *snmpmock.MockHandler) {
4148 + m.EXPECT().MaxOids().Return(10).AnyTimes()
4149 + m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
4150 +
4151 + // First collection: Full walk
4152 + gomock.InOrder(
4153 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
4154 + []gosnmp.SnmpPDU{
4155 + {
4156 + Name: "1.3.6.1.2.1.2.2.1.10.1",
4157 + Type: gosnmp.Counter32,
4158 + Value: uint(1000),
4159 + },
4160 + {
4161 + Name: "1.3.6.1.2.1.2.2.1.2.1",
4162 + Type: gosnmp.OctetString,
4163 + Value: []byte("eth0"),
4164 + },
4165 + }, nil,
4166 + ),
4167 +
4168 + // Second collection: Using cache
4169 + m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.10.1"}).Return(
4170 + &gosnmp.SnmpPacket{
4171 + Variables: []gosnmp.SnmpPDU{
4172 + {
4173 + Name: "1.3.6.1.2.1.2.2.1.10.1",
4174 + Type: gosnmp.Counter32,
4175 + Value: uint(1100),
4176 + },
4177 + },
4178 + }, nil,
4179 + ),
4180 +
4181 + // Third collection: After expiration, full walk again
4182 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
4183 + []gosnmp.SnmpPDU{
4184 + {
4185 + Name: "1.3.6.1.2.1.2.2.1.10.1",
4186 + Type: gosnmp.Counter32,
4187 + Value: uint(1200),
4188 + },
4189 + {
4190 + Name: "1.3.6.1.2.1.2.2.1.2.1",
4191 + Type: gosnmp.OctetString,
4192 + Value: []byte("eth0"),
4193 + },
4194 + {
4195 + Name: "1.3.6.1.2.1.2.2.1.10.2",
4196 + Type: gosnmp.Counter32,
4197 + Value: uint(2000),
4198 + },
4199 + {
4200 + Name: "1.3.6.1.2.1.2.2.1.2.2",
4201 + Type: gosnmp.OctetString,
4202 + Value: []byte("eth1"),
4203 + },
4204 + }, nil,
4205 + ),
4206 + )
4207 + },
4208 + expectedResult: []*ProfileMetrics{
4209 + {
4210 + Source: "test-profile.yaml",
4211 + DeviceMetadata: nil,
4212 + Metrics: []Metric{
4213 + {
4214 + Name: "ifInOctets",
4215 + Value: 1200,
4216 + Tags: map[string]string{"interface": "eth0"},
4217 + MetricType: "rate",
4218 + IsTable: true,
4219 + },
4220 + {
4221 + Name: "ifInOctets",
4222 + Value: 2000,
4223 + Tags: map[string]string{"interface": "eth1"},
4224 + MetricType: "rate",
4225 + IsTable: true,
4226 + },
4227 + },
4228 + },
4229 + },
4230 + expectedError: false,
4231 + enableCache: true,
4232 + cacheTTL: 100 * time.Millisecond,
4233 + collectCount: 3,
4234 + sleepBetween: 60 * time.Millisecond, // Sleep less than TTL for second collection, but total > TTL for third
4235 + },
4236 + "table cache with multiple configs same table": {
4237 + profiles: []*ddsnmp.Profile{
4238 + {
4239 + SourceFile: "test-profile.yaml",
4240 + Definition: &ddprofiledefinition.ProfileDefinition{
4241 + Metrics: []ddprofiledefinition.MetricsConfig{
4242 + {
4243 + Table: ddprofiledefinition.SymbolConfig{
4244 + OID: "1.3.6.1.2.1.2.2",
4245 + Name: "ifTable",
4246 + },
4247 + Symbols: []ddprofiledefinition.SymbolConfig{
4248 + {
4249 + OID: "1.3.6.1.2.1.2.2.1.10",
4250 + Name: "ifInOctets",
4251 + },
4252 + {
4253 + OID: "1.3.6.1.2.1.2.2.1.16",
4254 + Name: "ifOutOctets",
4255 + },
4256 + },
4257 + MetricTags: []ddprofiledefinition.MetricTagConfig{
4258 + {
4259 + Tag: "interface",
4260 + Symbol: ddprofiledefinition.SymbolConfigCompat{
4261 + OID: "1.3.6.1.2.1.2.2.1.2",
4262 + Name: "ifDescr",
4263 + },
4264 + },
4265 + },
4266 + },
4267 + {
4268 + Table: ddprofiledefinition.SymbolConfig{
4269 + OID: "1.3.6.1.2.1.2.2",
4270 + Name: "ifTable",
4271 + },
4272 + Symbols: []ddprofiledefinition.SymbolConfig{
4273 + {
4274 + OID: "1.3.6.1.2.1.2.2.1.14",
4275 + Name: "ifInErrors",
4276 + },
4277 + {
4278 + OID: "1.3.6.1.2.1.2.2.1.20",
4279 + Name: "ifOutErrors",
4280 + },
4281 + },
4282 + MetricTags: []ddprofiledefinition.MetricTagConfig{
4283 + {
4284 + Tag: "interface",
4285 + Symbol: ddprofiledefinition.SymbolConfigCompat{
4286 + OID: "1.3.6.1.2.1.2.2.1.2",
4287 + Name: "ifDescr",
4288 + },
4289 + },
4290 + },
4291 + },
4292 + },
4293 + },
4294 + },
4295 + },
4296 + setupMock: func(m *snmpmock.MockHandler) {
4297 + m.EXPECT().MaxOids().Return(10).AnyTimes()
4298 + m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
4299 +
4300 + // First collection: Walk table once
4301 + m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
4302 + []gosnmp.SnmpPDU{
4303 + // Interface 1
4304 + {
4305 + Name: "1.3.6.1.2.1.2.2.1.2.1",
4306 + Type: gosnmp.OctetString,
4307 + Value: []byte("eth0"),
4308 + },
4309 + {
4310 + Name: "1.3.6.1.2.1.2.2.1.10.1",
4311 + Type: gosnmp.Counter32,
4312 + Value: uint(1000),
4313 + },
4314 + {
4315 + Name: "1.3.6.1.2.1.2.2.1.16.1",
4316 + Type: gosnmp.Counter32,
4317 + Value: uint(500),
4318 + },
4319 + {
4320 + Name: "1.3.6.1.2.1.2.2.1.14.1",
4321 + Type: gosnmp.Counter32,
4322 + Value: uint(10),
4323 + },
4324 + {
4325 + Name: "1.3.6.1.2.1.2.2.1.20.1",
4326 + Type: gosnmp.Counter32,
4327 + Value: uint(5),
4328 + },
4329 + }, nil,
4330 + ).Times(1)
4331 +
4332 + // Second collection: Each config uses cache separately
4333 + // First config GET
4334 + m.EXPECT().Get(gomock.InAnyOrder([]string{
4335 + "1.3.6.1.2.1.2.2.1.10.1",
4336 + "1.3.6.1.2.1.2.2.1.16.1",
4337 + })).Return(
4338 + &gosnmp.SnmpPacket{
4339 + Variables: []gosnmp.SnmpPDU{
4340 + {
4341 + Name: "1.3.6.1.2.1.2.2.1.10.1",
4342 + Type: gosnmp.Counter32,
4343 + Value: uint(1100),
4344 + },
4345 + {
4346 + Name: "1.3.6.1.2.1.2.2.1.16.1",
4347 + Type: gosnmp.Counter32,
4348 + Value: uint(600),
4349 + },
4350 + },
4351 + }, nil,
4352 + ).Times(1)
4353 +
4354 + // Second config GET
4355 + m.EXPECT().Get(gomock.InAnyOrder([]string{
4356 + "1.3.6.1.2.1.2.2.1.14.1",
4357 + "1.3.6.1.2.1.2.2.1.20.1",
4358 + })).Return(
4359 + &gosnmp.SnmpPacket{
4360 + Variables: []gosnmp.SnmpPDU{
4361 + {
4362 + Name: "1.3.6.1.2.1.2.2.1.14.1",
4363 + Type: gosnmp.Counter32,
4364 + Value: uint(12),
4365 + },
4366 + {
4367 + Name: "1.3.6.1.2.1.2.2.1.20.1",
4368 + Type: gosnmp.Counter32,
4369 + Value: uint(6),
4370 + },
4371 + },
4372 + }, nil,
4373 + ).Times(1)
4374 + },
4375 + expectedResult: []*ProfileMetrics{
4376 + {
4377 + Source: "test-profile.yaml",
4378 + DeviceMetadata: nil,
4379 + Metrics: []Metric{
4380 + // From first config
4381 + {
4382 + Name: "ifInOctets",
4383 + Value: 1100,
4384 + Tags: map[string]string{"interface": "eth0"},
4385 + MetricType: "rate",
4386 + IsTable: true,
4387 + },
4388 + {
4389 + Name: "ifOutOctets",
4390 + Value: 600,
4391 + Tags: map[string]string{"interface": "eth0"},
4392 + MetricType: "rate",
4393 + IsTable: true,
4394 + },
4395 + // From second config
4396 + {
4397 + Name: "ifInErrors",
4398 + Value: 12,
4399 + Tags: map[string]string{"interface": "eth0"},
4400 + MetricType: "rate",
4401 + IsTable: true,
4402 + },
4403 + {
4404 + Name: "ifOutErrors",
4405 + Value: 6,
4406 + Tags: map[string]string{"interface": "eth0"},
4407 + MetricType: "rate",
4408 + IsTable: true,
4409 + },
4410 + },
4411 + },
4412 + },
4413 + expectedError: false,
4414 + enableCache: true,
4415 + cacheTTL: 30 * time.Second,
4416 + collectCount: 2,
4417 + sleepBetween: 10 * time.Millisecond,
4418 + },
4419 + }
4420 +
4421 + for name, tc := range tests {
4422 + t.Run(name, func(t *testing.T) {
4423 + ctrl := gomock.NewController(t)
4424 + defer ctrl.Finish()
4425 +
4426 + mockHandler := snmpmock.NewMockHandler(ctrl)
4427 + tc.setupMock(mockHandler)
4428 +
4429 + collector := New(mockHandler, tc.profiles, logger.New())
4430 + collector.DoTableMetrics = true
4431 +
4432 + // Configure cache based on test requirements
4433 + if tc.enableCache {
4434 + collector.tableCache.setTTL(tc.cacheTTL, 0)
4435 + } else {
4436 + collector.tableCache.setTTL(0, 0) // Disable cache
4437 + }
4438 +
4439 + var result []*ProfileMetrics
4440 + var err error
4441 +
4442 + // Perform multiple collections to test caching behavior
4443 + for i := 0; i < tc.collectCount; i++ {
4444 + if i > 0 && tc.sleepBetween > 0 {
4445 + time.Sleep(tc.sleepBetween)
4446 + }
4447 +
4448 + result, err = collector.Collect()
4449 +
4450 + // For intermediate collections, just verify no error
4451 + if i < tc.collectCount-1 {
4452 + assert.NoError(t, err)
4453 + }
4454 + }
4455 +
4456 + // Clear circular references in final result
4457 + for _, profile := range result {
4458 + for i := range profile.Metrics {
4459 + profile.Metrics[i].Profile = nil
4460 + }
4461 + }
4462 +
4463 + if tc.expectedError {
4464 + assert.Error(t, err)
4465 + if tc.errorContains != "" {
4466 + assert.Contains(t, err.Error(), tc.errorContains)
4467 + }
4468 + } else {
4469 + assert.NoError(t, err)
4470 + }
4471 +
4472 + if tc.expectedResult != nil {
4473 + require.Equal(t, len(tc.expectedResult), len(result))
4474 + for i := range tc.expectedResult {
4475 + assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
4476 + assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
4477 + assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
4478 + }
4479 + } else {
4480 + assert.Nil(t, result)
4481 + }
4482 + })
4483 + }
4484 +}
4485 +
4486 func mustCompileRegex(pattern string) *regexp.Regexp {
4487 re, err := regexp.Compile(pattern)
4488 if err != nil {