improvement(go.d/ddsnmp): add table metrics, tags from the same table (#20463)
Ilya Mashchenko committed
Jun 10, 2025 at 18:41 UTC
f540caf9b0bc57753e45eb39317fd4f93ffd84f6
6 files changed
+605
-39
src/go/plugin/go.d/collector/snmp/collect_profiles.go
+4
@@ -26,6 +26,10 @@ func (c *Collector) collectProfiles(mx map[string]int64) error {
26
27
for _, pm := range profMetrics {
28
for _, m := range pm.Metrics {
29
+ if m.IsTable {
30
+ continue
31
+ }
32
+
33
seen[m.Name] = true
34
if !c.seenScalarMetrics[m.Name] {
35
c.seenScalarMetrics[m.Name] = true
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_scalar.go
+16
-2
@@ -17,16 +17,30 @@ import (
17
18
func (c *Collector) collectScalarMetrics(prof *ddsnmp.Profile) ([]Metric, error) {
19
var oids []string
20
+ var missingOIDs []string
21
22
for _, m := range prof.Definition.Metrics {
22
- if m.IsScalar() {
23
- oids = append(oids, m.Symbol.OID)
23
+ if !m.IsScalar() {
24
+ continue
25
+ }
26
+ if c.missingOIDs[trimOID(m.Symbol.OID)] {
27
+ missingOIDs = append(missingOIDs, m.Symbol.OID)
28
+ continue
29
}
30
+ oids = append(oids, m.Symbol.OID)
31
+ }
32
+
33
+ if len(missingOIDs) > 0 {
34
+ c.log.Debugf("scalar metrics missing OIDs: %v", missingOIDs)
35
}
36
37
slices.Sort(oids)
38
oids = slices.Compact(oids)
39
40
+ if len(oids) == 0 {
41
+ return nil, nil
42
+ }
43
+
44
pdus, err := c.snmpGet(oids)
45
if err != nil {
46
return nil, err
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_table.go
+117
-11
@@ -5,6 +5,7 @@ package ddsnmpcollector
5
import (
6
"errors"
7
"fmt"
8
+ "maps"
9
"strings"
10
11
"github.com/gosnmp/gosnmp"
@@ -16,6 +17,7 @@ import (
17
func (c *Collector) collectTableMetrics(prof *ddsnmp.Profile) ([]Metric, error) {
18
var metrics []Metric
19
var errs []error
20
+ var missingOIDs []string
21
22
doneOids := make(map[string]bool)
23
@@ -23,6 +25,10 @@ func (c *Collector) collectTableMetrics(prof *ddsnmp.Profile) ([]Metric, error)
25
if cfg.IsScalar() || cfg.Table.OID == "" || doneOids[cfg.Table.OID] {
26
continue
27
}
28
+ if c.missingOIDs[trimOID(cfg.Table.OID)] {
29
+ missingOIDs = append(missingOIDs, cfg.Table.OID)
30
+ continue
31
+ }
32
33
doneOids[cfg.Table.OID] = true
34
tableMetrics, err := c.collectSingleTable(cfg)
@@ -33,6 +39,10 @@ func (c *Collector) collectTableMetrics(prof *ddsnmp.Profile) ([]Metric, error)
39
metrics = append(metrics, tableMetrics...)
40
}
41
42
+ if len(missingOIDs) > 0 {
43
+ c.log.Debugf("table metrics missing OIDs: %v", missingOIDs)
44
+ }
45
+
46
if len(metrics) == 0 && len(errs) > 0 {
47
return nil, errors.Join(errs...)
48
}
@@ -41,6 +51,17 @@ func (c *Collector) collectTableMetrics(prof *ddsnmp.Profile) ([]Metric, error)
51
}
52
53
func (c *Collector) collectSingleTable(cfg ddprofiledefinition.MetricsConfig) ([]Metric, error) {
54
+ for _, tagCfg := range cfg.MetricTags {
55
+ if tagCfg.Table != "" && tagCfg.Table != cfg.Table.Name {
56
+ c.log.Debugf("Skipping table %s: has cross-table tag from %s", cfg.Table.Name, tagCfg.Table)
57
+ return nil, nil
58
+ }
59
+ if len(tagCfg.IndexTransform) > 0 {
60
+ c.log.Debugf("Skipping table %s: has index transformation", cfg.Table.Name)
61
+ return nil, nil
62
+ }
63
+ }
64
+
65
pdus, err := c.snmpWalk(cfg.Table.OID)
66
if err != nil {
67
return nil, fmt.Errorf("failed to walk table: %w", err)
@@ -50,18 +71,31 @@ func (c *Collector) collectSingleTable(cfg ddprofiledefinition.MetricsConfig) ([
71
return nil, nil
72
}
73
53
- // Build a set of column OIDs we're interested in
54
- columnOIDs := make(map[string]ddprofiledefinition.SymbolConfig)
74
+ symColumnOIDs := make(map[string]ddprofiledefinition.SymbolConfig)
75
for _, sym := range cfg.Symbols {
56
- columnOIDs[trimOID(sym.OID)] = sym
76
+ symColumnOIDs[trimOID(sym.OID)] = sym
77
}
78
59
- // Group PDUs by row index
60
- rows := make(map[string]map[string]gosnmp.SnmpPDU) // index -> column OID -> PDU
79
+ tagColumnOIDs := make(map[string]ddprofiledefinition.MetricTagConfig)
80
+ for _, tagCfg := range cfg.MetricTags {
81
+ if tagCfg.Table == "" || tagCfg.Table == cfg.Table.Name {
82
+ tagColumnOIDs[trimOID(tagCfg.Symbol.OID)] = tagCfg
83
+ }
84
+ }
85
+
86
+ allColumnOIDs := make([]string, 0, len(symColumnOIDs)+len(tagColumnOIDs))
87
+ for oid := range symColumnOIDs {
88
+ allColumnOIDs = append(allColumnOIDs, oid)
89
+ }
90
+ for oid := range tagColumnOIDs {
91
+ allColumnOIDs = append(allColumnOIDs, oid)
92
+ }
93
+
94
+ // Group PDUs by row index (index -> column OID -> PDU)
95
+ rows := make(map[string]map[string]gosnmp.SnmpPDU, len(pdus)/len(allColumnOIDs))
96
97
for oid, pdu := range pdus {
63
- // Check if this OID belongs to any of our columns
64
- for columnOID := range columnOIDs {
98
+ for _, columnOID := range allColumnOIDs {
99
if strings.HasPrefix(oid, columnOID+".") {
100
index := strings.TrimPrefix(oid, columnOID+".")
101
@@ -76,7 +110,7 @@ func (c *Collector) collectSingleTable(cfg ddprofiledefinition.MetricsConfig) ([
110
111
var metrics []Metric
112
for index, rowPDUs := range rows {
79
- rowMetrics, err := c.processTableRow(rowPDUs, columnOIDs)
113
+ rowMetrics, err := c.processTableRow(rowPDUs, symColumnOIDs, tagColumnOIDs, cfg.StaticTags)
114
if err != nil {
115
c.log.Debugf("Error processing row %s: %v", index, err)
116
continue
@@ -87,9 +121,40 @@ func (c *Collector) collectSingleTable(cfg ddprofiledefinition.MetricsConfig) ([
121
return metrics, nil
122
}
123
90
-func (c *Collector) processTableRow(rowPDUs map[string]gosnmp.SnmpPDU, columnOIDs map[string]ddprofiledefinition.SymbolConfig) ([]Metric, error) {
124
+func (c *Collector) processTableRow(
125
+ rowPDUs map[string]gosnmp.SnmpPDU,
126
+ columnOIDs map[string]ddprofiledefinition.SymbolConfig,
127
+ tagColumnOIDs map[string]ddprofiledefinition.MetricTagConfig,
128
+ staticTags []string,
129
+) ([]Metric, error) {
130
var metrics []Metric
131
132
+ // Process tags first to ensure all metrics in the row get the same tags
133
+ rowTags := make(map[string]string)
134
+
135
+ for _, tag := range staticTags {
136
+ if n, v, _ := strings.Cut(tag, ":"); n != "" && v != "" {
137
+ rowTags[n] = v
138
+ }
139
+ }
140
+
141
+ for columnOID, tagCfg := range tagColumnOIDs {
142
+ pdu, ok := rowPDUs[columnOID]
143
+ if !ok {
144
+ continue
145
+ }
146
+
147
+ tags, err := processTableMetricTagValue(tagCfg, pdu)
148
+ if err != nil {
149
+ c.log.Debugf("Error processing tag %s: %v", tagCfg.Tag, err)
150
+ continue
151
+ }
152
+
153
+ for k, v := range tags {
154
+ rowTags[k] = v
155
+ }
156
+ }
157
+
158
for columnOID, sym := range columnOIDs {
159
pdu, ok := rowPDUs[columnOID]
160
if !ok {
@@ -111,14 +176,53 @@ func (c *Collector) processTableRow(rowPDUs map[string]gosnmp.SnmpPDU, columnOID
176
MetricType: getMetricType(sym, pdu),
177
Family: sym.Family,
178
Mappings: convSymMappingToNumeric(sym),
179
+ IsTable: true,
180
}
181
182
+ maps.Copy(metric.Tags, rowTags)
183
+
184
metrics = append(metrics, metric)
185
}
186
187
return metrics, nil
188
}
189
190
+func processTableMetricTagValue(cfg ddprofiledefinition.MetricTagConfig, pdu gosnmp.SnmpPDU) (map[string]string, error) {
191
+ val, err := convPduToStringf(pdu, cfg.Symbol.Format)
192
+ if err != nil {
193
+ return nil, err
194
+ }
195
+
196
+ tags := make(map[string]string)
197
+ tagName := ternary(cfg.Tag != "", cfg.Tag, cfg.Symbol.Name)
198
+
199
+ switch {
200
+ case len(cfg.Mapping) > 0:
201
+ if v, ok := cfg.Mapping[val]; ok {
202
+ val = v
203
+ }
204
+ tags[tagName] = val
205
+ case cfg.Pattern != nil:
206
+ if sm := cfg.Pattern.FindStringSubmatch(val); len(sm) > 0 {
207
+ for name, tmpl := range cfg.Tags {
208
+ tags[name] = replaceSubmatches(tmpl, sm)
209
+ }
210
+ }
211
+ case cfg.Symbol.ExtractValueCompiled != nil:
212
+ if sm := cfg.Symbol.ExtractValueCompiled.FindStringSubmatch(val); len(sm) > 1 {
213
+ tags[tagName] = sm[1]
214
+ }
215
+ case cfg.Symbol.MatchPatternCompiled != nil:
216
+ if sm := cfg.Symbol.MatchPatternCompiled.FindStringSubmatch(val); len(sm) > 0 {
217
+ tags[tagName] = replaceSubmatches(cfg.Symbol.MatchValue, sm)
218
+ }
219
+ default:
220
+ tags[tagName] = val
221
+ }
222
+
223
+ return tags, nil
224
+}
225
+
226
func (c *Collector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error) {
227
pdus := make(map[string]gosnmp.SnmpPDU)
228
@@ -135,9 +239,11 @@ func (c *Collector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error) {
239
}
240
241
for _, pdu := range resp {
138
- if isPduWithData(pdu) {
139
- pdus[trimOID(pdu.Name)] = pdu
242
+ if !isPduWithData(pdu) {
243
+ c.missingOIDs[trimOID(pdu.Name)] = true
244
+ continue
245
}
246
+ pdus[trimOID(pdu.Name)] = pdu
247
}
248
249
return pdus, nil
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+43
-22
@@ -31,14 +31,16 @@ type Metric struct {
31
MetricType ddprofiledefinition.ProfileMetricType
32
Tags map[string]string
33
Mappings map[int64]string
34
+ IsTable bool
35
Value int64
36
}
37
38
func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logger) *Collector {
39
coll := &Collector{
39
- log: log.With(slog.String("ddsnmp", "collector")),
40
- snmpClient: snmpClient,
41
- profiles: make(map[string]*profileState),
40
+ log: log.With(slog.String("ddsnmp", "collector")),
41
+ snmpClient: snmpClient,
42
+ profiles: make(map[string]*profileState),
43
+ missingOIDs: make(map[string]bool),
44
}
45
46
for _, prof := range profiles {
@@ -51,9 +53,12 @@ func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logg
53
54
type (
55
Collector struct {
54
- log *logger.Logger
55
- snmpClient gosnmp.Handler
56
- profiles map[string]*profileState
56
+ log *logger.Logger
57
+ snmpClient gosnmp.Handler
58
+ profiles map[string]*profileState
59
+ missingOIDs map[string]bool
60
+
61
+ doTableMetrics bool
62
}
63
profileState struct {
64
profile *ddsnmp.Profile
@@ -68,12 +73,11 @@ func (c *Collector) Collect() ([]*ProfileMetrics, error) {
73
var errs []error
74
75
for _, prof := range c.profiles {
71
- ms, err := c.collectProfile(prof)
72
- if err != nil {
76
+ if ms, err := c.collectProfile(prof); err != nil {
77
errs = append(errs, err)
74
- continue
78
+ } else if ms != nil {
79
+ metrics = append(metrics, ms)
80
}
76
- metrics = append(metrics, ms)
81
}
82
83
if len(metrics) == 0 && len(errs) > 0 {
@@ -105,10 +109,21 @@ func (c *Collector) collectProfile(ps *profileState) (*ProfileMetrics, error) {
109
ps.initialized = true
110
}
111
108
- metrics, err := c.collectScalarMetrics(ps.profile)
112
+ var metrics []Metric
113
+
114
+ scalarMetrics, err := c.collectScalarMetrics(ps.profile)
115
if err != nil {
116
return nil, err
117
}
118
+ metrics = append(metrics, scalarMetrics...)
119
+
120
+ if c.doTableMetrics {
121
+ tableMetrics, err := c.collectTableMetrics(ps.profile)
122
+ if err != nil {
123
+ return nil, err
124
+ }
125
+ metrics = append(metrics, tableMetrics...)
126
+ }
127
128
for _, m := range metrics {
129
maps.Copy(m.Tags, ps.globalTags)
@@ -131,17 +146,21 @@ func (c *Collector) updateMetricFamily(pms []*ProfileMetrics) {
146
if !ps.initialized {
147
continue
148
}
134
- if res, ok := ps.profile.Definition.Metadata["device"]; ok {
135
- if dt, dv := res.Fields["type"].Value, res.Fields["vendor"].Value; dt != "" && dv != "" {
136
- for _, pm := range pms {
137
- for i := range pm.Metrics {
138
- m := &pm.Metrics[i]
139
- m.Family = processMetricFamily(m.Family, dt, dv)
140
- }
141
- }
142
- return
149
+ res, ok := ps.profile.Definition.Metadata["device"]
150
+ if !ok {
151
+ continue
152
+ }
153
+ dt, dv := res.Fields["type"].Value, res.Fields["vendor"].Value
154
+ if dt == "" || dv == "" {
155
+ continue
156
+ }
157
+ for _, pm := range pms {
158
+ for i := range pm.Metrics {
159
+ m := &pm.Metrics[i]
160
+ m.Family = processMetricFamily(m.Family, dt, dv)
161
}
162
}
163
+ return
164
}
165
}
166
@@ -155,9 +174,11 @@ func (c *Collector) snmpGet(oids []string) (map[string]gosnmp.SnmpPDU, error) {
174
}
175
176
for _, pdu := range result.Variables {
158
- if isPduWithData(pdu) {
159
- pdus[trimOID(pdu.Name)] = pdu
177
+ if !isPduWithData(pdu) {
178
+ c.missingOIDs[trimOID(pdu.Name)] = true
179
+ continue
180
}
181
+ pdus[trimOID(pdu.Name)] = pdu
182
}
183
}
184
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
+422
@@ -887,6 +887,427 @@ func TestCollector_Collect(t *testing.T) {
887
},
888
expectedError: false,
889
},
890
+ "table metrics with same-table tags": {
891
+ profiles: []*ddsnmp.Profile{
892
+ {
893
+ SourceFile: "test-profile.yaml",
894
+ Definition: &ddprofiledefinition.ProfileDefinition{
895
+ Metrics: []ddprofiledefinition.MetricsConfig{
896
+ {
897
+ MIB: "IF-MIB",
898
+ Table: ddprofiledefinition.SymbolConfig{
899
+ OID: "1.3.6.1.2.1.2.2",
900
+ Name: "ifTable",
901
+ },
902
+ Symbols: []ddprofiledefinition.SymbolConfig{
903
+ {
904
+ OID: "1.3.6.1.2.1.2.2.1.10",
905
+ Name: "ifInOctets",
906
+ },
907
+ {
908
+ OID: "1.3.6.1.2.1.2.2.1.16",
909
+ Name: "ifOutOctets",
910
+ },
911
+ },
912
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
913
+ {
914
+ Tag: "interface",
915
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
916
+ OID: "1.3.6.1.2.1.2.2.1.2",
917
+ Name: "ifDescr",
918
+ },
919
+ },
920
+ },
921
+ },
922
+ },
923
+ },
924
+ },
925
+ },
926
+ setupMock: func(m *snmpmock.MockHandler) {
927
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
928
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
929
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
930
+ []gosnmp.SnmpPDU{
931
+ // Row 1 - index 1
932
+ {
933
+ Name: "1.3.6.1.2.1.2.2.1.2.1",
934
+ Type: gosnmp.OctetString,
935
+ Value: []byte("eth0"),
936
+ },
937
+ {
938
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
939
+ Type: gosnmp.Counter32,
940
+ Value: uint(1000),
941
+ },
942
+ {
943
+ Name: "1.3.6.1.2.1.2.2.1.16.1",
944
+ Type: gosnmp.Counter32,
945
+ Value: uint(2000),
946
+ },
947
+ // Row 2 - index 2
948
+ {
949
+ Name: "1.3.6.1.2.1.2.2.1.2.2",
950
+ Type: gosnmp.OctetString,
951
+ Value: []byte("eth1"),
952
+ },
953
+ {
954
+ Name: "1.3.6.1.2.1.2.2.1.10.2",
955
+ Type: gosnmp.Counter32,
956
+ Value: uint(3000),
957
+ },
958
+ {
959
+ Name: "1.3.6.1.2.1.2.2.1.16.2",
960
+ Type: gosnmp.Counter32,
961
+ Value: uint(4000),
962
+ },
963
+ }, nil,
964
+ )
965
+ },
966
+ expectedResult: []*ProfileMetrics{
967
+ {
968
+ DeviceMetadata: nil,
969
+ Metrics: []Metric{
970
+ {
971
+ Name: "ifInOctets",
972
+ Value: 1000,
973
+ Tags: map[string]string{"interface": "eth0"},
974
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
975
+ IsTable: true,
976
+ },
977
+ {
978
+ Name: "ifOutOctets",
979
+ Value: 2000,
980
+ Tags: map[string]string{"interface": "eth0"},
981
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
982
+ IsTable: true,
983
+ },
984
+ {
985
+ Name: "ifInOctets",
986
+ Value: 3000,
987
+ Tags: map[string]string{"interface": "eth1"},
988
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
989
+ IsTable: true,
990
+ },
991
+ {
992
+ Name: "ifOutOctets",
993
+ Value: 4000,
994
+ Tags: map[string]string{"interface": "eth1"},
995
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
996
+ IsTable: true,
997
+ },
998
+ },
999
+ },
1000
+ },
1001
+ expectedError: false,
1002
+ },
1003
+ "table metrics with tag mapping": {
1004
+ profiles: []*ddsnmp.Profile{
1005
+ {
1006
+ SourceFile: "test-profile.yaml",
1007
+ Definition: &ddprofiledefinition.ProfileDefinition{
1008
+ Metrics: []ddprofiledefinition.MetricsConfig{
1009
+ {
1010
+ MIB: "IF-MIB",
1011
+ Table: ddprofiledefinition.SymbolConfig{
1012
+ OID: "1.3.6.1.2.1.2.2",
1013
+ Name: "ifTable",
1014
+ },
1015
+ Symbols: []ddprofiledefinition.SymbolConfig{
1016
+ {
1017
+ OID: "1.3.6.1.2.1.2.2.1.10",
1018
+ Name: "ifInOctets",
1019
+ },
1020
+ },
1021
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1022
+ {
1023
+ Tag: "if_type",
1024
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1025
+ OID: "1.3.6.1.2.1.2.2.1.3",
1026
+ Name: "ifType",
1027
+ },
1028
+ Mapping: map[string]string{
1029
+ "1": "other",
1030
+ "2": "regular1822",
1031
+ "6": "ethernetCsmacd",
1032
+ },
1033
+ },
1034
+ },
1035
+ },
1036
+ },
1037
+ },
1038
+ },
1039
+ },
1040
+ setupMock: func(m *snmpmock.MockHandler) {
1041
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1042
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1043
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1044
+ []gosnmp.SnmpPDU{
1045
+ // Row 1
1046
+ {
1047
+ Name: "1.3.6.1.2.1.2.2.1.3.1",
1048
+ Type: gosnmp.Integer,
1049
+ Value: 6, // ethernetCsmacd
1050
+ },
1051
+ {
1052
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
1053
+ Type: gosnmp.Counter32,
1054
+ Value: uint(1000),
1055
+ },
1056
+ // Row 2
1057
+ {
1058
+ Name: "1.3.6.1.2.1.2.2.1.3.2",
1059
+ Type: gosnmp.Integer,
1060
+ Value: 1, // other
1061
+ },
1062
+ {
1063
+ Name: "1.3.6.1.2.1.2.2.1.10.2",
1064
+ Type: gosnmp.Counter32,
1065
+ Value: uint(2000),
1066
+ },
1067
+ }, nil,
1068
+ )
1069
+ },
1070
+ expectedResult: []*ProfileMetrics{
1071
+ {
1072
+ DeviceMetadata: nil,
1073
+ Metrics: []Metric{
1074
+ {
1075
+ Name: "ifInOctets",
1076
+ Value: 1000,
1077
+ Tags: map[string]string{"if_type": "ethernetCsmacd"},
1078
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1079
+ IsTable: true,
1080
+ },
1081
+ {
1082
+ Name: "ifInOctets",
1083
+ Value: 2000,
1084
+ Tags: map[string]string{"if_type": "other"},
1085
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1086
+ IsTable: true,
1087
+ },
1088
+ },
1089
+ },
1090
+ },
1091
+ expectedError: false,
1092
+ },
1093
+ "table metrics with pattern matching tags": {
1094
+ profiles: []*ddsnmp.Profile{
1095
+ {
1096
+ SourceFile: "test-profile.yaml",
1097
+ Definition: &ddprofiledefinition.ProfileDefinition{
1098
+ Metrics: []ddprofiledefinition.MetricsConfig{
1099
+ {
1100
+ MIB: "MY-MIB",
1101
+ Table: ddprofiledefinition.SymbolConfig{
1102
+ OID: "1.3.6.1.4.1.1000.1",
1103
+ Name: "myTable",
1104
+ },
1105
+ Symbols: []ddprofiledefinition.SymbolConfig{
1106
+ {
1107
+ OID: "1.3.6.1.4.1.1000.1.1.1",
1108
+ Name: "myMetric",
1109
+ },
1110
+ },
1111
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1112
+ {
1113
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1114
+ OID: "1.3.6.1.4.1.1000.1.1.2",
1115
+ Name: "myDescription",
1116
+ ExtractValueCompiled: mustCompileRegex(`Interface (\w+)`),
1117
+ },
1118
+ Tag: "port",
1119
+ },
1120
+ },
1121
+ },
1122
+ },
1123
+ },
1124
+ },
1125
+ },
1126
+ setupMock: func(m *snmpmock.MockHandler) {
1127
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1128
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1129
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
1130
+ []gosnmp.SnmpPDU{
1131
+ {
1132
+ Name: "1.3.6.1.4.1.1000.1.1.1.1",
1133
+ Type: gosnmp.Gauge32,
1134
+ Value: uint(100),
1135
+ },
1136
+ {
1137
+ Name: "1.3.6.1.4.1.1000.1.1.2.1",
1138
+ Type: gosnmp.OctetString,
1139
+ Value: []byte("Interface Gi0/1"),
1140
+ },
1141
+ }, nil,
1142
+ )
1143
+ },
1144
+ expectedResult: []*ProfileMetrics{
1145
+ {
1146
+ DeviceMetadata: nil,
1147
+ Metrics: []Metric{
1148
+ {
1149
+ Name: "myMetric",
1150
+ Value: 100,
1151
+ Tags: map[string]string{"port": "Gi0"},
1152
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1153
+ IsTable: true,
1154
+ },
1155
+ },
1156
+ },
1157
+ },
1158
+ expectedError: false,
1159
+ },
1160
+ "table metrics with static tags": {
1161
+ profiles: []*ddsnmp.Profile{
1162
+ {
1163
+ SourceFile: "test-profile.yaml",
1164
+ Definition: &ddprofiledefinition.ProfileDefinition{
1165
+ Metrics: []ddprofiledefinition.MetricsConfig{
1166
+ {
1167
+ MIB: "MY-MIB",
1168
+ Table: ddprofiledefinition.SymbolConfig{
1169
+ OID: "1.3.6.1.4.1.1000.1",
1170
+ Name: "myTable",
1171
+ },
1172
+ Symbols: []ddprofiledefinition.SymbolConfig{
1173
+ {
1174
+ OID: "1.3.6.1.4.1.1000.1.1.1",
1175
+ Name: "myMetric",
1176
+ },
1177
+ },
1178
+ StaticTags: []string{"table_type:performance", "source:snmp"},
1179
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1180
+ {
1181
+ Tag: "interface",
1182
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1183
+ OID: "1.3.6.1.4.1.1000.1.1.2",
1184
+ Name: "ifName",
1185
+ },
1186
+ },
1187
+ },
1188
+ },
1189
+ },
1190
+ },
1191
+ },
1192
+ },
1193
+ setupMock: func(m *snmpmock.MockHandler) {
1194
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1195
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1196
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.1000.1").Return(
1197
+ []gosnmp.SnmpPDU{
1198
+ {
1199
+ Name: "1.3.6.1.4.1.1000.1.1.1.1",
1200
+ Type: gosnmp.Gauge32,
1201
+ Value: uint(100),
1202
+ },
1203
+ {
1204
+ Name: "1.3.6.1.4.1.1000.1.1.2.1",
1205
+ Type: gosnmp.OctetString,
1206
+ Value: []byte("eth0"),
1207
+ },
1208
+ }, nil,
1209
+ )
1210
+ },
1211
+ expectedResult: []*ProfileMetrics{
1212
+ {
1213
+ DeviceMetadata: nil,
1214
+ Metrics: []Metric{
1215
+ {
1216
+ Name: "myMetric",
1217
+ Value: 100,
1218
+ Tags: map[string]string{
1219
+ "table_type": "performance",
1220
+ "source": "snmp",
1221
+ "interface": "eth0",
1222
+ },
1223
+ MetricType: ddprofiledefinition.ProfileMetricTypeGauge,
1224
+ IsTable: true,
1225
+ },
1226
+ },
1227
+ },
1228
+ },
1229
+ expectedError: false,
1230
+ },
1231
+ "table metrics with missing tag values": {
1232
+ profiles: []*ddsnmp.Profile{
1233
+ {
1234
+ SourceFile: "test-profile.yaml",
1235
+ Definition: &ddprofiledefinition.ProfileDefinition{
1236
+ Metrics: []ddprofiledefinition.MetricsConfig{
1237
+ {
1238
+ MIB: "IF-MIB",
1239
+ Table: ddprofiledefinition.SymbolConfig{
1240
+ OID: "1.3.6.1.2.1.2.2",
1241
+ Name: "ifTable",
1242
+ },
1243
+ Symbols: []ddprofiledefinition.SymbolConfig{
1244
+ {
1245
+ OID: "1.3.6.1.2.1.2.2.1.10",
1246
+ Name: "ifInOctets",
1247
+ },
1248
+ },
1249
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
1250
+ {
1251
+ Tag: "interface",
1252
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
1253
+ OID: "1.3.6.1.2.1.2.2.1.2",
1254
+ Name: "ifDescr",
1255
+ },
1256
+ },
1257
+ },
1258
+ },
1259
+ },
1260
+ },
1261
+ },
1262
+ },
1263
+ setupMock: func(m *snmpmock.MockHandler) {
1264
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1265
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
1266
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
1267
+ []gosnmp.SnmpPDU{
1268
+ // Row 1 - has both metric and tag
1269
+ {
1270
+ Name: "1.3.6.1.2.1.2.2.1.2.1",
1271
+ Type: gosnmp.OctetString,
1272
+ Value: []byte("eth0"),
1273
+ },
1274
+ {
1275
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
1276
+ Type: gosnmp.Counter32,
1277
+ Value: uint(1000),
1278
+ },
1279
+ // Row 2 - missing tag value
1280
+ {
1281
+ Name: "1.3.6.1.2.1.2.2.1.10.2",
1282
+ Type: gosnmp.Counter32,
1283
+ Value: uint(2000),
1284
+ },
1285
+ }, nil,
1286
+ )
1287
+ },
1288
+ expectedResult: []*ProfileMetrics{
1289
+ {
1290
+ DeviceMetadata: nil,
1291
+ Metrics: []Metric{
1292
+ {
1293
+ Name: "ifInOctets",
1294
+ Value: 1000,
1295
+ Tags: map[string]string{"interface": "eth0"},
1296
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1297
+ IsTable: true,
1298
+ },
1299
+ {
1300
+ Name: "ifInOctets",
1301
+ Value: 2000,
1302
+ Tags: map[string]string{}, // No interface tag because it's missing
1303
+ MetricType: ddprofiledefinition.ProfileMetricTypeRate,
1304
+ IsTable: true,
1305
+ },
1306
+ },
1307
+ },
1308
+ },
1309
+ expectedError: false,
1310
+ },
1311
}
1312
1313
for name, tc := range tests {
@@ -898,6 +1319,7 @@ func TestCollector_Collect(t *testing.T) {
1319
tc.setupMock(mockHandler)
1320
1321
collector := New(mockHandler, tc.profiles, logger.New())
1322
+ collector.doTableMetrics = true
1323
1324
result, err := collector.Collect()
1325
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/utils.go
+3
-4
@@ -8,6 +8,7 @@ import (
8
"regexp"
9
"strconv"
10
"strings"
11
+ "unicode/utf8"
12
13
"github.com/gosnmp/gosnmp"
14
@@ -137,10 +138,8 @@ func convPduToString(pdu gosnmp.SnmpPDU) (string, error) {
138
return "", fmt.Errorf("OctetString has unexpected type %T", pdu.Value)
139
}
140
140
- // Convert to string and check if it can be represented as a raw string literal
141
- s := string(bs)
142
- if strconv.CanBackquote(s) {
143
- return s, nil
141
+ if utf8.Valid(bs) {
142
+ return strings.ToValidUTF8(string(bs), "�"), nil
143
}
144
return hex.EncodeToString(bs), nil
145
case gosnmp.Counter32, gosnmp.Counter64, gosnmp.Integer, gosnmp.Gauge32, gosnmp.Uinteger32, gosnmp.TimeTicks: