@cryptotaxi247 / netdata-1 / commits / a6307e68d

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: