@cryptotaxi247 / netdata / commits / f540caf9b

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: