chore(go.d/snmp): add collection stats (#21409)
Ilya Mashchenko committed
Dec 5, 2025 at 22:23 UTC
d763e6f909eb1a25afbbd6dfc0b0cc9c4fc1dd5d
17 files changed
+543
-72
src/go/plugin/go.d/collector/snmp/charts.go
+115
@@ -16,6 +16,12 @@ const (
16
prioProfileChart = module.Priority + iota
17
prioPingRtt
18
prioPingStdDev
19
+
20
+ prioInternalStatsTimings
21
+ prioInternalStatsSnmpOps
22
+ prioInternalStatsMetrics
23
+ prioInternalStatsTableCache
24
+ prioInternalStatsErrors
25
)
26
27
var (
@@ -66,6 +72,115 @@ func (c *Collector) addPingCharts() {
72
}
73
}
74
75
+var (
76
+ profileStatsChartsTmpl = module.Charts{
77
+ profileStatsTimingsChartTmpl.Copy(),
78
+ profileStatsSnmpChartTmpl.Copy(),
79
+ profileStatsMetricsChartTmpl.Copy(),
80
+ profileStatsTableCacheChartTmpl.Copy(),
81
+ profileStatsErrorsChartTmpl.Copy(),
82
+ }
83
+
84
+ profileStatsTimingsChartTmpl = module.Chart{
85
+ ID: "snmp_device_prof_%s_stats_timings",
86
+ Title: "SNMP profile collection timings",
87
+ Units: "milliseconds",
88
+ Fam: "Internal/Stats",
89
+ Ctx: "snmp.device_prof_stats_timings",
90
+ Priority: prioInternalStatsTimings,
91
+ Dims: module.Dims{
92
+ {ID: "snmp_device_prof_%s_stats_timings_scalar", Name: "scalar"},
93
+ {ID: "snmp_device_prof_%s_stats_timings_table", Name: "table"},
94
+ {ID: "snmp_device_prof_%s_stats_timings_virtual", Name: "virtual"},
95
+ },
96
+ }
97
+
98
+ profileStatsSnmpChartTmpl = module.Chart{
99
+ ID: "snmp_device_prof_%s_stats_snmp",
100
+ Title: "SNMP profile operations",
101
+ Units: "operations",
102
+ Fam: "Internal/Stats",
103
+ Ctx: "snmp.device_prof_stats_snmp",
104
+ Priority: prioInternalStatsSnmpOps,
105
+ Dims: module.Dims{
106
+ {ID: "snmp_device_prof_%s_stats_snmp_get_requests", Name: "get_requests"},
107
+ {ID: "snmp_device_prof_%s_stats_snmp_get_oids", Name: "get_oids"},
108
+ {ID: "snmp_device_prof_%s_stats_snmp_walk_requests", Name: "walk_requests"},
109
+ {ID: "snmp_device_prof_%s_stats_snmp_walk_pdus", Name: "walk_pdus"},
110
+ {ID: "snmp_device_prof_%s_stats_snmp_tables_walked", Name: "tables_walked"},
111
+ {ID: "snmp_device_prof_%s_stats_snmp_tables_cached", Name: "tables_cached"},
112
+ },
113
+ }
114
+
115
+ profileStatsMetricsChartTmpl = module.Chart{
116
+ ID: "snmp_device_prof_%s_stats_metrics",
117
+ Title: "SNMP profile metric counts",
118
+ Units: "metrics",
119
+ Fam: "Internal/Stats",
120
+ Ctx: "snmp.device_prof_stats_metrics",
121
+ Priority: prioInternalStatsMetrics,
122
+ Dims: module.Dims{
123
+ {ID: "snmp_device_prof_%s_stats_metrics_scalar", Name: "scalar"},
124
+ {ID: "snmp_device_prof_%s_stats_metrics_table", Name: "table"},
125
+ {ID: "snmp_device_prof_%s_stats_metrics_virtual", Name: "virtual"},
126
+ {ID: "snmp_device_prof_%s_stats_metrics_tables", Name: "tables"},
127
+ {ID: "snmp_device_prof_%s_stats_metrics_rows", Name: "rows"},
128
+ },
129
+ }
130
+
131
+ profileStatsTableCacheChartTmpl = module.Chart{
132
+ ID: "snmp_device_prof_%s_stats_table_cache",
133
+ Title: "SNMP profile table cache",
134
+ Units: "tables",
135
+ Fam: "Internal/Stats",
136
+ Ctx: "snmp.device_prof_stats_table_cache",
137
+ Priority: prioInternalStatsTableCache,
138
+ Dims: module.Dims{
139
+ {ID: "snmp_device_prof_%s_stats_table_cache_hits", Name: "hits"},
140
+ {ID: "snmp_device_prof_%s_stats_table_cache_misses", Name: "misses"},
141
+ },
142
+ }
143
+
144
+ profileStatsErrorsChartTmpl = module.Chart{
145
+ ID: "snmp_device_prof_%s_stats_errors",
146
+ Title: "SNMP profile errors",
147
+ Units: "errors",
148
+ Fam: "Internal/Stats",
149
+ Ctx: "snmp.device_prof_stats_errors",
150
+ Priority: prioInternalStatsErrors,
151
+ Dims: module.Dims{
152
+ {ID: "snmp_device_prof_%s_stats_errors_snmp", Name: "snmp"},
153
+ {ID: "snmp_device_prof_%s_stats_errors_processing_scalar", Name: "processing_scalar"},
154
+ {ID: "snmp_device_prof_%s_stats_errors_processing_table", Name: "processing_table"},
155
+ },
156
+ }
157
+)
158
+
159
+func (c *Collector) addProfileStatsCharts(name string) {
160
+ if name == "" {
161
+ return
162
+ }
163
+
164
+ charts := profileStatsChartsTmpl.Copy()
165
+
166
+ labels := c.chartBaseLabels()
167
+ labels["profile"] = name
168
+
169
+ for _, chart := range *charts {
170
+ chart.ID = fmt.Sprintf(chart.ID, name)
171
+ for _, dim := range chart.Dims {
172
+ dim.ID = fmt.Sprintf(dim.ID, name)
173
+ }
174
+ for k, v := range labels {
175
+ chart.Labels = append(chart.Labels, module.Label{Key: k, Value: v})
176
+ }
177
+ }
178
+
179
+ if err := c.Charts().Add(*charts...); err != nil {
180
+ c.Warningf("failed to add profile stats charts for %s: %v", name, err)
181
+ }
182
+}
183
+
184
func (c *Collector) addProfileScalarMetricChart(m ddsnmp.Metric) {
185
if m.Name == "" {
186
return
src/go/plugin/go.d/collector/snmp/collect_snmp.go
+38
@@ -4,6 +4,7 @@ package snmp
4
5
import (
6
"fmt"
7
+ "path/filepath"
8
"sort"
9
"strings"
10
@@ -22,6 +23,7 @@ func (c *Collector) collectSNMP(mx map[string]int64) error {
23
24
c.collectProfileScalarMetrics(mx, pms)
25
c.collectProfileTableMetrics(mx, pms)
26
+ c.collectProfileStats(mx, pms)
27
28
return nil
29
}
@@ -92,6 +94,38 @@ func (c *Collector) collectProfileTableMetrics(mx map[string]int64, pms []*ddsnm
94
}
95
}
96
97
+func (c *Collector) collectProfileStats(mx map[string]int64, pms []*ddsnmp.ProfileMetrics) {
98
+ for _, pm := range pms {
99
+ name := stripFileNameExt(pm.Source)
100
+
101
+ if !c.seenProfiles[name] {
102
+ c.seenProfiles[name] = true
103
+ c.addProfileStatsCharts(name)
104
+ }
105
+
106
+ px := fmt.Sprintf("snmp_device_prof_%s_stats_", name)
107
+ mx[px+"timings_scalar"] = pm.Stats.Timing.Scalar.Milliseconds()
108
+ mx[px+"timings_table"] = pm.Stats.Timing.Table.Milliseconds()
109
+ mx[px+"timings_virtual"] = pm.Stats.Timing.VirtualMetrics.Milliseconds()
110
+ mx[px+"snmp_get_requests"] = pm.Stats.SNMP.GetRequests
111
+ mx[px+"snmp_get_oids"] = pm.Stats.SNMP.GetOIDs
112
+ mx[px+"snmp_walk_pdus"] = pm.Stats.SNMP.WalkPDUs
113
+ mx[px+"snmp_walk_requests"] = pm.Stats.SNMP.WalkRequests
114
+ mx[px+"snmp_tables_walked"] = pm.Stats.SNMP.TablesWalked
115
+ mx[px+"snmp_tables_cached"] = pm.Stats.SNMP.TablesCached
116
+ mx[px+"metrics_scalar"] = pm.Stats.Metrics.Scalar
117
+ mx[px+"metrics_table"] = pm.Stats.Metrics.Table
118
+ mx[px+"metrics_virtual"] = pm.Stats.Metrics.Virtual
119
+ mx[px+"metrics_tables"] = pm.Stats.Metrics.Tables
120
+ mx[px+"metrics_rows"] = pm.Stats.Metrics.Rows
121
+ mx[px+"table_cache_hits"] = pm.Stats.TableCache.Hits
122
+ mx[px+"table_cache_misses"] = pm.Stats.TableCache.Misses
123
+ mx[px+"errors_snmp"] = pm.Stats.Errors.SNMP
124
+ mx[px+"errors_processing_scalar"] = pm.Stats.Errors.Processing.Scalar
125
+ mx[px+"errors_processing_table"] = pm.Stats.Errors.Processing.Table
126
+ }
127
+}
128
+
129
func tableMetricKey(m ddsnmp.Metric) string {
130
if m.Name == "" {
131
return ""
@@ -124,3 +158,7 @@ func tableMetricKey(m ddsnmp.Metric) string {
158
159
return sb.String()
160
}
161
+
162
+func stripFileNameExt(path string) string {
163
+ return strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
164
+}
src/go/plugin/go.d/collector/snmp/collector.go
+2
@@ -67,6 +67,7 @@ func New() *Collector {
67
charts: &module.Charts{},
68
seenScalarMetrics: make(map[string]bool),
69
seenTableMetrics: make(map[string]bool),
70
+ seenProfiles: make(map[string]bool),
71
72
newProber: ping.NewProber,
73
newSnmpClient: gosnmp.NewHandler,
@@ -86,6 +87,7 @@ type (
87
charts *module.Charts
88
seenScalarMetrics map[string]bool
89
seenTableMetrics map[string]bool
90
+ seenProfiles map[string]bool
91
92
prober ping.Prober
93
newProber func(ping.ProberConfig, *logger.Logger) ping.Prober
src/go/plugin/go.d/collector/snmp/collector_test.go
+43
-3
@@ -226,6 +226,7 @@ func TestCollector_Collect(t *testing.T) {
226
collr.newDdSnmpColl = func(ddsnmpcollector.Config) ddCollector {
227
return &mockDdSnmpCollector{pms: []*ddsnmp.ProfileMetrics{
228
{
229
+ Source: "test",
230
Metrics: []ddsnmp.Metric{
231
{
232
Name: "uptime",
@@ -243,7 +244,26 @@ func TestCollector_Collect(t *testing.T) {
244
},
245
want: map[string]int64{
246
// scalar → "snmp_device_prof_<name>"
246
- "snmp_device_prof_uptime": 123,
247
+ "snmp_device_prof_test_stats_errors_processing_scalar": 0,
248
+ "snmp_device_prof_test_stats_errors_processing_table": 0,
249
+ "snmp_device_prof_test_stats_errors_snmp": 0,
250
+ "snmp_device_prof_test_stats_metrics_rows": 0,
251
+ "snmp_device_prof_test_stats_metrics_scalar": 0,
252
+ "snmp_device_prof_test_stats_metrics_table": 0,
253
+ "snmp_device_prof_test_stats_metrics_tables": 0,
254
+ "snmp_device_prof_test_stats_metrics_virtual": 0,
255
+ "snmp_device_prof_test_stats_snmp_get_oids": 0,
256
+ "snmp_device_prof_test_stats_snmp_get_requests": 0,
257
+ "snmp_device_prof_test_stats_snmp_tables_cached": 0,
258
+ "snmp_device_prof_test_stats_snmp_tables_walked": 0,
259
+ "snmp_device_prof_test_stats_snmp_walk_pdus": 0,
260
+ "snmp_device_prof_test_stats_snmp_walk_requests": 0,
261
+ "snmp_device_prof_test_stats_table_cache_hits": 0,
262
+ "snmp_device_prof_test_stats_table_cache_misses": 0,
263
+ "snmp_device_prof_test_stats_timings_scalar": 0,
264
+ "snmp_device_prof_test_stats_timings_table": 0,
265
+ "snmp_device_prof_test_stats_timings_virtual": 0,
266
+ "snmp_device_prof_uptime": 123,
267
},
268
},
269
"collects table multivalue metric": {
@@ -260,6 +280,7 @@ func TestCollector_Collect(t *testing.T) {
280
collr.newDdSnmpColl = func(ddsnmpcollector.Config) ddCollector {
281
return &mockDdSnmpCollector{pms: []*ddsnmp.ProfileMetrics{
282
{
283
+ Source: "test",
284
Metrics: []ddsnmp.Metric{
285
{
286
Name: "if_octets",
@@ -281,8 +302,27 @@ func TestCollector_Collect(t *testing.T) {
302
want: map[string]int64{
303
// table key: "snmp_device_prof_<name>_<sorted tag values>_<subkey>"
304
// 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,
305
+ "snmp_device_prof_test_stats_errors_processing_scalar": 0,
306
+ "snmp_device_prof_test_stats_errors_processing_table": 0,
307
+ "snmp_device_prof_test_stats_errors_snmp": 0,
308
+ "snmp_device_prof_test_stats_metrics_rows": 0,
309
+ "snmp_device_prof_test_stats_metrics_scalar": 0,
310
+ "snmp_device_prof_test_stats_metrics_table": 0,
311
+ "snmp_device_prof_test_stats_metrics_tables": 0,
312
+ "snmp_device_prof_test_stats_metrics_virtual": 0,
313
+ "snmp_device_prof_test_stats_snmp_get_oids": 0,
314
+ "snmp_device_prof_test_stats_snmp_get_requests": 0,
315
+ "snmp_device_prof_test_stats_snmp_tables_cached": 0,
316
+ "snmp_device_prof_test_stats_snmp_tables_walked": 0,
317
+ "snmp_device_prof_test_stats_snmp_walk_pdus": 0,
318
+ "snmp_device_prof_test_stats_snmp_walk_requests": 0,
319
+ "snmp_device_prof_test_stats_table_cache_hits": 0,
320
+ "snmp_device_prof_test_stats_table_cache_misses": 0,
321
+ "snmp_device_prof_test_stats_timings_scalar": 0,
322
+ "snmp_device_prof_test_stats_timings_table": 0,
323
+ "snmp_device_prof_test_stats_timings_virtual": 0,
324
+ "snmp_device_prof_if_octets_eth0_in": 1,
325
+ "snmp_device_prof_if_octets_eth0_out": 2,
326
},
327
},
328
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+33
-22
@@ -63,10 +63,12 @@ type (
63
vmetricsCollector *vmetricsCollector
64
}
65
profileState struct {
66
- profile *ddsnmp.Profile
67
- initialized bool
68
- globalTags map[string]string
69
- deviceMetadata map[string]ddsnmp.MetaTag
66
+ profile *ddsnmp.Profile
67
+ initialized bool
68
+ cache struct {
69
+ globalTags map[string]string
70
+ deviceMetadata map[string]ddsnmp.MetaTag
71
+ }
72
}
73
)
74
@@ -74,7 +76,7 @@ func (c *Collector) CollectDeviceMetadata() (map[string]ddsnmp.MetaTag, error) {
76
meta := make(map[string]ddsnmp.MetaTag)
77
78
for _, prof := range c.profiles {
77
- profDeviceMeta, err := c.deviceMetadataCollector.Collect(prof.profile)
79
+ profDeviceMeta, err := c.deviceMetadataCollector.collect(prof.profile)
80
if err != nil {
81
return nil, err
82
}
@@ -91,7 +93,8 @@ func (c *Collector) Collect() ([]*ddsnmp.ProfileMetrics, error) {
93
var metrics []*ddsnmp.ProfileMetrics
94
var errs []error
95
94
- if expired := c.tableCache.clearExpired(); len(expired) > 0 {
96
+ expired := c.tableCache.clearExpired()
97
+ if len(expired) > 0 {
98
c.log.Debugf("Cleared %d expired table cache entries", len(expired))
99
}
100
@@ -106,13 +109,16 @@ func (c *Collector) Collect() ([]*ddsnmp.ProfileMetrics, error) {
109
110
metrics = append(metrics, pm)
111
109
- if vmetrics := c.vmetricsCollector.Collect(prof.profile.Definition, pm.Metrics); len(vmetrics) > 0 {
112
+ now := time.Now()
113
+ if vmetrics := c.vmetricsCollector.collect(prof.profile.Definition, pm.Metrics); len(vmetrics) > 0 {
114
for i := range vmetrics {
115
vmetrics[i].Profile = pm
116
}
117
118
pm.Metrics = slices.DeleteFunc(pm.Metrics, func(m ddsnmp.Metric) bool { return strings.HasPrefix(m.Name, "_") })
119
pm.Metrics = append(pm.Metrics, vmetrics...)
120
+ pm.Stats.Metrics.Virtual += int64(len(vmetrics))
121
+ pm.Stats.Timing.VirtualMetrics = time.Since(now)
122
}
123
}
124
@@ -142,42 +148,47 @@ func (c *Collector) SetSNMPClient(snmpClient gosnmp.Handler) {
148
}
149
150
func (c *Collector) collectProfile(ps *profileState) (*ddsnmp.ProfileMetrics, error) {
151
+ pm := &ddsnmp.ProfileMetrics{
152
+ Source: ps.profile.SourceFile,
153
+ }
154
+
155
if !ps.initialized {
146
- globalTag, err := c.globalTagsCollector.Collect(ps.profile)
156
+ globalTag, err := c.globalTagsCollector.collect(ps.profile)
157
if err != nil {
158
return nil, fmt.Errorf("failed to collect global tags: %w", err)
159
}
160
+ ps.cache.globalTags = globalTag
161
151
- deviceMeta, err := c.deviceMetadataCollector.Collect(ps.profile)
162
+ deviceMeta, err := c.deviceMetadataCollector.collect(ps.profile)
163
if err != nil {
164
return nil, fmt.Errorf("failed to collect device metadata: %w", err)
165
}
166
+ ps.cache.deviceMetadata = deviceMeta
167
156
- ps.globalTags = globalTag
157
- ps.deviceMetadata = deviceMeta
168
ps.initialized = true
169
}
170
161
- var metrics []ddsnmp.Metric
171
+ pm.Tags = maps.Clone(ps.cache.globalTags)
172
+ pm.DeviceMetadata = maps.Clone(ps.cache.deviceMetadata)
173
163
- scalarMetrics, err := c.scalarCollector.Collect(ps.profile)
174
+ now := time.Now()
175
+ scalarMetrics, err := c.scalarCollector.collect(ps.profile, &pm.Stats)
176
if err != nil {
177
return nil, err
178
}
167
- metrics = append(metrics, scalarMetrics...)
179
+ pm.Metrics = append(pm.Metrics, scalarMetrics...)
180
+ pm.Stats.Timing.Scalar = time.Since(now)
181
+ pm.Stats.Metrics.Scalar += int64(len(scalarMetrics))
182
169
- tableMetrics, err := c.tableCollector.Collect(ps.profile)
183
+ now = time.Now()
184
+ tableMetrics, err := c.tableCollector.collect(ps.profile, &pm.Stats)
185
if err != nil {
186
return nil, err
187
}
173
- metrics = append(metrics, tableMetrics...)
188
+ pm.Metrics = append(pm.Metrics, tableMetrics...)
189
+ pm.Stats.Timing.Table = time.Since(now)
190
+ pm.Stats.Metrics.Table += int64(len(tableMetrics))
191
175
- pm := &ddsnmp.ProfileMetrics{
176
- Source: ps.profile.SourceFile,
177
- DeviceMetadata: maps.Clone(ps.deviceMetadata),
178
- Tags: maps.Clone(ps.globalTags),
179
- Metrics: metrics,
180
- }
192
for i := range pm.Metrics {
193
pm.Metrics[i].Profile = pm
194
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_device_meta.go
+1
-1
@@ -31,7 +31,7 @@ func newDeviceMetadataCollector(snmpClient gosnmp.Handler, missingOIDs map[strin
31
}
32
}
33
34
-func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]ddsnmp.MetaTag, error) {
34
+func (dc *deviceMetadataCollector) collect(prof *ddsnmp.Profile) (map[string]ddsnmp.MetaTag, error) {
35
if len(prof.Definition.Metadata) == 0 && len(prof.Definition.SysobjectIDMetadata) == 0 {
36
return nil, nil
37
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_device_meta_test.go
+1
-1
@@ -915,7 +915,7 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
915
missingOIDs := make(map[string]bool)
916
collector := newDeviceMetadataCollector(mockHandler, missingOIDs, logger.New(), tc.sysobjectid)
917
918
- result, err := collector.Collect(tc.profile)
918
+ result, err := collector.collect(tc.profile)
919
920
if tc.expectedError {
921
assert.Error(t, err)
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_global_tags.go
+1
-1
@@ -32,7 +32,7 @@ func newGlobalTagsCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bo
32
}
33
34
// Collect gathers all global tags from the profile
35
-func (gc *globalTagsCollector) Collect(prof *ddsnmp.Profile) (map[string]string, error) {
35
+func (gc *globalTagsCollector) collect(prof *ddsnmp.Profile) (map[string]string, error) {
36
if len(prof.Definition.MetricTags) == 0 && len(prof.Definition.StaticTags) == 0 {
37
return nil, nil
38
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_global_tags_test.go
+1
-1
@@ -389,7 +389,7 @@ func TestGlobalTagsCollector_Collect(t *testing.T) {
389
missingOIDs := make(map[string]bool)
390
collector := newGlobalTagsCollector(mockHandler, missingOIDs, logger.New())
391
392
- result, err := collector.Collect(tc.profile)
392
+ result, err := collector.collect(tc.profile)
393
394
if tc.expectedError {
395
assert.Error(t, err)
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_scalar.go
+12
-5
@@ -32,23 +32,25 @@ func newScalarCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool,
32
}
33
34
// Collect gathers all scalar metrics from the profile
35
-func (sc *scalarCollector) Collect(prof *ddsnmp.Profile) ([]ddsnmp.Metric, error) {
35
+func (sc *scalarCollector) collect(prof *ddsnmp.Profile, stats *ddsnmp.CollectionStats) ([]ddsnmp.Metric, error) {
36
oids, missingOIDs := sc.identifyScalarOIDs(prof.Definition.Metrics)
37
38
if len(missingOIDs) > 0 {
39
sc.log.Debugf("scalar metrics missing OIDs: %v", missingOIDs)
40
+ stats.Errors.MissingOIDs += int64(len(missingOIDs))
41
}
42
43
if len(oids) == 0 {
44
return nil, nil
45
}
46
46
- pdus, err := sc.getScalarValues(oids)
47
+ pdus, err := sc.getScalarValues(oids, stats)
48
if err != nil {
49
+ stats.Errors.SNMP++
50
return nil, err
51
}
52
51
- return sc.processScalarMetrics(prof.Definition.Metrics, pdus)
53
+ return sc.processScalarMetrics(prof.Definition.Metrics, pdus, stats)
54
}
55
56
// identifyScalarOIDs returns OIDs to collect and OIDs that are known to be missing
@@ -77,11 +79,14 @@ func (sc *scalarCollector) identifyScalarOIDs(configs []ddprofiledefinition.Metr
79
return oids, missingOIDs
80
}
81
80
-func (sc *scalarCollector) getScalarValues(oids []string) (map[string]gosnmp.SnmpPDU, error) {
82
+func (sc *scalarCollector) getScalarValues(oids []string, stats *ddsnmp.CollectionStats) (map[string]gosnmp.SnmpPDU, error) {
83
pdus := make(map[string]gosnmp.SnmpPDU)
84
maxOids := sc.snmpClient.MaxOids()
85
86
for chunk := range slices.Chunk(oids, maxOids) {
87
+ stats.SNMP.GetOIDs += int64(len(chunk))
88
+ stats.SNMP.GetRequests++
89
+
90
result, err := sc.snmpClient.Get(chunk)
91
if err != nil {
92
return nil, err
@@ -90,6 +95,7 @@ func (sc *scalarCollector) getScalarValues(oids []string) (map[string]gosnmp.Snm
95
for _, pdu := range result.Variables {
96
if !isPduWithData(pdu) {
97
sc.missingOIDs[trimOID(pdu.Name)] = true
98
+ stats.Errors.MissingOIDs++
99
continue
100
}
101
pdus[trimOID(pdu.Name)] = pdu
@@ -100,7 +106,7 @@ func (sc *scalarCollector) getScalarValues(oids []string) (map[string]gosnmp.Snm
106
}
107
108
// processScalarMetrics converts PDUs into metrics
103
-func (sc *scalarCollector) processScalarMetrics(configs []ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU) ([]ddsnmp.Metric, error) {
109
+func (sc *scalarCollector) processScalarMetrics(configs []ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU, stats *ddsnmp.CollectionStats) ([]ddsnmp.Metric, error) {
110
var metrics []ddsnmp.Metric
111
var errs []error
112
@@ -113,6 +119,7 @@ func (sc *scalarCollector) processScalarMetrics(configs []ddprofiledefinition.Me
119
if err != nil {
120
errs = append(errs, fmt.Errorf("metric '%s': %w", cfg.Symbol.Name, err))
121
sc.log.Debugf("Error processing scalar metric '%s': %v", cfg.Symbol.Name, err)
122
+ stats.Errors.Processing.Scalar++
123
continue
124
}
125
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_scalar_test.go
+2
-1
@@ -709,7 +709,8 @@ func TestScalarCollector_Collect(t *testing.T) {
709
missingOIDs := make(map[string]bool)
710
collector := newScalarCollector(mockHandler, missingOIDs, logger.New())
711
712
- result, err := collector.Collect(tc.profile)
712
+ var stats ddsnmp.CollectionStats
713
+ result, err := collector.collect(tc.profile, &stats)
714
715
if tc.expectedError {
716
assert.Error(t, err)
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table.go
+59
-30
@@ -40,13 +40,13 @@ func newTableCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, t
40
}
41
42
// Collect gathers all table metrics from the profile
43
-func (tc *tableCollector) Collect(prof *ddsnmp.Profile) ([]ddsnmp.Metric, error) {
44
- walkResults, err := tc.walkTablesAsNeeded(prof)
43
+func (tc *tableCollector) collect(prof *ddsnmp.Profile, stats *ddsnmp.CollectionStats) ([]ddsnmp.Metric, error) {
44
+ walkResults, err := tc.walkTablesAsNeeded(prof, stats)
45
if err != nil {
46
return nil, err
47
}
48
49
- return tc.processWalkResults(walkResults)
49
+ return tc.processWalkResults(walkResults, stats)
50
}
51
52
// tableWalkResult holds the walked data for a single table
@@ -159,10 +159,10 @@ type cacheProcessingContext struct {
159
}
160
161
// walkTablesAsNeeded walks only tables that aren't fully cached
162
-func (tc *tableCollector) walkTablesAsNeeded(prof *ddsnmp.Profile) ([]tableWalkResult, error) {
163
- toWalk := tc.identifyTablesToWalk(prof)
162
+func (tc *tableCollector) walkTablesAsNeeded(prof *ddsnmp.Profile, stats *ddsnmp.CollectionStats) ([]tableWalkResult, error) {
163
+ toWalk := tc.identifyTablesToWalk(prof, stats)
164
165
- walkedData, errs := tc.walkTables(toWalk.tablesToWalk)
165
+ walkedData, errs := tc.walkTables(toWalk.tablesToWalk, stats)
166
167
results := tc.buildWalkResults(walkedData, toWalk)
168
@@ -181,7 +181,7 @@ type tablesToWalkInfo struct {
181
}
182
183
// identifyTablesToWalk determines which tables need to be walked
184
-func (tc *tableCollector) identifyTablesToWalk(prof *ddsnmp.Profile) *tablesToWalkInfo {
184
+func (tc *tableCollector) identifyTablesToWalk(prof *ddsnmp.Profile, stats *ddsnmp.CollectionStats) *tablesToWalkInfo {
185
info := &tablesToWalkInfo{
186
tablesToWalk: make(map[string]bool),
187
tableConfigs: make(map[string][]ddprofiledefinition.MetricsConfig),
@@ -194,6 +194,7 @@ func (tc *tableCollector) identifyTablesToWalk(prof *ddsnmp.Profile) *tablesToWa
194
195
tableOID := cfg.Table.OID
196
if tc.missingOIDs[trimOID(tableOID)] {
197
+ stats.Errors.MissingOIDs++
198
info.missingOIDs = append(info.missingOIDs, tableOID)
199
continue
200
}
@@ -202,6 +203,9 @@ func (tc *tableCollector) identifyTablesToWalk(prof *ddsnmp.Profile) *tablesToWa
203
204
if !tc.tableCache.isConfigCached(cfg) {
205
info.tablesToWalk[tableOID] = true
206
+ stats.TableCache.Misses++
207
+ } else {
208
+ stats.TableCache.Hits++
209
}
210
}
211
@@ -216,18 +220,20 @@ func (tc *tableCollector) identifyTablesToWalk(prof *ddsnmp.Profile) *tablesToWa
220
}
221
222
// walkTables performs SNMP walks for the specified tables
219
-func (tc *tableCollector) walkTables(tablesToWalk map[string]bool) (map[string]map[string]gosnmp.SnmpPDU, []error) {
223
+func (tc *tableCollector) walkTables(tablesToWalk map[string]bool, stats *ddsnmp.CollectionStats) (map[string]map[string]gosnmp.SnmpPDU, []error) {
224
walkedData := make(map[string]map[string]gosnmp.SnmpPDU)
225
var errs []error
226
227
for tableOID := range tablesToWalk {
224
- pdus, err := tc.snmpWalk(tableOID)
228
+ pdus, err := tc.snmpWalk(tableOID, stats)
229
if err != nil {
230
+ stats.Errors.SNMP++
231
errs = append(errs, fmt.Errorf("failed to walk table OID '%s': %w", tableOID, err))
232
continue
233
}
234
235
if len(pdus) > 0 {
236
+ stats.SNMP.TablesWalked++
237
walkedData[tableOID] = pdus
238
}
239
}
@@ -263,7 +269,7 @@ func (tc *tableCollector) buildWalkResults(walkedData map[string]map[string]gosn
269
}
270
271
// processWalkResults processes all table walk results
266
-func (tc *tableCollector) processWalkResults(walkResults []tableWalkResult) ([]ddsnmp.Metric, error) {
272
+func (tc *tableCollector) processWalkResults(walkResults []tableWalkResult, stats *ddsnmp.CollectionStats) ([]ddsnmp.Metric, error) {
273
// Build lookup maps
274
walkedData := tc.buildWalkedDataMap(walkResults)
275
tableNameToOID := tc.buildTableNameMap(walkResults)
@@ -271,14 +277,19 @@ func (tc *tableCollector) processWalkResults(walkResults []tableWalkResult) ([]d
277
var metrics []ddsnmp.Metric
278
var errs []error
279
280
+ tablesSeen := make(map[string]bool)
281
+
282
for _, result := range walkResults {
275
- tableMetrics, err := tc.processTableResult(result, walkedData, tableNameToOID)
283
+ tableMetrics, err := tc.processTableResult(result, walkedData, tableNameToOID, stats)
284
if err != nil {
285
+ stats.Errors.Processing.Table++
286
errs = append(errs, fmt.Errorf("table '%s': %w", result.config.Table.Name, err))
287
continue
288
}
289
metrics = append(metrics, tableMetrics...)
290
+ tablesSeen[result.tableOID] = true
291
}
292
+ stats.Metrics.Tables = int64(len(tablesSeen))
293
294
if len(metrics) == 0 && len(errs) > 0 {
295
return nil, errors.Join(errs...)
@@ -310,9 +321,10 @@ func (tc *tableCollector) buildTableNameMap(walkResults []tableWalkResult) map[s
321
}
322
323
// processTableResult processes a single table result
313
-func (tc *tableCollector) processTableResult(result tableWalkResult, walkedData map[string]map[string]gosnmp.SnmpPDU, tableNameToOID map[string]string) ([]ddsnmp.Metric, error) {
324
+func (tc *tableCollector) processTableResult(result tableWalkResult, walkedData map[string]map[string]gosnmp.SnmpPDU, tableNameToOID map[string]string, stats *ddsnmp.CollectionStats) ([]ddsnmp.Metric, error) {
325
// Try cache first
315
- if metrics := tc.tryCollectFromCache(result.config); metrics != nil {
326
+ if metrics := tc.tryCollectFromCache(result.config, stats); metrics != nil {
327
+ stats.SNMP.TablesCached++
328
return metrics, nil
329
}
330
@@ -324,14 +336,16 @@ func (tc *tableCollector) processTableResult(result tableWalkResult, walkedData
336
walkedData: walkedData,
337
tableNameToOID: tableNameToOID,
338
}
327
- return tc.processTableData(ctx)
339
+ metrics, err := tc.processTableData(ctx, stats)
340
+ stats.Metrics.Rows += int64(len(ctx.rows))
341
+ return metrics, err
342
}
343
344
return nil, nil
345
}
346
347
// tryCollectFromCache attempts to collect metrics using cached data
334
-func (tc *tableCollector) tryCollectFromCache(cfg ddprofiledefinition.MetricsConfig) []ddsnmp.Metric {
348
+func (tc *tableCollector) tryCollectFromCache(cfg ddprofiledefinition.MetricsConfig, stats *ddsnmp.CollectionStats) []ddsnmp.Metric {
349
cachedOIDs, cachedTags, ok := tc.tableCache.getCachedData(cfg)
350
if !ok {
351
return nil
@@ -347,18 +361,19 @@ func (tc *tableCollector) tryCollectFromCache(cfg ddprofiledefinition.MetricsCon
361
tableName: cfg.Table.Name,
362
}
363
350
- metrics, err := tc.collectWithCache(ctx)
351
- if err == nil {
352
- tc.log.Debugf("Successfully collected table %s using cache", cfg.Table.Name)
353
- return metrics
364
+ metrics, err := tc.collectWithCache(ctx, stats)
365
+ if err != nil {
366
+ tc.log.Debugf("Cached collection failed for table %s: %v", cfg.Table.Name, err)
367
+ return nil
368
}
369
356
- tc.log.Debugf("Cached collection failed for table %s: %v", cfg.Table.Name, err)
357
- return nil
370
+ stats.Metrics.Rows += int64(len(cachedOIDs))
371
+ tc.log.Debugf("Successfully collected table %s using cache", cfg.Table.Name)
372
+ return metrics
373
}
374
375
// processTableData processes walked table data
361
-func (tc *tableCollector) processTableData(ctx *tableProcessingContext) ([]ddsnmp.Metric, error) {
376
+func (tc *tableCollector) processTableData(ctx *tableProcessingContext, stats *ddsnmp.CollectionStats) ([]ddsnmp.Metric, error) {
377
ctx.columnOIDs = buildColumnOIDs(ctx.config)
378
379
ctx.orderedTags = buildOrderedTags(ctx.config)
@@ -367,7 +382,7 @@ func (tc *tableCollector) processTableData(ctx *tableProcessingContext) ([]ddsnm
382
383
ctx.staticTags = parseStaticTags(ctx.config.StaticTags)
384
370
- metrics, err := tc.processRows(ctx)
385
+ metrics, err := tc.processRows(ctx, stats)
386
387
// Cache the processed data
388
deps := extractTableDependencies(ctx.config, ctx.tableNameToOID)
@@ -417,7 +432,7 @@ func (tc *tableCollector) organizePDUsByRow(ctx *tableProcessingContext) (rows m
432
}
433
434
// processRows processes all rows and returns metrics
420
-func (tc *tableCollector) processRows(ctx *tableProcessingContext) ([]ddsnmp.Metric, error) {
435
+func (tc *tableCollector) processRows(ctx *tableProcessingContext, stats *ddsnmp.CollectionStats) ([]ddsnmp.Metric, error) {
436
var metrics []ddsnmp.Metric
437
var errs []error
438
@@ -444,6 +459,7 @@ func (tc *tableCollector) processRows(ctx *tableProcessingContext) ([]ddsnmp.Met
459
}
460
rowMetrics, err := tc.rowProcessor.processRow(row, rowCtx)
461
if err != nil {
462
+ stats.Errors.Processing.Table++
463
errs = append(errs, err)
464
continue
465
}
@@ -464,7 +480,7 @@ func (tc *tableCollector) processRows(ctx *tableProcessingContext) ([]ddsnmp.Met
480
}
481
482
// collectWithCache collects metrics using cached structure
467
-func (tc *tableCollector) collectWithCache(ctx *cacheProcessingContext) ([]ddsnmp.Metric, error) {
483
+func (tc *tableCollector) collectWithCache(ctx *cacheProcessingContext, stats *ddsnmp.CollectionStats) ([]ddsnmp.Metric, error) {
484
// Build list of OIDs to GET
485
var oidsToGet []string
486
for _, columns := range ctx.cachedOIDs {
@@ -480,7 +496,7 @@ func (tc *tableCollector) collectWithCache(ctx *cacheProcessingContext) ([]ddsnm
496
}
497
498
// GET current values
483
- pdus, err := tc.snmpGet(oidsToGet)
499
+ pdus, err := tc.snmpGet(oidsToGet, stats)
500
if err != nil {
501
return nil, fmt.Errorf("failed to get cached OIDs: %w", err)
502
}
@@ -492,11 +508,11 @@ func (tc *tableCollector) collectWithCache(ctx *cacheProcessingContext) ([]ddsnm
508
509
// Add PDUs to context and build metrics
510
ctx.pdus = pdus
495
- return tc.buildMetricsFromCache(ctx)
511
+ return tc.buildMetricsFromCache(ctx, stats)
512
}
513
514
// buildMetricsFromCache builds metrics from cached structure and current values
499
-func (tc *tableCollector) buildMetricsFromCache(ctx *cacheProcessingContext) ([]ddsnmp.Metric, error) {
515
+func (tc *tableCollector) buildMetricsFromCache(ctx *cacheProcessingContext, stats *ddsnmp.CollectionStats) ([]ddsnmp.Metric, error) {
516
staticTags := parseStaticTags(ctx.config.StaticTags)
517
var metrics []ddsnmp.Metric
518
var errs []error
@@ -525,12 +541,14 @@ func (tc *tableCollector) buildMetricsFromCache(ctx *cacheProcessingContext) ([]
541
542
value, err := tc.valProc.processValue(sym, pdu)
543
if err != nil {
544
+ stats.Errors.Processing.Table++
545
tc.log.Debugf("Error processing value for %s: %v", sym.Name, err)
546
continue
547
}
548
549
metric, err := buildTableMetric(sym, pdu, value, rowTags, staticTags, ctx.tableName)
550
if err != nil {
551
+ stats.Errors.Processing.Table++
552
errs = append(errs, err)
553
continue
554
}
@@ -548,12 +566,14 @@ func (tc *tableCollector) buildMetricsFromCache(ctx *cacheProcessingContext) ([]
566
567
// SNMP operations
568
551
-func (tc *tableCollector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error) {
569
+func (tc *tableCollector) snmpWalk(oid string, stats *ddsnmp.CollectionStats) (map[string]gosnmp.SnmpPDU, error) {
570
pdus := make(map[string]gosnmp.SnmpPDU)
571
572
var resp []gosnmp.SnmpPDU
573
var err error
574
575
+ stats.SNMP.WalkRequests++
576
+
577
if tc.snmpClient.Version() == gosnmp.Version1 || tc.disableBulkWalk {
578
resp, err = tc.snmpClient.WalkAll(oid)
579
} else {
@@ -563,9 +583,13 @@ func (tc *tableCollector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error
583
return nil, err
584
}
585
586
+ stats.SNMP.WalkPDUs += int64(len(resp))
587
+
588
for _, pdu := range resp {
589
if isPduWithData(pdu) {
590
pdus[trimOID(pdu.Name)] = pdu
591
+ } else {
592
+ stats.Errors.MissingOIDs++
593
}
594
}
595
@@ -576,17 +600,22 @@ func (tc *tableCollector) snmpWalk(oid string) (map[string]gosnmp.SnmpPDU, error
600
return pdus, nil
601
}
602
579
-func (tc *tableCollector) snmpGet(oids []string) (map[string]gosnmp.SnmpPDU, error) {
603
+func (tc *tableCollector) snmpGet(oids []string, stats *ddsnmp.CollectionStats) (map[string]gosnmp.SnmpPDU, error) {
604
pdus := make(map[string]gosnmp.SnmpPDU)
605
606
for chunk := range slices.Chunk(oids, tc.snmpClient.MaxOids()) {
607
+ stats.SNMP.GetRequests++
608
+ stats.SNMP.GetOIDs += int64(len(chunk))
609
+
610
result, err := tc.snmpClient.Get(chunk)
611
if err != nil {
612
+ stats.Errors.SNMP++
613
return nil, err
614
}
615
616
for _, pdu := range result.Variables {
617
if !isPduWithData(pdu) {
618
+ stats.Errors.MissingOIDs++
619
tc.missingOIDs[trimOID(pdu.Name)] = true
620
continue
621
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table_test.go
+4
-3
@@ -3820,10 +3820,11 @@ func TestTableCollector_Collect(t *testing.T) {
3820
}
3821
3822
missingOIDs := make(map[string]bool)
3823
- tableCache := newTableCache(0, 0) // Cache disabled
3824
- collector := newTableCollector(mockHandler, missingOIDs, tableCache, logger.New(), false)
3823
+ tcache := newTableCache(0, 0) // Cache disabled
3824
+ collector := newTableCollector(mockHandler, missingOIDs, tcache, logger.New(), false)
3825
3826
- result, err := collector.Collect(tc.profile)
3826
+ var stats ddsnmp.CollectionStats
3827
+ result, err := collector.collect(tc.profile, &stats)
3828
3829
if tc.expectedError {
3830
assert.Error(t, err)
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
new
+151
@@ -0,0 +1,151 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "testing"
7
+
8
+ "github.com/gosnmp/gosnmp"
9
+ "github.com/stretchr/testify/assert"
10
+ "github.com/stretchr/testify/require"
11
+
12
+ "github.com/netdata/netdata/go/plugins/logger"
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/ddprofiledefinition"
15
+)
16
+
17
+func TestCollector_Collect_StatsSnapshot(t *testing.T) {
18
+ ctrl, mockHandler := setupMockHandler(t)
19
+ defer ctrl.Finish()
20
+
21
+ // --- SNMP expectations ---------------------------------------------------
22
+
23
+ // Scalar: sysUpTime.0
24
+ expectSNMPGet(mockHandler,
25
+ []string{"1.3.6.1.2.1.1.3.0"},
26
+ []gosnmp.SnmpPDU{
27
+ createTimeTicksPDU("1.3.6.1.2.1.1.3.0", 123456),
28
+ },
29
+ )
30
+
31
+ // Table: ifTable, we only care about ifInOctets with 2 rows
32
+ expectSNMPWalk(mockHandler,
33
+ gosnmp.Version2c,
34
+ "1.3.6.1.2.1.2.2",
35
+ []gosnmp.SnmpPDU{
36
+ // Row 1
37
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.1", 1000), // ifInOctets.1
38
+ // Row 2
39
+ createCounter32PDU("1.3.6.1.2.1.2.2.1.10.2", 2000), // ifInOctets.2
40
+ },
41
+ )
42
+
43
+ // --- Profile definition --------------------------------------------------
44
+
45
+ profile := &ddsnmp.Profile{
46
+ SourceFile: "stats-toy-profile.yaml",
47
+ Definition: &ddprofiledefinition.ProfileDefinition{
48
+ Metrics: []ddprofiledefinition.MetricsConfig{
49
+ // Simple scalar metric
50
+ {
51
+ Symbol: ddprofiledefinition.SymbolConfig{
52
+ OID: "1.3.6.1.2.1.1.3.0",
53
+ Name: "sysUpTime",
54
+ },
55
+ },
56
+ // Simple table metric: ifInOctets over ifTable
57
+ {
58
+ Table: ddprofiledefinition.SymbolConfig{
59
+ OID: "1.3.6.1.2.1.2.2",
60
+ Name: "ifTable",
61
+ },
62
+ Symbols: []ddprofiledefinition.SymbolConfig{
63
+ {
64
+ OID: "1.3.6.1.2.1.2.2.1.10",
65
+ Name: "ifInOctets",
66
+ },
67
+ },
68
+ },
69
+ },
70
+ // One virtual metric that sums ifInOctets across the table.
71
+ VirtualMetrics: []ddprofiledefinition.VirtualMetricConfig{
72
+ {
73
+ Name: "ifInOctets_total",
74
+ Sources: []ddprofiledefinition.VirtualMetricSourceConfig{
75
+ {
76
+ Metric: "ifInOctets",
77
+ Table: "ifTable",
78
+ },
79
+ },
80
+ },
81
+ },
82
+ },
83
+ }
84
+
85
+ handleCrossTableTagsWithoutMetrics(profile)
86
+ require.NoError(t, ddsnmp.CompileTransforms(profile))
87
+
88
+ collector := New(Config{
89
+ SnmpClient: mockHandler,
90
+ Profiles: []*ddsnmp.Profile{profile},
91
+ Log: logger.New(),
92
+ SysObjectID: "",
93
+ })
94
+
95
+ // --- Run collection ------------------------------------------------------
96
+
97
+ results, err := collector.Collect()
98
+ require.NoError(t, err)
99
+ require.Len(t, results, 1)
100
+
101
+ pm := results[0]
102
+
103
+ // --- Sanity check on actual metrics -------------------------------------
104
+
105
+ // We expect:
106
+ // - 1 scalar metric (sysUpTime)
107
+ // - 2 table metrics (ifInOctets for 2 rows)
108
+ // - 1 virtual metric (ifInOctets_total)
109
+ require.Len(t, pm.Metrics, 4, "total number of metrics")
110
+
111
+ // --- Assert CollectionStats as a snapshot -------------------------------
112
+
113
+ // Ignore timing (it's inherently variable).
114
+ stats := pm.Stats
115
+ stats.Timing = ddsnmp.TimingStats{}
116
+ pm.Stats = stats
117
+
118
+ expected := ddsnmp.CollectionStats{
119
+ SNMP: ddsnmp.SNMPOperationStats{
120
+ // Scalar: 1 GET with 1 OID
121
+ GetRequests: 1,
122
+ GetOIDs: 1,
123
+
124
+ // Table: 1 WALK with 2 PDUs, 1 table walked, no cached tables
125
+ WalkRequests: 1,
126
+ WalkPDUs: 2,
127
+ TablesWalked: 1,
128
+ // TablesCached should be 0 on first run
129
+ },
130
+ Metrics: ddsnmp.MetricCountStats{
131
+ Scalar: 1, // sysUpTime
132
+ Table: 2, // ifInOctets.1, ifInOctets.2
133
+ Virtual: 1, // ifInOctets_total
134
+ Tables: 1, // ifTable
135
+ Rows: 2, // 2 interfaces
136
+ },
137
+ TableCache: ddsnmp.TableCacheStats{
138
+ Hits: 0, // first run → no cache hits
139
+ Misses: 1, // one table config had to be walked
140
+ // Expired intentionally ignored / omitted
141
+ },
142
+ Errors: ddsnmp.ErrorStats{
143
+ SNMP: 0,
144
+ MissingOIDs: 0,
145
+ },
146
+ // Timing left as zero-value for comparison
147
+ Timing: ddsnmp.TimingStats{},
148
+ }
149
+
150
+ assert.Equal(t, expected, pm.Stats)
151
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_vmetrics.go
+2
-3
@@ -22,7 +22,7 @@ func newVirtualMetricsCollector(log *logger.Logger) *vmetricsCollector {
22
}
23
}
24
25
-func (p *vmetricsCollector) Collect(profDef *ddprofiledefinition.ProfileDefinition, collected []ddsnmp.Metric) []ddsnmp.Metric {
25
+func (p *vmetricsCollector) collect(profDef *ddprofiledefinition.ProfileDefinition, collected []ddsnmp.Metric) []ddsnmp.Metric {
26
if len(profDef.VirtualMetrics) == 0 {
27
return nil
28
}
@@ -418,8 +418,7 @@ type aggregatorsBuilder struct {
418
aggregators []*vmetricsAggregator
419
}
420
421
-func newAggregatorsBuilder(
422
- log *logger.Logger, prof *ddprofiledefinition.ProfileDefinition, existingNames map[string]bool) *aggregatorsBuilder {
421
+func newAggregatorsBuilder(log *logger.Logger, prof *ddprofiledefinition.ProfileDefinition, existingNames map[string]bool) *aggregatorsBuilder {
422
return &aggregatorsBuilder{
423
log: log,
424
prof: prof,
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_vmetrics_test.go
+1
-1
@@ -1517,7 +1517,7 @@ func TestVirtualMetricsCollector_Collect(t *testing.T) {
1517
for name, tc := range tests {
1518
t.Run(name, func(t *testing.T) {
1519
vmc := newVirtualMetricsCollector(logger.New())
1520
- result := vmc.Collect(tc.profileDef, tc.collectedMetrics)
1520
+ result := vmc.collect(tc.profileDef, tc.collectedMetrics)
1521
1522
// Sort both slices for consistent comparison
1523
assert.ElementsMatch(t, tc.expected, result)
src/go/plugin/go.d/collector/snmp/ddsnmp/metric.go
+77
@@ -1,6 +1,8 @@
1
package ddsnmp
2
3
import (
4
+ "time"
5
+
6
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
7
)
8
@@ -9,6 +11,7 @@ type ProfileMetrics struct {
11
DeviceMetadata map[string]MetaTag
12
Tags map[string]string
13
Metrics []Metric
14
+ Stats CollectionStats
15
}
16
17
type Metric struct {
@@ -32,3 +35,77 @@ type MetaTag struct {
35
Value string
36
IsExactMatch bool // whether this value is from an exact match context
37
}
38
+
39
+// CollectionStats contains statistics for a single profile collection cycle.
40
+type CollectionStats struct {
41
+ Timing TimingStats
42
+ SNMP SNMPOperationStats
43
+ Metrics MetricCountStats
44
+ TableCache TableCacheStats
45
+ Errors ErrorStats
46
+}
47
+
48
+// TimingStats captures duration of each collection phase.
49
+type TimingStats struct {
50
+ // Scalar is time spent collecting scalar (non-table) metrics.
51
+ Scalar time.Duration
52
+ // Table is time spent collecting table metrics.
53
+ Table time.Duration
54
+ // VirtualMetrics is time spent computing derived/aggregated metrics.
55
+ VirtualMetrics time.Duration
56
+}
57
+
58
+func (s TimingStats) Total() time.Duration {
59
+ return s.Scalar + s.Table + s.VirtualMetrics
60
+}
61
+
62
+// SNMPOperationStats captures SNMP protocol-level operations.
63
+type SNMPOperationStats struct {
64
+ // GetRequests is the number of SNMP GET operations performed.
65
+ GetRequests int64
66
+ // GetOIDs is the total number of OIDs requested across all GETs.
67
+ GetOIDs int64
68
+ // WalkRequests is the number of SNMP Walk/BulkWalk operations.
69
+ WalkRequests int64
70
+ // WalkPDUs is the total number of PDUs returned from all walks.
71
+ WalkPDUs int64
72
+ // TablesWalked is the count of tables that required walking.
73
+ TablesWalked int64
74
+ // TablesCached is the count of tables served from cache.
75
+ TablesCached int64
76
+}
77
+
78
+// MetricCountStats captures the number of metrics produced.
79
+type MetricCountStats struct {
80
+ // Scalar is the count of scalar (non-table) metrics.
81
+ Scalar int64
82
+ // Table is the count of table metrics.
83
+ Table int64
84
+ // Virtual is the count of computed/derived metrics.
85
+ Virtual int64
86
+ // Tables is the count of unique tables with metrics.
87
+ Tables int64
88
+ // Rows is the total number of table rows across all tables.
89
+ Rows int64
90
+}
91
+
92
+// TableCacheStats captures table cache performance.
93
+type TableCacheStats struct {
94
+ // Hits is the number of table configs served from cache.
95
+ Hits int64
96
+ // Misses is the number of table configs that required walking.
97
+ Misses int64
98
+}
99
+
100
+// ErrorStats captures categorized error counts.
101
+type ErrorStats struct {
102
+ // SNMP is the count of SNMP-level errors (timeouts, network issues).
103
+ SNMP int64
104
+ // Processing is the count of value conversion/transform errors.
105
+ Processing struct {
106
+ Scalar int64
107
+ Table int64
108
+ }
109
+ // MissingOIDs is the count of NoSuchObject/NoSuchName responses.
110
+ MissingOIDs int64
111
+}