feat(go.d/snmp): add structured mapping config and bitmask value mappings (#22200)
Ilya Mashchenko committed
Apr 13, 2026 at 17:34 UTC
a6307e68d3e35ff60299249d3f905b18c4f28ed7
25 files changed
+845
-145
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/mapping.go
new
+126
@@ -0,0 +1,126 @@
1
+// Unless explicitly stated otherwise all files in this repository are licensed
2
+// under the Apache License Version 2.0.
3
+// This product includes software developed at Datadog (https://www.datadoghq.com/).
4
+// Copyright 2026-present Datadog, Inc.
5
+
6
+package ddprofiledefinition
7
+
8
+import (
9
+ "fmt"
10
+ "maps"
11
+ "reflect"
12
+)
13
+
14
+type MappingMode string
15
+
16
+const (
17
+ MappingModeExact MappingMode = "exact"
18
+ MappingModeBitmask MappingMode = "bitmask"
19
+)
20
+
21
+// MappingConfig defines mapping behavior for metric values, tags, and metadata.
22
+// Legacy flat-map YAML remains supported and is interpreted as exact mode.
23
+type MappingConfig struct {
24
+ Mode MappingMode `yaml:"mode,omitempty" json:"mode,omitempty"`
25
+ Items map[string]string `yaml:"items,omitempty" json:"items,omitempty"`
26
+}
27
+
28
+func NewExactMapping(items map[string]string) MappingConfig {
29
+ return MappingConfig{
30
+ Mode: MappingModeExact,
31
+ Items: maps.Clone(items),
32
+ }
33
+}
34
+
35
+func NewBitmaskMapping(items map[string]string) MappingConfig {
36
+ return MappingConfig{
37
+ Mode: MappingModeBitmask,
38
+ Items: maps.Clone(items),
39
+ }
40
+}
41
+
42
+func (m MappingConfig) Clone() MappingConfig {
43
+ return MappingConfig{
44
+ Mode: m.Mode,
45
+ Items: maps.Clone(m.Items),
46
+ }
47
+}
48
+
49
+func (m MappingConfig) HasItems() bool {
50
+ return len(m.Items) > 0
51
+}
52
+
53
+func (m MappingConfig) Lookup(key string) (string, bool) {
54
+ if len(m.Items) == 0 {
55
+ return "", false
56
+ }
57
+ v, ok := m.Items[key]
58
+ return v, ok
59
+}
60
+
61
+func (m MappingConfig) EffectiveMode() MappingMode {
62
+ if m.Mode == "" {
63
+ return MappingModeExact
64
+ }
65
+ return m.Mode
66
+}
67
+
68
+func (m MappingConfig) String() string {
69
+ if m.Mode == "" || m.Mode == MappingModeExact {
70
+ return fmt.Sprintf("%v", m.Items)
71
+ }
72
+ return fmt.Sprintf("{mode:%s items:%v}", m.Mode, m.Items)
73
+}
74
+
75
+// UnmarshalYAML supports both the legacy flat-map syntax and the structured object syntax.
76
+func (m *MappingConfig) UnmarshalYAML(unmarshal func(any) error) error {
77
+ var raw map[string]any
78
+ if err := unmarshal(&raw); err == nil {
79
+ if items, ok := raw["items"]; ok && isStructuredMappingItems(items) {
80
+ return m.unmarshalStructured(unmarshal)
81
+ }
82
+ }
83
+
84
+ var items map[string]string
85
+ if err := unmarshal(&items); err != nil {
86
+ return err
87
+ }
88
+
89
+ if len(items) == 0 {
90
+ *m = MappingConfig{}
91
+ return nil
92
+ }
93
+
94
+ *m = MappingConfig{
95
+ Mode: MappingModeExact,
96
+ Items: items,
97
+ }
98
+
99
+ return nil
100
+}
101
+
102
+func (m *MappingConfig) unmarshalStructured(unmarshal func(any) error) error {
103
+ type plain MappingConfig
104
+
105
+ var cfg plain
106
+ if err := unmarshal(&cfg); err != nil {
107
+ return err
108
+ }
109
+
110
+ mode := MappingConfig(cfg).Mode
111
+ items := MappingConfig(cfg).Items
112
+ if len(items) == 0 {
113
+ items = nil
114
+ }
115
+ if len(items) > 0 && mode == "" {
116
+ mode = MappingModeExact
117
+ }
118
+
119
+ *m = MappingConfig{Mode: mode, Items: items}
120
+ return nil
121
+}
122
+
123
+func isStructuredMappingItems(v any) bool {
124
+ rv := reflect.ValueOf(v)
125
+ return rv.IsValid() && rv.Kind() == reflect.Map
126
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/metadata_test.go
+2
-2
@@ -65,10 +65,10 @@ func makeMetadata() MetadataConfig {
65
End: 5,
66
},
67
},
68
- Mapping: map[string]string{
68
+ Mapping: NewExactMapping(map[string]string{
69
"1": "on",
70
"2": "off",
71
- },
71
+ }),
72
Match: ".*",
73
Pattern: regexp.MustCompile(".*"),
74
Tags: map[string]string{
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/metrics.go
+5
-5
@@ -137,7 +137,7 @@ type SymbolConfig struct {
137
138
ChartMeta ChartMeta `yaml:"chart_meta,omitempty" json:"chart_meta"`
139
140
- Mapping map[string]string `yaml:"mapping,omitempty" json:"mapping,omitempty"`
140
+ Mapping MappingConfig `yaml:"mapping,omitempty" json:"mapping,omitempty"`
141
Transform string `yaml:"transform,omitempty" json:"transform,omitempty"`
142
TransformCompiled *template.Template `yaml:"-" json:"-"`
143
}
@@ -145,7 +145,7 @@ type SymbolConfig struct {
145
// Clone creates a duplicate of this SymbolConfig
146
func (s SymbolConfig) Clone() SymbolConfig {
147
ss := s
148
- ss.Mapping = maps.Clone(ss.Mapping)
148
+ ss.Mapping = ss.Mapping.Clone()
149
return ss
150
}
151
@@ -183,8 +183,8 @@ type MetricTagConfig struct {
183
184
IndexTransform []MetricIndexTransform `yaml:"index_transform,omitempty" json:"index_transform,omitempty"`
185
186
- MappingRef string `yaml:"mapping_ref,omitempty" json:"mapping_ref,omitempty"`
187
- Mapping map[string]string `yaml:"mapping,omitempty" json:"mapping,omitempty"`
186
+ MappingRef string `yaml:"mapping_ref,omitempty" json:"mapping_ref,omitempty"`
187
+ Mapping MappingConfig `yaml:"mapping,omitempty" json:"mapping,omitempty"`
188
189
// Regex
190
// Match/Tags are not exposed as json (UI) since ExtractValue can be used instead
@@ -203,7 +203,7 @@ func (m MetricTagConfig) Clone() MetricTagConfig {
203
m2.Symbol = m.Symbol.Clone()
204
m2.LookupSymbol = m.LookupSymbol.Clone()
205
m2.IndexTransform = slices.Clone(m.IndexTransform)
206
- m2.Mapping = maps.Clone(m.Mapping)
206
+ m2.Mapping = m.Mapping.Clone()
207
m2.Tags = maps.Clone(m.Tags)
208
return m2
209
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/metrics_test.go
+5
-5
@@ -74,10 +74,10 @@ func TestCloneMetricTagConfig(t *testing.T) {
74
End: 1,
75
},
76
},
77
- Mapping: map[string]string{
77
+ Mapping: NewExactMapping(map[string]string{
78
"1": "bar",
79
"2": "baz",
80
- },
80
+ }),
81
Match: ".*",
82
Pattern: regexp.MustCompile(".*"),
83
Tags: map[string]string{
@@ -89,7 +89,7 @@ func TestCloneMetricTagConfig(t *testing.T) {
89
assert.Equal(t, c, c2)
90
c2.Tags["bar"] = "$2"
91
c2.IndexTransform = append(c2.IndexTransform, MetricIndexTransform{Start: 1, End: 3})
92
- c2.Mapping["3"] = "foo"
92
+ c2.Mapping.Items["3"] = "foo"
93
c2.Tag = "bar"
94
assert.NotEqual(t, c, c2)
95
// Validate that c has not changed
@@ -114,10 +114,10 @@ func TestCloneMetricTagConfig(t *testing.T) {
114
End: 1,
115
},
116
},
117
- Mapping: map[string]string{
117
+ Mapping: NewExactMapping(map[string]string{
118
"1": "bar",
119
"2": "baz",
120
- },
120
+ }),
121
Match: ".*",
122
Pattern: regexp.MustCompile(".*"),
123
Tags: map[string]string{
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/validation.go
+41
-3
@@ -10,6 +10,7 @@ import (
10
"fmt"
11
"regexp"
12
"slices"
13
+ "strconv"
14
"strings"
15
)
16
@@ -296,6 +297,10 @@ func validateEnrichSymbol(symbol *SymbolConfig, symbolContext SymbolContext) err
297
symbol.MatchPatternCompiled = pattern
298
}
299
}
300
+ errs = append(errs, validateMapping(symbol.Mapping, symbolContext))
301
+ if symbol.Mapping.EffectiveMode() == MappingModeBitmask && symbol.Mapping.HasItems() && symbol.ScaleFactor != 0 {
302
+ errs = append(errs, errors.New("`scale_factor` cannot be used with `mapping.mode: bitmask`"))
303
+ }
304
if symbolContext != ColumnSymbol && symbol.ConstantValueOne {
305
errs = append(errs, errors.New("`constant_value_one` cannot be used outside of tables"))
306
}
@@ -379,7 +384,8 @@ func validateEnrichMetricTag(metricTag *MetricTagConfig) error {
384
errs = append(errs, fmt.Errorf("`tags` mapping must be provided if `match` (`%s`) is defined", metricTag.Match))
385
}
386
}
382
- if len(metricTag.Mapping) > 0 && metricTag.Tag == "" && metricTag.Symbol.Name == "" {
387
+ errs = append(errs, validateMapping(metricTag.Mapping, MetricTagSymbol))
388
+ if metricTag.Mapping.HasItems() && metricTag.Tag == "" && metricTag.Symbol.Name == "" {
389
errs = append(errs, fmt.Errorf("`tag` or `symbol.name` must be provided if `mapping` (`%v`) is defined", metricTag.Mapping))
390
}
391
for _, transform := range metricTag.IndexTransform {
@@ -403,14 +409,46 @@ func isRawIndexMetricTag(metricTag MetricTagConfig) bool {
409
return metricTag.Symbol.Format != "" ||
410
metricTag.Symbol.ExtractValue != "" ||
411
metricTag.Symbol.MatchPattern != "" ||
406
- len(metricTag.Mapping) > 0
412
+ metricTag.Mapping.HasItems()
413
}
414
415
return len(metricTag.IndexTransform) > 0 ||
416
metricTag.Symbol.Format != "" ||
417
metricTag.Symbol.ExtractValue != "" ||
418
metricTag.Symbol.MatchPattern != "" ||
413
- len(metricTag.Mapping) > 0
419
+ metricTag.Mapping.HasItems()
420
+}
421
+
422
+func validateMapping(mapping MappingConfig, symbolContext SymbolContext) error {
423
+ if !mapping.HasItems() {
424
+ if mapping.Mode != "" {
425
+ return errors.New("`mapping.mode` requires `mapping.items`")
426
+ }
427
+ return nil
428
+ }
429
+
430
+ var errs []error
431
+
432
+ switch mapping.EffectiveMode() {
433
+ case MappingModeExact:
434
+ case MappingModeBitmask:
435
+ if symbolContext != ScalarSymbol && symbolContext != ColumnSymbol {
436
+ errs = append(errs, errors.New("`mapping.mode: bitmask` is only supported for scalar/table metric symbols"))
437
+ }
438
+ for key, value := range mapping.Items {
439
+ bit, err := strconv.ParseInt(key, 10, 64)
440
+ if err != nil || bit < 0 || (bit != 0 && bit&(bit-1) != 0) {
441
+ errs = append(errs, fmt.Errorf("`mapping.mode: bitmask` requires keys to be 0 or a single power-of-two bit, got %q", key))
442
+ }
443
+ if value == "" {
444
+ errs = append(errs, fmt.Errorf("`mapping.mode: bitmask` requires non-empty values, got empty value for key %q", key))
445
+ }
446
+ }
447
+ default:
448
+ errs = append(errs, fmt.Errorf("invalid `mapping.mode` %q", mapping.Mode))
449
+ }
450
+
451
+ return errors.Join(errs...)
452
}
453
454
func validateEnrichVirtualMetrics(metrics []MetricsConfig, vmetrics []VirtualMetricConfig) error {
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/validation_test.go
+168
-7
@@ -450,10 +450,50 @@ func Test_validateEnrichMetrics(t *testing.T) {
450
OID: "1.2",
451
Name: "abc",
452
},
453
- Mapping: map[string]string{
453
+ Mapping: NewExactMapping(map[string]string{
454
"1": "abc",
455
"2": "def",
456
+ }),
457
+ },
458
+ },
459
+ },
460
+ },
461
+ },
462
+ "bitmask mapping usage in scalar symbol": {
463
+ wantError: false,
464
+ metrics: []MetricsConfig{
465
+ {
466
+ Symbol: SymbolConfig{
467
+ OID: "1.2.3",
468
+ Name: "processorStatus",
469
+ Mapping: NewBitmaskMapping(map[string]string{
470
+ "1": "internalError",
471
+ "128": "processorPresent",
472
+ }),
473
+ },
474
+ },
475
+ },
476
+ },
477
+ "ERROR bitmask mapping usage in metric_tags": {
478
+ wantError: true,
479
+ metrics: []MetricsConfig{
480
+ {
481
+ Symbols: []SymbolConfig{
482
+ {
483
+ OID: "1.2",
484
+ Name: "abc",
485
+ },
486
+ },
487
+ MetricTags: MetricTagConfigList{
488
+ MetricTagConfig{
489
+ Tag: "state",
490
+ Symbol: SymbolConfigCompat{
491
+ OID: "1.2",
492
+ Name: "abc",
493
},
494
+ Mapping: NewBitmaskMapping(map[string]string{
495
+ "1": "internalError",
496
+ }),
497
},
498
},
499
},
@@ -577,9 +617,9 @@ func Test_validateEnrichMetrics(t *testing.T) {
617
618
func Test_validateEnrichMetricTag_MappingErrorUsesReadableFormat(t *testing.T) {
619
tag := MetricTagConfig{
580
- Mapping: map[string]string{
620
+ Mapping: NewExactMapping(map[string]string{
621
"1": "up",
582
- },
622
+ }),
623
}
624
625
err := validateEnrichMetricTag(&tag)
@@ -590,6 +630,97 @@ func Test_validateEnrichMetricTag_MappingErrorUsesReadableFormat(t *testing.T) {
630
}
631
}
632
633
+func Test_validateEnrichSymbol_BitmaskMappingRequiresSingleBitKeys(t *testing.T) {
634
+ sym := SymbolConfig{
635
+ OID: "1.2.3",
636
+ Name: "processorStatus",
637
+ Mapping: NewBitmaskMapping(map[string]string{
638
+ "internal": "internalError",
639
+ }),
640
+ }
641
+
642
+ err := validateEnrichSymbol(&sym, ScalarSymbol)
643
+
644
+ if assert.Error(t, err) {
645
+ assert.Contains(t, err.Error(), "requires keys to be 0 or a single power-of-two bit")
646
+ }
647
+}
648
+
649
+func Test_validateEnrichSymbol_BitmaskMappingRejectsCompositeMasks(t *testing.T) {
650
+ sym := SymbolConfig{
651
+ OID: "1.2.3",
652
+ Name: "processorStatus",
653
+ Mapping: NewBitmaskMapping(map[string]string{
654
+ "3": "combinedFault",
655
+ }),
656
+ }
657
+
658
+ err := validateEnrichSymbol(&sym, ScalarSymbol)
659
+
660
+ if assert.Error(t, err) {
661
+ assert.Contains(t, err.Error(), "requires keys to be 0 or a single power-of-two bit")
662
+ assert.Contains(t, err.Error(), "\"3\"")
663
+ }
664
+}
665
+
666
+func Test_validateEnrichMetadata_BitmaskMappingUnsupported(t *testing.T) {
667
+ metadata := MetadataConfig{
668
+ "device": {
669
+ Fields: map[string]MetadataField{
670
+ "description": {
671
+ Symbol: SymbolConfig{
672
+ OID: "1.2.3",
673
+ Name: "deviceStatus",
674
+ Mapping: NewBitmaskMapping(map[string]string{
675
+ "1": "internalError",
676
+ }),
677
+ },
678
+ },
679
+ },
680
+ },
681
+ }
682
+
683
+ err := validateEnrichMetadata(metadata)
684
+
685
+ if assert.Error(t, err) {
686
+ assert.Contains(t, err.Error(), "only supported for scalar/table metric symbols")
687
+ }
688
+}
689
+
690
+func Test_validateEnrichSymbol_BitmaskMappingRejectsScaleFactor(t *testing.T) {
691
+ sym := SymbolConfig{
692
+ OID: "1.2.3",
693
+ Name: "processorStatus",
694
+ ScaleFactor: 2,
695
+ Mapping: NewBitmaskMapping(map[string]string{
696
+ "1": "internalError",
697
+ "128": "processorPresent",
698
+ }),
699
+ }
700
+
701
+ err := validateEnrichSymbol(&sym, ScalarSymbol)
702
+
703
+ if assert.Error(t, err) {
704
+ assert.Contains(t, err.Error(), "`scale_factor` cannot be used with `mapping.mode: bitmask`")
705
+ }
706
+}
707
+
708
+func Test_validateEnrichSymbol_MappingModeRequiresItems(t *testing.T) {
709
+ sym := SymbolConfig{
710
+ OID: "1.2.3",
711
+ Name: "processorStatus",
712
+ Mapping: MappingConfig{
713
+ Mode: MappingModeBitmask,
714
+ },
715
+ }
716
+
717
+ err := validateEnrichSymbol(&sym, ScalarSymbol)
718
+
719
+ if assert.Error(t, err) {
720
+ assert.Contains(t, err.Error(), "`mapping.mode` requires `mapping.items`")
721
+ }
722
+}
723
+
724
func Test_validateEnrichVirtualMetrics(t *testing.T) {
725
baseMetrics := []MetricsConfig{
726
{
@@ -667,10 +798,10 @@ func Test_validateEnrichVirtualMetrics(t *testing.T) {
798
{
799
OID: "1.3.6.1.2.1.15.3.1.2",
800
Name: "bgpPeerAdminStatus",
670
- Mapping: map[string]string{
801
+ Mapping: NewExactMapping(map[string]string{
802
"1": "stop",
803
"2": "start",
673
- },
804
+ }),
805
},
806
},
807
MetricTags: MetricTagConfigList{
@@ -687,6 +818,36 @@ func Test_validateEnrichVirtualMetrics(t *testing.T) {
818
},
819
},
820
},
821
+ "valid bitmask mapped source dim": {
822
+ metrics: append(baseMetrics, MetricsConfig{
823
+ Table: SymbolConfig{
824
+ OID: "1.3.6.1.4.1.674.10892.1.1100.32",
825
+ Name: "processorDeviceStatusTable",
826
+ },
827
+ Symbols: []SymbolConfig{
828
+ {
829
+ OID: "1.3.6.1.4.1.674.10892.1.1100.32.1.6",
830
+ Name: "processorDeviceStatusReading",
831
+ Mapping: NewBitmaskMapping(map[string]string{
832
+ "1": "internalError",
833
+ "128": "processorPresent",
834
+ }),
835
+ },
836
+ },
837
+ MetricTags: MetricTagConfigList{
838
+ {Tag: "processor", Index: 1},
839
+ },
840
+ }),
841
+ virtualMetrics: []VirtualMetricConfig{
842
+ {
843
+ Name: "processorDeviceHealth",
844
+ PerRow: true,
845
+ Sources: []VirtualMetricSourceConfig{
846
+ {Metric: "processorDeviceStatusReading", Table: "processorDeviceStatusTable", As: "present", Dim: "processorPresent"},
847
+ },
848
+ },
849
+ },
850
+ },
851
"invalid mapped source dim": {
852
metrics: append(baseMetrics, MetricsConfig{
853
Table: SymbolConfig{
@@ -697,10 +858,10 @@ func Test_validateEnrichVirtualMetrics(t *testing.T) {
858
{
859
OID: "1.3.6.1.2.1.15.3.1.2",
860
Name: "bgpPeerAdminStatus",
700
- Mapping: map[string]string{
861
+ Mapping: NewExactMapping(map[string]string{
862
"1": "stop",
863
"2": "start",
703
- },
864
+ }),
865
},
866
},
867
MetricTags: MetricTagConfigList{
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/virtual_metric_dim_validation.go
+18
-5
@@ -66,14 +66,27 @@ func buildVirtualMetricSourceSpec(sym SymbolConfig) virtualMetricSourceSpec {
66
}
67
}
68
69
-func buildMappingVirtualMetricDimSupport(mapping map[string]string) virtualMetricDimSupport {
70
- if len(mapping) == 0 {
69
+func buildMappingVirtualMetricDimSupport(mapping MappingConfig) virtualMetricDimSupport {
70
+ if !mapping.HasItems() {
71
return virtualMetricDimSupport{mode: virtualMetricDimUnsupported}
72
}
73
74
+ if mapping.EffectiveMode() == MappingModeBitmask {
75
+ dims := make(map[string]bool)
76
+ for _, value := range mapping.Items {
77
+ if value != "" {
78
+ dims[value] = true
79
+ }
80
+ }
81
+ if len(dims) == 0 {
82
+ return virtualMetricDimSupport{mode: virtualMetricDimUnsupported}
83
+ }
84
+ return virtualMetricDimSupport{mode: virtualMetricDimKnown, dims: dims}
85
+ }
86
+
87
keysNumeric := true
88
valuesNumeric := true
76
- for key, value := range mapping {
89
+ for key, value := range mapping.Items {
90
if !isIntegerString(key) {
91
keysNumeric = false
92
}
@@ -87,13 +100,13 @@ func buildMappingVirtualMetricDimSupport(mapping map[string]string) virtualMetri
100
return virtualMetricDimSupport{mode: virtualMetricDimUnsupported}
101
case keysNumeric:
102
dims := make(map[string]bool)
90
- for _, value := range mapping {
103
+ for _, value := range mapping.Items {
104
dims[value] = true
105
}
106
return virtualMetricDimSupport{mode: virtualMetricDimKnown, dims: dims}
107
default:
108
dims := make(map[string]bool)
96
- for key, value := range mapping {
109
+ for key, value := range mapping.Items {
110
if isIntegerString(value) {
111
dims[key] = true
112
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/yaml_utils_test.go
+105
@@ -21,6 +21,10 @@ type MySymbolStruct struct {
21
SymbolField SymbolConfigCompat `yaml:"my_symbol_field"`
22
}
23
24
+type MyMappingStruct struct {
25
+ Mapping MappingConfig `yaml:"mapping"`
26
+}
27
+
28
func Test_metricTagConfig_UnmarshalYAML(t *testing.T) {
29
myStruct := MetricsConfig{}
30
expected := MetricsConfig{MetricTags: []MetricTagConfig{{Index: 3}}}
@@ -92,3 +96,104 @@ my_symbol_field: aSymbol
96
97
assert.Equal(t, expected, myStruct)
98
}
99
+
100
+func TestMappingConfig_UnmarshalYAML_legacyMap(t *testing.T) {
101
+ myStruct := MyMappingStruct{}
102
+ expected := MyMappingStruct{
103
+ Mapping: NewExactMapping(map[string]string{
104
+ "1": "up",
105
+ "2": "down",
106
+ }),
107
+ }
108
+
109
+ yaml.Unmarshal([]byte(`
110
+mapping:
111
+ 1: up
112
+ 2: down
113
+`), &myStruct)
114
+
115
+ assert.Equal(t, expected, myStruct)
116
+}
117
+
118
+func TestMappingConfig_UnmarshalYAML_structuredExact(t *testing.T) {
119
+ myStruct := MyMappingStruct{}
120
+ expected := MyMappingStruct{
121
+ Mapping: NewExactMapping(map[string]string{
122
+ "1": "up",
123
+ "2": "down",
124
+ }),
125
+ }
126
+
127
+ yaml.Unmarshal([]byte(`
128
+mapping:
129
+ items:
130
+ 1: up
131
+ 2: down
132
+`), &myStruct)
133
+
134
+ assert.Equal(t, expected, myStruct)
135
+}
136
+
137
+func TestMappingConfig_UnmarshalYAML_structuredBitmask(t *testing.T) {
138
+ myStruct := MyMappingStruct{}
139
+ expected := MyMappingStruct{
140
+ Mapping: NewBitmaskMapping(map[string]string{
141
+ "1": "internalError",
142
+ "128": "processorPresent",
143
+ }),
144
+ }
145
+
146
+ yaml.Unmarshal([]byte(`
147
+mapping:
148
+ mode: bitmask
149
+ items:
150
+ 1: internalError
151
+ 128: processorPresent
152
+`), &myStruct)
153
+
154
+ assert.Equal(t, expected, myStruct)
155
+}
156
+
157
+func TestMappingConfig_UnmarshalYAML_legacyMapWithLiteralModeAndItemsKeys(t *testing.T) {
158
+ myStruct := MyMappingStruct{}
159
+ expected := MyMappingStruct{
160
+ Mapping: NewExactMapping(map[string]string{
161
+ "mode": "active",
162
+ "items": "present",
163
+ }),
164
+ }
165
+
166
+ err := yaml.Unmarshal([]byte(`
167
+mapping:
168
+ mode: active
169
+ items: present
170
+`), &myStruct)
171
+
172
+ assert.NoError(t, err)
173
+ assert.Equal(t, expected, myStruct)
174
+}
175
+
176
+func TestMappingConfig_UnmarshalYAML_emptyLegacyMapNormalizesToZeroValue(t *testing.T) {
177
+ myStruct := MyMappingStruct{}
178
+ expected := MyMappingStruct{}
179
+
180
+ err := yaml.Unmarshal([]byte(`
181
+mapping: {}
182
+`), &myStruct)
183
+
184
+ assert.NoError(t, err)
185
+ assert.Equal(t, expected, myStruct)
186
+}
187
+
188
+func TestMappingConfig_UnmarshalYAML_emptyStructuredItemsNormalizesToZeroValue(t *testing.T) {
189
+ myStruct := MyMappingStruct{}
190
+ expected := MyMappingStruct{}
191
+
192
+ err := yaml.Unmarshal([]byte(`
193
+mapping:
194
+ items: {}
195
+`), &myStruct)
196
+
197
+ assert.NoError(t, err)
198
+ assert.Equal(t, expected, myStruct)
199
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_device_meta.go
+1
-1
@@ -203,7 +203,7 @@ func (dc *deviceMetadataCollector) processSymbolValue(cfg ddprofiledefinition.Sy
203
val = replaceSubmatches(cfg.MatchValue, sm)
204
}
205
206
- if v, ok := cfg.Mapping[val]; ok {
206
+ if v, ok := cfg.Mapping.Lookup(val); ok {
207
val = v
208
}
209
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_device_meta_test.go
+2
-2
@@ -215,14 +215,14 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
215
Symbol: ddprofiledefinition.SymbolConfig{
216
OID: "1.3.6.1.4.1.674.10892.5.2.1.0",
217
Name: "globalSystemStatus",
218
- Mapping: map[string]string{
218
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
219
"1": "other",
220
"2": "unknown",
221
"3": "ok",
222
"4": "nonCritical",
223
"5": "critical",
224
"6": "nonRecoverable",
225
- },
225
+ }),
226
},
227
},
228
},
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_global_tags_test.go
+2
-2
@@ -152,11 +152,11 @@ func TestGlobalTagsCollector_Collect(t *testing.T) {
152
OID: "1.3.6.1.2.1.1.2.0",
153
Name: "sysObjectID",
154
},
155
- Mapping: map[string]string{
155
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
156
"1.3.6.1.4.1.9.1.1": "router",
157
"1.3.6.1.4.1.9.1.2": "switch",
158
"1.3.6.1.4.1.9.1.3": "firewall",
159
- },
159
+ }),
160
},
161
},
162
},
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_scalar_test.go
+60
-20
@@ -112,10 +112,10 @@ func TestScalarCollector_Collect(t *testing.T) {
112
Symbol: ddprofiledefinition.SymbolConfig{
113
OID: "1.3.6.1.4.1.2604.5.1.5.1.1.0",
114
Name: "_license_row",
115
- Mapping: map[string]string{
115
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
116
"1": "1",
117
"4": "2",
118
- },
118
+ }),
119
},
120
StaticTags: []ddprofiledefinition.StaticMetricTagConfig{
121
{Tag: "_license_id", Value: "base_firewall"},
@@ -126,10 +126,10 @@ func TestScalarCollector_Collect(t *testing.T) {
126
Symbol: ddprofiledefinition.SymbolConfigCompat{
127
OID: "1.3.6.1.4.1.2604.5.1.5.1.1.0",
128
},
129
- Mapping: map[string]string{
129
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
130
"1": "trial",
131
"4": "expired",
132
- },
132
+ }),
133
},
134
{
135
Tag: "license_expiry",
@@ -423,11 +423,11 @@ func TestScalarCollector_Collect(t *testing.T) {
423
Symbol: ddprofiledefinition.SymbolConfig{
424
OID: "1.3.6.1.4.1.12124.1.1.2",
425
Name: "clusterHealth",
426
- Mapping: map[string]string{
426
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
427
"OK": "0",
428
"WARNING": "1",
429
"CRITICAL": "2",
430
- },
430
+ }),
431
},
432
},
433
},
@@ -461,13 +461,13 @@ func TestScalarCollector_Collect(t *testing.T) {
461
Symbol: ddprofiledefinition.SymbolConfig{
462
OID: "1.3.6.1.2.1.2.2.1.8",
463
Name: "ifOperStatus",
464
- Mapping: map[string]string{
464
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
465
"1": "up",
466
"2": "down",
467
"3": "testing",
468
"4": "unknown",
469
"5": "dormant",
470
- },
470
+ }),
471
},
472
},
473
},
@@ -504,11 +504,11 @@ func TestScalarCollector_Collect(t *testing.T) {
504
OID: "1.3.6.1.4.1.12124.1.1.8",
505
Name: "fanStatus",
506
ExtractValueCompiled: mustCompileRegex(`Fan(\d+)`),
507
- Mapping: map[string]string{
507
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
508
"1": "normal",
509
"2": "warning",
510
"3": "critical",
511
- },
511
+ }),
512
},
513
},
514
},
@@ -542,11 +542,11 @@ func TestScalarCollector_Collect(t *testing.T) {
542
Symbol: ddprofiledefinition.SymbolConfig{
543
OID: "1.3.6.1.2.1.2.2.1.7",
544
Name: "ifAdminStatus",
545
- Mapping: map[string]string{
545
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
546
"1": "1", // up -> 1
547
"2": "0", // down -> 0
548
"3": "0", // testing -> 0
549
- },
549
+ }),
550
},
551
},
552
},
@@ -575,11 +575,11 @@ func TestScalarCollector_Collect(t *testing.T) {
575
Symbol: ddprofiledefinition.SymbolConfig{
576
OID: "1.3.6.1.4.1.12124.1.1.2",
577
Name: "clusterHealth",
578
- Mapping: map[string]string{
578
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
579
"OK": "0",
580
"WARNING": "1",
581
// CRITICAL is not mapped
582
- },
582
+ }),
583
},
584
},
585
},
@@ -603,11 +603,11 @@ func TestScalarCollector_Collect(t *testing.T) {
603
Symbol: ddprofiledefinition.SymbolConfig{
604
OID: "1.3.6.1.4.1.12124.1.1.2",
605
Name: "deviceStatus",
606
- Mapping: map[string]string{
606
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
607
"OK": "0",
608
"WARNING": "1",
609
"ERROR": "invalid", // This will cause metric to be skipped
610
- },
610
+ }),
611
},
612
},
613
},
@@ -650,11 +650,11 @@ func TestScalarCollector_Collect(t *testing.T) {
650
Symbol: ddprofiledefinition.SymbolConfig{
651
OID: "1.3.6.1.2.1.2.2.1.7",
652
Name: "ifAdminStatus",
653
- Mapping: map[string]string{
653
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
654
"1": "up",
655
"2": "down",
656
"3": "testing",
657
- },
657
+ }),
658
},
659
},
660
},
@@ -679,6 +679,46 @@ func TestScalarCollector_Collect(t *testing.T) {
679
},
680
expectedError: false,
681
},
682
+ "metric with numeric value and bitmask mapping": {
683
+ profile: &ddsnmp.Profile{
684
+ SourceFile: "test-profile.yaml",
685
+ Definition: &ddprofiledefinition.ProfileDefinition{
686
+ Metrics: []ddprofiledefinition.MetricsConfig{
687
+ {
688
+ Symbol: ddprofiledefinition.SymbolConfig{
689
+ OID: "1.3.6.1.4.1.674.10892.1.1100.32.1.6",
690
+ Name: "processorDeviceStatusReading",
691
+ Mapping: ddprofiledefinition.NewBitmaskMapping(map[string]string{
692
+ "1": "internalError",
693
+ "2": "thermalTrip",
694
+ "128": "processorPresent",
695
+ "1024": "processorThrottled",
696
+ }),
697
+ },
698
+ },
699
+ },
700
+ },
701
+ },
702
+ setupMock: func(m *snmpmock.MockHandler) {
703
+ expectSNMPGet(m, []string{"1.3.6.1.4.1.674.10892.1.1100.32.1.6"}, []gosnmp.SnmpPDU{
704
+ createIntegerPDU("1.3.6.1.4.1.674.10892.1.1100.32.1.6", 129), // internalError + processorPresent
705
+ })
706
+ },
707
+ expectedResult: []ddsnmp.Metric{
708
+ {
709
+ Name: "processorDeviceStatusReading",
710
+ Value: 129,
711
+ MetricType: "gauge",
712
+ MultiValue: map[string]int64{
713
+ "internalError": 1,
714
+ "thermalTrip": 0,
715
+ "processorPresent": 1,
716
+ "processorThrottled": 0,
717
+ },
718
+ },
719
+ },
720
+ expectedError: false,
721
+ },
722
"metric with string value and string to int mapping": {
723
profile: &ddsnmp.Profile{
724
SourceFile: "test-profile.yaml",
@@ -688,12 +728,12 @@ func TestScalarCollector_Collect(t *testing.T) {
728
Symbol: ddprofiledefinition.SymbolConfig{
729
OID: "1.3.6.1.4.1.318.1.1.1.2.2.1.0",
730
Name: "upsBasicBatteryStatus",
691
- Mapping: map[string]string{
731
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
732
"batteryNormal": "0",
733
"batteryLow": "1",
734
"batteryDepleted": "2",
735
"batteryCharging": "3",
696
- },
736
+ }),
737
},
738
},
739
},
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table.go
+1
-1
@@ -711,5 +711,5 @@ func isIndexTagConfig(tagCfg ddprofiledefinition.MetricTagConfig) bool {
711
tagCfg.Symbol.Format != "" ||
712
tagCfg.Symbol.ExtractValue != "" ||
713
tagCfg.Symbol.MatchPattern != "" ||
714
- len(tagCfg.Mapping) > 0
714
+ tagCfg.Mapping.HasItems()
715
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table_test.go
+89
-30
@@ -590,11 +590,11 @@ func TestTableCollector_Collect(t *testing.T) {
590
OID: "1.3.6.1.2.1.2.2.1.3",
591
Name: "ifType",
592
},
593
- Mapping: map[string]string{
593
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
594
"1": "other",
595
"6": "ethernetCsmacd",
596
"24": "softwareLoopback",
597
- },
597
+ }),
598
},
599
},
600
},
@@ -728,11 +728,11 @@ func TestTableCollector_Collect(t *testing.T) {
728
OID: "1.3.6.1.2.1.2.2.1.7",
729
Name: "ifAdminStatus",
730
},
731
- Mapping: map[string]string{
731
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
732
"1": "up",
733
"2": "down",
734
"3": "testing",
735
- },
735
+ }),
736
},
737
},
738
},
@@ -1026,9 +1026,9 @@ func TestTableCollector_Collect(t *testing.T) {
1026
OID: "1.3.6.1.2.1.2.2.1.3",
1027
Name: "ifType",
1028
},
1029
- Mapping: map[string]string{
1029
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
1030
"6": "ethernet",
1031
- },
1031
+ }),
1032
},
1033
},
1034
},
@@ -1086,10 +1086,10 @@ func TestTableCollector_Collect(t *testing.T) {
1086
OID: "1.3.6.1.2.1.2.2.1.3",
1087
Name: "ifType",
1088
},
1089
- Mapping: map[string]string{
1089
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
1090
"6": "ethernet",
1091
// No mapping for value 131
1092
- },
1092
+ }),
1093
},
1094
},
1095
},
@@ -1326,13 +1326,13 @@ func TestTableCollector_Collect(t *testing.T) {
1326
{
1327
OID: "1.3.6.1.2.1.2.2.1.8",
1328
Name: "ifOperStatus",
1329
- Mapping: map[string]string{
1329
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
1330
"1": "up",
1331
"2": "down",
1332
"3": "testing",
1333
"4": "unknown",
1334
"5": "dormant",
1335
- },
1335
+ }),
1336
},
1337
},
1338
MetricTags: []ddprofiledefinition.MetricTagConfig{
@@ -1390,6 +1390,65 @@ func TestTableCollector_Collect(t *testing.T) {
1390
},
1391
expectedError: false,
1392
},
1393
+ "table with bitmask value mapping": {
1394
+ profile: &ddsnmp.Profile{
1395
+ SourceFile: "test-profile.yaml",
1396
+ Definition: &ddprofiledefinition.ProfileDefinition{
1397
+ Metrics: []ddprofiledefinition.MetricsConfig{
1398
+ {
1399
+ Table: ddprofiledefinition.SymbolConfig{
1400
+ OID: "1.3.6.1.4.1.674.10892.1.1100.32",
1401
+ Name: "processorDeviceStatusTable",
1402
+ },
1403
+ Symbols: []ddprofiledefinition.SymbolConfig{
1404
+ {
1405
+ OID: "1.3.6.1.4.1.674.10892.1.1100.32.1.6",
1406
+ Name: "processorDeviceStatusReading",
1407
+ Mapping: ddprofiledefinition.NewBitmaskMapping(map[string]string{
1408
+ "1": "internalError",
1409
+ "2": "thermalTrip",
1410
+ "128": "processorPresent",
1411
+ "1024": "processorThrottled",
1412
+ }),
1413
+ },
1414
+ },
1415
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1416
+ {
1417
+ Tag: "processor",
1418
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1419
+ OID: "1.3.6.1.4.1.674.10892.1.1100.32.1.7",
1420
+ Name: "processorDeviceStatusLocationName",
1421
+ },
1422
+ },
1423
+ },
1424
+ },
1425
+ },
1426
+ },
1427
+ },
1428
+ setupMock: func(m *snmpmock.MockHandler) {
1429
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.674.10892.1.1100.32", []gosnmp.SnmpPDU{
1430
+ createIntegerPDU("1.3.6.1.4.1.674.10892.1.1100.32.1.6.1", 129), // internalError + processorPresent
1431
+ createStringPDU("1.3.6.1.4.1.674.10892.1.1100.32.1.7.1", "CPU 1"),
1432
+ })
1433
+ },
1434
+ expectedResult: []ddsnmp.Metric{
1435
+ {
1436
+ Name: "processorDeviceStatusReading",
1437
+ Value: 129,
1438
+ Tags: map[string]string{"processor": "CPU 1"},
1439
+ MetricType: "gauge",
1440
+ IsTable: true,
1441
+ Table: "processorDeviceStatusTable",
1442
+ MultiValue: map[string]int64{
1443
+ "internalError": 1,
1444
+ "thermalTrip": 0,
1445
+ "processorPresent": 1,
1446
+ "processorThrottled": 0,
1447
+ },
1448
+ },
1449
+ },
1450
+ expectedError: false,
1451
+ },
1452
"table with extract value and mapping": {
1453
profile: &ddsnmp.Profile{
1454
SourceFile: "test-profile.yaml",
@@ -1405,11 +1464,11 @@ func TestTableCollector_Collect(t *testing.T) {
1464
OID: "1.3.6.1.4.1.12124.1.13.1.3",
1465
Name: "fanStatus",
1466
ExtractValueCompiled: mustCompileRegex(`Status(\d+)`),
1408
- Mapping: map[string]string{
1467
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
1468
"1": "normal",
1469
"2": "warning",
1470
"3": "critical",
1412
- },
1471
+ }),
1472
},
1473
},
1474
MetricTags: []ddprofiledefinition.MetricTagConfig{
@@ -1548,12 +1607,12 @@ func TestTableCollector_Collect(t *testing.T) {
1607
OID: "1.3.6.1.4.1.318.1.1.10.4.3.3.1.1",
1608
Name: "upsPhaseInputPhaseIndex",
1609
},
1551
- Mapping: map[string]string{
1610
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
1611
"1": "L1",
1612
"2": "L2",
1613
"3": "L3",
1614
// No mapping for value 4
1556
- },
1615
+ }),
1616
},
1617
},
1618
},
@@ -1602,12 +1661,12 @@ func TestTableCollector_Collect(t *testing.T) {
1661
{
1662
OID: "1.3.6.1.4.1.12124.1.1.1.5",
1663
Name: "nodeHealth",
1605
- Mapping: map[string]string{
1664
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
1665
"OK": "0",
1666
"ATTN": "1",
1667
"DOWN": "2",
1668
"INVALID": "3",
1610
- },
1669
+ }),
1670
},
1671
},
1672
MetricTags: []ddprofiledefinition.MetricTagConfig{
@@ -2098,17 +2157,17 @@ func TestTableCollector_Collect(t *testing.T) {
2157
{
2158
Tag: "_address_family",
2159
Index: 2,
2101
- Mapping: map[string]string{
2160
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
2161
"1": "ipv4",
2162
"2": "ipv6",
2163
"25": "vpls",
2164
"196": "l2vpn",
2106
- },
2165
+ }),
2166
},
2167
{
2168
Tag: "_subsequent_address_family",
2169
Index: 3,
2111
- Mapping: map[string]string{
2170
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
2171
"1": "unicast",
2172
"2": "multicast",
2173
"4": "mpls",
@@ -2118,19 +2177,19 @@ func TestTableCollector_Collect(t *testing.T) {
2177
"74": "sd-wan",
2178
"128": "vpn",
2179
"132": "route-target",
2121
- },
2180
+ }),
2181
},
2182
{
2183
Tag: "_neighbor_address_type",
2184
Index: 4,
2126
- Mapping: map[string]string{
2185
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
2186
"0": "unknown",
2187
"1": "ipv4",
2188
"2": "ipv6",
2189
"3": "ipv4z",
2190
"4": "ipv6z",
2191
"16": "dns",
2133
- },
2192
+ }),
2193
},
2194
},
2195
},
@@ -2272,10 +2331,10 @@ func TestTableCollector_Collect(t *testing.T) {
2331
Name: "ifType",
2332
},
2333
Table: "ifTable",
2275
- Mapping: map[string]string{
2334
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
2335
"6": "ethernet",
2336
"24": "loopback",
2278
- },
2337
+ }),
2338
},
2339
},
2340
},
@@ -2540,20 +2599,20 @@ func TestTableCollector_Collect(t *testing.T) {
2599
{
2600
Tag: "address_family",
2601
Index: 2,
2543
- Mapping: map[string]string{
2602
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
2603
"1": "ipv4",
2604
"2": "ipv6",
2605
"25": "l2vpn",
2547
- },
2606
+ }),
2607
},
2608
{
2609
Tag: "subsequent_address_family",
2610
Index: 3,
2552
- Mapping: map[string]string{
2611
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
2612
"1": "unicast",
2613
"70": "evpn",
2614
"128": "vpn",
2556
- },
2615
+ }),
2616
},
2617
},
2618
},
@@ -3281,10 +3340,10 @@ func TestTableCollector_Collect(t *testing.T) {
3340
{
3341
Index: 1,
3342
Tag: "ip_version",
3284
- Mapping: map[string]string{
3343
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
3344
"1": "ipv4",
3345
"2": "ipv6",
3287
- },
3346
+ }),
3347
},
3348
},
3349
},
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_vmetrics_test.go
+4
-4
@@ -444,13 +444,13 @@ func TestVirtualMetricsCollector_Collect(t *testing.T) {
444
{
445
OID: "1.3.6.1.2.1.2.2.1.8",
446
Name: "ifOperStatus",
447
- Mapping: map[string]string{
447
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
448
"1": "up",
449
"2": "down",
450
"3": "testing",
451
"4": "unknown",
452
"5": "dormant",
453
- },
453
+ }),
454
},
455
},
456
},
@@ -563,10 +563,10 @@ func TestVirtualMetricsCollector_Collect(t *testing.T) {
563
{
564
OID: "1.3.6.1.2.1.2.2.1.8",
565
Name: "ifOperStatus",
566
- Mapping: map[string]string{
566
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
567
"1": "up",
568
"2": "down",
569
- },
569
+ }),
570
},
571
},
572
},
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/cross_table_lookup.go
+1
-1
@@ -136,7 +136,7 @@ func (r *crossTableResolver) normalizeLookupText(sym ddprofiledefinition.SymbolC
136
}
137
}
138
139
- if mapped, ok := sym.Mapping[val]; ok {
139
+ if mapped, ok := sym.Mapping.Lookup(val); ok {
140
val = mapped
141
}
142
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/index_tag_value.go
+1
-1
@@ -37,7 +37,7 @@ func processRawIndexTagValue(cfg ddprofiledefinition.MetricTagConfig, raw string
37
val = formatted
38
}
39
40
- if mapped, ok := cfg.Mapping[val]; ok {
40
+ if mapped, ok := cfg.Mapping.Lookup(val); ok {
41
val = mapped
42
}
43
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/index_tag_value_test.go
+4
-4
@@ -43,9 +43,9 @@ func TestTableRowProcessor_ProcessIndexTag_RegexMappedFamily(t *testing.T) {
43
Name: "cbgpPeer2AddrFamilySafiIndex",
44
ExtractValueCompiled: mustCompileRegex(`^(?:\d+\.)+\d+\.(\d+)$`),
45
},
46
- Mapping: map[string]string{
46
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
47
"128": "vpn",
48
- },
48
+ }),
49
}, "1.4.192.0.2.1.1.128")
50
51
require.NoError(t, err)
@@ -61,9 +61,9 @@ func TestTableRowProcessor_ProcessIndexTag_PositionUsesSymbolNameFallback(t *tes
61
Symbol: ddprofiledefinition.SymbolConfigCompat{
62
Name: "neighbor",
63
},
64
- Mapping: map[string]string{
64
+ Mapping: ddprofiledefinition.NewExactMapping(map[string]string{
65
"42": "mapped",
66
- },
66
+ }),
67
}, "7.42.9")
68
69
require.NoError(t, err)
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/metric_builder.go
+37
-6
@@ -104,14 +104,18 @@ func buildTableMetric(cfg ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU,
104
return &metric, nil
105
}
106
107
-func buildMultiValue(value int64, mappings map[string]string) map[string]int64 {
108
- if len(mappings) == 0 {
107
+func buildMultiValue(value int64, mapping ddprofiledefinition.MappingConfig) map[string]int64 {
108
+ if !mapping.HasItems() {
109
return nil
110
}
111
112
+ if mapping.EffectiveMode() == ddprofiledefinition.MappingModeBitmask {
113
+ return buildBitmaskMultiValue(value, mapping.Items)
114
+ }
115
+
116
// Check if this is an int→int mapping (value transformation)
117
if isIntToIntMapping := func() bool {
114
- for k, v := range mappings {
118
+ for k, v := range mapping.Items {
119
if !isInt(k) || !isInt(v) {
120
return false
121
}
@@ -122,7 +126,7 @@ func buildMultiValue(value int64, mappings map[string]string) map[string]int64 {
126
}
127
128
isMappingKeysNumeric := func() bool {
125
- for k := range mappings {
129
+ for k := range mapping.Items {
130
if !isInt(k) {
131
return false
132
}
@@ -134,7 +138,7 @@ func buildMultiValue(value int64, mappings map[string]string) map[string]int64 {
138
139
if isMappingKeysNumeric {
140
// int→string mapping (e.g., 1→"up", 2→"down")
137
- for k, v := range mappings {
141
+ for k, v := range mapping.Items {
142
intKey, _ := strconv.ParseInt(k, 10, 64)
143
// Only set the value if:
144
// 1. We haven't seen this state name before (!ok), OR
@@ -149,7 +153,7 @@ func buildMultiValue(value int64, mappings map[string]string) map[string]int64 {
153
// string→int mapping (e.g., "OK"→"0", "WARNING"→"1", "CRITICAL"→"2")
154
// value has already been converted from string to int by the value processor
155
// We need to find which original string maps to our current value
152
- for k, v := range mappings {
156
+ for k, v := range mapping.Items {
157
if intVal, err := strconv.ParseInt(v, 10, 64); err == nil {
158
multiValue[k] = oldmetrix.Bool(value == intVal)
159
}
@@ -159,6 +163,33 @@ func buildMultiValue(value int64, mappings map[string]string) map[string]int64 {
163
return multiValue
164
}
165
166
+func buildBitmaskMultiValue(value int64, mappings map[string]string) map[string]int64 {
167
+ multiValue := make(map[string]int64)
168
+
169
+ for k, v := range mappings {
170
+ bit, err := strconv.ParseInt(k, 10, 64)
171
+ if err != nil {
172
+ continue
173
+ }
174
+
175
+ active := false
176
+ // Bitmask mappings are intended for non-negative flag values. If multiple
177
+ // bits map to the same dimension, we OR them by keeping the active state.
178
+ switch {
179
+ case bit == 0:
180
+ active = value == 0
181
+ default:
182
+ active = value&bit == bit
183
+ }
184
+
185
+ if _, ok := multiValue[v]; !ok || active {
186
+ multiValue[v] = oldmetrix.Bool(active)
187
+ }
188
+ }
189
+
190
+ return multiValue
191
+}
192
+
193
func applyTransform(metric *ddsnmp.Metric, sym ddprofiledefinition.SymbolConfig) error {
194
if sym.TransformCompiled == nil {
195
return nil
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/metric_builder_test.go
+45
-5
@@ -5,7 +5,9 @@ package ddsnmpcollector
5
import (
6
"testing"
7
8
- "github.com/stretchr/testify/assert"
8
+ "github.com/stretchr/testify/require"
9
+
10
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
11
)
12
13
func TestMetricBuilder_WithStaticTagsFillsMissingAndEmptyValues(t *testing.T) {
@@ -20,12 +22,12 @@ func TestMetricBuilder_WithStaticTagsFillsMissingAndEmptyValues(t *testing.T) {
22
}).
23
build()
24
23
- assert.Equal(t, map[string]string{
25
+ require.Equal(t, map[string]string{
26
"region": "eu-west",
27
"neighbor": "192.0.2.10",
28
"site": "athens",
29
}, metric.Tags)
28
- assert.Equal(t, map[string]string{
30
+ require.Equal(t, map[string]string{
31
"region": "eu-west",
32
"site": "athens",
33
}, metric.StaticTags)
@@ -41,6 +43,44 @@ func TestMetricBuilder_WithStaticTagsKeepsExistingNonEmptyValues(t *testing.T) {
43
}).
44
build()
45
44
- assert.Equal(t, map[string]string{"region": "edge"}, metric.Tags)
45
- assert.Equal(t, map[string]string{"region": "core"}, metric.StaticTags)
46
+ require.Equal(t, map[string]string{"region": "edge"}, metric.Tags)
47
+ require.Equal(t, map[string]string{"region": "core"}, metric.StaticTags)
48
+}
49
+
50
+func TestBuildMultiValue_BitmaskZeroKeyMatchesOnlyZero(t *testing.T) {
51
+ mapping := ddprofiledefinition.NewBitmaskMapping(map[string]string{
52
+ "0": "noFaults",
53
+ "1": "warning",
54
+ "2": "failure",
55
+ })
56
+
57
+ require.Equal(t, map[string]int64{
58
+ "noFaults": 1,
59
+ "warning": 0,
60
+ "failure": 0,
61
+ }, buildMultiValue(0, mapping))
62
+
63
+ require.Equal(t, map[string]int64{
64
+ "noFaults": 0,
65
+ "warning": 1,
66
+ "failure": 0,
67
+ }, buildMultiValue(1, mapping))
68
+}
69
+
70
+func TestBuildMultiValue_BitmaskDuplicateDimsAreCombined(t *testing.T) {
71
+ mapping := ddprofiledefinition.NewBitmaskMapping(map[string]string{
72
+ "1": "fault",
73
+ "2": "fault",
74
+ "4": "present",
75
+ })
76
+
77
+ require.Equal(t, map[string]int64{
78
+ "fault": 1,
79
+ "present": 1,
80
+ }, buildMultiValue(5, mapping))
81
+
82
+ require.Equal(t, map[string]int64{
83
+ "fault": 0,
84
+ "present": 0,
85
+ }, buildMultiValue(0, mapping))
86
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/tag_processor.go
+2
-2
@@ -65,8 +65,8 @@ func (p *tableTagProcessor) processTag(cfg ddprofiledefinition.MetricTagConfig,
65
}
66
67
switch {
68
- case len(cfg.Mapping) > 0:
69
- if v, ok := cfg.Mapping[val]; ok {
68
+ case cfg.Mapping.HasItems():
69
+ if v, ok := cfg.Mapping.Lookup(val); ok {
70
val = v
71
}
72
ta.addTag(tagName, val)
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/value_processor.go
+6
-4
@@ -87,9 +87,9 @@ func (p *numericValueProcessor) processInteger(sym ddprofiledefinition.SymbolCon
87
return 0, err
88
}
89
90
- if len(sym.Mapping) > 0 {
90
+ if sym.Mapping.EffectiveMode() == ddprofiledefinition.MappingModeExact && sym.Mapping.HasItems() {
91
s := strconv.FormatInt(value, 10)
92
- if v, ok := sym.Mapping[s]; ok && isInt(v) {
92
+ if v, ok := sym.Mapping.Lookup(s); ok && isInt(v) {
93
value, _ = strconv.ParseInt(v, 10, 64)
94
}
95
}
@@ -125,8 +125,10 @@ func (p *stringValueProcessor) processValue(sym ddprofiledefinition.SymbolConfig
125
s = replaceSubmatches(sym.MatchValue, sm)
126
}
127
128
- if v, ok := sym.Mapping[s]; ok && isInt(v) {
129
- s = v
128
+ if sym.Mapping.EffectiveMode() == ddprofiledefinition.MappingModeExact && sym.Mapping.HasItems() {
129
+ if v, ok := sym.Mapping.Lookup(s); ok && isInt(v) {
130
+ s = v
131
+ }
132
}
133
134
value, err := parseStringMetricValue(sym, s)
src/go/plugin/go.d/collector/snmp/ddsnmp/profile.go
+3
-3
@@ -343,15 +343,15 @@ func enrichProfiles(profiles []*Profile) {
343
for j := range metric.MetricTags {
344
tagCfg := &metric.MetricTags[j]
345
346
- if tagCfg.Mapping != nil {
346
+ if tagCfg.Mapping.HasItems() {
347
continue
348
}
349
350
switch tagCfg.MappingRef {
351
case "ifType":
352
- tagCfg.Mapping = sharedMappings.ifType
352
+ tagCfg.Mapping = ddprofiledefinition.NewExactMapping(sharedMappings.ifType)
353
case "ifTypeGroup":
354
- tagCfg.Mapping = sharedMappings.ifTypeGroup
354
+ tagCfg.Mapping = ddprofiledefinition.NewExactMapping(sharedMappings.ifTypeGroup)
355
}
356
}
357
}
src/go/plugin/go.d/collector/snmp/profile-format.md
+84
-22
@@ -1257,6 +1257,7 @@ They work the same in **both** places:
1257
| **No match behavior** | • `extract_value`: keeps the original value.<br/>• `match_pattern`: skips the value (tag not emitted).<br/>• `match` + `tags`: emits no tags. |
1258
| **Multiple symbols** | If multiple `symbols` are listed for the same tag, the **first non-empty result** is used. |
1259
| **Mapping key consistency** | Keys in a `mapping` must all be the same type — all numeric or all string. |
1260
+| **Mapping modes** | Tags and metadata support only exact-match mapping. Use `mapping.items`; `mapping.mode` is optional and defaults to exact. |
1261
| **Safety** | Keep regexes simple and, when possible, **anchor them** (e.g. `^pattern$`) to prevent unwanted matches. |
1262
1263
**Quick Syntax Recap**:
@@ -1264,8 +1265,9 @@ They work the same in **both** places:
1265
- `mapping`
1266
```yaml
1267
mapping:
1267
- 6: "ethernet"
1268
- 161: "lag"
1268
+ items:
1269
+ 6: "ethernet"
1270
+ 161: "lag"
1271
```
1272
- `extract_value`
1273
```yaml
@@ -1291,6 +1293,8 @@ They work the same in **both** places:
1293
1294
Use `mapping` to replace raw tag values with **human-readable text labels**.
1295
1296
+`mapping.mode` is optional here and defaults to exact. `bitmask` mode is not supported for tags or metadata.
1297
+
1298
**The collector**:
1299
1300
- Looks up the raw value in the mapping table.
@@ -1314,11 +1318,12 @@ metrics:
1318
OID: 1.3.6.1.2.1.2.2.1.3
1319
name: ifType
1320
mapping:
1317
- 1: "other"
1318
- 6: "ethernet"
1319
- 24: "loopback"
1320
- 131: "tunnel"
1321
- 161: "lag"
1321
+ items:
1322
+ 1: "other"
1323
+ 6: "ethernet"
1324
+ 24: "loopback"
1325
+ 131: "tunnel"
1326
+ 161: "lag"
1327
```
1328
1329
**What this does**:
@@ -1326,6 +1331,8 @@ metrics:
1331
- Replaces numeric interface type codes (1, 6, 24, 131, 161) with readable names (`other`, `ethernet`, `loopback`, `tunnel`, `lag`).
1332
- If a device reports an unknown type, the original numeric value is used.
1333
- Works identically for `metadata` fields and `metric_tags`.
1334
+- Legacy flat-map syntax remains supported for backward compatibility:
1335
+ `mapping: { 6: "ethernet", 161: "lag" }`
1336
1337
### Extract Value
1338
@@ -1481,22 +1488,34 @@ These transformations are typically used to:
1488
|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
1489
| **Where** | Metric value transformations are used inside `metrics[*].symbol` or `metrics[*].symbols[]`; `format` also applies when symbols are used for metric tags or device metadata. |
1490
| **Order of application** | For string-decoded metric values: 1️⃣ `format` (if present) → 2️⃣ `extract_value` (if present) → 3️⃣ `match_pattern` + `match_value` (if present) → 4️⃣ `mapping` → 5️⃣ numeric parsing → 6️⃣ `scale_factor`. Ordinary numeric PDUs skip the string-only `extract_value` and `match_pattern` steps and use numeric parsing → `mapping` → `scale_factor`. |
1484
-| **Scale factor position** | `scale_factor` is always applied **last**, after all other metric value transformations. |
1491
+| **Scale factor position** | `scale_factor` is always applied **last**, after all other metric value transformations. It cannot be combined with `mapping.mode: bitmask`. |
1492
| **String base parsing** | String-like values are parsed as base-10 by default. If `format: hex` is set, extracted values are parsed as base-16. |
1493
| **Data type handling** | Transformations preserve numeric type (integer/float) unless the mapping converts it to a multi-value metric. |
1494
| **Error handling** | `extract_value` keeps the original value when it does not match; `match_pattern` fails the metric value when it does not match; no-value `format` sentinels are treated as missing. |
1495
| **Applicability** | Metric value transformations affect metric values only; `format` also decodes tag and metadata symbol values. |
1489
-| **Mapping behavior** | Always produces a multi-value metric where each mapped entry becomes a dimension; the active one reports `1`, others `0`. |
1496
+| **Mapping syntax** | `mapping.items` defines the lookup table. `mapping.mode` is optional and defaults to exact. Legacy flat-map syntax remains supported for backward compatibility. |
1497
+| **Mapping behavior** | Exact mode preserves the legacy exact-match/remap behavior. `bitmask` mode is metric-value only: every mapped bit becomes a dimension, the raw numeric value is preserved, key `0` matches only raw value `0`, and unknown bits are ignored. |
1498
1499
**Quick Syntax Recap**:
1500
1501
- `mapping`
1502
```yaml
1503
mapping:
1496
- 1: up
1497
- 2: down
1498
- 3: testing
1499
- ```
1504
+ items:
1505
+ 1: up
1506
+ 2: down
1507
+ 3: testing
1508
+ ```
1509
+
1510
+- `mapping` (bitmask mode)
1511
+ ```yaml
1512
+ mapping:
1513
+ mode: bitmask
1514
+ items:
1515
+ 1: internalError
1516
+ 128: processorPresent
1517
+ 1024: processorThrottled
1518
+ ```
1519
1520
- `extract_value`
1521
```yaml
@@ -1526,17 +1545,27 @@ These transformations are typically used to:
1545
1546
### Mapping
1547
1529
-Use `mapping` to convert raw metric values into **state dimensions**.
1548
+Use `mapping` to convert raw metric values into **state dimensions** or **decoded bitmask dimensions**.
1549
+
1550
+`mapping.mode` defaults to exact. Use `mapping.mode: bitmask` when the raw metric value is a flag field where multiple bits may be set at the same time.
1551
1531
-Each mapping entry defines a **dimension name** and the numeric or string value that triggers it.
1552
+In exact mode, the emitted dimension names come from the **string side** of the mapping:
1553
+
1554
+- Numeric key -> string value (`1: up`) emits dimensions named after the mapped string values (`up`, `down`, ...).
1555
+- String key -> numeric value (`OK: 0`) first normalizes the value to the numeric target, then emits dimensions named after the original string keys (`OK`, `WARNING`, ...).
1556
+- Numeric key -> numeric value is treated as value normalization only and does not emit multi-value dimensions.
1557
1558
**The collector**:
1559
1535
-- Evaluates the value against the mapping table.
1536
-- For each mapping entry, creates a **dimension** named after the mapped key.
1560
+- Evaluates the value against `mapping.items`.
1561
+- In exact mode, creates dimensions using the mapping's string labels, following the rules above.
1562
- Sets that dimension to `1` if the current value matches the key, or `0` otherwise.
1538
-- If the value doesn’t match any key, all mapped dimensions are `0`.
1539
-- Works only for **metric values**, not for tags or metadata.
1563
+- In bitmask mode, sets every mapped dimension whose bit is active to `1`, and inactive mapped bits to `0`.
1564
+- If the value doesn’t match any exact key, all exact-mode dimensions are `0`.
1565
+- `mapping.mode: bitmask` works only for **metric values**, not for tags or metadata.
1566
+- `mapping.mode: bitmask` keys must be `0` or a single power-of-two bit (`1`, `2`, `4`, `8`, ...).
1567
+- `scale_factor` cannot be combined with `mapping.mode: bitmask`.
1568
+- If multiple bit keys map to the same dimension, that dimension is active when any mapped bit is active.
1569
1570
```yaml
1571
metrics:
@@ -1547,15 +1576,48 @@ metrics:
1576
family: 'Network/Interface/Status/Admin'
1577
unit: "{status}"
1578
mapping:
1550
- 1: up
1551
- 2: down
1552
- 3: testing
1579
+ items:
1580
+ 1: up
1581
+ 2: down
1582
+ 3: testing
1583
```
1584
1585
**What this does**:
1586
1587
- Converts SNMP integer values (1, 2, 3) into a **multi-value metric** with dimensions `up`, `down`, and `testing`.
1588
- The dimension corresponding to the current value reports `1`; all others report `0`.
1589
+- Legacy flat-map syntax remains supported and is equivalent to exact mode.
1590
+- For string -> numeric exact mappings, the emitted dimensions use the original string keys instead of the normalized numeric values.
1591
+
1592
+Bitmask example:
1593
+
1594
+```yaml
1595
+metrics:
1596
+ - table:
1597
+ OID: 1.3.6.1.4.1.674.10892.1.1100.32
1598
+ name: processorDeviceStatusTable
1599
+ symbols:
1600
+ - OID: 1.3.6.1.4.1.674.10892.1.1100.32.1.6
1601
+ name: processorDeviceStatusReading
1602
+ mapping:
1603
+ mode: bitmask
1604
+ items:
1605
+ 1: internalError
1606
+ 2: thermalTrip
1607
+ 32: configurationError
1608
+ 128: processorPresent
1609
+ 256: processorDisabled
1610
+ 512: terminatorPresent
1611
+ 1024: processorThrottled
1612
+```
1613
+
1614
+**What this does**:
1615
+
1616
+- Treats the raw value as a bitmask where multiple bits can be active at once.
1617
+- Emits a multi-value metric with one dimension per mapped bit.
1618
+- Requires each declared key to represent either `0` or exactly one bit.
1619
+- Preserves the raw numeric metric value alongside the decoded dimensions.
1620
+- Ignores active bits that are not declared in `mapping.items`.
1621
1622
### Extract Value
1623
src/go/plugin/go.d/config/go.d/snmp.profiles/default/dell-poweredge.yaml
+33
-10
@@ -878,11 +878,22 @@ metrics:
878
5: critical
879
6: nonRecoverable
880
7: absent
881
- # TODO: bitmask support (https://github.com/DanielleHuisman/observium-community-edition/blob/9f799d444ac7aee4fff8a87b2daa8a719e24d474/mibs/dell/MIB-Dell-10892#L7869)
882
- # - OID: 1.3.6.1.4.1.674.10892.1.1100.32.1.6
883
- # name: processorDeviceStatusReading
884
- # description: Reading value of the processor device status probe
885
- # unit: "TBD"
881
+ - OID: 1.3.6.1.4.1.674.10892.1.1100.32.1.6
882
+ name: processorDeviceStatusReading
883
+ chart_meta:
884
+ family: 'Hardware/Processor/Device/Status/Reading'
885
+ description: Reading value of the processor device status probe
886
+ unit: "{reading}"
887
+ mapping:
888
+ mode: bitmask
889
+ items:
890
+ 1: internalError
891
+ 2: thermalTrip
892
+ 32: configurationError
893
+ 128: processorPresent
894
+ 256: processorDisabled
895
+ 512: terminatorPresent
896
+ 1024: processorThrottled
897
metric_tags:
898
- tag: chassis_index
899
symbol:
@@ -971,11 +982,23 @@ metrics:
982
5: critical
983
6: nonRecoverable
984
7: absent
974
- # TODO: bitmask support (https://github.com/DanielleHuisman/observium-community-edition/blob/9f799d444ac7aee4fff8a87b2daa8a719e24d474/mibs/dell/MIB-Dell-10892#L8324)
975
- # - OID: 1.3.6.1.4.1.674.10892.1.1100.50.1.20
976
- # name: memoryDeviceFailureModes
977
- # description: Failure modes of the memory device
978
- # unit: "TBD"
985
+ - OID: 1.3.6.1.4.1.674.10892.1.1100.50.1.20
986
+ name: memoryDeviceFailureModes
987
+ chart_meta:
988
+ family: 'Hardware/Memory/FailureModes'
989
+ description: Failure modes of the memory device
990
+ unit: "{failure_mode}"
991
+ mapping:
992
+ mode: bitmask
993
+ items:
994
+ # 0 means the device reports no active failure bits.
995
+ 0: noFaults
996
+ 1: eccSingleBitCorrectionWarningRate
997
+ 2: eccSingleBitCorrectionFailureRate
998
+ 4: eccMultiBitFault
999
+ 8: eccSingleBitCorrectionLoggingDisabled
1000
+ 16: deviceDisabledBySpareActivation
1001
+ 32: correctableMemoryComponentFault
1002
metric_tags:
1003
- tag: chassis_index
1004
symbol: