@cryptotaxi247 / netdata / commits / c1fd278ba

chore(go.d/ddsnmp): fix table collection with caching (#20509)

Ilya Mashchenko committed Jun 18, 2025 at 00:31 UTC c1fd278ba64316aaa287eec2111d96493c70d222
2 files changed +340 -2587
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_table.go
+55 -33
@@ -22,18 +22,16 @@ 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)
25 + walkResults, err := c.walkTablesAsNeeded(prof)
26 if err != nil {
27 return nil, err
28 }
29
31 - // Phase 2: Process walked data into metrics
30 return c.processTableWalkResults(walkResults)
31 }
32
35 -// Phase 1: Walk all tables
36 -func (c *Collector) walkAllTables(prof *ddsnmp.Profile) ([]tableWalkResult, error) {
33 +// Phase 1: Walk only tables that aren't fully cached
34 +func (c *Collector) walkTablesAsNeeded(prof *ddsnmp.Profile) ([]tableWalkResult, error) {
35 var results []tableWalkResult
36 var errs []error
37 var missingOIDs []string
@@ -44,7 +42,10 @@ func (c *Collector) walkAllTables(prof *ddsnmp.Profile) ([]tableWalkResult, erro
42 // Map to track which tables need to be walked
43 tablesToWalk := make(map[string]bool)
44
47 - // First pass: identify unique tables to walk
45 + // Map configs by table OID
46 + tableConfigs := make(map[string][]ddprofiledefinition.MetricsConfig)
47 +
48 + // First pass: identify tables and check cache status
49 for _, cfg := range prof.Definition.Metrics {
50 if cfg.IsScalar() || cfg.Table.OID == "" {
51 continue
@@ -56,10 +57,16 @@ func (c *Collector) walkAllTables(prof *ddsnmp.Profile) ([]tableWalkResult, erro
57 continue
58 }
59
59 - tablesToWalk[tableOID] = true
60 + tableConfigs[tableOID] = append(tableConfigs[tableOID], cfg)
61 +
62 + if !c.tableCache.isConfigCached(cfg) {
63 + tablesToWalk[tableOID] = true
64 + }
65 }
66
62 - // Walk each unique table once
67 + // Walk each table that needs it (only once per table)
68 + c.log.Debugf("Tables walking %d cached %d (%d total)",
69 + len(tablesToWalk), len(tableConfigs)-len(tablesToWalk), len(tableConfigs))
70 for tableOID := range tablesToWalk {
71 pdus, err := c.snmpWalk(tableOID)
72 if err != nil {
@@ -72,22 +79,31 @@ func (c *Collector) walkAllTables(prof *ddsnmp.Profile) ([]tableWalkResult, erro
79 }
80 }
81
75 - // Second pass: create results for ALL metric configs
82 + // Second pass: create results for ALL configs
83 for _, cfg := range prof.Definition.Metrics {
84 if cfg.IsScalar() || cfg.Table.OID == "" {
85 continue
86 }
87
81 - pdus, ok := walkedTables[cfg.Table.OID]
82 - if !ok {
88 + if c.missingOIDs[trimOID(cfg.Table.OID)] {
89 continue
90 }
91
86 - results = append(results, tableWalkResult{
87 - tableOID: cfg.Table.OID,
88 - pdus: pdus,
89 - config: cfg,
90 - })
92 + // Add to results if we have walked data OR if it's cached
93 + if pdus, ok := walkedTables[cfg.Table.OID]; ok {
94 + results = append(results, tableWalkResult{
95 + tableOID: cfg.Table.OID,
96 + pdus: pdus,
97 + config: cfg,
98 + })
99 + } else if c.tableCache.isConfigCached(cfg) {
100 + // Add config without PDUs - will use cache in processing
101 + results = append(results, tableWalkResult{
102 + tableOID: cfg.Table.OID,
103 + pdus: nil,
104 + config: cfg,
105 + })
106 + }
107 }
108
109 if len(missingOIDs) > 0 {
@@ -109,7 +125,9 @@ func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]Me
125 // Build a map for quick lookup of walked data by table OID
126 walkedData := make(map[string]map[string]gosnmp.SnmpPDU)
127 for _, result := range walkResults {
112 - walkedData[result.tableOID] = result.pdus
128 + if result.pdus != nil {
129 + walkedData[result.tableOID] = result.pdus
130 + }
131 }
132
133 // Build a map of table name to OID for cross-table lookups
@@ -122,12 +140,27 @@ func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]Me
140
141 // Process each table's walked data
142 for _, result := range walkResults {
125 - tableMetrics, err := c.processTableData(result.config, result.pdus, walkedData, tableNameToOID)
126 - if err != nil {
127 - errs = append(errs, fmt.Errorf("table '%s': %w", result.config.Table.Name, err))
128 - continue
143 + // Try cache first
144 + if cachedOIDs, cachedTags, ok := c.tableCache.getCachedData(result.config); ok {
145 + columnOIDs := buildColumnOIDs(result.config)
146 + cacheMetrics, err := c.collectTableWithCache(result.config, cachedOIDs, cachedTags, columnOIDs)
147 + if err == nil {
148 + c.log.Debugf("Successfully collected table %s using cache", result.config.Table.Name)
149 + metrics = append(metrics, cacheMetrics...)
150 + continue
151 + }
152 + c.log.Debugf("Cached collection failed for table %s, falling back to process walked data: %v", result.config.Table.Name, err)
153 + }
154 +
155 + // Process walked data if available
156 + if result.pdus != nil {
157 + tableMetrics, err := c.processTableData(result.config, result.pdus, walkedData, tableNameToOID)
158 + if err != nil {
159 + errs = append(errs, fmt.Errorf("table '%s': %w", result.config.Table.Name, err))
160 + continue
161 + }
162 + metrics = append(metrics, tableMetrics...)
163 }
130 - metrics = append(metrics, tableMetrics...)
164 }
165
166 if len(metrics) == 0 && len(errs) > 0 {
@@ -139,17 +172,6 @@ func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]Me
172
173 // Process a single table's data
174 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) {
142 - // Try to use cache if available
143 - if cachedOIDs, cachedTags, ok := c.tableCache.getCachedData(cfg); ok {
144 - metrics, err := c.collectTableWithCache(cfg, cachedOIDs, cachedTags, buildColumnOIDs(cfg))
145 - if err == nil {
146 - c.log.Debugf("Successfully collected table %s using cache", cfg.Table.Name)
147 - return metrics, nil
148 - }
149 - c.log.Debugf("Cached collection failed for table %s, falling back to process walked data: %v", cfg.Table.Name, err)
150 - }
151 -
152 - // Process without cache
175 columnOIDs := buildColumnOIDs(cfg)
176 tagColumnOIDs := buildTagColumnOIDs(cfg)
177
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
+285 -2554
@@ -6,6 +6,7 @@ import (
6 "errors"
7 "regexp"
8 "testing"
9 + "time"
10
11 "github.com/golang/mock/gomock"
12 "github.com/gosnmp/gosnmp"
@@ -19,14 +20,14 @@ import (
20 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
21 )
22
22 -func TestCollector_Collect(t *testing.T) {
23 +func TestCollector_Collect_Group1_ScalarMetrics(t *testing.T) {
24 tests := map[string]struct {
24 - name string
25 profiles []*ddsnmp.Profile
26 setupMock func(m *snmpmock.MockHandler)
27 expectedResult []*ProfileMetrics
28 expectedError bool
29 errorContains string
30 + enableCache bool // Whether to enable cache for this test
31 }{
32 "successful collection with scalar metrics only": {
33 profiles: []*ddsnmp.Profile{
@@ -83,7 +84,7 @@ func TestCollector_Collect(t *testing.T) {
84 },
85 },
86 },
86 - expectedError: false, // Changed to false - partial success is not an error
87 + expectedError: false,
88 },
89 "successful collection with global tags": {
90 profiles: []*ddsnmp.Profile{
@@ -275,6 +276,124 @@ func TestCollector_Collect(t *testing.T) {
276 },
277 expectedError: false,
278 },
279 + "metric with extract_value": {
280 + profiles: []*ddsnmp.Profile{
281 + {
282 + SourceFile: "test-profile.yaml",
283 + Definition: &ddprofiledefinition.ProfileDefinition{
284 + Metrics: []ddprofiledefinition.MetricsConfig{
285 + {
286 + Symbol: ddprofiledefinition.SymbolConfig{
287 + OID: "1.3.6.1.4.1.12124.1.1.8",
288 + Name: "temperature",
289 + ExtractValueCompiled: mustCompileRegex(`(\d+)C`),
290 + },
291 + },
292 + },
293 + },
294 + },
295 + },
296 + setupMock: func(m *snmpmock.MockHandler) {
297 + m.EXPECT().MaxOids().Return(10).AnyTimes()
298 + m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.8"}).Return(
299 + &gosnmp.SnmpPacket{
300 + Variables: []gosnmp.SnmpPDU{
301 + {
302 + Name: "1.3.6.1.4.1.12124.1.1.8",
303 + Type: gosnmp.OctetString,
304 + Value: []byte("25C"),
305 + },
306 + },
307 + }, nil,
308 + )
309 + },
310 + expectedResult: []*ProfileMetrics{
311 + {
312 + Source: "test-profile.yaml",
313 + DeviceMetadata: nil,
314 + Metrics: []Metric{
315 + {
316 + Name: "temperature",
317 + Value: 25,
318 + MetricType: "gauge",
319 + },
320 + },
321 + },
322 + },
323 + expectedError: false,
324 + },
325 + "global tags with mapping": {
326 + profiles: []*ddsnmp.Profile{
327 + {
328 + SourceFile: "test-profile.yaml",
329 + Definition: &ddprofiledefinition.ProfileDefinition{
330 + MetricTags: []ddprofiledefinition.MetricTagConfig{
331 + {
332 + Tag: "device_type",
333 + Symbol: ddprofiledefinition.SymbolConfigCompat{
334 + OID: "1.3.6.1.2.1.1.2.0",
335 + Name: "sysObjectID",
336 + },
337 + Mapping: map[string]string{
338 + "1.3.6.1.4.1.9.1.1": "router",
339 + "1.3.6.1.4.1.9.1.2": "switch",
340 + },
341 + },
342 + },
343 + Metrics: []ddprofiledefinition.MetricsConfig{
344 + {
345 + Symbol: ddprofiledefinition.SymbolConfig{
346 + OID: "1.3.6.1.2.1.1.3.0",
347 + Name: "sysUpTime",
348 + },
349 + },
350 + },
351 + },
352 + },
353 + },
354 + setupMock: func(m *snmpmock.MockHandler) {
355 + m.EXPECT().MaxOids().Return(10).AnyTimes()
356 + // First call for global tags
357 + m.EXPECT().Get([]string{"1.3.6.1.2.1.1.2.0"}).Return(
358 + &gosnmp.SnmpPacket{
359 + Variables: []gosnmp.SnmpPDU{
360 + {
361 + Name: "1.3.6.1.2.1.1.2.0",
362 + Type: gosnmp.ObjectIdentifier,
363 + Value: "1.3.6.1.4.1.9.1.1",
364 + },
365 + },
366 + }, nil,
367 + )
368 + // Second call for metrics
369 + m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
370 + &gosnmp.SnmpPacket{
371 + Variables: []gosnmp.SnmpPDU{
372 + {
373 + Name: "1.3.6.1.2.1.1.3.0",
374 + Type: gosnmp.TimeTicks,
375 + Value: uint32(123456),
376 + },
377 + },
378 + }, nil,
379 + )
380 + },
381 + expectedResult: []*ProfileMetrics{
382 + {
383 + Source: "test-profile.yaml",
384 + DeviceMetadata: nil,
385 + Tags: map[string]string{"device_type": "router"},
386 + Metrics: []Metric{
387 + {
388 + Name: "sysUpTime",
389 + Value: 123456,
390 + MetricType: "gauge",
391 + },
392 + },
393 + },
394 + },
395 + expectedError: false,
396 + },
397 "OID not found - returns empty metrics": {
398 profiles: []*ddsnmp.Profile{
399 {
@@ -340,6 +459,26 @@ func TestCollector_Collect(t *testing.T) {
459 expectedError: true,
460 errorContains: "SNMP timeout",
461 },
462 + "empty profile - no metrics defined": {
463 + profiles: []*ddsnmp.Profile{
464 + {
465 + SourceFile: "empty-profile.yaml",
466 + Definition: &ddprofiledefinition.ProfileDefinition{
467 + Metrics: []ddprofiledefinition.MetricsConfig{},
468 + },
469 + },
470 + },
471 + setupMock: func(m *snmpmock.MockHandler) {
472 + m.EXPECT().MaxOids().Return(10).AnyTimes()
473 + },
474 + expectedResult: []*ProfileMetrics{
475 + {
476 + DeviceMetadata: nil,
477 + Metrics: []Metric{},
478 + },
479 + },
480 + expectedError: false,
481 + },
482 "multiple profiles - one fails": {
483 profiles: []*ddsnmp.Profile{
484 {
@@ -404,7 +543,67 @@ func TestCollector_Collect(t *testing.T) {
543 },
544 expectedError: false, // Should return partial results
545 },
407 - "metric with extract_value": {
546 + }
547 +
548 + for name, tc := range tests {
549 + t.Run(name, func(t *testing.T) {
550 + ctrl := gomock.NewController(t)
551 + defer ctrl.Finish()
552 +
553 + mockHandler := snmpmock.NewMockHandler(ctrl)
554 + tc.setupMock(mockHandler)
555 +
556 + collector := New(mockHandler, tc.profiles, logger.New())
557 + collector.DoTableMetrics = true
558 +
559 + // Configure cache based on test requirements
560 + if tc.enableCache {
561 + collector.tableCache.setTTL(30*time.Second, 0)
562 + } else {
563 + collector.tableCache.setTTL(0, 0) // Disable cache
564 + }
565 +
566 + result, err := collector.Collect()
567 +
568 + // Clear circular references
569 + for _, profile := range result {
570 + for i := range profile.Metrics {
571 + profile.Metrics[i].Profile = nil
572 + }
573 + }
574 +
575 + if tc.expectedError {
576 + assert.Error(t, err)
577 + if tc.errorContains != "" {
578 + assert.Contains(t, err.Error(), tc.errorContains)
579 + }
580 + } else {
581 + assert.NoError(t, err)
582 + }
583 +
584 + if tc.expectedResult != nil {
585 + require.Equal(t, len(tc.expectedResult), len(result))
586 + for i := range tc.expectedResult {
587 + assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
588 + assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
589 + assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
590 + }
591 + } else {
592 + assert.Nil(t, result)
593 + }
594 + })
595 + }
596 +}
597 +
598 +func TestCollector_Collect_Group2_ValueMappings(t *testing.T) {
599 + tests := map[string]struct {
600 + profiles []*ddsnmp.Profile
601 + setupMock func(m *snmpmock.MockHandler)
602 + expectedResult []*ProfileMetrics
603 + expectedError bool
604 + errorContains string
605 + }{
606 + "metric with string to int mapping": {
607 profiles: []*ddsnmp.Profile{
608 {
609 SourceFile: "test-profile.yaml",
@@ -412,9 +611,13 @@ func TestCollector_Collect(t *testing.T) {
611 Metrics: []ddprofiledefinition.MetricsConfig{
612 {
613 Symbol: ddprofiledefinition.SymbolConfig{
415 - OID: "1.3.6.1.4.1.12124.1.1.8",
416 - Name: "temperature",
417 - ExtractValueCompiled: mustCompileRegex(`(\d+)C`),
614 + OID: "1.3.6.1.4.1.12124.1.1.2",
615 + Name: "clusterHealth",
616 + Mapping: map[string]string{
617 + "OK": "0",
618 + "WARNING": "1",
619 + "CRITICAL": "2",
620 + },
621 },
622 },
623 },
@@ -423,13 +626,13 @@ func TestCollector_Collect(t *testing.T) {
626 },
627 setupMock: func(m *snmpmock.MockHandler) {
628 m.EXPECT().MaxOids().Return(10).AnyTimes()
426 - m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.8"}).Return(
629 + m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.2"}).Return(
630 &gosnmp.SnmpPacket{
631 Variables: []gosnmp.SnmpPDU{
632 {
430 - Name: "1.3.6.1.4.1.12124.1.1.8",
633 + Name: "1.3.6.1.4.1.12124.1.1.2",
634 Type: gosnmp.OctetString,
432 - Value: []byte("25C"),
635 + Value: []byte("WARNING"),
636 },
637 },
638 }, nil,
@@ -441,38 +644,37 @@ func TestCollector_Collect(t *testing.T) {
644 DeviceMetadata: nil,
645 Metrics: []Metric{
646 {
444 - Name: "temperature",
445 - Value: 25,
647 + Name: "clusterHealth",
648 + Value: 1,
649 MetricType: "gauge",
650 + Mappings: map[int64]string{
651 + 0: "OK",
652 + 1: "WARNING",
653 + 2: "CRITICAL",
654 + },
655 },
656 },
657 },
658 },
659 expectedError: false,
660 },
453 - "global tags with mapping": {
661 + "metric with int to string mapping": {
662 profiles: []*ddsnmp.Profile{
663 {
664 SourceFile: "test-profile.yaml",
665 Definition: &ddprofiledefinition.ProfileDefinition{
458 - MetricTags: []ddprofiledefinition.MetricTagConfig{
459 - {
460 - Tag: "device_type",
461 - Symbol: ddprofiledefinition.SymbolConfigCompat{
462 - OID: "1.3.6.1.2.1.1.2.0",
463 - Name: "sysObjectID",
464 - },
465 - Mapping: map[string]string{
466 - "1.3.6.1.4.1.9.1.1": "router",
467 - "1.3.6.1.4.1.9.1.2": "switch",
468 - },
469 - },
470 - },
666 Metrics: []ddprofiledefinition.MetricsConfig{
667 {
668 Symbol: ddprofiledefinition.SymbolConfig{
474 - OID: "1.3.6.1.2.1.1.3.0",
475 - Name: "sysUpTime",
669 + OID: "1.3.6.1.2.1.2.2.1.8",
670 + Name: "ifOperStatus",
671 + Mapping: map[string]string{
672 + "1": "up",
673 + "2": "down",
674 + "3": "testing",
675 + "4": "unknown",
676 + "5": "dormant",
677 + },
678 },
679 },
680 },
@@ -481,154 +683,13 @@ func TestCollector_Collect(t *testing.T) {
683 },
684 setupMock: func(m *snmpmock.MockHandler) {
685 m.EXPECT().MaxOids().Return(10).AnyTimes()
484 - // First call for global tags
485 - m.EXPECT().Get([]string{"1.3.6.1.2.1.1.2.0"}).Return(
686 + m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.8"}).Return(
687 &gosnmp.SnmpPacket{
688 Variables: []gosnmp.SnmpPDU{
689 {
489 - Name: "1.3.6.1.2.1.1.2.0",
490 - Type: gosnmp.ObjectIdentifier,
491 - Value: "1.3.6.1.4.1.9.1.1",
492 - },
493 - },
494 - }, nil,
495 - )
496 - // Second call for metrics
497 - m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
498 - &gosnmp.SnmpPacket{
499 - Variables: []gosnmp.SnmpPDU{
500 - {
501 - Name: "1.3.6.1.2.1.1.3.0",
502 - Type: gosnmp.TimeTicks,
503 - Value: uint32(123456),
504 - },
505 - },
506 - }, nil,
507 - )
508 - },
509 - expectedResult: []*ProfileMetrics{
510 - {
511 - Source: "test-profile.yaml",
512 - DeviceMetadata: nil,
513 - Tags: map[string]string{"device_type": "router"},
514 - Metrics: []Metric{
515 - {
516 - Name: "sysUpTime",
517 - Value: 123456,
518 - MetricType: "gauge",
519 - },
520 - },
521 - },
522 - },
523 - expectedError: false,
524 - },
525 - "empty profile - no metrics defined": {
526 - profiles: []*ddsnmp.Profile{
527 - {
528 - SourceFile: "empty-profile.yaml",
529 - Definition: &ddprofiledefinition.ProfileDefinition{
530 - Metrics: []ddprofiledefinition.MetricsConfig{},
531 - },
532 - },
533 - },
534 - setupMock: func(m *snmpmock.MockHandler) {
535 - m.EXPECT().MaxOids().Return(10).AnyTimes()
536 - },
537 - expectedResult: []*ProfileMetrics{
538 - {
539 - DeviceMetadata: nil,
540 - Metrics: []Metric{},
541 - },
542 - },
543 - expectedError: false,
544 - },
545 - "metric with string to int mapping": {
546 - profiles: []*ddsnmp.Profile{
547 - {
548 - SourceFile: "test-profile.yaml",
549 - Definition: &ddprofiledefinition.ProfileDefinition{
550 - Metrics: []ddprofiledefinition.MetricsConfig{
551 - {
552 - Symbol: ddprofiledefinition.SymbolConfig{
553 - OID: "1.3.6.1.4.1.12124.1.1.2",
554 - Name: "clusterHealth",
555 - Mapping: map[string]string{
556 - "OK": "0",
557 - "WARNING": "1",
558 - "CRITICAL": "2",
559 - },
560 - },
561 - },
562 - },
563 - },
564 - },
565 - },
566 - setupMock: func(m *snmpmock.MockHandler) {
567 - m.EXPECT().MaxOids().Return(10).AnyTimes()
568 - m.EXPECT().Get([]string{"1.3.6.1.4.1.12124.1.1.2"}).Return(
569 - &gosnmp.SnmpPacket{
570 - Variables: []gosnmp.SnmpPDU{
571 - {
572 - Name: "1.3.6.1.4.1.12124.1.1.2",
573 - Type: gosnmp.OctetString,
574 - Value: []byte("WARNING"),
575 - },
576 - },
577 - }, nil,
578 - )
579 - },
580 - expectedResult: []*ProfileMetrics{
581 - {
582 - Source: "test-profile.yaml",
583 - DeviceMetadata: nil,
584 - Metrics: []Metric{
585 - {
586 - Name: "clusterHealth",
587 - Value: 1,
588 - MetricType: "gauge",
589 - Mappings: map[int64]string{
590 - 0: "OK",
591 - 1: "WARNING",
592 - 2: "CRITICAL",
593 - },
594 - },
595 - },
596 - },
597 - },
598 - expectedError: false,
599 - },
600 - "metric with int to string mapping": {
601 - profiles: []*ddsnmp.Profile{
602 - {
603 - SourceFile: "test-profile.yaml",
604 - Definition: &ddprofiledefinition.ProfileDefinition{
605 - Metrics: []ddprofiledefinition.MetricsConfig{
606 - {
607 - Symbol: ddprofiledefinition.SymbolConfig{
608 - OID: "1.3.6.1.2.1.2.2.1.8",
609 - Name: "ifOperStatus",
610 - Mapping: map[string]string{
611 - "1": "up",
612 - "2": "down",
613 - "3": "testing",
614 - "4": "unknown",
615 - "5": "dormant",
616 - },
617 - },
618 - },
619 - },
620 - },
621 - },
622 - },
623 - setupMock: func(m *snmpmock.MockHandler) {
624 - m.EXPECT().MaxOids().Return(10).AnyTimes()
625 - m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.8"}).Return(
626 - &gosnmp.SnmpPacket{
627 - Variables: []gosnmp.SnmpPDU{
628 - {
629 - Name: "1.3.6.1.2.1.2.2.1.8",
630 - Type: gosnmp.Integer,
631 - Value: 2, // down
690 + Name: "1.3.6.1.2.1.2.2.1.8",
691 + Type: gosnmp.Integer,
692 + Value: 2, // down
693 },
694 },
695 }, nil,
@@ -804,6 +865,7 @@ func TestCollector_Collect(t *testing.T) {
865 },
866 expectedResult: nil,
867 expectedError: true,
868 + errorContains: "strconv.ParseInt",
869 },
870 "metric with mixed mapping values": {
871 profiles: []*ddsnmp.Profile{
@@ -842,6 +904,7 @@ func TestCollector_Collect(t *testing.T) {
904 },
905 expectedResult: nil,
906 expectedError: true,
907 + errorContains: "strconv.ParseInt",
908 },
909 "metric with no mapping": {
910 profiles: []*ddsnmp.Profile{
@@ -889,35 +952,20 @@ func TestCollector_Collect(t *testing.T) {
952 },
953 expectedError: false,
954 },
892 - "table metrics with same-table tags": {
955 + "metric with numeric value and int to string mapping": {
956 profiles: []*ddsnmp.Profile{
957 {
958 SourceFile: "test-profile.yaml",
959 Definition: &ddprofiledefinition.ProfileDefinition{
960 Metrics: []ddprofiledefinition.MetricsConfig{
961 {
899 - MIB: "IF-MIB",
900 - Table: ddprofiledefinition.SymbolConfig{
901 - OID: "1.3.6.1.2.1.2.2",
902 - Name: "ifTable",
903 - },
904 - Symbols: []ddprofiledefinition.SymbolConfig{
905 - {
906 - OID: "1.3.6.1.2.1.2.2.1.10",
907 - Name: "ifInOctets",
908 - },
909 - {
910 - OID: "1.3.6.1.2.1.2.2.1.16",
911 - Name: "ifOutOctets",
912 - },
913 - },
914 - MetricTags: []ddprofiledefinition.MetricTagConfig{
915 - {
916 - Tag: "interface",
917 - Symbol: ddprofiledefinition.SymbolConfigCompat{
918 - OID: "1.3.6.1.2.1.2.2.1.2",
919 - Name: "ifDescr",
920 - },
962 + Symbol: ddprofiledefinition.SymbolConfig{
963 + OID: "1.3.6.1.2.1.2.2.1.7",
964 + Name: "ifAdminStatus",
965 + Mapping: map[string]string{
966 + "1": "up",
967 + "2": "down",
968 + "3": "testing",
969 },
970 },
971 },
@@ -927,146 +975,15 @@ func TestCollector_Collect(t *testing.T) {
975 },
976 setupMock: func(m *snmpmock.MockHandler) {
977 m.EXPECT().MaxOids().Return(10).AnyTimes()
930 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
931 - m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
932 - []gosnmp.SnmpPDU{
933 - // Row 1 - index 1
934 - {
935 - Name: "1.3.6.1.2.1.2.2.1.2.1",
936 - Type: gosnmp.OctetString,
937 - Value: []byte("eth0"),
938 - },
939 - {
940 - Name: "1.3.6.1.2.1.2.2.1.10.1",
941 - Type: gosnmp.Counter32,
942 - Value: uint(1000),
943 - },
944 - {
945 - Name: "1.3.6.1.2.1.2.2.1.16.1",
946 - Type: gosnmp.Counter32,
947 - Value: uint(2000),
948 - },
949 - // Row 2 - index 2
950 - {
951 - Name: "1.3.6.1.2.1.2.2.1.2.2",
952 - Type: gosnmp.OctetString,
953 - Value: []byte("eth1"),
954 - },
955 - {
956 - Name: "1.3.6.1.2.1.2.2.1.10.2",
957 - Type: gosnmp.Counter32,
958 - Value: uint(3000),
959 - },
960 - {
961 - Name: "1.3.6.1.2.1.2.2.1.16.2",
962 - Type: gosnmp.Counter32,
963 - Value: uint(4000),
964 - },
965 - }, nil,
966 - )
967 - },
968 - expectedResult: []*ProfileMetrics{
969 - {
970 - Source: "test-profile.yaml",
971 - DeviceMetadata: nil,
972 - Metrics: []Metric{
973 - {
974 - Name: "ifInOctets",
975 - Value: 1000,
976 - Tags: map[string]string{"interface": "eth0"},
977 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
978 - IsTable: true,
979 - },
980 - {
981 - Name: "ifOutOctets",
982 - Value: 2000,
983 - Tags: map[string]string{"interface": "eth0"},
984 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
985 - IsTable: true,
986 - },
987 - {
988 - Name: "ifInOctets",
989 - Value: 3000,
990 - Tags: map[string]string{"interface": "eth1"},
991 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
992 - IsTable: true,
993 - },
994 - {
995 - Name: "ifOutOctets",
996 - Value: 4000,
997 - Tags: map[string]string{"interface": "eth1"},
998 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
999 - IsTable: true,
1000 - },
1001 - },
1002 - },
1003 - },
1004 - expectedError: false,
1005 - },
1006 - "table metrics with tag mapping": {
1007 - profiles: []*ddsnmp.Profile{
1008 - {
1009 - SourceFile: "test-profile.yaml",
1010 - Definition: &ddprofiledefinition.ProfileDefinition{
1011 - Metrics: []ddprofiledefinition.MetricsConfig{
978 + m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.7"}).Return(
979 + &gosnmp.SnmpPacket{
980 + Variables: []gosnmp.SnmpPDU{
981 {
1013 - MIB: "IF-MIB",
1014 - Table: ddprofiledefinition.SymbolConfig{
1015 - OID: "1.3.6.1.2.1.2.2",
1016 - Name: "ifTable",
1017 - },
1018 - Symbols: []ddprofiledefinition.SymbolConfig{
1019 - {
1020 - OID: "1.3.6.1.2.1.2.2.1.10",
1021 - Name: "ifInOctets",
1022 - },
1023 - },
1024 - MetricTags: []ddprofiledefinition.MetricTagConfig{
1025 - {
1026 - Tag: "if_type",
1027 - Symbol: ddprofiledefinition.SymbolConfigCompat{
1028 - OID: "1.3.6.1.2.1.2.2.1.3",
1029 - Name: "ifType",
1030 - },
1031 - Mapping: map[string]string{
1032 - "1": "other",
1033 - "2": "regular1822",
1034 - "6": "ethernetCsmacd",
1035 - },
1036 - },
1037 - },
982 + Name: "1.3.6.1.2.1.2.2.1.7",
983 + Type: gosnmp.Integer,
984 + Value: 1, // up
985 },
986 },
1040 - },
1041 - },
1042 - },
1043 - setupMock: func(m *snmpmock.MockHandler) {
1044 - m.EXPECT().MaxOids().Return(10).AnyTimes()
1045 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1046 - m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1047 - []gosnmp.SnmpPDU{
1048 - // Row 1
1049 - {
1050 - Name: "1.3.6.1.2.1.2.2.1.3.1",
1051 - Type: gosnmp.Integer,
1052 - Value: 6, // ethernetCsmacd
1053 - },
1054 - {
1055 - Name: "1.3.6.1.2.1.2.2.1.10.1",
1056 - Type: gosnmp.Counter32,
1057 - Value: uint(1000),
1058 - },
1059 - // Row 2
1060 - {
1061 - Name: "1.3.6.1.2.1.2.2.1.3.2",
1062 - Type: gosnmp.Integer,
1063 - Value: 1, // other
1064 - },
1065 - {
1066 - Name: "1.3.6.1.2.1.2.2.1.10.2",
1067 - Type: gosnmp.Counter32,
1068 - Value: uint(2000),
1069 - },
987 }, nil,
988 )
989 },
@@ -1076,50 +993,35 @@ func TestCollector_Collect(t *testing.T) {
993 DeviceMetadata: nil,
994 Metrics: []Metric{
995 {
1079 - Name: "ifInOctets",
1080 - Value: 1000,
1081 - Tags: map[string]string{"if_type": "ethernetCsmacd"},
1082 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1083 - IsTable: true,
1084 - },
1085 - {
1086 - Name: "ifInOctets",
1087 - Value: 2000,
1088 - Tags: map[string]string{"if_type": "other"},
1089 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1090 - IsTable: true,
996 + Name: "ifAdminStatus",
997 + Value: 1,
998 + MetricType: "gauge",
999 + Mappings: map[int64]string{
1000 + 1: "up",
1001 + 2: "down",
1002 + 3: "testing",
1003 + },
1004 },
1005 },
1006 },
1007 },
1008 expectedError: false,
1009 },
1097 - "table metrics with pattern matching tags": {
1010 + "metric with string value and string to int mapping": {
1011 profiles: []*ddsnmp.Profile{
1012 {
1013 SourceFile: "test-profile.yaml",
1014 Definition: &ddprofiledefinition.ProfileDefinition{
1015 Metrics: []ddprofiledefinition.MetricsConfig{
1016 {
1104 - MIB: "MY-MIB",
1105 - Table: ddprofiledefinition.SymbolConfig{
1106 - OID: "1.3.6.1.4.1.1000.1",
1107 - Name: "myTable",
1108 - },
1109 - Symbols: []ddprofiledefinition.SymbolConfig{
1110 - {
1111 - OID: "1.3.6.1.4.1.1000.1.1.1",
1112 - Name: "myMetric",
1113 - },
1114 - },
1115 - MetricTags: []ddprofiledefinition.MetricTagConfig{
1116 - {
1117 - Symbol: ddprofiledefinition.SymbolConfigCompat{
1118 - OID: "1.3.6.1.4.1.1000.1.1.2",
1119 - Name: "myDescription",
1120 - ExtractValueCompiled: mustCompileRegex(`Interface (\w+)`),
1121 - },
1122 - Tag: "port",
1017 + Symbol: ddprofiledefinition.SymbolConfig{
1018 + OID: "1.3.6.1.4.1.318.1.1.1.2.2.1.0",
1019 + Name: "upsBasicBatteryStatus",
1020 + Mapping: map[string]string{
1021 + "batteryNormal": "0",
1022 + "batteryLow": "1",
1023 + "batteryDepleted": "2",
1024 + "batteryCharging": "3",
1025 },
1026 },
1027 },
@@ -1129,18 +1031,14 @@ func TestCollector_Collect(t *testing.T) {
1031 },
1032 setupMock: func(m *snmpmock.MockHandler) {
1033 m.EXPECT().MaxOids().Return(10).AnyTimes()
1132 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1133 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
1134 - []gosnmp.SnmpPDU{
1135 - {
1136 - Name: "1.3.6.1.4.1.1000.1.1.1.1",
1137 - Type: gosnmp.Gauge32,
1138 - Value: uint(100),
1139 - },
1140 - {
1141 - Name: "1.3.6.1.4.1.1000.1.1.2.1",
1142 - Type: gosnmp.OctetString,
1143 - Value: []byte("Interface Gi0/1"),
1034 + m.EXPECT().Get([]string{"1.3.6.1.4.1.318.1.1.1.2.2.1.0"}).Return(
1035 + &gosnmp.SnmpPacket{
1036 + Variables: []gosnmp.SnmpPDU{
1037 + {
1038 + Name: "1.3.6.1.4.1.318.1.1.1.2.2.1.0",
1039 + Type: gosnmp.OctetString,
1040 + Value: []byte("batteryLow"),
1041 + },
1042 },
1043 }, nil,
1044 )
@@ -1151,2182 +1049,16 @@ func TestCollector_Collect(t *testing.T) {
1049 DeviceMetadata: nil,
1050 Metrics: []Metric{
1051 {
1154 - Name: "myMetric",
1155 - Value: 100,
1156 - Tags: map[string]string{"port": "Gi0"},
1157 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1158 - IsTable: true,
1159 - },
1160 - },
1161 - },
1162 - },
1163 - expectedError: false,
1164 - },
1165 - "table metrics with static tags": {
1166 - profiles: []*ddsnmp.Profile{
1167 - {
1168 - SourceFile: "test-profile.yaml",
1169 - Definition: &ddprofiledefinition.ProfileDefinition{
1170 - Metrics: []ddprofiledefinition.MetricsConfig{
1171 - {
1172 - MIB: "MY-MIB",
1173 - Table: ddprofiledefinition.SymbolConfig{
1174 - OID: "1.3.6.1.4.1.1000.1",
1175 - Name: "myTable",
1176 - },
1177 - Symbols: []ddprofiledefinition.SymbolConfig{
1178 - {
1179 - OID: "1.3.6.1.4.1.1000.1.1.1",
1180 - Name: "myMetric",
1181 - },
1182 - },
1183 - StaticTags: []string{"table_type:performance", "source:snmp"},
1184 - MetricTags: []ddprofiledefinition.MetricTagConfig{
1185 - {
1186 - Tag: "interface",
1187 - Symbol: ddprofiledefinition.SymbolConfigCompat{
1188 - OID: "1.3.6.1.4.1.1000.1.1.2",
1189 - Name: "ifName",
1190 - },
1191 - },
1192 - },
1193 - },
1194 - },
1195 - },
1196 - },
1197 - },
1198 - setupMock: func(m *snmpmock.MockHandler) {
1199 - m.EXPECT().MaxOids().Return(10).AnyTimes()
1200 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1201 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
1202 - []gosnmp.SnmpPDU{
1203 - {
1204 - Name: "1.3.6.1.4.1.1000.1.1.1.1",
1205 - Type: gosnmp.Gauge32,
1206 - Value: uint(100),
1207 - },
1208 - {
1209 - Name: "1.3.6.1.4.1.1000.1.1.2.1",
1210 - Type: gosnmp.OctetString,
1211 - Value: []byte("eth0"),
1212 - },
1213 - }, nil,
1214 - )
1215 - },
1216 - expectedResult: []*ProfileMetrics{
1217 - {
1218 - Source: "test-profile.yaml",
1219 - DeviceMetadata: nil,
1220 - Metrics: []Metric{
1221 - {
1222 - Name: "myMetric",
1223 - Value: 100,
1224 - StaticTags: map[string]string{
1225 - "table_type": "performance",
1226 - "source": "snmp",
1227 - },
1228 - Tags: map[string]string{
1229 - "interface": "eth0",
1230 - },
1231 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1232 - IsTable: true,
1233 - },
1234 - },
1235 - },
1236 - },
1237 - expectedError: false,
1238 - },
1239 - "table metrics with missing tag values": {
1240 - profiles: []*ddsnmp.Profile{
1241 - {
1242 - SourceFile: "test-profile.yaml",
1243 - Definition: &ddprofiledefinition.ProfileDefinition{
1244 - Metrics: []ddprofiledefinition.MetricsConfig{
1245 - {
1246 - MIB: "IF-MIB",
1247 - Table: ddprofiledefinition.SymbolConfig{
1248 - OID: "1.3.6.1.2.1.2.2",
1249 - Name: "ifTable",
1250 - },
1251 - Symbols: []ddprofiledefinition.SymbolConfig{
1252 - {
1253 - OID: "1.3.6.1.2.1.2.2.1.10",
1254 - Name: "ifInOctets",
1255 - },
1256 - },
1257 - MetricTags: []ddprofiledefinition.MetricTagConfig{
1258 - {
1259 - Tag: "interface",
1260 - Symbol: ddprofiledefinition.SymbolConfigCompat{
1261 - OID: "1.3.6.1.2.1.2.2.1.2",
1262 - Name: "ifDescr",
1263 - },
1264 - },
1265 - },
1266 - },
1267 - },
1268 - },
1269 - },
1270 - },
1271 - setupMock: func(m *snmpmock.MockHandler) {
1272 - m.EXPECT().MaxOids().Return(10).AnyTimes()
1273 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1274 - m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1275 - []gosnmp.SnmpPDU{
1276 - // Row 1 - has both metric and tag
1277 - {
1278 - Name: "1.3.6.1.2.1.2.2.1.2.1",
1279 - Type: gosnmp.OctetString,
1280 - Value: []byte("eth0"),
1281 - },
1282 - {
1283 - Name: "1.3.6.1.2.1.2.2.1.10.1",
1284 - Type: gosnmp.Counter32,
1285 - Value: uint(1000),
1286 - },
1287 - // Row 2 - missing tag value
1288 - {
1289 - Name: "1.3.6.1.2.1.2.2.1.10.2",
1290 - Type: gosnmp.Counter32,
1291 - Value: uint(2000),
1292 - },
1293 - }, nil,
1294 - )
1295 - },
1296 - expectedResult: []*ProfileMetrics{
1297 - {
1298 - Source: "test-profile.yaml",
1299 - DeviceMetadata: nil,
1300 - Metrics: []Metric{
1301 - {
1302 - Name: "ifInOctets",
1303 - Value: 1000,
1304 - Tags: map[string]string{"interface": "eth0"},
1305 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1306 - IsTable: true,
1307 - },
1308 - {
1309 - Name: "ifInOctets",
1310 - Value: 2000,
1311 - Tags: nil, // No interface tag because it's missing
1312 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1313 - IsTable: true,
1314 - },
1315 - },
1316 - },
1317 - },
1318 - expectedError: false,
1319 - },
1320 -
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 - },
1348 - },
1349 - },
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 - },
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 - },
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 - },
1442 - },
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 - },
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 - {
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 - {
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 - },
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 - },
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 - },
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 - },
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 - {
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 - "multiple cross-table tags from different tables": {
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 - },
1738 - {
1739 - Symbol: ddprofiledefinition.SymbolConfigCompat{
1740 - OID: "1.3.6.1.2.1.2.2.1.2",
1741 - Name: "ifDescr",
1742 - },
1743 - Table: "ifTable",
1744 - Tag: "description",
1745 - },
1746 - },
1747 - },
1748 - {
1749 - MIB: "IF-MIB",
1750 - Table: ddprofiledefinition.SymbolConfig{
1751 - OID: "1.3.6.1.2.1.31.1.1",
1752 - Name: "ifXTable",
1753 - },
1754 - Symbols: []ddprofiledefinition.SymbolConfig{
1755 - // No symbols needed - this table is only used for cross-table tags
1756 - },
1757 - },
1758 - {
1759 - MIB: "IF-MIB",
1760 - Table: ddprofiledefinition.SymbolConfig{
1761 - OID: "1.3.6.1.2.1.2.2",
1762 - Name: "ifTable",
1763 - },
1764 - Symbols: []ddprofiledefinition.SymbolConfig{
1765 - // No symbols needed - this table is only used for cross-table tags
1766 - },
1767 - },
1768 - },
1769 - },
1770 - },
1771 - },
1772 - setupMock: func(m *snmpmock.MockHandler) {
1773 - m.EXPECT().MaxOids().Return(10).AnyTimes()
1774 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1775 -
1776 - // Walk myTable
1777 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
1778 - []gosnmp.SnmpPDU{
1779 - {
1780 - Name: "1.3.6.1.4.1.1000.1.1.1.1",
1781 - Type: gosnmp.Gauge32,
1782 - Value: uint(100),
1783 - },
1784 - }, nil,
1785 - )
1786 -
1787 - // Walk ifXTable
1788 - m.EXPECT().BulkWalkAll("1.3.6.1.2.1.31.1.1").Return(
1789 - []gosnmp.SnmpPDU{
1790 - {
1791 - Name: "1.3.6.1.2.1.31.1.1.1.1.1",
1792 - Type: gosnmp.OctetString,
1793 - Value: []byte("GigabitEthernet0/1"),
1794 - },
1795 - }, nil,
1796 - )
1797 -
1798 - // Walk ifTable
1799 - m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1800 - []gosnmp.SnmpPDU{
1801 - {
1802 - Name: "1.3.6.1.2.1.2.2.1.2.1",
1803 - Type: gosnmp.OctetString,
1804 - Value: []byte("GigE0/1"),
1805 - },
1806 - }, nil,
1807 - )
1808 - },
1809 - expectedResult: []*ProfileMetrics{
1810 - {
1811 - Source: "test-profile.yaml",
1812 - DeviceMetadata: nil,
1813 - Metrics: []Metric{
1814 - {
1815 - Name: "myMetric",
1816 - Value: 100,
1817 - Tags: map[string]string{
1818 - "interface": "GigabitEthernet0/1",
1819 - "description": "GigE0/1",
1820 - },
1821 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1822 - IsTable: true,
1823 - },
1824 - },
1825 - },
1826 - },
1827 - expectedError: false,
1828 - },
1829 -
1830 - "index-based tags with single position": {
1831 - profiles: []*ddsnmp.Profile{
1832 - {
1833 - SourceFile: "test-profile.yaml",
1834 - Definition: &ddprofiledefinition.ProfileDefinition{
1835 - Metrics: []ddprofiledefinition.MetricsConfig{
1836 - {
1837 - MIB: "IP-MIB",
1838 - Table: ddprofiledefinition.SymbolConfig{
1839 - OID: "1.3.6.1.2.1.4.31.1",
1840 - Name: "ipSystemStatsTable",
1841 - },
1842 - Symbols: []ddprofiledefinition.SymbolConfig{
1843 - {
1844 - OID: "1.3.6.1.2.1.4.31.1.1.4",
1845 - Name: "ipSystemStatsHCInReceives",
1846 - },
1847 - },
1848 - MetricTags: []ddprofiledefinition.MetricTagConfig{
1849 - {
1850 - Index: 1,
1851 - Tag: "ipversion",
1852 - Mapping: map[string]string{
1853 - "0": "unknown",
1854 - "1": "ipv4",
1855 - "2": "ipv6",
1856 - "3": "ipv4z",
1857 - "4": "ipv6z",
1858 - "16": "dns",
1859 - },
1860 - },
1861 - },
1862 - },
1863 - },
1864 - },
1865 - },
1866 - },
1867 - setupMock: func(m *snmpmock.MockHandler) {
1868 - m.EXPECT().MaxOids().Return(10).AnyTimes()
1869 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1870 -
1871 - m.EXPECT().BulkWalkAll("1.3.6.1.2.1.4.31.1").Return(
1872 - []gosnmp.SnmpPDU{
1873 - // IPv4 row
1874 - {
1875 - Name: "1.3.6.1.2.1.4.31.1.1.4.1",
1876 - Type: gosnmp.Counter64,
1877 - Value: uint64(1000),
1878 - },
1879 - // IPv6 row
1880 - {
1881 - Name: "1.3.6.1.2.1.4.31.1.1.4.2",
1882 - Type: gosnmp.Counter64,
1883 - Value: uint64(2000),
1884 - },
1885 - }, nil,
1886 - )
1887 - },
1888 - expectedResult: []*ProfileMetrics{
1889 - {
1890 - Source: "test-profile.yaml",
1891 - DeviceMetadata: nil,
1892 - Metrics: []Metric{
1893 - {
1894 - Name: "ipSystemStatsHCInReceives",
1895 - Value: 1000,
1896 - Tags: map[string]string{"ipversion": "ipv4"},
1897 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1898 - IsTable: true,
1899 - },
1900 - {
1901 - Name: "ipSystemStatsHCInReceives",
1902 - Value: 2000,
1903 - Tags: map[string]string{"ipversion": "ipv6"},
1904 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1905 - IsTable: true,
1906 - },
1907 - },
1908 - },
1909 - },
1910 - expectedError: false,
1911 - },
1912 - "index-based tags with multiple positions": {
1913 - profiles: []*ddsnmp.Profile{
1914 - {
1915 - SourceFile: "test-profile.yaml",
1916 - Definition: &ddprofiledefinition.ProfileDefinition{
1917 - Metrics: []ddprofiledefinition.MetricsConfig{
1918 - {
1919 - MIB: "CISCO-FIREWALL-MIB",
1920 - Table: ddprofiledefinition.SymbolConfig{
1921 - OID: "1.3.6.1.4.1.9.9.147.1.2.2.2",
1922 - Name: "cfwConnectionStatTable",
1923 - },
1924 - Symbols: []ddprofiledefinition.SymbolConfig{
1925 - {
1926 - OID: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5",
1927 - Name: "cfwConnectionStatValue",
1928 - },
1929 - },
1930 - MetricTags: []ddprofiledefinition.MetricTagConfig{
1931 - {
1932 - Index: 1,
1933 - Tag: "service_type",
1934 - },
1935 - {
1936 - Index: 2,
1937 - Tag: "stat_type",
1938 - },
1939 - },
1940 - },
1941 - },
1942 - },
1943 - },
1944 - },
1945 - setupMock: func(m *snmpmock.MockHandler) {
1946 - m.EXPECT().MaxOids().Return(10).AnyTimes()
1947 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1948 -
1949 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.9.9.147.1.2.2.2").Return(
1950 - []gosnmp.SnmpPDU{
1951 - // Index: 20.2 (service_type=20, stat_type=2)
1952 - {
1953 - Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.20.2",
1954 - Type: gosnmp.Counter64,
1955 - Value: uint64(4087850099),
1956 - },
1957 - // Index: 21.3 (service_type=21, stat_type=3)
1958 - {
1959 - Name: "1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.21.3",
1960 - Type: gosnmp.Counter64,
1961 - Value: uint64(5000000),
1962 - },
1963 - }, nil,
1964 - )
1965 - },
1966 - expectedResult: []*ProfileMetrics{
1967 - {
1968 - Source: "test-profile.yaml",
1969 - DeviceMetadata: nil,
1970 - Metrics: []Metric{
1971 - {
1972 - Name: "cfwConnectionStatValue",
1973 - Value: 4087850099,
1974 - Tags: map[string]string{
1975 - "service_type": "20",
1976 - "stat_type": "2",
1977 - },
1978 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1979 - IsTable: true,
1980 - },
1981 - {
1982 - Name: "cfwConnectionStatValue",
1983 - Value: 5000000,
1984 - Tags: map[string]string{
1985 - "service_type": "21",
1986 - "stat_type": "3",
1987 - },
1988 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1989 - IsTable: true,
1990 - },
1991 - },
1992 - },
1993 - },
1994 - expectedError: false,
1995 - },
1996 - "index-based tags with missing mapping value": {
1997 - profiles: []*ddsnmp.Profile{
1998 - {
1999 - SourceFile: "test-profile.yaml",
2000 - Definition: &ddprofiledefinition.ProfileDefinition{
2001 - Metrics: []ddprofiledefinition.MetricsConfig{
2002 - {
2003 - MIB: "IP-MIB",
2004 - Table: ddprofiledefinition.SymbolConfig{
2005 - OID: "1.3.6.1.2.1.4.31.1",
2006 - Name: "ipSystemStatsTable",
2007 - },
2008 - Symbols: []ddprofiledefinition.SymbolConfig{
2009 - {
2010 - OID: "1.3.6.1.2.1.4.31.1.1.4",
2011 - Name: "ipSystemStatsHCInReceives",
2012 - },
2013 - },
2014 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2015 - {
2016 - Index: 1,
2017 - Tag: "ipversion",
2018 - Mapping: map[string]string{
2019 - "1": "ipv4",
2020 - "2": "ipv6",
2021 - // Missing mapping for "99"
2022 - },
2023 - },
2024 - },
2025 - },
2026 - },
2027 - },
2028 - },
2029 - },
2030 - setupMock: func(m *snmpmock.MockHandler) {
2031 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2032 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2033 -
2034 - m.EXPECT().BulkWalkAll("1.3.6.1.2.1.4.31.1").Return(
2035 - []gosnmp.SnmpPDU{
2036 - // Index 99 - no mapping defined
2037 - {
2038 - Name: "1.3.6.1.2.1.4.31.1.1.4.99",
2039 - Type: gosnmp.Counter64,
2040 - Value: uint64(3000),
2041 - },
2042 - }, nil,
2043 - )
2044 - },
2045 - expectedResult: []*ProfileMetrics{
2046 - {
2047 - Source: "test-profile.yaml",
2048 - DeviceMetadata: nil,
2049 - Metrics: []Metric{
2050 - {
2051 - Name: "ipSystemStatsHCInReceives",
2052 - Value: 3000,
2053 - Tags: map[string]string{"ipversion": "99"}, // Raw value when no mapping exists
2054 - MetricType: ddprofiledefinition.ProfileMetricTypeRate,
2055 - IsTable: true,
2056 - },
2057 - },
2058 - },
2059 - },
2060 - expectedError: false,
2061 - },
2062 - "index-based tags with complex multi-part index": {
2063 - profiles: []*ddsnmp.Profile{
2064 - {
2065 - SourceFile: "test-profile.yaml",
2066 - Definition: &ddprofiledefinition.ProfileDefinition{
2067 - Metrics: []ddprofiledefinition.MetricsConfig{
2068 - {
2069 - MIB: "MY-MIB",
2070 - Table: ddprofiledefinition.SymbolConfig{
2071 - OID: "1.3.6.1.4.1.1000.1",
2072 - Name: "myComplexTable",
2073 - },
2074 - Symbols: []ddprofiledefinition.SymbolConfig{
2075 - {
2076 - OID: "1.3.6.1.4.1.1000.1.1.1",
2077 - Name: "myMetric",
2078 - },
2079 - },
2080 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2081 - {
2082 - Index: 1,
2083 - Tag: "first",
2084 - },
2085 - {
2086 - Index: 3,
2087 - Tag: "third",
2088 - },
2089 - {
2090 - Index: 5,
2091 - Tag: "fifth",
2092 - },
2093 - },
2094 - },
2095 - },
2096 - },
2097 - },
2098 - },
2099 - setupMock: func(m *snmpmock.MockHandler) {
2100 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2101 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2102 -
2103 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2104 - []gosnmp.SnmpPDU{
2105 - // Complex index: 10.20.30.40.50
2106 - {
2107 - Name: "1.3.6.1.4.1.1000.1.1.1.10.20.30.40.50",
2108 - Type: gosnmp.Gauge32,
2109 - Value: uint(100),
2110 - },
2111 - }, nil,
2112 - )
2113 - },
2114 - expectedResult: []*ProfileMetrics{
2115 - {
2116 - Source: "test-profile.yaml",
2117 - DeviceMetadata: nil,
2118 - Metrics: []Metric{
2119 - {
2120 - Name: "myMetric",
2121 - Value: 100,
2122 - Tags: map[string]string{
2123 - "first": "10",
2124 - "third": "30",
2125 - "fifth": "50",
2126 - },
2127 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2128 - IsTable: true,
2129 - },
2130 - },
2131 - },
2132 - },
2133 - expectedError: false,
2134 - },
2135 - "index-based tags with out-of-range position": {
2136 - profiles: []*ddsnmp.Profile{
2137 - {
2138 - SourceFile: "test-profile.yaml",
2139 - Definition: &ddprofiledefinition.ProfileDefinition{
2140 - Metrics: []ddprofiledefinition.MetricsConfig{
2141 - {
2142 - MIB: "MY-MIB",
2143 - Table: ddprofiledefinition.SymbolConfig{
2144 - OID: "1.3.6.1.4.1.1000.1",
2145 - Name: "myTable",
2146 - },
2147 - Symbols: []ddprofiledefinition.SymbolConfig{
2148 - {
2149 - OID: "1.3.6.1.4.1.1000.1.1.1",
2150 - Name: "myMetric",
2151 - },
2152 - },
2153 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2154 - {
2155 - Index: 1,
2156 - Tag: "first",
2157 - },
2158 - {
2159 - Index: 5, // This position doesn't exist in index "1.2"
2160 - Tag: "fifth",
2161 - },
2162 - },
2163 - },
2164 - },
2165 - },
2166 - },
2167 - },
2168 - setupMock: func(m *snmpmock.MockHandler) {
2169 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2170 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2171 -
2172 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2173 - []gosnmp.SnmpPDU{
2174 - // Simple index: 1.2
2175 - {
2176 - Name: "1.3.6.1.4.1.1000.1.1.1.1.2",
2177 - Type: gosnmp.Gauge32,
2178 - Value: uint(100),
2179 - },
2180 - }, nil,
2181 - )
2182 - },
2183 - expectedResult: []*ProfileMetrics{
2184 - {
2185 - Source: "test-profile.yaml",
2186 - DeviceMetadata: nil,
2187 - Metrics: []Metric{
2188 - {
2189 - Name: "myMetric",
2190 - Value: 100,
2191 - Tags: map[string]string{
2192 - "first": "1",
2193 - // "fifth" tag is not present because position 5 doesn't exist
2194 - },
2195 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2196 - IsTable: true,
2197 - },
2198 - },
2199 - },
2200 - },
2201 - expectedError: false,
2202 - },
2203 - "index-based tags combined with symbol tags": {
2204 - profiles: []*ddsnmp.Profile{
2205 - {
2206 - SourceFile: "test-profile.yaml",
2207 - Definition: &ddprofiledefinition.ProfileDefinition{
2208 - Metrics: []ddprofiledefinition.MetricsConfig{
2209 - {
2210 - MIB: "MY-MIB",
2211 - Table: ddprofiledefinition.SymbolConfig{
2212 - OID: "1.3.6.1.4.1.1000.1",
2213 - Name: "myTable",
2214 - },
2215 - Symbols: []ddprofiledefinition.SymbolConfig{
2216 - {
2217 - OID: "1.3.6.1.4.1.1000.1.1.1",
2218 - Name: "myMetric",
2219 - },
2220 - },
2221 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2222 - {
2223 - Index: 1,
2224 - Tag: "index_tag",
2225 - },
2226 - {
2227 - Tag: "name_tag",
2228 - Symbol: ddprofiledefinition.SymbolConfigCompat{
2229 - OID: "1.3.6.1.4.1.1000.1.1.2",
2230 - Name: "myName",
2231 - },
2232 - },
2233 - },
2234 - },
2235 - },
2236 - },
2237 - },
2238 - },
2239 - setupMock: func(m *snmpmock.MockHandler) {
2240 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2241 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2242 -
2243 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2244 - []gosnmp.SnmpPDU{
2245 - {
2246 - Name: "1.3.6.1.4.1.1000.1.1.1.5",
2247 - Type: gosnmp.Gauge32,
2248 - Value: uint(100),
2249 - },
2250 - {
2251 - Name: "1.3.6.1.4.1.1000.1.1.2.5",
2252 - Type: gosnmp.OctetString,
2253 - Value: []byte("device-5"),
2254 - },
2255 - }, nil,
2256 - )
2257 - },
2258 - expectedResult: []*ProfileMetrics{
2259 - {
2260 - Source: "test-profile.yaml",
2261 - DeviceMetadata: nil,
2262 - Metrics: []Metric{
2263 - {
2264 - Name: "myMetric",
2265 - Value: 100,
2266 - Tags: map[string]string{
2267 - "index_tag": "5",
2268 - "name_tag": "device-5",
2269 - },
2270 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2271 - IsTable: true,
2272 - },
2273 - },
2274 - },
2275 - },
2276 - expectedError: false,
2277 - },
2278 - "index-based tags with default tag name": {
2279 - profiles: []*ddsnmp.Profile{
2280 - {
2281 - SourceFile: "test-profile.yaml",
2282 - Definition: &ddprofiledefinition.ProfileDefinition{
2283 - Metrics: []ddprofiledefinition.MetricsConfig{
2284 - {
2285 - MIB: "MY-MIB",
2286 - Table: ddprofiledefinition.SymbolConfig{
2287 - OID: "1.3.6.1.4.1.1000.1",
2288 - Name: "myTable",
2289 - },
2290 - Symbols: []ddprofiledefinition.SymbolConfig{
2291 - {
2292 - OID: "1.3.6.1.4.1.1000.1.1.1",
2293 - Name: "myMetric",
2294 - },
2295 - },
2296 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2297 - {
2298 - Index: 2,
2299 - // No tag name specified, should default to "index2"
2300 - },
2301 - },
2302 - },
2303 - },
2304 - },
2305 - },
2306 - },
2307 - setupMock: func(m *snmpmock.MockHandler) {
2308 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2309 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2310 -
2311 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2312 - []gosnmp.SnmpPDU{
2313 - {
2314 - Name: "1.3.6.1.4.1.1000.1.1.1.10.20",
2315 - Type: gosnmp.Gauge32,
2316 - Value: uint(100),
2317 - },
2318 - }, nil,
2319 - )
2320 - },
2321 - expectedResult: []*ProfileMetrics{
2322 - {
2323 - Source: "test-profile.yaml",
2324 - DeviceMetadata: nil,
2325 - Metrics: []Metric{
2326 - {
2327 - Name: "myMetric",
2328 - Value: 100,
2329 - Tags: map[string]string{"index2": "20"},
2330 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2331 - IsTable: true,
2332 - },
2333 - },
2334 - },
2335 - },
2336 - expectedError: false,
2337 - },
2338 - "index-based tags with single component index": {
2339 - profiles: []*ddsnmp.Profile{
2340 - {
2341 - SourceFile: "test-profile.yaml",
2342 - Definition: &ddprofiledefinition.ProfileDefinition{
2343 - Metrics: []ddprofiledefinition.MetricsConfig{
2344 - {
2345 - MIB: "MY-MIB",
2346 - Table: ddprofiledefinition.SymbolConfig{
2347 - OID: "1.3.6.1.4.1.1000.1",
2348 - Name: "myTable",
2349 - },
2350 - Symbols: []ddprofiledefinition.SymbolConfig{
2351 - {
2352 - OID: "1.3.6.1.4.1.1000.1.1.1",
2353 - Name: "myMetric",
2354 - },
2355 - },
2356 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2357 - {
2358 - Index: 1,
2359 - Tag: "id",
2360 - },
2361 - },
2362 - },
2363 - },
2364 - },
2365 - },
2366 - },
2367 - setupMock: func(m *snmpmock.MockHandler) {
2368 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2369 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2370 -
2371 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2372 - []gosnmp.SnmpPDU{
2373 - // Single component index: just "42"
2374 - {
2375 - Name: "1.3.6.1.4.1.1000.1.1.1.42",
2376 - Type: gosnmp.Gauge32,
2377 - Value: uint(100),
2378 - },
2379 - }, nil,
2380 - )
2381 - },
2382 - expectedResult: []*ProfileMetrics{
2383 - {
2384 - Source: "test-profile.yaml",
2385 - DeviceMetadata: nil,
2386 - Metrics: []Metric{
2387 - {
2388 - Name: "myMetric",
2389 - Value: 100,
2390 - Tags: map[string]string{"id": "42"},
2391 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2392 - IsTable: true,
2393 - },
2394 - },
2395 - },
2396 - },
2397 - expectedError: false,
2398 - },
2399 -
2400 - "cross-table tags with index transformation": {
2401 - profiles: []*ddsnmp.Profile{
2402 - {
2403 - SourceFile: "test-profile.yaml",
2404 - Definition: &ddprofiledefinition.ProfileDefinition{
2405 - Metrics: []ddprofiledefinition.MetricsConfig{
2406 - {
2407 - MIB: "CPI-UNITY-MIB",
2408 - Table: ddprofiledefinition.SymbolConfig{
2409 - OID: "1.3.6.1.4.1.30932.1.10.1.3.110",
2410 - Name: "cpiPduBranchTable",
2411 - },
2412 - Symbols: []ddprofiledefinition.SymbolConfig{
2413 - {
2414 - OID: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3",
2415 - Name: "cpiPduBranchCurrent",
2416 - },
2417 - },
2418 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2419 - {
2420 - Symbol: ddprofiledefinition.SymbolConfigCompat{
2421 - OID: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3",
2422 - Name: "cpiPduName",
2423 - },
2424 - Table: "cpiPduTable",
2425 - IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2426 - {Start: 1, End: 7},
2427 - },
2428 - Tag: "pdu_name",
2429 - },
2430 - },
2431 - },
2432 - {
2433 - MIB: "CPI-UNITY-MIB",
2434 - Table: ddprofiledefinition.SymbolConfig{
2435 - OID: "1.3.6.1.4.1.30932.1.10.1.2.10",
2436 - Name: "cpiPduTable",
2437 - },
2438 - Symbols: []ddprofiledefinition.SymbolConfig{
2439 - // No symbols needed - only used for cross-table reference
2440 - },
2441 - },
2442 - },
2443 - },
2444 - },
2445 - },
2446 - setupMock: func(m *snmpmock.MockHandler) {
2447 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2448 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2449 -
2450 - // Walk cpiPduBranchTable
2451 - // Index structure: <branch_id>.<mac_address>
2452 - // Example: 1.6.0.36.155.53.3.246
2453 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.30932.1.10.1.3.110").Return(
2454 - []gosnmp.SnmpPDU{
2455 - {
2456 - Name: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3.1.6.0.36.155.53.3.246",
2457 - Type: gosnmp.Gauge32,
2458 - Value: uint(150), // 1.5 Amps
2459 - },
2460 - {
2461 - Name: "1.3.6.1.4.1.30932.1.10.1.3.110.1.3.2.6.0.36.155.53.3.247",
2462 - Type: gosnmp.Gauge32,
2463 - Value: uint(200), // 2.0 Amps
2464 - },
2465 - }, nil,
2466 - )
2467 -
2468 - // Walk cpiPduTable
2469 - // Index structure: <mac_address> only
2470 - // Example: 6.0.36.155.53.3.246
2471 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.30932.1.10.1.2.10").Return(
2472 - []gosnmp.SnmpPDU{
2473 - {
2474 - Name: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3.6.0.36.155.53.3.246",
2475 - Type: gosnmp.OctetString,
2476 - Value: []byte("PDU-A"),
2477 - },
2478 - {
2479 - Name: "1.3.6.1.4.1.30932.1.10.1.2.10.1.3.6.0.36.155.53.3.247",
2480 - Type: gosnmp.OctetString,
2481 - Value: []byte("PDU-B"),
2482 - },
2483 - }, nil,
2484 - )
2485 - },
2486 - expectedResult: []*ProfileMetrics{
2487 - {
2488 - Source: "test-profile.yaml",
2489 - DeviceMetadata: nil,
2490 - Metrics: []Metric{
2491 - {
2492 - Name: "cpiPduBranchCurrent",
2493 - Value: 150,
2494 - Tags: map[string]string{"pdu_name": "PDU-A"},
2495 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2496 - IsTable: true,
2497 - },
2498 - {
2499 - Name: "cpiPduBranchCurrent",
2500 - Value: 200,
2501 - Tags: map[string]string{"pdu_name": "PDU-B"},
2502 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2503 - IsTable: true,
2504 - },
2505 - },
2506 - },
2507 - },
2508 - expectedError: false,
2509 - },
2510 - "cross-table tags with multiple index transformations": {
2511 - profiles: []*ddsnmp.Profile{
2512 - {
2513 - SourceFile: "test-profile.yaml",
2514 - Definition: &ddprofiledefinition.ProfileDefinition{
2515 - Metrics: []ddprofiledefinition.MetricsConfig{
2516 - {
2517 - MIB: "MY-MIB",
2518 - Table: ddprofiledefinition.SymbolConfig{
2519 - OID: "1.3.6.1.4.1.1000.1",
2520 - Name: "myComplexTable",
2521 - },
2522 - Symbols: []ddprofiledefinition.SymbolConfig{
2523 - {
2524 - OID: "1.3.6.1.4.1.1000.1.1.1",
2525 - Name: "myMetric",
2526 - },
2527 - },
2528 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2529 - {
2530 - Symbol: ddprofiledefinition.SymbolConfigCompat{
2531 - OID: "1.3.6.1.4.1.1000.2.1.1",
2532 - Name: "refName",
2533 - },
2534 - Table: "refTable",
2535 - IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2536 - {Start: 0, End: 1},
2537 - {Start: 3, End: 5},
2538 - },
2539 - Tag: "ref_name",
2540 - },
2541 - },
2542 - },
2543 - {
2544 - MIB: "MY-MIB",
2545 - Table: ddprofiledefinition.SymbolConfig{
2546 - OID: "1.3.6.1.4.1.1000.2",
2547 - Name: "refTable",
2548 - },
2549 - Symbols: []ddprofiledefinition.SymbolConfig{
2550 - // No symbols needed
2551 - },
2552 - },
2553 - },
2554 - },
2555 - },
2556 - },
2557 - setupMock: func(m *snmpmock.MockHandler) {
2558 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2559 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2560 -
2561 - // Walk myComplexTable
2562 - // Index: 1.2.3.4.5.6.7
2563 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2564 - []gosnmp.SnmpPDU{
2565 - {
2566 - Name: "1.3.6.1.4.1.1000.1.1.1.1.2.3.4.5.6.7",
2567 - Type: gosnmp.Gauge32,
2568 - Value: uint(100),
2569 - },
2570 - }, nil,
2571 - )
2572 -
2573 - // Walk refTable
2574 - // Expected transformed index: 1.2.4.5.6 (positions 1-2 and 4-6)
2575 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2576 - []gosnmp.SnmpPDU{
2577 - {
2578 - Name: "1.3.6.1.4.1.1000.2.1.1.1.2.4.5.6",
2579 - Type: gosnmp.OctetString,
2580 - Value: []byte("Complex-Ref"),
2581 - },
2582 - }, nil,
2583 - )
2584 - },
2585 - expectedResult: []*ProfileMetrics{
2586 - {
2587 - Source: "test-profile.yaml",
2588 - DeviceMetadata: nil,
2589 - Metrics: []Metric{
2590 - {
2591 - Name: "myMetric",
2592 - Value: 100,
2593 - Tags: map[string]string{"ref_name": "Complex-Ref"},
2594 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2595 - IsTable: true,
2596 - },
2597 - },
2598 - },
2599 - },
2600 - expectedError: false,
2601 - },
2602 - "cross-table tags with index transformation no match": {
2603 - profiles: []*ddsnmp.Profile{
2604 - {
2605 - SourceFile: "test-profile.yaml",
2606 - Definition: &ddprofiledefinition.ProfileDefinition{
2607 - Metrics: []ddprofiledefinition.MetricsConfig{
2608 - {
2609 - MIB: "MY-MIB",
2610 - Table: ddprofiledefinition.SymbolConfig{
2611 - OID: "1.3.6.1.4.1.1000.1",
2612 - Name: "myTable",
2613 - },
2614 - Symbols: []ddprofiledefinition.SymbolConfig{
2615 - {
2616 - OID: "1.3.6.1.4.1.1000.1.1.1",
2617 - Name: "myMetric",
2618 - },
2619 - },
2620 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2621 - {
2622 - Symbol: ddprofiledefinition.SymbolConfigCompat{
2623 - OID: "1.3.6.1.4.1.1000.2.1.1",
2624 - Name: "refName",
2625 - },
2626 - Table: "refTable",
2627 - IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2628 - {Start: 2, End: 4},
2629 - },
2630 - Tag: "ref_name",
2631 - },
2632 - },
2633 - },
2634 - {
2635 - MIB: "MY-MIB",
2636 - Table: ddprofiledefinition.SymbolConfig{
2637 - OID: "1.3.6.1.4.1.1000.2",
2638 - Name: "refTable",
2639 - },
2640 - Symbols: []ddprofiledefinition.SymbolConfig{
2641 - // No symbols needed
2642 - },
2643 - },
2644 - },
2645 - },
2646 - },
2647 - },
2648 - setupMock: func(m *snmpmock.MockHandler) {
2649 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2650 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2651 -
2652 - // Walk myTable
2653 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2654 - []gosnmp.SnmpPDU{
2655 - {
2656 - Name: "1.3.6.1.4.1.1000.1.1.1.1.2.3",
2657 - Type: gosnmp.Gauge32,
2658 - Value: uint(100),
2659 - },
2660 - }, nil,
2661 - )
2662 -
2663 - // Walk refTable - but it doesn't have the transformed index
2664 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2665 - []gosnmp.SnmpPDU{
2666 - {
2667 - Name: "1.3.6.1.4.1.1000.2.1.1.9.9.9", // Different index
2668 - Type: gosnmp.OctetString,
2669 - Value: []byte("Other-Ref"),
2670 - },
2671 - }, nil,
2672 - )
2673 - },
2674 - expectedResult: []*ProfileMetrics{
2675 - {
2676 - Source: "test-profile.yaml",
2677 - DeviceMetadata: nil,
2678 - Metrics: []Metric{
2679 - {
2680 - Name: "myMetric",
2681 - Value: 100,
2682 - Tags: nil, // No tag because transformed index not found
2683 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2684 - IsTable: true,
2685 - },
2686 - },
2687 - },
2688 - },
2689 - expectedError: false,
2690 - },
2691 - "cross-table tags with invalid index transformation": {
2692 - profiles: []*ddsnmp.Profile{
2693 - {
2694 - SourceFile: "test-profile.yaml",
2695 - Definition: &ddprofiledefinition.ProfileDefinition{
2696 - Metrics: []ddprofiledefinition.MetricsConfig{
2697 - {
2698 - MIB: "MY-MIB",
2699 - Table: ddprofiledefinition.SymbolConfig{
2700 - OID: "1.3.6.1.4.1.1000.1",
2701 - Name: "myTable",
2702 - },
2703 - Symbols: []ddprofiledefinition.SymbolConfig{
2704 - {
2705 - OID: "1.3.6.1.4.1.1000.1.1.1",
2706 - Name: "myMetric",
2707 - },
2708 - },
2709 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2710 - {
2711 - Symbol: ddprofiledefinition.SymbolConfigCompat{
2712 - OID: "1.3.6.1.4.1.1000.2.1.1",
2713 - Name: "refName",
2714 - },
2715 - Table: "refTable",
2716 - IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2717 - {Start: 5, End: 10}, // Out of bounds
2718 - },
2719 - Tag: "ref_name",
2720 - },
2721 - },
2722 - },
2723 - {
2724 - MIB: "MY-MIB",
2725 - Table: ddprofiledefinition.SymbolConfig{
2726 - OID: "1.3.6.1.4.1.1000.2",
2727 - Name: "refTable",
2728 - },
2729 - Symbols: []ddprofiledefinition.SymbolConfig{
2730 - // No symbols needed
2731 - },
2732 - },
2733 - },
2734 - },
2735 - },
2736 - },
2737 - setupMock: func(m *snmpmock.MockHandler) {
2738 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2739 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2740 -
2741 - // Walk myTable with short index
2742 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2743 - []gosnmp.SnmpPDU{
2744 - {
2745 - Name: "1.3.6.1.4.1.1000.1.1.1.1.2",
2746 - Type: gosnmp.Gauge32,
2747 - Value: uint(100),
2748 - },
2749 - }, nil,
2750 - )
2751 -
2752 - // Walk refTable
2753 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2754 - []gosnmp.SnmpPDU{
2755 - {
2756 - Name: "1.3.6.1.4.1.1000.2.1.1.1.2",
2757 - Type: gosnmp.OctetString,
2758 - Value: []byte("Ref"),
2759 - },
2760 - }, nil,
2761 - )
2762 - },
2763 - expectedResult: []*ProfileMetrics{
2764 - {
2765 - Source: "test-profile.yaml",
2766 - DeviceMetadata: nil,
2767 - Metrics: []Metric{
2768 - {
2769 - Name: "myMetric",
2770 - Value: 100,
2771 - Tags: nil, // No tag because transformation failed
2772 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2773 - IsTable: true,
2774 - },
2775 - },
2776 - },
2777 - },
2778 - expectedError: false,
2779 - },
2780 - "cross-table tags with transformation and mapping": {
2781 - profiles: []*ddsnmp.Profile{
2782 - {
2783 - SourceFile: "test-profile.yaml",
2784 - Definition: &ddprofiledefinition.ProfileDefinition{
2785 - Metrics: []ddprofiledefinition.MetricsConfig{
2786 - {
2787 - MIB: "MY-MIB",
2788 - Table: ddprofiledefinition.SymbolConfig{
2789 - OID: "1.3.6.1.4.1.1000.1",
2790 - Name: "myTable",
2791 - },
2792 - Symbols: []ddprofiledefinition.SymbolConfig{
2793 - {
2794 - OID: "1.3.6.1.4.1.1000.1.1.1",
2795 - Name: "myMetric",
2796 - },
2797 - },
2798 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2799 - {
2800 - Symbol: ddprofiledefinition.SymbolConfigCompat{
2801 - OID: "1.3.6.1.4.1.1000.2.1.1",
2802 - Name: "refType",
2803 - },
2804 - Table: "refTable",
2805 - IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2806 - {Start: 1, End: 2},
2807 - },
2808 - Tag: "ref_type",
2809 - Mapping: map[string]string{
2810 - "1": "primary",
2811 - "2": "secondary",
2812 - "3": "backup",
2813 - },
2814 - },
2815 - },
2816 - },
2817 - {
2818 - MIB: "MY-MIB",
2819 - Table: ddprofiledefinition.SymbolConfig{
2820 - OID: "1.3.6.1.4.1.1000.2",
2821 - Name: "refTable",
2822 - },
2823 - Symbols: []ddprofiledefinition.SymbolConfig{
2824 - // No symbols needed
2825 - },
2826 - },
2827 - },
2828 - },
2829 - },
2830 - },
2831 - setupMock: func(m *snmpmock.MockHandler) {
2832 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2833 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2834 -
2835 - // Walk myTable
2836 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2837 - []gosnmp.SnmpPDU{
2838 - {
2839 - Name: "1.3.6.1.4.1.1000.1.1.1.5.10.20",
2840 - Type: gosnmp.Gauge32,
2841 - Value: uint(100),
2842 - },
2843 - {
2844 - Name: "1.3.6.1.4.1.1000.1.1.1.6.20.30",
2845 - Type: gosnmp.Gauge32,
2846 - Value: uint(200),
2847 - },
2848 - }, nil,
2849 - )
2850 -
2851 - // Walk refTable with transformed indexes
2852 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2853 - []gosnmp.SnmpPDU{
2854 - {
2855 - Name: "1.3.6.1.4.1.1000.2.1.1.10.20", // Matches first row (positions 2-3)
2856 - Type: gosnmp.Integer,
2857 - Value: 1, // Will map to "primary"
2858 - },
2859 - {
2860 - Name: "1.3.6.1.4.1.1000.2.1.1.20.30", // Matches second row (positions 2-3)
2861 - Type: gosnmp.Integer,
2862 - Value: 3, // Will map to "backup"
2863 - },
2864 - }, nil,
2865 - )
2866 - },
2867 - expectedResult: []*ProfileMetrics{
2868 - {
2869 - Source: "test-profile.yaml",
2870 - DeviceMetadata: nil,
2871 - Metrics: []Metric{
2872 - {
2873 - Name: "myMetric",
2874 - Value: 100,
2875 - Tags: map[string]string{"ref_type": "primary"},
2876 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2877 - IsTable: true,
2878 - },
2879 - {
2880 - Name: "myMetric",
2881 - Value: 200,
2882 - Tags: map[string]string{"ref_type": "backup"},
2883 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2884 - IsTable: true,
2885 - },
2886 - },
2887 - },
2888 - },
2889 - expectedError: false,
2890 - },
2891 - "cross-table tags mixed with index-based tags": {
2892 - profiles: []*ddsnmp.Profile{
2893 - {
2894 - SourceFile: "test-profile.yaml",
2895 - Definition: &ddprofiledefinition.ProfileDefinition{
2896 - Metrics: []ddprofiledefinition.MetricsConfig{
2897 - {
2898 - MIB: "MY-MIB",
2899 - Table: ddprofiledefinition.SymbolConfig{
2900 - OID: "1.3.6.1.4.1.1000.1",
2901 - Name: "myTable",
2902 - },
2903 - Symbols: []ddprofiledefinition.SymbolConfig{
2904 - {
2905 - OID: "1.3.6.1.4.1.1000.1.1.1",
2906 - Name: "myMetric",
2907 - },
2908 - },
2909 - MetricTags: []ddprofiledefinition.MetricTagConfig{
2910 - {
2911 - Index: 1,
2912 - Tag: "branch_id",
2913 - },
2914 - {
2915 - Symbol: ddprofiledefinition.SymbolConfigCompat{
2916 - OID: "1.3.6.1.4.1.1000.2.1.1",
2917 - Name: "pduName",
2918 - },
2919 - Table: "pduTable",
2920 - IndexTransform: []ddprofiledefinition.MetricIndexTransform{
2921 - {Start: 1, End: 7},
2922 - },
2923 - Tag: "pdu_name",
2924 - },
2925 - },
2926 - },
2927 - {
2928 - MIB: "MY-MIB",
2929 - Table: ddprofiledefinition.SymbolConfig{
2930 - OID: "1.3.6.1.4.1.1000.2",
2931 - Name: "pduTable",
2932 - },
2933 - Symbols: []ddprofiledefinition.SymbolConfig{
2934 - // No symbols needed
2935 - },
2936 - },
2937 - },
2938 - },
2939 - },
2940 - },
2941 - setupMock: func(m *snmpmock.MockHandler) {
2942 - m.EXPECT().MaxOids().Return(10).AnyTimes()
2943 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
2944 -
2945 - // Walk myTable
2946 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
2947 - []gosnmp.SnmpPDU{
2948 - {
2949 - Name: "1.3.6.1.4.1.1000.1.1.1.1.6.0.36.155.53.3.246",
2950 - Type: gosnmp.Gauge32,
2951 - Value: uint(100),
2952 - },
2953 - }, nil,
2954 - )
2955 -
2956 - // Walk pduTable
2957 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.2").Return(
2958 - []gosnmp.SnmpPDU{
2959 - {
2960 - Name: "1.3.6.1.4.1.1000.2.1.1.6.0.36.155.53.3.246",
2961 - Type: gosnmp.OctetString,
2962 - Value: []byte("Main-PDU"),
2963 - },
2964 - }, nil,
2965 - )
2966 - },
2967 - expectedResult: []*ProfileMetrics{
2968 - {
2969 - Source: "test-profile.yaml",
2970 - DeviceMetadata: nil,
2971 - Metrics: []Metric{
2972 - {
2973 - Name: "myMetric",
2974 - Value: 100,
2975 - Tags: map[string]string{
2976 - "branch_id": "1",
2977 - "pdu_name": "Main-PDU",
2978 - },
2979 - MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
2980 - IsTable: true,
2981 - },
2982 - },
2983 - },
2984 - },
2985 - expectedError: false,
2986 - },
2987 -
2988 - "scalar metric with OpaqueFloat": {
2989 - profiles: []*ddsnmp.Profile{
2990 - {
2991 - SourceFile: "test-profile.yaml",
2992 - Definition: &ddprofiledefinition.ProfileDefinition{
2993 - Metrics: []ddprofiledefinition.MetricsConfig{
2994 - {
2995 - Symbol: ddprofiledefinition.SymbolConfig{
2996 - OID: "1.3.6.1.4.1.6574.4.2.12.1.0",
2997 - Name: "temperature",
2998 - },
2999 - },
3000 - },
3001 - },
3002 - },
3003 - },
3004 - setupMock: func(m *snmpmock.MockHandler) {
3005 - m.EXPECT().MaxOids().Return(10).AnyTimes()
3006 - m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.2.12.1.0"}).Return(
3007 - &gosnmp.SnmpPacket{
3008 - Variables: []gosnmp.SnmpPDU{
3009 - {
3010 - Name: "1.3.6.1.4.1.6574.4.2.12.1.0",
3011 - Type: gosnmp.OpaqueFloat,
3012 - Value: float32(29.5),
3013 - },
3014 - },
3015 - }, nil,
3016 - )
3017 - },
3018 - expectedResult: []*ProfileMetrics{
3019 - {
3020 - Source: "test-profile.yaml",
3021 - DeviceMetadata: nil,
3022 - Metrics: []Metric{
3023 - {
3024 - Name: "temperature",
3025 - Value: 29, // Truncated from 29.5
3026 - MetricType: "gauge",
3027 - },
3028 - },
3029 - },
3030 - },
3031 - expectedError: false,
3032 - },
3033 - "scalar metric with OpaqueDouble": {
3034 - profiles: []*ddsnmp.Profile{
3035 - {
3036 - SourceFile: "test-profile.yaml",
3037 - Definition: &ddprofiledefinition.ProfileDefinition{
3038 - Metrics: []ddprofiledefinition.MetricsConfig{
3039 - {
3040 - Symbol: ddprofiledefinition.SymbolConfig{
3041 - OID: "1.3.6.1.4.1.6574.4.4.1.1.0",
3042 - Name: "voltage",
3043 - },
3044 - },
3045 - },
3046 - },
3047 - },
3048 - },
3049 - setupMock: func(m *snmpmock.MockHandler) {
3050 - m.EXPECT().MaxOids().Return(10).AnyTimes()
3051 - m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.4.1.1.0"}).Return(
3052 - &gosnmp.SnmpPacket{
3053 - Variables: []gosnmp.SnmpPDU{
3054 - {
3055 - Name: "1.3.6.1.4.1.6574.4.4.1.1.0",
3056 - Type: gosnmp.OpaqueDouble,
3057 - Value: float64(232.75),
3058 - },
3059 - },
3060 - }, nil,
3061 - )
3062 - },
3063 - expectedResult: []*ProfileMetrics{
3064 - {
3065 - Source: "test-profile.yaml",
3066 - DeviceMetadata: nil,
3067 - Metrics: []Metric{
3068 - {
3069 - Name: "voltage",
3070 - Value: 232, // Truncated from 232.75
3071 - MetricType: "gauge",
3072 - },
3073 - },
3074 - },
3075 - },
3076 - expectedError: false,
3077 - },
3078 - "scalar metric with OpaqueFloat and scale factor": {
3079 - profiles: []*ddsnmp.Profile{
3080 - {
3081 - SourceFile: "test-profile.yaml",
3082 - Definition: &ddprofiledefinition.ProfileDefinition{
3083 - Metrics: []ddprofiledefinition.MetricsConfig{
3084 - {
3085 - Symbol: ddprofiledefinition.SymbolConfig{
3086 - OID: "1.3.6.1.4.1.6574.4.2.12.1.0",
3087 - Name: "temperatureMilliDegrees",
3088 - ScaleFactor: 1000, // Convert to milli-degrees to preserve precision
3089 - },
3090 - },
3091 - },
3092 - },
3093 - },
3094 - },
3095 - setupMock: func(m *snmpmock.MockHandler) {
3096 - m.EXPECT().MaxOids().Return(10).AnyTimes()
3097 - m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.2.12.1.0"}).Return(
3098 - &gosnmp.SnmpPacket{
3099 - Variables: []gosnmp.SnmpPDU{
3100 - {
3101 - Name: "1.3.6.1.4.1.6574.4.2.12.1.0",
3102 - Type: gosnmp.OpaqueFloat,
3103 - Value: float32(29.567),
3104 - },
3105 - },
3106 - }, nil,
3107 - )
3108 - },
3109 - expectedResult: []*ProfileMetrics{
3110 - {
3111 - Source: "test-profile.yaml",
3112 - DeviceMetadata: nil,
3113 - Metrics: []Metric{
3114 - {
3115 - Name: "temperatureMilliDegrees",
3116 - Value: 29566, // 29.567 * 1000
3117 - MetricType: "gauge",
3118 - },
3119 - },
3120 - },
3121 - },
3122 - expectedError: false,
3123 - },
3124 - "table metric with OpaqueFloat": {
3125 - profiles: []*ddsnmp.Profile{
3126 - {
3127 - SourceFile: "test-profile.yaml",
3128 - Definition: &ddprofiledefinition.ProfileDefinition{
3129 - Metrics: []ddprofiledefinition.MetricsConfig{
3130 - {
3131 - MIB: "SYNOLOGY-SYSTEM-MIB",
3132 - Table: ddprofiledefinition.SymbolConfig{
3133 - OID: "1.3.6.1.4.1.6574.1",
3134 - Name: "temperatureTable",
3135 - },
3136 - Symbols: []ddprofiledefinition.SymbolConfig{
3137 - {
3138 - OID: "1.3.6.1.4.1.6574.1.2",
3139 - Name: "temperature",
3140 - },
3141 - },
3142 - MetricTags: []ddprofiledefinition.MetricTagConfig{
3143 - {
3144 - Tag: "sensor",
3145 - Symbol: ddprofiledefinition.SymbolConfigCompat{
3146 - OID: "1.3.6.1.4.1.6574.1.1",
3147 - Name: "temperatureIndex",
3148 - },
3149 - },
3150 - },
3151 - },
3152 - },
3153 - },
3154 - },
3155 - },
3156 - setupMock: func(m *snmpmock.MockHandler) {
3157 - m.EXPECT().MaxOids().Return(10).AnyTimes()
3158 - m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
3159 -
3160 - m.EXPECT().BulkWalkAll("1.3.6.1.4.1.6574.1").Return(
3161 - []gosnmp.SnmpPDU{
3162 - // Row 1
3163 - {
3164 - Name: "1.3.6.1.4.1.6574.1.1.1",
3165 - Type: gosnmp.Integer,
3166 - Value: 1,
3167 - },
3168 - {
3169 - Name: "1.3.6.1.4.1.6574.1.2.1",
3170 - Type: gosnmp.OpaqueFloat,
3171 - Value: float32(65.5),
3172 - },
3173 - // Row 2
3174 - {
3175 - Name: "1.3.6.1.4.1.6574.1.1.2",
3176 - Type: gosnmp.Integer,
3177 - Value: 2,
3178 - },
3179 - {
3180 - Name: "1.3.6.1.4.1.6574.1.2.2",
3181 - Type: gosnmp.OpaqueFloat,
3182 - Value: float32(71.25),
3183 - },
3184 - }, nil,
3185 - )
3186 - },
3187 - expectedResult: []*ProfileMetrics{
3188 - {
3189 - Source: "test-profile.yaml",
3190 - DeviceMetadata: nil,
3191 - Metrics: []Metric{
3192 - {
3193 - Name: "temperature",
3194 - Value: 65, // Truncated from 65.5
3195 - Tags: map[string]string{"sensor": "1"},
3196 - MetricType: "gauge",
3197 - IsTable: true,
3198 - },
3199 - {
3200 - Name: "temperature",
3201 - Value: 71, // Truncated from 71.25
3202 - Tags: map[string]string{"sensor": "2"},
3203 - MetricType: "gauge",
3204 - IsTable: true,
3205 - },
3206 - },
3207 - },
3208 - },
3209 - expectedError: false,
3210 - },
3211 - "OpaqueFloat with unexpected type": {
3212 - profiles: []*ddsnmp.Profile{
3213 - {
3214 - SourceFile: "test-profile.yaml",
3215 - Definition: &ddprofiledefinition.ProfileDefinition{
3216 - Metrics: []ddprofiledefinition.MetricsConfig{
3217 - {
3218 - Symbol: ddprofiledefinition.SymbolConfig{
3219 - OID: "1.3.6.1.4.1.6574.4.2.12.1.0",
3220 - Name: "temperature",
3221 - },
3222 - },
3223 - },
3224 - },
3225 - },
3226 - },
3227 - setupMock: func(m *snmpmock.MockHandler) {
3228 - m.EXPECT().MaxOids().Return(10).AnyTimes()
3229 - m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.2.12.1.0"}).Return(
3230 - &gosnmp.SnmpPacket{
3231 - Variables: []gosnmp.SnmpPDU{
3232 - {
3233 - Name: "1.3.6.1.4.1.6574.4.2.12.1.0",
3234 - Type: gosnmp.OpaqueFloat,
3235 - Value: "29.5", // Wrong type - should be float32
3236 - },
3237 - },
3238 - }, nil,
3239 - )
3240 - },
3241 - expectedResult: nil,
3242 - expectedError: true,
3243 - errorContains: "OpaqueFloat has unexpected type",
3244 - },
3245 - "OpaqueFloat with negative value": {
3246 - profiles: []*ddsnmp.Profile{
3247 - {
3248 - SourceFile: "test-profile.yaml",
3249 - Definition: &ddprofiledefinition.ProfileDefinition{
3250 - Metrics: []ddprofiledefinition.MetricsConfig{
3251 - {
3252 - Symbol: ddprofiledefinition.SymbolConfig{
3253 - OID: "1.3.6.1.4.1.6574.4.2.12.1.0",
3254 - Name: "temperature",
3255 - },
3256 - },
3257 - },
3258 - },
3259 - },
3260 - },
3261 - setupMock: func(m *snmpmock.MockHandler) {
3262 - m.EXPECT().MaxOids().Return(10).AnyTimes()
3263 - m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.2.12.1.0"}).Return(
3264 - &gosnmp.SnmpPacket{
3265 - Variables: []gosnmp.SnmpPDU{
3266 - {
3267 - Name: "1.3.6.1.4.1.6574.4.2.12.1.0",
3268 - Type: gosnmp.OpaqueFloat,
3269 - Value: float32(-15.5),
3270 - },
3271 - },
3272 - }, nil,
3273 - )
3274 - },
3275 - expectedResult: []*ProfileMetrics{
3276 - {
3277 - Source: "test-profile.yaml",
3278 - DeviceMetadata: nil,
3279 - Metrics: []Metric{
3280 - {
3281 - Name: "temperature",
3282 - Value: -15, // Truncated from -15.5
1052 + Name: "upsBasicBatteryStatus",
1053 + Value: 1,
1054 MetricType: "gauge",
3284 - },
3285 - },
3286 - },
3287 - },
3288 - expectedError: false,
3289 - },
3290 - "OpaqueFloat with very large value": {
3291 - profiles: []*ddsnmp.Profile{
3292 - {
3293 - SourceFile: "test-profile.yaml",
3294 - Definition: &ddprofiledefinition.ProfileDefinition{
3295 - Metrics: []ddprofiledefinition.MetricsConfig{
3296 - {
3297 - Symbol: ddprofiledefinition.SymbolConfig{
3298 - OID: "1.3.6.1.4.1.6574.4.4.2.2.0",
3299 - Name: "power",
3300 - },
3301 - },
3302 - },
3303 - },
3304 - },
3305 - },
3306 - setupMock: func(m *snmpmock.MockHandler) {
3307 - m.EXPECT().MaxOids().Return(10).AnyTimes()
3308 - m.EXPECT().Get([]string{"1.3.6.1.4.1.6574.4.4.2.2.0"}).Return(
3309 - &gosnmp.SnmpPacket{
3310 - Variables: []gosnmp.SnmpPDU{
3311 - {
3312 - Name: "1.3.6.1.4.1.6574.4.4.2.2.0",
3313 - Type: gosnmp.OpaqueFloat,
3314 - Value: float32(1234567.89),
1055 + Mappings: map[int64]string{
1056 + 0: "batteryNormal",
1057 + 1: "batteryLow",
1058 + 2: "batteryDepleted",
1059 + 3: "batteryCharging",
1060 },
1061 },
3317 - }, nil,
3318 - )
3319 - },
3320 - expectedResult: []*ProfileMetrics{
3321 - {
3322 - Source: "test-profile.yaml",
3323 - DeviceMetadata: nil,
3324 - Metrics: []Metric{
3325 - {
3326 - Name: "power",
3327 - Value: 1234567, // Truncated from 1234567.89
3328 - MetricType: "gauge",
3329 - },
1062 },
1063 },
1064 },
@@ -3344,13 +1076,11 @@ func TestCollector_Collect(t *testing.T) {
1076
1077 collector := New(mockHandler, tc.profiles, logger.New())
1078 collector.DoTableMetrics = true
3347 - collector.tableCache.setTTL(0, 0)
1079 + collector.tableCache.setTTL(0, 0) // Disable cache
1080
1081 result, err := collector.Collect()
1082
3351 - // The Metric struct has a Profile field that contains a pointer to ProfileMetrics,
3352 - // which itself contains the Metrics slice.
3353 - // This creates a circular reference that makes ElementsMatch fail.
1083 + // Clear circular references
1084 for _, profile := range result {
1085 for i := range profile.Metrics {
1086 profile.Metrics[i].Profile = nil
@@ -3370,6 +1100,7 @@ func TestCollector_Collect(t *testing.T) {
1100 require.Equal(t, len(tc.expectedResult), len(result))
1101 for i := range tc.expectedResult {
1102 assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
1103 + assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
1104 assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
1105 }
1106 } else {