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 {