chore(go.d/ddsnmp): collect cross-table metrics and tags (#20481)
Ilya Mashchenko committed
Jun 13, 2025 at 17:05 UTC
f0d0990eecfd76c6f618102b1519928968169a53
2 files changed
+607
-266
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_table.go
+67
-85
@@ -14,6 +14,7 @@ import (
14
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
)
16
17
+// tableWalkResult holds the walked data for a single table
18
type tableWalkResult struct {
19
tableOID string
20
pdus map[string]gosnmp.SnmpPDU
@@ -21,14 +22,17 @@ type tableWalkResult struct {
22
}
23
24
func (c *Collector) collectTableMetrics(prof *ddsnmp.Profile) ([]Metric, error) {
25
+ // Phase 1: Walk all tables and collect raw data
26
walkResults, err := c.walkAllTables(prof)
27
if err != nil {
28
return nil, err
29
}
30
31
+ // Phase 2: Process walked data into metrics
32
return c.processTableWalkResults(walkResults)
33
}
34
35
+// Phase 1: Walk all tables
36
func (c *Collector) walkAllTables(prof *ddsnmp.Profile) ([]tableWalkResult, error) {
37
var results []tableWalkResult
38
var errs []error
@@ -48,15 +52,9 @@ func (c *Collector) walkAllTables(prof *ddsnmp.Profile) ([]tableWalkResult, erro
52
53
doneOids[cfg.Table.OID] = true
54
51
- // Check if we should skip this table
55
+ // Check if we should skip this table (only skip for index transforms now)
56
skipTable := false
53
-
57
for _, tagCfg := range cfg.MetricTags {
55
- if tagCfg.Table != "" && tagCfg.Table != cfg.Table.Name {
56
- c.log.Debugf("Skipping table %s: has cross-table tag from %s", cfg.Table.Name, tagCfg.Table)
57
- skipTable = true
58
- break
59
- }
58
if len(tagCfg.IndexTransform) > 0 {
59
c.log.Debugf("Skipping table %s: has index transformation", cfg.Table.Name)
60
skipTable = true
@@ -68,6 +66,7 @@ func (c *Collector) walkAllTables(prof *ddsnmp.Profile) ([]tableWalkResult, erro
66
continue
67
}
68
69
+ // Walk the table
70
pdus, err := c.snmpWalk(cfg.Table.OID)
71
if err != nil {
72
errs = append(errs, fmt.Errorf("failed to walk table '%s': %w", cfg.Table.Name, err))
@@ -94,6 +93,7 @@ func (c *Collector) walkAllTables(prof *ddsnmp.Profile) ([]tableWalkResult, erro
93
return results, nil
94
}
95
96
+// Phase 2: Process walked data
97
func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]Metric, error) {
98
var metrics []Metric
99
var errs []error
@@ -104,9 +104,17 @@ func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]Me
104
walkedData[result.tableOID] = result.pdus
105
}
106
107
+ // Build a map of table name to OID for cross-table lookups
108
+ tableNameToOID := make(map[string]string)
109
+ for _, result := range walkResults {
110
+ if result.config.Table.Name != "" {
111
+ tableNameToOID[result.config.Table.Name] = result.tableOID
112
+ }
113
+ }
114
+
115
// Process each table's walked data
116
for _, result := range walkResults {
109
- tableMetrics, err := c.processTableData(result.config, result.pdus, walkedData)
117
+ tableMetrics, err := c.processTableData(result.config, result.pdus, walkedData, tableNameToOID)
118
if err != nil {
119
errs = append(errs, fmt.Errorf("table '%s': %w", result.config.Table.Name, err))
120
continue
@@ -121,7 +129,8 @@ func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]Me
129
return metrics, nil
130
}
131
124
-func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU, allWalkedData map[string]map[string]gosnmp.SnmpPDU) ([]Metric, error) {
132
+// Process a single table's data
133
+func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU, allWalkedData map[string]map[string]gosnmp.SnmpPDU, tableNameToOID map[string]string) ([]Metric, error) {
134
// Try to use cache if available
135
if cachedOIDs, cachedTags, ok := c.tableCache.getCachedData(cfg.Table.OID); ok {
136
metrics, err := c.collectTableWithCache(cfg, cachedOIDs, cachedTags, buildColumnOIDs(cfg))
@@ -197,6 +206,55 @@ func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus
206
}
207
}
208
209
+ // Process cross-table tags
210
+ for _, tagCfg := range cfg.MetricTags {
211
+ // Skip if not a cross-table tag
212
+ if tagCfg.Table == "" || tagCfg.Table == cfg.Table.Name {
213
+ continue
214
+ }
215
+
216
+ // Skip if has index transformation (not supported yet)
217
+ if len(tagCfg.IndexTransform) > 0 {
218
+ continue
219
+ }
220
+
221
+ // Find the referenced table's OID
222
+ refTableOID, ok := tableNameToOID[tagCfg.Table]
223
+ if !ok {
224
+ c.log.Debugf("Cannot find table OID for referenced table %s", tagCfg.Table)
225
+ continue
226
+ }
227
+
228
+ // Get the walked data for the referenced table
229
+ refTablePDUs, ok := allWalkedData[refTableOID]
230
+ if !ok {
231
+ c.log.Debugf("No walked data for referenced table %s (OID: %s)", tagCfg.Table, refTableOID)
232
+ continue
233
+ }
234
+
235
+ // Look up the value from the referenced table using the same index
236
+ refColumnOID := trimOID(tagCfg.Symbol.OID)
237
+ refFullOID := refColumnOID + "." + index
238
+
239
+ pdu, ok := refTablePDUs[refFullOID]
240
+ if !ok {
241
+ c.log.Debugf("Cannot find cross-table tag value at OID %s for table %s", refFullOID, tagCfg.Table)
242
+ continue
243
+ }
244
+
245
+ // Process the cross-table tag value
246
+ tags, err := processTableMetricTagValue(tagCfg, pdu)
247
+ if err != nil {
248
+ c.log.Debugf("Error processing cross-table tag %s from table %s: %v", tagCfg.Tag, tagCfg.Table, err)
249
+ continue
250
+ }
251
+
252
+ for k, v := range tags {
253
+ rowTags[k] = v
254
+ tagCache[index][k] = v
255
+ }
256
+ }
257
+
258
// Process metrics for this row
259
for columnOID, sym := range columnOIDs {
260
pdu, ok := rowPDUs[columnOID]
@@ -400,79 +458,3 @@ func (c *Collector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error) {
458
459
return pdus, nil
460
}
403
-
404
-func (c *Collector) analyzeTableDependencies(prof *ddsnmp.Profile) map[string][]string {
405
- deps := make(map[string][]string)
406
-
407
- // Build a map of table name to OID for quick lookup
408
- tableNameToOID := make(map[string]string)
409
- for _, cfg := range prof.Definition.Metrics {
410
- if cfg.Table.OID != "" {
411
- tableNameToOID[cfg.Table.Name] = cfg.Table.OID
412
- }
413
- }
414
-
415
- // Analyze each metric configuration
416
- for _, cfg := range prof.Definition.Metrics {
417
- if cfg.Table.OID == "" {
418
- continue
419
- }
420
-
421
- mainTableOID := cfg.Table.OID
422
- seenDeps := make(map[string]bool)
423
-
424
- // Find all tables referenced in metric tags
425
- for _, tagCfg := range cfg.MetricTags {
426
- // Check if this tag references a different table
427
- if tagCfg.Table != "" && tagCfg.Table != cfg.Table.Name {
428
- // Skip if uses index transformation (Phase 5)
429
- if len(tagCfg.IndexTransform) > 0 {
430
- c.log.Debugf("Table %s has cross-table tag with index transformation from %s (not supported yet)",
431
- cfg.Table.Name, tagCfg.Table)
432
- continue
433
- }
434
-
435
- // Find the OID for the referenced table
436
- if refTableOID, ok := tableNameToOID[tagCfg.Table]; ok {
437
- if !seenDeps[refTableOID] {
438
- deps[mainTableOID] = append(deps[mainTableOID], refTableOID)
439
- seenDeps[refTableOID] = true
440
- }
441
- } else {
442
- c.log.Debugf("Table %s references unknown table %s in metric tags",
443
- cfg.Table.Name, tagCfg.Table)
444
- }
445
- }
446
- }
447
- }
448
-
449
- // Log the dependencies for debugging
450
- for tableOID, depList := range deps {
451
- if len(depList) > 0 {
452
- c.log.Debugf("Table %s depends on tables: %v", tableOID, depList)
453
- }
454
- }
455
-
456
- return deps
457
-}
458
-
459
-// findTableOIDByName searches through the profile to find a table's OID given its name
460
-func (c *Collector) findTableOIDByName(prof *ddsnmp.Profile, tableName string) string {
461
- for _, cfg := range prof.Definition.Metrics {
462
- if cfg.Table.Name == tableName {
463
- return cfg.Table.OID
464
- }
465
- }
466
- return ""
467
-}
468
-
469
-// getConfigsForTable returns all metric configs that define metrics for a given table OID
470
-func (c *Collector) getConfigsForTable(prof *ddsnmp.Profile, tableOID string) []ddprofiledefinition.MetricsConfig {
471
- var configs []ddprofiledefinition.MetricsConfig
472
- for _, cfg := range prof.Definition.Metrics {
473
- if cfg.Table.OID == tableOID {
474
- configs = append(configs, cfg)
475
- }
476
- }
477
- return configs
478
-}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
+540
-181
@@ -1317,228 +1317,563 @@ func TestCollector_Collect(t *testing.T) {
1317
},
1318
expectedError: false,
1319
},
1320
- }
1321
-
1322
- for name, tc := range tests {
1323
- t.Run(name, func(t *testing.T) {
1324
- ctrl := gomock.NewController(t)
1325
- defer ctrl.Finish()
1326
-
1327
- mockHandler := snmpmock.NewMockHandler(ctrl)
1328
- tc.setupMock(mockHandler)
1329
-
1330
- collector := New(mockHandler, tc.profiles, logger.New())
1331
- collector.doTableMetrics = true
1332
- collector.tableCache.setTTL(0, 0)
1333
-
1334
- result, err := collector.Collect()
1335
-
1336
- // The Metric struct has a Profile field that contains a pointer to ProfileMetrics,
1337
- // which itself contains the Metrics slice.
1338
- // This creates a circular reference that makes ElementsMatch fail.
1339
- for _, profile := range result {
1340
- for i := range profile.Metrics {
1341
- profile.Metrics[i].Profile = nil
1342
- }
1343
- }
1344
-
1345
- if tc.expectedError {
1346
- assert.Error(t, err)
1347
- if tc.errorContains != "" {
1348
- assert.Contains(t, err.Error(), tc.errorContains)
1349
- }
1350
- } else {
1351
- assert.NoError(t, err)
1352
- }
1353
-
1354
- if tc.expectedResult != nil {
1355
- require.Equal(t, len(tc.expectedResult), len(result))
1356
- for i := range tc.expectedResult {
1357
- assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
1358
- assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
1359
- }
1360
- } else {
1361
- assert.Nil(t, result)
1362
- }
1363
- })
1364
- }
1365
-}
1320
1367
-func TestAnalyzeTableDependencies(t *testing.T) {
1368
- tests := map[string]struct {
1369
- profile *ddsnmp.Profile
1370
- expected map[string][]string
1371
- }{
1372
- "simple cross-table reference": {
1373
- profile: &ddsnmp.Profile{
1374
- Definition: &ddprofiledefinition.ProfileDefinition{
1375
- Metrics: []ddprofiledefinition.MetricsConfig{
1376
- {
1377
- MIB: "CISCO-IF-EXTENSION-MIB",
1378
- Table: ddprofiledefinition.SymbolConfig{
1379
- OID: "1.3.6.1.4.1.9.9.276.1.1.2",
1380
- Name: "cieIfInterfaceTable",
1381
- },
1382
- Symbols: []ddprofiledefinition.SymbolConfig{
1383
- {OID: "1.3.6.1.4.1.9.9.276.1.1.2.1.1", Name: "cieIfResetCount"},
1384
- },
1385
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1386
- {
1387
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1388
- OID: "1.3.6.1.2.1.31.1.1.1.1",
1389
- Name: "ifName",
1321
+ "cross-table tags with same index": {
1322
+ profiles: []*ddsnmp.Profile{
1323
+ {
1324
+ SourceFile: "test-profile.yaml",
1325
+ Definition: &ddprofiledefinition.ProfileDefinition{
1326
+ Metrics: []ddprofiledefinition.MetricsConfig{
1327
+ {
1328
+ MIB: "CISCO-IF-EXTENSION-MIB",
1329
+ Table: ddprofiledefinition.SymbolConfig{
1330
+ OID: "1.3.6.1.4.1.9.9.276.1.1.2",
1331
+ Name: "cieIfInterfaceTable",
1332
+ },
1333
+ Symbols: []ddprofiledefinition.SymbolConfig{
1334
+ {
1335
+ OID: "1.3.6.1.4.1.9.9.276.1.1.2.1.1",
1336
+ Name: "cieIfResetCount",
1337
+ },
1338
+ },
1339
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1340
+ {
1341
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1342
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1343
+ Name: "ifName",
1344
+ },
1345
+ Table: "ifXTable",
1346
+ Tag: "interface",
1347
},
1391
- Table: "ifXTable",
1392
- Tag: "interface",
1348
},
1349
},
1395
- },
1396
- {
1397
- MIB: "IF-MIB",
1398
- Table: ddprofiledefinition.SymbolConfig{
1399
- OID: "1.3.6.1.2.1.31.1.1",
1400
- Name: "ifXTable",
1401
- },
1402
- Symbols: []ddprofiledefinition.SymbolConfig{
1403
- {OID: "1.3.6.1.2.1.31.1.1.1.18", Name: "ifAlias"},
1350
+ {
1351
+ MIB: "IF-MIB",
1352
+ Table: ddprofiledefinition.SymbolConfig{
1353
+ OID: "1.3.6.1.2.1.31.1.1",
1354
+ Name: "ifXTable",
1355
+ },
1356
+ Symbols: []ddprofiledefinition.SymbolConfig{
1357
+ // No symbols needed - this table is only used for cross-table tags
1358
+ },
1359
},
1360
},
1361
},
1362
},
1363
},
1409
- expected: map[string][]string{
1410
- "1.3.6.1.4.1.9.9.276.1.1.2": {"1.3.6.1.2.1.31.1.1"},
1364
+ setupMock: func(m *snmpmock.MockHandler) {
1365
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1366
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1367
+
1368
+ // Walk cieIfInterfaceTable
1369
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.9.9.276.1.1.2").Return(
1370
+ []gosnmp.SnmpPDU{
1371
+ {
1372
+ Name: "1.3.6.1.4.1.9.9.276.1.1.2.1.1.1",
1373
+ Type: gosnmp.Counter32,
1374
+ Value: uint(10),
1375
+ },
1376
+ {
1377
+ Name: "1.3.6.1.4.1.9.9.276.1.1.2.1.1.2",
1378
+ Type: gosnmp.Counter32,
1379
+ Value: uint(20),
1380
+ },
1381
+ }, nil,
1382
+ )
1383
+
1384
+ // Walk ifXTable
1385
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
1386
+ []gosnmp.SnmpPDU{
1387
+ // ifName values that will be used as tags
1388
+ {
1389
+ Name: "1.3.6.1.2.1.31.1.1.1.1.1",
1390
+ Type: gosnmp.OctetString,
1391
+ Value: []byte("GigabitEthernet0/1"),
1392
+ },
1393
+ {
1394
+ Name: "1.3.6.1.2.1.31.1.1.1.1.2",
1395
+ Type: gosnmp.OctetString,
1396
+ Value: []byte("GigabitEthernet0/2"),
1397
+ },
1398
+ }, nil,
1399
+ )
1400
},
1401
+ expectedResult: []*ProfileMetrics{
1402
+ {
1403
+ Source: "test-profile.yaml",
1404
+ DeviceMetadata: nil,
1405
+ Metrics: []Metric{
1406
+ {
1407
+ Name: "cieIfResetCount",
1408
+ Value: 10,
1409
+ Tags: map[string]string{"interface": "GigabitEthernet0/1"},
1410
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1411
+ IsTable: true,
1412
+ },
1413
+ {
1414
+ Name: "cieIfResetCount",
1415
+ Value: 20,
1416
+ Tags: map[string]string{"interface": "GigabitEthernet0/2"},
1417
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1418
+ IsTable: true,
1419
+ },
1420
+ },
1421
+ },
1422
+ },
1423
+ expectedError: false,
1424
},
1413
- "multiple dependencies": {
1414
- profile: &ddsnmp.Profile{
1415
- Definition: &ddprofiledefinition.ProfileDefinition{
1416
- Metrics: []ddprofiledefinition.MetricsConfig{
1417
- {
1418
- MIB: "MY-MIB",
1419
- Table: ddprofiledefinition.SymbolConfig{
1420
- OID: "1.3.6.1.4.1.1000.1",
1421
- Name: "myTable",
1422
- },
1423
- Symbols: []ddprofiledefinition.SymbolConfig{
1424
- {OID: "1.3.6.1.4.1.1000.1.1.1", Name: "myMetric"},
1425
- },
1426
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1427
- {
1428
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1429
- OID: "1.3.6.1.2.1.31.1.1.1.1",
1430
- Name: "ifName",
1425
+ "cross-table tags with missing referenced table": {
1426
+ profiles: []*ddsnmp.Profile{
1427
+ {
1428
+ SourceFile: "test-profile.yaml",
1429
+ Definition: &ddprofiledefinition.ProfileDefinition{
1430
+ Metrics: []ddprofiledefinition.MetricsConfig{
1431
+ {
1432
+ MIB: "MY-MIB",
1433
+ Table: ddprofiledefinition.SymbolConfig{
1434
+ OID: "1.3.6.1.4.1.1000.1",
1435
+ Name: "myTable",
1436
+ },
1437
+ Symbols: []ddprofiledefinition.SymbolConfig{
1438
+ {
1439
+ OID: "1.3.6.1.4.1.1000.1.1.1",
1440
+ Name: "myMetric",
1441
},
1432
- Table: "ifXTable",
1433
- Tag: "interface",
1442
},
1435
- {
1436
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1437
- OID: "1.3.6.1.4.1.2000.1.1.1",
1438
- Name: "otherName",
1443
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1444
+ {
1445
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1446
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1447
+ Name: "ifName",
1448
+ },
1449
+ Table: "ifXTable", // This table is not defined
1450
+ Tag: "interface",
1451
},
1440
- Table: "otherTable",
1441
- Tag: "other_name",
1452
},
1453
},
1454
},
1455
+ },
1456
+ },
1457
+ },
1458
+ setupMock: func(m *snmpmock.MockHandler) {
1459
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1460
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1461
+
1462
+ // Walk myTable
1463
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
1464
+ []gosnmp.SnmpPDU{
1465
{
1446
- MIB: "IF-MIB",
1447
- Table: ddprofiledefinition.SymbolConfig{
1448
- OID: "1.3.6.1.2.1.31.1.1",
1449
- Name: "ifXTable",
1450
- },
1451
- Symbols: []ddprofiledefinition.SymbolConfig{
1452
- {OID: "1.3.6.1.2.1.31.1.1.1.18", Name: "ifAlias"},
1453
- },
1466
+ Name: "1.3.6.1.4.1.1000.1.1.1.1",
1467
+ Type: gosnmp.Gauge32,
1468
+ Value: uint(100),
1469
},
1470
+ }, nil,
1471
+ )
1472
+ },
1473
+ expectedResult: []*ProfileMetrics{
1474
+ {
1475
+ Source: "test-profile.yaml",
1476
+ DeviceMetadata: nil,
1477
+ Metrics: []Metric{
1478
{
1456
- MIB: "OTHER-MIB",
1457
- Table: ddprofiledefinition.SymbolConfig{
1458
- OID: "1.3.6.1.4.1.2000.1",
1459
- Name: "otherTable",
1479
+ Name: "myMetric",
1480
+ Value: 100,
1481
+ Tags: nil, // No cross-table tag because referenced table is missing
1482
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1483
+ IsTable: true,
1484
+ },
1485
+ },
1486
+ },
1487
+ },
1488
+ expectedError: false,
1489
+ },
1490
+ "cross-table tags with missing value in referenced table": {
1491
+ profiles: []*ddsnmp.Profile{
1492
+ {
1493
+ SourceFile: "test-profile.yaml",
1494
+ Definition: &ddprofiledefinition.ProfileDefinition{
1495
+ Metrics: []ddprofiledefinition.MetricsConfig{
1496
+ {
1497
+ MIB: "MY-MIB",
1498
+ Table: ddprofiledefinition.SymbolConfig{
1499
+ OID: "1.3.6.1.4.1.1000.1",
1500
+ Name: "myTable",
1501
+ },
1502
+ Symbols: []ddprofiledefinition.SymbolConfig{
1503
+ {
1504
+ OID: "1.3.6.1.4.1.1000.1.1.1",
1505
+ Name: "myMetric",
1506
+ },
1507
+ },
1508
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1509
+ {
1510
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1511
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1512
+ Name: "ifName",
1513
+ },
1514
+ Table: "ifXTable",
1515
+ Tag: "interface",
1516
+ },
1517
+ },
1518
},
1461
- Symbols: []ddprofiledefinition.SymbolConfig{
1462
- {OID: "1.3.6.1.4.1.2000.1.1.2", Name: "otherMetric"},
1519
+ {
1520
+ MIB: "IF-MIB",
1521
+ Table: ddprofiledefinition.SymbolConfig{
1522
+ OID: "1.3.6.1.2.1.31.1.1",
1523
+ Name: "ifXTable",
1524
+ },
1525
+ Symbols: []ddprofiledefinition.SymbolConfig{
1526
+ // No symbols needed - this table is only used for cross-table tags
1527
+ },
1528
},
1529
},
1530
},
1531
},
1532
},
1468
- expected: map[string][]string{
1469
- "1.3.6.1.4.1.1000.1": {"1.3.6.1.2.1.31.1.1", "1.3.6.1.4.1.2000.1"},
1533
+ setupMock: func(m *snmpmock.MockHandler) {
1534
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1535
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1536
+
1537
+ // Walk myTable - has rows 1 and 2
1538
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
1539
+ []gosnmp.SnmpPDU{
1540
+ {
1541
+ Name: "1.3.6.1.4.1.1000.1.1.1.1",
1542
+ Type: gosnmp.Gauge32,
1543
+ Value: uint(100),
1544
+ },
1545
+ {
1546
+ Name: "1.3.6.1.4.1.1000.1.1.1.2",
1547
+ Type: gosnmp.Gauge32,
1548
+ Value: uint(200),
1549
+ },
1550
+ }, nil,
1551
+ )
1552
+
1553
+ // Walk ifXTable - only has ifName for row 1, missing row 2
1554
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
1555
+ []gosnmp.SnmpPDU{
1556
+ {
1557
+ Name: "1.3.6.1.2.1.31.1.1.1.1.1",
1558
+ Type: gosnmp.OctetString,
1559
+ Value: []byte("eth0"),
1560
+ },
1561
+ // Missing ifName for index 2
1562
+ }, nil,
1563
+ )
1564
},
1565
+ expectedResult: []*ProfileMetrics{
1566
+ {
1567
+ Source: "test-profile.yaml",
1568
+ DeviceMetadata: nil,
1569
+ Metrics: []Metric{
1570
+ {
1571
+ Name: "myMetric",
1572
+ Value: 100,
1573
+ Tags: map[string]string{"interface": "eth0"},
1574
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1575
+ IsTable: true,
1576
+ },
1577
+ {
1578
+ Name: "myMetric",
1579
+ Value: 200,
1580
+ Tags: nil, // No tag because ifName is missing for this index
1581
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1582
+ IsTable: true,
1583
+ },
1584
+ },
1585
+ },
1586
+ },
1587
+ expectedError: false,
1588
},
1472
- "skip index transformation": {
1473
- profile: &ddsnmp.Profile{
1474
- Definition: &ddprofiledefinition.ProfileDefinition{
1475
- Metrics: []ddprofiledefinition.MetricsConfig{
1476
- {
1477
- MIB: "MY-MIB",
1478
- Table: ddprofiledefinition.SymbolConfig{
1479
- OID: "1.3.6.1.4.1.1000.1",
1480
- Name: "myTable",
1481
- },
1482
- Symbols: []ddprofiledefinition.SymbolConfig{
1483
- {OID: "1.3.6.1.4.1.1000.1.1.1", Name: "myMetric"},
1484
- },
1485
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1486
- {
1487
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1488
- OID: "1.3.6.1.2.1.31.1.1.1.1",
1489
- Name: "ifName",
1589
+ "cross-table tags with mapping": {
1590
+ profiles: []*ddsnmp.Profile{
1591
+ {
1592
+ SourceFile: "test-profile.yaml",
1593
+ Definition: &ddprofiledefinition.ProfileDefinition{
1594
+ Metrics: []ddprofiledefinition.MetricsConfig{
1595
+ {
1596
+ MIB: "MY-MIB",
1597
+ Table: ddprofiledefinition.SymbolConfig{
1598
+ OID: "1.3.6.1.4.1.1000.1",
1599
+ Name: "myTable",
1600
+ },
1601
+ Symbols: []ddprofiledefinition.SymbolConfig{
1602
+ {
1603
+ OID: "1.3.6.1.4.1.1000.1.1.1",
1604
+ Name: "myMetric",
1605
+ },
1606
+ },
1607
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1608
+ {
1609
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1610
+ OID: "1.3.6.1.2.1.2.2.1.3",
1611
+ Name: "ifType",
1612
+ },
1613
+ Table: "ifTable",
1614
+ Tag: "if_type",
1615
+ Mapping: map[string]string{
1616
+ "6": "ethernet",
1617
+ "71": "wifi",
1618
+ },
1619
},
1491
- Table: "ifXTable",
1492
- Tag: "interface",
1493
- IndexTransform: []ddprofiledefinition.MetricIndexTransform{
1494
- {Start: 1, End: 5},
1620
+ },
1621
+ },
1622
+ {
1623
+ MIB: "IF-MIB",
1624
+ Table: ddprofiledefinition.SymbolConfig{
1625
+ OID: "1.3.6.1.2.1.2.2",
1626
+ Name: "ifTable",
1627
+ },
1628
+ Symbols: []ddprofiledefinition.SymbolConfig{
1629
+ {
1630
+ OID: "1.3.6.1.2.1.2.2.1.10",
1631
+ Name: "ifInOctets",
1632
},
1633
},
1634
},
1635
},
1636
+ },
1637
+ },
1638
+ },
1639
+ setupMock: func(m *snmpmock.MockHandler) {
1640
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1641
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1642
+
1643
+ // Walk myTable
1644
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
1645
+ []gosnmp.SnmpPDU{
1646
+ {
1647
+ Name: "1.3.6.1.4.1.1000.1.1.1.1",
1648
+ Type: gosnmp.Gauge32,
1649
+ Value: uint(100),
1650
+ },
1651
{
1500
- MIB: "IF-MIB",
1501
- Table: ddprofiledefinition.SymbolConfig{
1502
- OID: "1.3.6.1.2.1.31.1.1",
1503
- Name: "ifXTable",
1504
- },
1505
- Symbols: []ddprofiledefinition.SymbolConfig{
1506
- {OID: "1.3.6.1.2.1.31.1.1.1.18", Name: "ifAlias"},
1652
+ Name: "1.3.6.1.4.1.1000.1.1.1.2",
1653
+ Type: gosnmp.Gauge32,
1654
+ Value: uint(200),
1655
+ },
1656
+ }, nil,
1657
+ )
1658
+
1659
+ // Walk ifTable
1660
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1661
+ []gosnmp.SnmpPDU{
1662
+ {
1663
+ Name: "1.3.6.1.2.1.2.2.1.3.1",
1664
+ Type: gosnmp.Integer,
1665
+ Value: 6, // ethernet
1666
+ },
1667
+ {
1668
+ Name: "1.3.6.1.2.1.2.2.1.3.2",
1669
+ Type: gosnmp.Integer,
1670
+ Value: 71, // wifi
1671
+ },
1672
+ {
1673
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
1674
+ Type: gosnmp.Counter32,
1675
+ Value: uint(1000),
1676
+ },
1677
+ }, nil,
1678
+ )
1679
+ },
1680
+ expectedResult: []*ProfileMetrics{
1681
+ {
1682
+ Source: "test-profile.yaml",
1683
+ DeviceMetadata: nil,
1684
+ Metrics: []Metric{
1685
+ {
1686
+ Name: "myMetric",
1687
+ Value: 100,
1688
+ Tags: map[string]string{"if_type": "ethernet"},
1689
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1690
+ IsTable: true,
1691
+ },
1692
+ {
1693
+ Name: "myMetric",
1694
+ Value: 200,
1695
+ Tags: map[string]string{"if_type": "wifi"},
1696
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1697
+ IsTable: true,
1698
+ },
1699
+ {
1700
+ Name: "ifInOctets",
1701
+ Value: 1000,
1702
+ Tags: nil,
1703
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1704
+ IsTable: true,
1705
+ },
1706
+ },
1707
+ },
1708
+ },
1709
+ expectedError: false,
1710
+ },
1711
+ "cross-table tags with index transformation should be skipped": {
1712
+ profiles: []*ddsnmp.Profile{
1713
+ {
1714
+ SourceFile: "test-profile.yaml",
1715
+ Definition: &ddprofiledefinition.ProfileDefinition{
1716
+ Metrics: []ddprofiledefinition.MetricsConfig{
1717
+ {
1718
+ MIB: "MY-MIB",
1719
+ Table: ddprofiledefinition.SymbolConfig{
1720
+ OID: "1.3.6.1.4.1.1000.1",
1721
+ Name: "myTable",
1722
+ },
1723
+ Symbols: []ddprofiledefinition.SymbolConfig{
1724
+ {
1725
+ OID: "1.3.6.1.4.1.1000.1.1.1",
1726
+ Name: "myMetric",
1727
+ },
1728
+ },
1729
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1730
+ {
1731
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1732
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1733
+ Name: "ifName",
1734
+ },
1735
+ Table: "ifXTable",
1736
+ Tag: "interface",
1737
+ IndexTransform: []ddprofiledefinition.MetricIndexTransform{
1738
+ {Start: 1, End: 3},
1739
+ },
1740
+ },
1741
+ },
1742
},
1743
},
1744
},
1745
},
1746
},
1512
- expected: map[string][]string{}, // Should skip due to index transformation
1747
+ setupMock: func(m *snmpmock.MockHandler) {
1748
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1749
+ // Table should be skipped due to index transformation
1750
+ },
1751
+ expectedResult: []*ProfileMetrics{
1752
+ {
1753
+ Source: "test-profile.yaml",
1754
+ DeviceMetadata: nil,
1755
+ Metrics: []Metric{},
1756
+ },
1757
+ },
1758
+ expectedError: false,
1759
},
1514
- "no cross-table tags": {
1515
- profile: &ddsnmp.Profile{
1516
- Definition: &ddprofiledefinition.ProfileDefinition{
1517
- Metrics: []ddprofiledefinition.MetricsConfig{
1518
- {
1519
- MIB: "IF-MIB",
1520
- Table: ddprofiledefinition.SymbolConfig{
1521
- OID: "1.3.6.1.2.1.2.2",
1522
- Name: "ifTable",
1760
+ "multiple cross-table tags from different tables": {
1761
+ profiles: []*ddsnmp.Profile{
1762
+ {
1763
+ SourceFile: "test-profile.yaml",
1764
+ Definition: &ddprofiledefinition.ProfileDefinition{
1765
+ Metrics: []ddprofiledefinition.MetricsConfig{
1766
+ {
1767
+ MIB: "MY-MIB",
1768
+ Table: ddprofiledefinition.SymbolConfig{
1769
+ OID: "1.3.6.1.4.1.1000.1",
1770
+ Name: "myTable",
1771
+ },
1772
+ Symbols: []ddprofiledefinition.SymbolConfig{
1773
+ {
1774
+ OID: "1.3.6.1.4.1.1000.1.1.1",
1775
+ Name: "myMetric",
1776
+ },
1777
+ },
1778
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1779
+ {
1780
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1781
+ OID: "1.3.6.1.2.1.31.1.1.1.1",
1782
+ Name: "ifName",
1783
+ },
1784
+ Table: "ifXTable",
1785
+ Tag: "interface",
1786
+ },
1787
+ {
1788
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1789
+ OID: "1.3.6.1.2.1.2.2.1.2",
1790
+ Name: "ifDescr",
1791
+ },
1792
+ Table: "ifTable",
1793
+ Tag: "description",
1794
+ },
1795
+ },
1796
},
1524
- Symbols: []ddprofiledefinition.SymbolConfig{
1525
- {OID: "1.3.6.1.2.1.2.2.1.10", Name: "ifInOctets"},
1797
+ {
1798
+ MIB: "IF-MIB",
1799
+ Table: ddprofiledefinition.SymbolConfig{
1800
+ OID: "1.3.6.1.2.1.31.1.1",
1801
+ Name: "ifXTable",
1802
+ },
1803
+ Symbols: []ddprofiledefinition.SymbolConfig{
1804
+ // No symbols needed - this table is only used for cross-table tags
1805
+ },
1806
},
1527
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1528
- {
1529
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1530
- OID: "1.3.6.1.2.1.2.2.1.2",
1531
- Name: "ifDescr",
1532
- },
1533
- Tag: "interface",
1534
- // No Table field means same table
1807
+ {
1808
+ MIB: "IF-MIB",
1809
+ Table: ddprofiledefinition.SymbolConfig{
1810
+ OID: "1.3.6.1.2.1.2.2",
1811
+ Name: "ifTable",
1812
+ },
1813
+ Symbols: []ddprofiledefinition.SymbolConfig{
1814
+ // No symbols needed - this table is only used for cross-table tags
1815
},
1816
},
1817
},
1818
},
1819
},
1820
},
1541
- expected: map[string][]string{},
1821
+ setupMock: func(m *snmpmock.MockHandler) {
1822
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1823
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1824
+
1825
+ // Walk myTable
1826
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
1827
+ []gosnmp.SnmpPDU{
1828
+ {
1829
+ Name: "1.3.6.1.4.1.1000.1.1.1.1",
1830
+ Type: gosnmp.Gauge32,
1831
+ Value: uint(100),
1832
+ },
1833
+ }, nil,
1834
+ )
1835
+
1836
+ // Walk ifXTable
1837
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
1838
+ []gosnmp.SnmpPDU{
1839
+ {
1840
+ Name: "1.3.6.1.2.1.31.1.1.1.1.1",
1841
+ Type: gosnmp.OctetString,
1842
+ Value: []byte("GigabitEthernet0/1"),
1843
+ },
1844
+ }, nil,
1845
+ )
1846
+
1847
+ // Walk ifTable
1848
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1849
+ []gosnmp.SnmpPDU{
1850
+ {
1851
+ Name: "1.3.6.1.2.1.2.2.1.2.1",
1852
+ Type: gosnmp.OctetString,
1853
+ Value: []byte("GigE0/1"),
1854
+ },
1855
+ }, nil,
1856
+ )
1857
+ },
1858
+ expectedResult: []*ProfileMetrics{
1859
+ {
1860
+ Source: "test-profile.yaml",
1861
+ DeviceMetadata: nil,
1862
+ Metrics: []Metric{
1863
+ {
1864
+ Name: "myMetric",
1865
+ Value: 100,
1866
+ Tags: map[string]string{
1867
+ "interface": "GigabitEthernet0/1",
1868
+ "description": "GigE0/1",
1869
+ },
1870
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1871
+ IsTable: true,
1872
+ },
1873
+ },
1874
+ },
1875
+ },
1876
+ expectedError: false,
1877
},
1878
}
1879
@@ -1548,16 +1883,40 @@ func TestAnalyzeTableDependencies(t *testing.T) {
1883
defer ctrl.Finish()
1884
1885
mockHandler := snmpmock.NewMockHandler(ctrl)
1551
- collector := New(mockHandler, []*ddsnmp.Profile{tc.profile}, logger.New())
1886
+ tc.setupMock(mockHandler)
1887
+
1888
+ collector := New(mockHandler, tc.profiles, logger.New())
1889
+ collector.doTableMetrics = true
1890
+ collector.tableCache.setTTL(0, 0)
1891
1553
- deps := collector.analyzeTableDependencies(tc.profile)
1892
+ result, err := collector.Collect()
1893
1555
- assert.Equal(t, len(tc.expected), len(deps), "Wrong number of tables with dependencies")
1894
+ // The Metric struct has a Profile field that contains a pointer to ProfileMetrics,
1895
+ // which itself contains the Metrics slice.
1896
+ // This creates a circular reference that makes ElementsMatch fail.
1897
+ for _, profile := range result {
1898
+ for i := range profile.Metrics {
1899
+ profile.Metrics[i].Profile = nil
1900
+ }
1901
+ }
1902
1557
- for tableOID, expectedDeps := range tc.expected {
1558
- actualDeps, ok := deps[tableOID]
1559
- assert.True(t, ok, "Missing dependencies for table %s", tableOID)
1560
- assert.ElementsMatch(t, expectedDeps, actualDeps, "Wrong dependencies for table %s", tableOID)
1903
+ if tc.expectedError {
1904
+ assert.Error(t, err)
1905
+ if tc.errorContains != "" {
1906
+ assert.Contains(t, err.Error(), tc.errorContains)
1907
+ }
1908
+ } else {
1909
+ assert.NoError(t, err)
1910
+ }
1911
+
1912
+ if tc.expectedResult != nil {
1913
+ require.Equal(t, len(tc.expectedResult), len(result))
1914
+ for i := range tc.expectedResult {
1915
+ assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
1916
+ assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
1917
+ }
1918
+ } else {
1919
+ assert.Nil(t, result)
1920
}
1921
})
1922
}