@cryptotaxi247 / netdata / commits / f0d0990ee

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 }