@cryptotaxi247 / netdata-1 / commits / 2a4c7abea

chore(go.d/snmp): remove legacy custom oid collection (#21056)

Ilya Mashchenko committed Sep 26, 2025 at 16:46 UTC 2a4c7abea7042fd673bff068a7a57ea41243797b
13 files changed +207 -669
src/go/plugin/go.d/collector/snmp/charts.go
-108
@@ -225,111 +225,3 @@ func dimAlgoFromDdSnmpType(m ddsnmp.Metric) module.DimAlgo {
225 }
226
227 var cleanMetricName = strings.NewReplacer(".", "_", " ", "_")
228 -
229 -// deprecated custom oid charts
230 -
231 -func newUserInputCharts(configs []ChartConfig) (*module.Charts, error) {
232 - charts := &module.Charts{}
233 - for _, cfg := range configs {
234 - if len(cfg.IndexRange) == 2 {
235 - cs, err := newUserInputChartsFromIndexRange(cfg)
236 - if err != nil {
237 - return nil, err
238 - }
239 - if err := charts.Add(*cs...); err != nil {
240 - return nil, err
241 - }
242 - } else {
243 - chart, err := newUserInputChart(cfg)
244 - if err != nil {
245 - return nil, err
246 - }
247 - if err = charts.Add(chart); err != nil {
248 - return nil, err
249 - }
250 - }
251 - }
252 - return charts, nil
253 -}
254 -
255 -func newUserInputChartsFromIndexRange(cfg ChartConfig) (*module.Charts, error) {
256 - var addPrio int
257 - charts := &module.Charts{}
258 - for i := cfg.IndexRange[0]; i <= cfg.IndexRange[1]; i++ {
259 - chart, err := newUserInputChartWithOIDIndex(i, cfg)
260 - if err != nil {
261 - return nil, err
262 - }
263 - chart.Priority += addPrio
264 - addPrio += 1
265 - if err = charts.Add(chart); err != nil {
266 - return nil, err
267 - }
268 - }
269 - return charts, nil
270 -}
271 -
272 -func newUserInputChartWithOIDIndex(oidIndex int, cfg ChartConfig) (*module.Chart, error) {
273 - chart, err := newUserInputChart(cfg)
274 - if err != nil {
275 - return nil, err
276 - }
277 -
278 - chart.ID = fmt.Sprintf("%s_%d", chart.ID, oidIndex)
279 - chart.Title = fmt.Sprintf("%s %d", chart.Title, oidIndex)
280 - for _, dim := range chart.Dims {
281 - dim.ID = fmt.Sprintf("%s.%d", dim.ID, oidIndex)
282 - }
283 -
284 - return chart, nil
285 -}
286 -
287 -func newUserInputChart(cfg ChartConfig) (*module.Chart, error) {
288 - chart := &module.Chart{
289 - ID: cfg.ID,
290 - Title: cfg.Title,
291 - Units: cfg.Units,
292 - Fam: cfg.Family,
293 - Ctx: fmt.Sprintf("snmp.%s", cfg.ID),
294 - Type: module.ChartType(cfg.Type),
295 - Priority: cfg.Priority,
296 - }
297 -
298 - if chart.Title == "" {
299 - chart.Title = "Untitled chart"
300 - }
301 - if chart.Units == "" {
302 - chart.Units = "num"
303 - }
304 - if chart.Priority < module.Priority {
305 - chart.Priority += module.Priority
306 - }
307 -
308 - seen := make(map[string]struct{})
309 - var a string
310 - for _, cfg := range cfg.Dimensions {
311 - if cfg.Algorithm != "" {
312 - seen[cfg.Algorithm] = struct{}{}
313 - a = cfg.Algorithm
314 - }
315 - dim := &module.Dim{
316 - ID: strings.TrimPrefix(cfg.OID, "."),
317 - Name: cfg.Name,
318 - Algo: module.DimAlgo(cfg.Algorithm),
319 - Mul: cfg.Multiplier,
320 - Div: cfg.Divisor,
321 - }
322 - if err := chart.AddDim(dim); err != nil {
323 - return nil, err
324 - }
325 - }
326 - if len(seen) == 1 && a != "" && len(chart.Dims) > 1 {
327 - for _, d := range chart.Dims {
328 - if d.Algo == "" {
329 - d.Algo = module.DimAlgo(a)
330 - }
331 - }
332 - }
333 -
334 - return chart, nil
335 -}
src/go/plugin/go.d/collector/snmp/collect.go
+9 -12
@@ -86,14 +86,7 @@ func (c *Collector) collectMetrics() (map[string]int64, error) {
86
87 func (c *Collector) ensureInitialized() error {
88 if c.snmpClient == nil {
89 - snmpClient, err := c.initAndConnectSNMPClient()
90 - if err != nil {
91 - return err
92 - }
93 - c.snmpClient = snmpClient
94 - if c.ddSnmpColl != nil {
95 - c.ddSnmpColl.SetSNMPClient(snmpClient)
96 - }
89 + return errors.New("snmp client not initialized")
90 }
91
92 if c.sysInfo != nil {
@@ -105,12 +98,17 @@ func (c *Collector) ensureInitialized() error {
98 return err
99 }
100
108 - if c.enableProfiles {
101 + if c.snmpProfiles == nil {
102 c.snmpProfiles = c.setupProfiles(si)
103 }
104
112 - if c.ddSnmpColl == nil {
113 - c.ddSnmpColl = ddsnmpcollector.New(c.snmpClient, c.snmpProfiles, c.Logger, si.SysObjectID)
105 + if c.ddSnmpColl == nil && len(c.snmpProfiles) > 0 {
106 + c.ddSnmpColl = c.newDdSnmpColl(ddsnmpcollector.Config{
107 + SnmpClient: c.snmpClient,
108 + Profiles: c.snmpProfiles,
109 + Log: c.Logger,
110 + SysObjectID: si.SysObjectID,
111 + })
112 }
113
114 if c.CreateVnode {
@@ -227,7 +225,6 @@ func (c *Collector) initAndConnectSNMPClient() (gosnmp.Handler, error) {
225 c.Warningf("SNMP bulk walk disabled: table metrics collection unavailable (device may not support GETBULK or max-repetitions adjustment failed)")
226 }
227 c.adjMaxRepetitions = snmpClient.MaxRepetitions()
230 - c.snmpBulkWalkOk = ok
228 }
229
230 return snmpClient, nil
src/go/plugin/go.d/collector/snmp/collect_snmp.go
+1 -55
@@ -7,27 +7,11 @@ import (
7 "sort"
8 "strings"
9
10 - "github.com/gosnmp/gosnmp"
11 -
10 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
11 )
12
13 func (c *Collector) collectSNMP(mx map[string]int64) error {
16 - if err := c.collectProfiles(mx); err != nil {
17 - return err
18 - }
19 -
20 - if !c.DisableLegacyCollection && len(c.customOids) > 0 {
21 - if err := c.collectLegacyCustomOIDs(mx); err != nil {
22 - return err
23 - }
24 - }
25 -
26 - return nil
27 -}
28 -
29 -func (c *Collector) collectProfiles(mx map[string]int64) error {
30 - if len(c.snmpProfiles) == 0 || c.ddSnmpColl == nil {
14 + if c.ddSnmpColl == nil {
15 return nil
16 }
17
@@ -140,41 +124,3 @@ func tableMetricKey(m ddsnmp.Metric) string {
124
125 return sb.String()
126 }
143 -
144 -func (c *Collector) collectLegacyCustomOIDs(mx map[string]int64) error {
145 - for i, end := 0, 0; i < len(c.customOids); i += c.Options.MaxOIDs {
146 - if end = i + c.Options.MaxOIDs; end > len(c.customOids) {
147 - end = len(c.customOids)
148 - }
149 -
150 - oids := c.customOids[i:end]
151 - resp, err := c.snmpClient.Get(oids)
152 - if err != nil {
153 - c.Errorf("cannot get SNMP data: %v", err)
154 - return err
155 - }
156 -
157 - for i, oid := range oids {
158 - if i >= len(resp.Variables) {
159 - continue
160 - }
161 -
162 - switch v := resp.Variables[i]; v.Type {
163 - case gosnmp.Boolean,
164 - gosnmp.Counter32,
165 - gosnmp.Counter64,
166 - gosnmp.Gauge32,
167 - gosnmp.TimeTicks,
168 - gosnmp.Uinteger32,
169 - gosnmp.OpaqueFloat,
170 - gosnmp.OpaqueDouble,
171 - gosnmp.Integer:
172 - mx[oid] = gosnmp.ToBigInt(v.Value).Int64()
173 - default:
174 - c.Debugf("skipping OID '%s' (unsupported type '%s')", oid, v.Type)
175 - }
176 - }
177 - }
178 -
179 - return nil
180 -}
src/go/plugin/go.d/collector/snmp/collector.go
+26 -34
@@ -40,7 +40,6 @@ func New() *Collector {
40 CreateVnode: true,
41 VnodeDeviceDownThreshold: 3,
42 Community: "public",
43 - DisableLegacyCollection: true,
43 Options: OptionsConfig{
44 Port: 161,
45 Retries: 1,
@@ -70,41 +69,42 @@ func New() *Collector {
69
70 newProber: ping.NewProber,
71 newSnmpClient: gosnmp.NewHandler,
73 -
74 - snmpBulkWalkOk: true,
75 - enableProfiles: true,
72 + newDdSnmpColl: func(cfg ddsnmpcollector.Config) ddCollector {
73 + return ddsnmpcollector.New(cfg)
74 + },
75 }
76 }
77
79 -type Collector struct {
80 - module.Base
81 - Config `yaml:",inline" json:""`
82 -
83 - vnode *vnodes.VirtualNode
78 +type (
79 + Collector struct {
80 + module.Base
81 + Config `yaml:",inline" json:""`
82
85 - charts *module.Charts
86 - seenScalarMetrics map[string]bool
87 - seenTableMetrics map[string]bool
83 + vnode *vnodes.VirtualNode
84
89 - prober ping.Prober
90 - newProber func(ping.ProberConfig, *logger.Logger) ping.Prober
85 + charts *module.Charts
86 + seenScalarMetrics map[string]bool
87 + seenTableMetrics map[string]bool
88
92 - newSnmpClient func() gosnmp.Handler
93 - snmpClient gosnmp.Handler
94 - ddSnmpColl *ddsnmpcollector.Collector
89 + prober ping.Prober
90 + newProber func(ping.ProberConfig, *logger.Logger) ping.Prober
91
96 - sysInfo *snmputils.SysInfo
97 - snmpProfiles []*ddsnmp.Profile
92 + snmpClient gosnmp.Handler
93 + newSnmpClient func() gosnmp.Handler
94
99 - adjMaxRepetitions uint32
100 - snmpBulkWalkOk bool
95 + ddSnmpColl ddCollector
96 + newDdSnmpColl func(ddsnmpcollector.Config) ddCollector
97
102 - // legacy data collection parameters
103 - customOids []string
98 + sysInfo *snmputils.SysInfo
99 + snmpProfiles []*ddsnmp.Profile
100
105 - // only for tests
106 - enableProfiles bool
107 -}
101 + adjMaxRepetitions uint32
102 + }
103 + ddCollector interface {
104 + Collect() ([]*ddsnmp.ProfileMetrics, error)
105 + CollectDeviceMetadata() (map[string]ddsnmp.MetaTag, error)
106 + }
107 +)
108
109 func (c *Collector) Configuration() any {
110 return c.Config
@@ -119,12 +119,6 @@ func (c *Collector) Init(context.Context) error {
119 return fmt.Errorf("failed to initialize SNMP client: %v", err)
120 }
121
122 - charts, err := newUserInputCharts(c.ChartsInput)
123 - if err != nil {
124 - return fmt.Errorf("failed to create user charts: %v", err)
125 - }
126 - c.charts = charts
127 -
122 if c.Ping.Enabled {
123 pr, err := c.initProber()
124 if err != nil {
@@ -133,8 +127,6 @@ func (c *Collector) Init(context.Context) error {
127 c.prober = pr
128 }
129
136 - c.customOids = c.initCustomOIDs()
137 -
130 return nil
131 }
132
src/go/plugin/go.d/collector/snmp/collector_test.go
+148 -211
@@ -4,14 +4,14 @@ package snmp
4
5 import (
6 "context"
7 - "encoding/hex"
7 "errors"
9 - "fmt"
8 "os"
9 "strings"
10 "testing"
11
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
13 + "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
14 + "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
15 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
16
17 "github.com/golang/mock/gomock"
@@ -130,89 +130,78 @@ func TestCollector_Cleanup(t *testing.T) {
130 }
131 }
132
133 -func TestCollector_Charts(t *testing.T) {
133 +func TestCollector_Check(t *testing.T) {
134 tests := map[string]struct {
135 - prepareSNMP func(t *testing.T, m *snmpmock.MockHandler) *Collector
136 - wantNumCharts int
137 - doCollect bool
135 + prepare func(m *snmpmock.MockHandler) *Collector
136 + wantErr bool
137 }{
139 - "custom, no if-mib": {
140 - wantNumCharts: 10,
141 - prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *Collector {
142 - collr := New()
143 - collr.Config = prepareConfigWithUserCharts(prepareV2Config(), 0, 9)
138 + "success: connects and reads sysInfo": {
139 + wantErr: false,
140 + prepare: func(m *snmpmock.MockHandler) *Collector {
141 + setMockClientInitExpect(m)
142 + setMockClientSysInfoExpect(m)
143
145 - return collr
144 + c := New()
145 + c.Config = prepareV2Config()
146 + c.CreateVnode = false
147 + c.Ping.Enabled = false
148 + c.newSnmpClient = func() gosnmp.Handler { return m }
149 + return c
150 },
151 },
148 - }
149 -
150 - for name, test := range tests {
151 - t.Run(name, func(t *testing.T) {
152 - mockSNMP, cleanup := mockInit(t)
153 - defer cleanup()
154 -
155 - setMockClientInitExpect(mockSNMP)
152
157 - collr := test.prepareSNMP(t, mockSNMP)
158 - collr.newSnmpClient = func() gosnmp.Handler { return mockSNMP }
159 -
160 - require.NoError(t, collr.Init(context.Background()))
161 -
162 - if test.doCollect {
163 - _ = collr.Check(context.Background())
164 - _ = collr.Collect(context.Background())
165 - }
166 -
167 - assert.Equal(t, test.wantNumCharts, len(*collr.Charts()))
168 - })
169 - }
170 -}
171 -
172 -func TestCollector_Check(t *testing.T) {
173 - tests := map[string]struct {
174 - wantFail bool
175 - prepareSNMP func(m *snmpmock.MockHandler) *Collector
176 - }{
177 - "success when sysinfo collected": {
178 - wantFail: false,
179 - prepareSNMP: func(m *snmpmock.MockHandler) *Collector {
180 - collr := New()
181 - collr.Config = prepareV2Config()
182 -
183 - setMockClientSysInfoExpect(m)
184 -
185 - return collr
153 + "failure: SNMP connect error": {
154 + wantErr: true,
155 + prepare: func(m *snmpmock.MockHandler) *Collector {
156 + setMockClientSetterExpect(m)
157 + m.EXPECT().Connect().Return(errors.New("connect failed")).AnyTimes()
158 +
159 + c := New()
160 + c.Config = prepareV2Config()
161 + c.CreateVnode = false
162 + c.Ping.Enabled = false
163 + c.newSnmpClient = func() gosnmp.Handler { return m }
164 + return c
165 },
166 },
188 - "fail when snmp client Get fails": {
189 - wantFail: true,
190 - prepareSNMP: func(m *snmpmock.MockHandler) *Collector {
191 - collr := New()
192 - collr.Config = prepareConfigWithUserCharts(prepareV2Config(), 0, 3)
193 - m.EXPECT().WalkAll(snmputils.RootOidMibSystem).Return(nil, errors.New("mock Get() error")).Times(1)
167
195 - return collr
168 + "failure: sysInfo walk error": {
169 + wantErr: true,
170 + prepare: func(m *snmpmock.MockHandler) *Collector {
171 + // Normal init succeeds
172 + setMockClientInitExpect(m)
173 + // But sysInfo retrieval (WalkAll on system tree) fails
174 + // If your helper is too opinionated, stub directly:
175 + // The collector ultimately calls WalkAll on the system OID tree.
176 + m.EXPECT().
177 + WalkAll(gomock.Any()).
178 + Return(nil, errors.New("walk failed"))
179 +
180 + c := New()
181 + c.Config = prepareV2Config()
182 + c.CreateVnode = false
183 + c.Ping.Enabled = false
184 + c.newSnmpClient = func() gosnmp.Handler { return m }
185 + return c
186 },
187 },
188 }
189
200 - for name, test := range tests {
190 + for name, tc := range tests {
191 t.Run(name, func(t *testing.T) {
202 - mockSNMP, cleanup := mockInit(t)
203 - defer cleanup()
204 -
205 - setMockClientInitExpect(mockSNMP)
192 + ctrl := gomock.NewController(t)
193 + defer ctrl.Finish()
194
207 - collr := test.prepareSNMP(mockSNMP)
208 - collr.newSnmpClient = func() gosnmp.Handler { return mockSNMP }
195 + mockSNMP := snmpmock.NewMockHandler(ctrl)
196
197 + collr := tc.prepare(mockSNMP)
198 require.NoError(t, collr.Init(context.Background()))
199
212 - if test.wantFail {
213 - assert.Error(t, collr.Check(context.Background()))
200 + err := collr.Check(context.Background())
201 + if tc.wantErr {
202 + assert.Error(t, err)
203 } else {
215 - assert.NoError(t, collr.Check(context.Background()))
204 + assert.NoError(t, err)
205 }
206 })
207 }
@@ -220,118 +209,114 @@ func TestCollector_Check(t *testing.T) {
209
210 func TestCollector_Collect(t *testing.T) {
211 tests := map[string]struct {
223 - prepareSNMP func(m *snmpmock.MockHandler) *Collector
224 - wantCollected map[string]int64
212 + prepare func(m *snmpmock.MockHandler) *Collector
213 + want map[string]int64
214 }{
226 - "success only custom OIDs supported type": {
227 - prepareSNMP: func(m *snmpmock.MockHandler) *Collector {
228 - collr := New()
229 - collr.Config = prepareConfigWithUserCharts(prepareV2Config(), 0, 3)
230 - collr.enableProfiles = false
231 -
232 - m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{
233 - Variables: []gosnmp.SnmpPDU{
234 - {Value: 10, Type: gosnmp.Counter32},
235 - {Value: 20, Type: gosnmp.Counter64},
236 - {Value: 30, Type: gosnmp.Gauge32},
237 - {Value: 1, Type: gosnmp.Boolean},
238 - {Value: 40, Type: gosnmp.Gauge32},
239 - {Value: 50, Type: gosnmp.TimeTicks},
240 - {Value: 60, Type: gosnmp.Uinteger32},
241 - {Value: 70, Type: gosnmp.Integer},
242 - },
243 - }, nil).Times(1)
215 + "collects scalar metric": {
216 + prepare: func(m *snmpmock.MockHandler) *Collector {
217 + setMockClientInitExpect(m)
218 + setMockClientSysInfoExpect(m)
219
245 - return collr
246 - },
247 - wantCollected: map[string]int64{
248 - //"TestMetric": 1,
249 - "1.3.6.1.2.1.2.2.1.10.0": 10,
250 - "1.3.6.1.2.1.2.2.1.16.0": 20,
251 - "1.3.6.1.2.1.2.2.1.10.1": 30,
252 - "1.3.6.1.2.1.2.2.1.16.1": 1,
253 - "1.3.6.1.2.1.2.2.1.10.2": 40,
254 - "1.3.6.1.2.1.2.2.1.16.2": 50,
255 - "1.3.6.1.2.1.2.2.1.10.3": 60,
256 - "1.3.6.1.2.1.2.2.1.16.3": 70,
257 - },
258 - },
259 - "success only custom OIDs supported and unsupported type": {
260 - prepareSNMP: func(m *snmpmock.MockHandler) *Collector {
220 collr := New()
262 - collr.Config = prepareConfigWithUserCharts(prepareV2Config(), 0, 2)
263 - collr.enableProfiles = false
264 -
265 - m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{
266 - Variables: []gosnmp.SnmpPDU{
267 - {Value: 10, Type: gosnmp.Counter32},
268 - {Value: 20, Type: gosnmp.Counter64},
269 - {Value: 30, Type: gosnmp.Gauge32},
270 - {Value: nil, Type: gosnmp.NoSuchInstance},
271 - {Value: nil, Type: gosnmp.NoSuchInstance},
272 - {Value: nil, Type: gosnmp.NoSuchInstance},
273 - },
274 - }, nil).Times(1)
275 -
221 + collr.Config = prepareV2Config()
222 + collr.CreateVnode = false
223 + collr.Ping.Enabled = false
224 + collr.snmpProfiles = []*ddsnmp.Profile{{}} // non-empty to enable collectSNMP()
225 + collr.newSnmpClient = func() gosnmp.Handler { return m }
226 + collr.newDdSnmpColl = func(ddsnmpcollector.Config) ddCollector {
227 + return &mockDdSnmpCollector{pms: []*ddsnmp.ProfileMetrics{
228 + {
229 + Metrics: []ddsnmp.Metric{
230 + {
231 + Name: "uptime",
232 + IsTable: false,
233 + Value: 123,
234 + Unit: "s",
235 + Tags: map[string]string{},
236 + Profile: &ddsnmp.ProfileMetrics{Tags: map[string]string{}},
237 + },
238 + },
239 + },
240 + }}
241 + }
242 return collr
243 },
278 - wantCollected: map[string]int64{
279 - //"TestMetric": 1,
280 - "1.3.6.1.2.1.2.2.1.10.0": 10,
281 - "1.3.6.1.2.1.2.2.1.16.0": 20,
282 - "1.3.6.1.2.1.2.2.1.10.1": 30,
244 + want: map[string]int64{
245 + // scalar → "snmp_device_prof_<name>"
246 + "snmp_device_prof_uptime": 123,
247 },
248 },
285 - "fails when only custom OIDs unsupported type": {
286 - prepareSNMP: func(m *snmpmock.MockHandler) *Collector {
287 - collr := New()
288 - collr.Config = prepareConfigWithUserCharts(prepareV2Config(), 0, 2)
289 - collr.enableProfiles = false
290 -
291 - m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{
292 - Variables: []gosnmp.SnmpPDU{
293 - {Value: nil, Type: gosnmp.NoSuchInstance},
294 - {Value: nil, Type: gosnmp.NoSuchInstance},
295 - {Value: nil, Type: gosnmp.NoSuchObject},
296 - {Value: "192.0.2.0", Type: gosnmp.NsapAddress},
297 - {Value: []uint8{118, 101, 116}, Type: gosnmp.OctetString},
298 - {Value: ".1.3.6.1.2.1.4.32.1.5.2.1.4.10.19.0.0.16", Type: gosnmp.ObjectIdentifier},
299 - },
300 - }, nil).Times(1)
249 + "collects table multivalue metric": {
250 + prepare: func(m *snmpmock.MockHandler) *Collector {
251 + setMockClientInitExpect(m)
252 + setMockClientSysInfoExpect(m)
253
254 + collr := New()
255 + collr.Config = prepareV2Config()
256 + collr.CreateVnode = false
257 + collr.Ping.Enabled = false
258 + collr.snmpProfiles = []*ddsnmp.Profile{{}}
259 + collr.newSnmpClient = func() gosnmp.Handler { return m }
260 + collr.newDdSnmpColl = func(ddsnmpcollector.Config) ddCollector {
261 + return &mockDdSnmpCollector{pms: []*ddsnmp.ProfileMetrics{
262 + {
263 + Metrics: []ddsnmp.Metric{
264 + {
265 + Name: "if_octets",
266 + IsTable: true,
267 + Unit: "bit/s",
268 + Tags: map[string]string{"ifName": "eth0"},
269 + Profile: &ddsnmp.ProfileMetrics{Tags: map[string]string{}},
270 + MultiValue: map[string]int64{
271 + "in": 1,
272 + "out": 2,
273 + },
274 + },
275 + },
276 + },
277 + }}
278 + }
279 return collr
280 },
281 + want: map[string]int64{
282 + // table key: "snmp_device_prof_<name>_<sorted tag values>_<subkey>"
283 + // here tags = {"ifName":"eth0"} → key part becomes "_eth0"
284 + "snmp_device_prof_if_octets_eth0_in": 1,
285 + "snmp_device_prof_if_octets_eth0_out": 2,
286 + },
287 },
288 }
289
307 - for name, test := range tests {
290 + for name, tc := range tests {
291 t.Run(name, func(t *testing.T) {
309 - mockSNMP, cleanup := mockInit(t)
310 - defer cleanup()
311 -
312 - setMockClientInitExpect(mockSNMP)
313 - setMockClientSysInfoExpect(mockSNMP)
292 + mockCtl := gomock.NewController(t)
293 + defer mockCtl.Finish()
294
315 - collr := test.prepareSNMP(mockSNMP)
316 - collr.newSnmpClient = func() gosnmp.Handler { return mockSNMP }
295 + mockSNMP := snmpmock.NewMockHandler(mockCtl)
296
297 + collr := tc.prepare(mockSNMP)
298 require.NoError(t, collr.Init(context.Background()))
299
300 _ = collr.Check(context.Background())
301
322 - mx := collr.Collect(context.Background())
323 -
324 - assert.Equal(t, test.wantCollected, mx)
302 + got := collr.Collect(context.Background())
303 + assert.Equal(t, tc.want, got)
304 })
305 }
306 }
307
329 -func mockInit(t *testing.T) (*snmpmock.MockHandler, func()) {
330 - mockCtl := gomock.NewController(t)
331 - cleanup := func() { mockCtl.Finish() }
332 - mockSNMP := snmpmock.NewMockHandler(mockCtl)
308 +type mockDdSnmpCollector struct {
309 + pms []*ddsnmp.ProfileMetrics
310 + meta map[string]ddsnmp.MetaTag
311 + err error
312 +}
313
334 - return mockSNMP, cleanup
314 +func (m *mockDdSnmpCollector) Collect() ([]*ddsnmp.ProfileMetrics, error) {
315 + return m.pms, m.err
316 +}
317 +
318 +func (m *mockDdSnmpCollector) CollectDeviceMetadata() (map[string]ddsnmp.MetaTag, error) {
319 + return m.meta, nil
320 }
321
322 func prepareV3Config() Config {
@@ -370,45 +355,20 @@ func prepareV1Config() Config {
355 }
356 }
357
373 -func prepareConfigWithUserCharts(cfg Config, start, end int) Config {
374 - if start > end || start < 0 || end < 1 {
375 - panic(fmt.Sprintf("invalid index range ('%d'-'%d')", start, end))
376 - }
377 - cfg.ChartsInput = []ChartConfig{
378 - {
379 - ID: "test_chart1",
380 - Title: "This is Test Chart1",
381 - Units: "kilobits/s",
382 - Family: "family",
383 - Type: module.Area.String(),
384 - Priority: module.Priority,
385 - Dimensions: []DimensionConfig{
386 - {
387 - OID: "1.3.6.1.2.1.2.2.1.10",
388 - Name: "in",
389 - Algorithm: module.Incremental.String(),
390 - Multiplier: 8,
391 - Divisor: 1000,
392 - },
393 - {
394 - OID: "1.3.6.1.2.1.2.2.1.16",
395 - Name: "out",
396 - Algorithm: module.Incremental.String(),
397 - Multiplier: 8,
398 - Divisor: 1000,
399 - },
400 - },
401 - },
402 - }
403 -
404 - for i := range cfg.ChartsInput {
405 - cfg.ChartsInput[i].IndexRange = []int{start, end}
406 - }
358 +func mockInit(t *testing.T) (*snmpmock.MockHandler, func()) {
359 + mockCtl := gomock.NewController(t)
360 + cleanup := func() { mockCtl.Finish() }
361 + mockSNMP := snmpmock.NewMockHandler(mockCtl)
362
408 - return cfg
363 + return mockSNMP, cleanup
364 }
365
366 func setMockClientInitExpect(m *snmpmock.MockHandler) {
367 + setMockClientSetterExpect(m)
368 + m.EXPECT().Connect().Return(nil).AnyTimes()
369 +}
370 +
371 +func setMockClientSetterExpect(m *snmpmock.MockHandler) {
372 m.EXPECT().Target().AnyTimes()
373 m.EXPECT().Port().AnyTimes()
374 m.EXPECT().Version().AnyTimes()
@@ -425,26 +385,9 @@ func setMockClientInitExpect(m *snmpmock.MockHandler) {
385 m.EXPECT().SetSecurityModel(gomock.Any()).AnyTimes()
386 m.EXPECT().SetMsgFlags(gomock.Any()).AnyTimes()
387 m.EXPECT().SetSecurityParameters(gomock.Any()).AnyTimes()
428 - m.EXPECT().Connect().Return(nil).AnyTimes()
388 m.EXPECT().MaxRepetitions().Return(uint32(25)).AnyTimes()
389 }
390
432 -func setMockClientSysObjectidExpect(m *snmpmock.MockHandler) {
433 - m.EXPECT().Get([]string{snmputils.OidSysObject}).Return(&gosnmp.SnmpPacket{
434 - Variables: []gosnmp.SnmpPDU{
435 - {Value: ".1.1.1",
436 - Name: ".1.3.6.1.2.1.1.2.0",
437 - Type: gosnmp.ObjectIdentifier},
438 - },
439 - }, nil).MinTimes(1)
440 - m.EXPECT().Get([]string{"1.1.1.0"}).Return(&gosnmp.SnmpPacket{
441 - Variables: []gosnmp.SnmpPDU{
442 - {Name: "1.1.1.0", Value: 1, Type: gosnmp.Integer},
443 - },
444 - }, nil).MinTimes(1)
445 -
446 -}
447 -
391 func setMockClientSysInfoExpect(m *snmpmock.MockHandler) {
392 m.EXPECT().WalkAll(snmputils.RootOidMibSystem).Return([]gosnmp.SnmpPDU{
393 {Name: snmputils.OidSysDescr, Value: []uint8("mock sysDescr"), Type: gosnmp.OctetString},
@@ -454,9 +397,3 @@ func setMockClientSysInfoExpect(m *snmpmock.MockHandler) {
397 {Name: snmputils.OidSysLocation, Value: []uint8("mock sysLocation"), Type: gosnmp.OctetString},
398 }, nil).MinTimes(1)
399 }
457 -
458 -func decodePhysAddr(s string) []uint8 {
459 - s = strings.ReplaceAll(s, ":", "")
460 - v, _ := hex.DecodeString(s)
461 - return v
462 -}
src/go/plugin/go.d/collector/snmp/config.go
-4
@@ -23,10 +23,6 @@ type (
23 ManualProfiles []string `yaml:"manual_profiles,omitempty" json:"manual_profiles"`
24
25 Ping PingConfig `yaml:"ping,omitempty" json:"ping"`
26 -
27 - // legacy
28 - ChartsInput []ChartConfig `yaml:"charts,omitempty" json:"charts"`
29 - DisableLegacyCollection bool `yaml:"disable_legacy_collection,omitempty" json:"disable_legacy_collection"`
26 }
27
28 PingConfig struct {
src/go/plugin/go.d/collector/snmp/config_schema.json
+3 -183
@@ -188,157 +188,9 @@
188 }
189 }
190 },
191 - "charts": {
192 - "title": "Charts configuration",
193 - "type": [
194 - "array",
195 - "null"
196 - ],
197 - "uniqueItems": true,
198 - "items": {
199 - "title": "Chart",
200 - "type": [
201 - "object",
202 - "null"
203 - ],
204 - "properties": {
205 - "id": {
206 - "title": "ID",
207 - "description": "Unique identifier for the chart.",
208 - "type": "string"
209 - },
210 - "title": {
211 - "title": "Title",
212 - "description": "Title of the chart.",
213 - "type": "string"
214 - },
215 - "units": {
216 - "title": "Units",
217 - "description": "Unit label for the vertical axis on charts.",
218 - "type": "string"
219 - },
220 - "family": {
221 - "title": "Family",
222 - "description": "Subsection on the dashboard where the chart will be displayed.",
223 - "type": "string"
224 - },
225 - "type": {
226 - "title": "Type",
227 - "type": "string",
228 - "enum": [
229 - "line",
230 - "area",
231 - "stacked"
232 - ],
233 - "default": "line"
234 - },
235 - "priority": {
236 - "title": "Priority",
237 - "description": "Rendering priority of the chart on the dashboard. Lower priority values will cause the chart to appear before those with higher priority values.",
238 - "type": "integer",
239 - "minimum": 1,
240 - "default": 90000
241 - },
242 - "multiply_range": {
243 - "title": "OID index range",
244 - "description": "Specifies the range of indexes used to create multiple charts. If set, a chart will be created for each index in the specified range. Each chart will have the index appended to the OID dimension.",
245 - "type": [
246 - "array",
247 - "null"
248 - ],
249 - "items": {
250 - "title": "Index",
251 - "type": "integer",
252 - "minimum": 0
253 - },
254 - "uniqueItems": true,
255 - "maxItems": 2
256 - },
257 - "dimensions": {
258 - "title": "Dimensions",
259 - "description": "Configuration for dimensions of the chart.",
260 - "type": [
261 - "array",
262 - "null"
263 - ],
264 - "uniqueItems": true,
265 - "minItems": 1,
266 - "items": {
267 - "title": "Dimension configuration",
268 - "type": [
269 - "object",
270 - "null"
271 - ],
272 - "properties": {
273 - "oid": {
274 - "title": "OID",
275 - "description": "SNMP OID.",
276 - "type": "string"
277 - },
278 - "name": {
279 - "title": "Dimension",
280 - "description": "Name of the dimension.",
281 - "type": "string"
282 - },
283 - "algorithm": {
284 - "title": "Algorithm",
285 - "description": "Algorithm of the dimension.",
286 - "type": "string",
287 - "enum": [
288 - "absolute",
289 - "incremental"
290 - ],
291 - "default": "absolute"
292 - },
293 - "multiplier": {
294 - "title": "Multiplier",
295 - "description": "Value to multiply the collected value.",
296 - "type": "integer",
297 - "not": {
298 - "const": 0
299 - },
300 - "default": 1
301 - },
302 - "divisor": {
303 - "title": "Divisor",
304 - "description": "Value to divide the collected value.",
305 - "type": "integer",
306 - "not": {
307 - "const": 0
308 - },
309 - "default": 1
310 - }
311 - },
312 - "required": [
313 - "oid",
314 - "name",
315 - "algorithm",
316 - "multiplier",
317 - "divisor"
318 - ]
319 - }
320 - }
321 - },
322 - "required": [
323 - "id",
324 - "title",
325 - "units",
326 - "family",
327 - "type",
328 - "priority",
329 - "dimensions"
330 - ]
331 - }
332 - },
333 - "disable_legacy_collection": {
334 - "title": "Disable Legacy SNMP Collection",
335 - "description": "Disable the legacy SNMP collection method, forcing the collector to use only SNMP profiles (YAML-based configuration). When enabled, the collector will ignore any non-profile based collection logic.",
336 - "type": "boolean",
337 - "default": true
338 - },
191 "manual_profiles": {
192 "title": "Manual SNMP Profiles",
341 - "description": "Profiles to apply if automatic detection cannot be used.",
193 + "description": "Profiles to apply if automatic detection cannot be used. In most cases, **leave this empty**.",
194 "type": [
195 "array",
196 "null"
@@ -467,32 +319,6 @@
319 "ui:widget": "hidden"
320 }
321 },
470 - "charts": {
471 - "ui:widget": "hidden",
472 - "items": {
473 - "ui:collapsible": true,
474 - "type": {
475 - "ui:widget": "radio",
476 - "ui:options": {
477 - "inline": true
478 - }
479 - },
480 - "multiply_range": {
481 - "ui:listFlavour": "list"
482 - },
483 - "dimensions": {
484 - "items": {
485 - "ui:collapsible": true,
486 - "algorithm": {
487 - "ui:widget": "radio",
488 - "ui:options": {
489 - "inline": true
490 - }
491 - }
492 - }
493 - }
494 - }
495 - },
322 "ui:flavour": "tabs",
323 "ui:options": {
324 "tabs": [
@@ -501,7 +327,8 @@
327 "fields": [
328 "update_every",
329 "hostname",
504 - "community"
330 + "community",
331 + "manual_profiles"
332 ]
333 },
334 {
@@ -529,13 +356,6 @@
356 "fields": [
357 "user"
358 ]
532 - },
533 - {
534 - "title": "Profiles",
535 - "fields": [
536 - "manual_profiles",
537 - "disable_legacy_collection"
538 - ]
359 }
360 ]
361 }
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+14 -7
@@ -18,24 +18,31 @@ import (
18 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
19 )
20
21 -func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logger, sysobjectid string) *Collector {
21 +type Config struct {
22 + SnmpClient gosnmp.Handler
23 + Profiles []*ddsnmp.Profile
24 + Log *logger.Logger
25 + SysObjectID string
26 +}
27 +
28 +func New(cfg Config) *Collector {
29 coll := &Collector{
23 - log: log.With(slog.String("ddsnmp", "collector")),
30 + log: cfg.Log.With(slog.String("ddsnmp", "collector")),
31 profiles: make(map[string]*profileState),
32 missingOIDs: make(map[string]bool),
33 tableCache: newTableCache(30*time.Minute, 1),
34 }
35
29 - for _, prof := range profiles {
36 + for _, prof := range cfg.Profiles {
37 prof := prof
38 handleCrossTableTagsWithoutMetrics(prof)
39 coll.profiles[prof.SourceFile] = &profileState{profile: prof}
40 }
41
35 - coll.globalTagsCollector = newGlobalTagsCollector(snmpClient, coll.missingOIDs, coll.log)
36 - coll.deviceMetadataCollector = newDeviceMetadataCollector(snmpClient, coll.missingOIDs, coll.log, sysobjectid)
37 - coll.scalarCollector = newScalarCollector(snmpClient, coll.missingOIDs, coll.log)
38 - coll.tableCollector = newTableCollector(snmpClient, coll.missingOIDs, coll.tableCache, coll.log)
42 + coll.globalTagsCollector = newGlobalTagsCollector(cfg.SnmpClient, coll.missingOIDs, coll.log)
43 + coll.deviceMetadataCollector = newDeviceMetadataCollector(cfg.SnmpClient, coll.missingOIDs, coll.log, cfg.SysObjectID)
44 + coll.scalarCollector = newScalarCollector(cfg.SnmpClient, coll.missingOIDs, coll.log)
45 + coll.tableCollector = newTableCollector(cfg.SnmpClient, coll.missingOIDs, coll.tableCache, coll.log)
46 coll.vmetricsCollector = newVirtualMetricsCollector(coll.log)
47
48 return coll
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table_test.go
+6 -1
@@ -4612,7 +4612,12 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
4612 mockHandler := snmpmock.NewMockHandler(ctrl)
4613 tc.setupMock(mockHandler)
4614
4615 - collector := New(mockHandler, tc.profiles, logger.New(), "")
4615 + collector := New(Config{
4616 + SnmpClient: mockHandler,
4617 + Profiles: tc.profiles,
4618 + Log: logger.New(),
4619 + SysObjectID: "",
4620 + })
4621
4622 // Configure cache based on test requirements
4623 if tc.enableCache {
src/go/plugin/go.d/collector/snmp/init.go
-9
@@ -83,12 +83,3 @@ func (c *Collector) initProber() (ping.Prober, error) {
83
84 return c.newProber(conf, c.Logger), nil
85 }
86 -
87 -func (c *Collector) initCustomOIDs() (oids []string) {
88 - for _, c := range *c.charts {
89 - for _, d := range c.Dims {
90 - oids = append(oids, d.ID)
91 - }
92 - }
93 - return oids
94 -}
src/go/plugin/go.d/collector/snmp/metadata.yaml
-5
@@ -226,11 +226,6 @@ modules:
226 description: Profiles to apply if automatic detection cannot be used.
227 default_value: "[]"
228 required: false
229 - - name: disable_legacy_collection
230 - group: Profiles
231 - description: Disable the legacy SNMP collection method, forcing the collector to use only SNMP profiles (YAML-based configuration). When enabled, the collector will ignore any non-profile based collection logic.
232 - default_value: "true"
233 - required: false
229
230 - name: create_vnode
231 group: Virtual node
src/go/plugin/go.d/collector/snmp/testdata/config.json
-23
@@ -28,29 +28,6 @@
28 "max_request_size": 123,
29 "max_repetitions": 123
30 },
31 - "charts": [
32 - {
33 - "id": "ok",
34 - "title": "ok",
35 - "units": "ok",
36 - "family": "ok",
37 - "type": "ok",
38 - "priority": 123,
39 - "multiply_range": [
40 - 123
41 - ],
42 - "dimensions": [
43 - {
44 - "oid": "ok",
45 - "name": "ok",
46 - "algorithm": "ok",
47 - "multiplier": 123,
48 - "divisor": 123
49 - }
50 - ]
51 - }
52 - ],
53 - "disable_legacy_collection": true,
31 "manual_profiles": [
32 "ok"
33 ],
src/go/plugin/go.d/collector/snmp/testdata/config.yaml
-17
@@ -2,7 +2,6 @@ update_every: 123
2 hostname: "ok"
3 create_vnode: yes
4 vnode_device_down_threshold: 123
5 -disable_legacy_collection: yes
5 manual_profiles:
6 - "ok"
7
@@ -31,22 +30,6 @@ options:
30 max_request_size: 123
31 max_repetitions: 123
32
34 -charts:
35 - - id: "ok"
36 - title: "ok"
37 - units: "ok"
38 - family: "ok"
39 - type: "ok"
40 - priority: 123
41 - multiply_range:
42 - - 123
43 - dimensions:
44 - - oid: "ok"
45 - name: "ok"
46 - algorithm: "ok"
47 - multiplier: 123
48 - divisor: 123
49 -
33 ping:
34 enabled: yes
35 network: ip