improve(go.d/snmp): add Go template-based metric transformations for SNMP profiles (#20528)
Ilya Mashchenko committed
Jun 20, 2025 at 09:04 UTC
c61dce679027ecd590d10154f421552fafd2a823
12 files changed
+938
-186
src/go/plugin/go.d/collector/snmp/charts.go
+7
-4
@@ -8,8 +8,8 @@ import (
8
"strings"
9
10
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
12
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
12
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
13
)
14
15
const (
@@ -310,7 +310,7 @@ func newUserInputChart(cfg ChartConfig) (*module.Chart, error) {
310
return chart, nil
311
}
312
313
-func (c *Collector) addProfileScalarMetricChart(m ddsnmpcollector.Metric) {
313
+func (c *Collector) addProfileScalarMetricChart(m ddsnmp.Metric) {
314
if m.Name == "" {
315
return
316
}
@@ -333,6 +333,9 @@ func (c *Collector) addProfileScalarMetricChart(m ddsnmpcollector.Metric) {
333
if chart.Fam == "" {
334
chart.Fam = m.Name
335
}
336
+ if chart.Units == "bits/s" {
337
+ chart.Type = module.Area
338
+ }
339
340
tags := map[string]string{
341
"vendor": c.sysInfo.Organization,
@@ -361,7 +364,7 @@ func (c *Collector) addProfileScalarMetricChart(m ddsnmpcollector.Metric) {
364
}
365
}
366
364
-func (c *Collector) addProfileTableMetricChart(m ddsnmpcollector.Metric) {
367
+func (c *Collector) addProfileTableMetricChart(m ddsnmp.Metric) {
368
if m.Name == "" {
369
return
370
}
@@ -415,7 +418,7 @@ func (c *Collector) addProfileTableMetricChart(m ddsnmpcollector.Metric) {
418
}
419
}
420
418
-func dimAlgoFromDdSnmpType(m ddsnmpcollector.Metric) module.DimAlgo {
421
+func dimAlgoFromDdSnmpType(m ddsnmp.Metric) module.DimAlgo {
422
if m.MetricType == ddprofiledefinition.ProfileMetricTypeGauge {
423
return module.Absolute
424
}
src/go/plugin/go.d/collector/snmp/collect_profiles.go
+4
-3
@@ -7,6 +7,7 @@ import (
7
"sort"
8
"strings"
9
10
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
11
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
12
"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/metrix"
13
)
@@ -31,7 +32,7 @@ func (c *Collector) collectProfiles(mx map[string]int64) error {
32
return nil
33
}
34
34
-func (c *Collector) collectProfileScalarMetrics(mx map[string]int64, pms []*ddsnmpcollector.ProfileMetrics) {
35
+func (c *Collector) collectProfileScalarMetrics(mx map[string]int64, pms []*ddsnmp.ProfileMetrics) {
36
for _, pm := range pms {
37
for _, m := range pm.Metrics {
38
if m.IsTable || m.Name == "" {
@@ -56,7 +57,7 @@ func (c *Collector) collectProfileScalarMetrics(mx map[string]int64, pms []*ddsn
57
}
58
}
59
59
-func (c *Collector) collectProfileTableMetrics(mx map[string]int64, pms []*ddsnmpcollector.ProfileMetrics) {
60
+func (c *Collector) collectProfileTableMetrics(mx map[string]int64, pms []*ddsnmp.ProfileMetrics) {
61
seen := make(map[string]bool)
62
63
for _, pm := range pms {
@@ -92,7 +93,7 @@ func (c *Collector) collectProfileTableMetrics(mx map[string]int64, pms []*ddsnm
93
}
94
}
95
95
-func tableMetricKey(m ddsnmpcollector.Metric) string {
96
+func tableMetricKey(m ddsnmp.Metric) string {
97
keys := make([]string, 0, len(m.Tags))
98
for k := range m.Tags {
99
keys = append(keys, k)
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/metrics.go
+10
-5
@@ -9,6 +9,7 @@ import (
9
"maps"
10
"regexp"
11
"slices"
12
+ "text/template"
13
)
14
15
// ProfileMetricType metric type used to override default type of the metric
@@ -78,11 +79,15 @@ type SymbolConfig struct {
79
// When empty, by default, the metric type is derived from SNMP OID value type.
80
// Valid `metric_type` types: `gauge`, `rate`, `monotonic_count`, `monotonic_count_and_rate`
81
// Deprecated types: `counter` (use `rate` instead), percent (use `scale_factor` instead)
81
- MetricType ProfileMetricType `yaml:"metric_type,omitempty" json:"metric_type,omitempty"`
82
- Unit string `yaml:"unit,omitempty" json:"unit,omitempty"`
83
- Description string `yaml:"description,omitempty" json:"description,omitempty"`
84
- Family string `yaml:"family,omitempty" json:"family,omitempty"`
85
- Mapping map[string]string `yaml:"mapping,omitempty" json:"mapping,omitempty"`
82
+ MetricType ProfileMetricType `yaml:"metric_type,omitempty" json:"metric_type,omitempty"`
83
+
84
+ Unit string `yaml:"unit,omitempty" json:"unit,omitempty"`
85
+ Description string `yaml:"description,omitempty" json:"description,omitempty"`
86
+ Family string `yaml:"family,omitempty" json:"family,omitempty"`
87
+
88
+ Mapping map[string]string `yaml:"mapping,omitempty" json:"mapping,omitempty"`
89
+ Transform string `yaml:"transform,omitempty" json:"transform,omitempty"`
90
+ TransformCompiled *template.Template `yaml:"-" json:"-"`
91
}
92
93
// Clone creates a duplicate of this SymbolConfig
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_scalar.go
+13
-5
@@ -15,7 +15,7 @@ import (
15
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
16
)
17
18
-func (c *Collector) collectScalarMetrics(prof *ddsnmp.Profile) ([]Metric, error) {
18
+func (c *Collector) collectScalarMetrics(prof *ddsnmp.Profile) ([]ddsnmp.Metric, error) {
19
var oids []string
20
var missingOIDs []string
21
@@ -46,7 +46,7 @@ func (c *Collector) collectScalarMetrics(prof *ddsnmp.Profile) ([]Metric, error)
46
return nil, err
47
}
48
49
- metrics := make([]Metric, 0, len(prof.Definition.Metrics))
49
+ metrics := make([]ddsnmp.Metric, 0, len(prof.Definition.Metrics))
50
var errs []error
51
52
for _, cfg := range prof.Definition.Metrics {
@@ -73,7 +73,7 @@ func (c *Collector) collectScalarMetrics(prof *ddsnmp.Profile) ([]Metric, error)
73
return metrics, nil
74
}
75
76
-func (c *Collector) collectScalarMetric(cfg ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU) (*Metric, error) {
76
+func (c *Collector) collectScalarMetric(cfg ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU) (*ddsnmp.Metric, error) {
77
pdu, ok := pdus[trimOID(cfg.Symbol.OID)]
78
if !ok {
79
return nil, nil
@@ -92,7 +92,7 @@ func (c *Collector) collectScalarMetric(cfg ddprofiledefinition.MetricsConfig, p
92
}
93
}
94
95
- return &Metric{
95
+ m := &ddsnmp.Metric{
96
Name: cfg.Symbol.Name,
97
Value: value,
98
StaticTags: ternary(len(staticTags) > 0, staticTags, nil),
@@ -101,7 +101,15 @@ func (c *Collector) collectScalarMetric(cfg ddprofiledefinition.MetricsConfig, p
101
Family: cfg.Symbol.Family,
102
Mappings: convSymMappingToNumeric(cfg.Symbol),
103
MetricType: getMetricType(cfg.Symbol, pdu),
104
- }, nil
104
+ }
105
+
106
+ if cfg.Symbol.TransformCompiled != nil {
107
+ if err := applyTransform(m, cfg.Symbol); err != nil {
108
+ return nil, fmt.Errorf("failed to apply transform for scalar metric '%s': %w", cfg.Symbol.Name, err)
109
+ }
110
+ }
111
+
112
+ return m, nil
113
}
114
115
func processSymbolValue(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) (int64, error) {
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collect_table.go
+39
-9
@@ -21,7 +21,7 @@ type tableWalkResult struct {
21
config ddprofiledefinition.MetricsConfig
22
}
23
24
-func (c *Collector) collectTableMetrics(prof *ddsnmp.Profile) ([]Metric, error) {
24
+func (c *Collector) collectTableMetrics(prof *ddsnmp.Profile) ([]ddsnmp.Metric, error) {
25
walkResults, err := c.walkTablesAsNeeded(prof)
26
if err != nil {
27
return nil, err
@@ -118,8 +118,8 @@ func (c *Collector) walkTablesAsNeeded(prof *ddsnmp.Profile) ([]tableWalkResult,
118
}
119
120
// Phase 2: Process walked data
121
-func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]Metric, error) {
122
- var metrics []Metric
121
+func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]ddsnmp.Metric, error) {
122
+ var metrics []ddsnmp.Metric
123
var errs []error
124
125
// Build a map for quick lookup of walked data by table OID
@@ -171,7 +171,11 @@ func (c *Collector) processTableWalkResults(walkResults []tableWalkResult) ([]Me
171
}
172
173
// Process a single table's data
174
-func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus map[string]gosnmp.SnmpPDU, allWalkedData map[string]map[string]gosnmp.SnmpPDU, tableNameToOID map[string]string) ([]Metric, error) {
174
+func (c *Collector) processTableData(
175
+ cfg ddprofiledefinition.MetricsConfig,
176
+ pdus map[string]gosnmp.SnmpPDU,
177
+ allWalkedData map[string]map[string]gosnmp.SnmpPDU,
178
+ tableNameToOID map[string]string) ([]ddsnmp.Metric, error) {
179
columnOIDs := buildColumnOIDs(cfg)
180
tagColumnOIDs := buildTagColumnOIDs(cfg)
181
@@ -212,7 +216,8 @@ func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus
216
}
217
}
218
215
- var metrics []Metric
219
+ var metrics []ddsnmp.Metric
220
+ var errs []error
221
222
for index, rowPDUs := range rows {
223
rowTags := make(map[string]string)
@@ -328,7 +333,7 @@ func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus
333
continue
334
}
335
331
- metric := Metric{
336
+ metric := ddsnmp.Metric{
337
Name: sym.Name,
338
Value: value,
339
StaticTags: ternary(len(rowStaticTags) > 0, rowStaticTags, nil),
@@ -341,10 +346,22 @@ func (c *Collector) processTableData(cfg ddprofiledefinition.MetricsConfig, pdus
346
IsTable: true,
347
}
348
349
+ if sym.TransformCompiled != nil {
350
+ if err := applyTransform(&metric, sym); err != nil {
351
+ errs = append(errs, fmt.Errorf("failed to apply transform for table metric '%s:%s': %w",
352
+ cfg.Table.Name, sym.Name, err))
353
+ continue
354
+ }
355
+ }
356
+
357
metrics = append(metrics, metric)
358
}
359
}
360
361
+ if len(errs) > 0 {
362
+ c.log.Warningf("failed to collect table metrics: %v", errors.Join(errs...))
363
+ }
364
+
365
deps := extractTableDependencies(cfg, tableNameToOID)
366
367
// Cache the processed data
@@ -378,7 +395,7 @@ func (c *Collector) collectTableWithCache(
395
cachedOIDs map[string]map[string]string,
396
cachedTags map[string]map[string]string,
397
columnOIDs map[string]ddprofiledefinition.SymbolConfig,
381
-) ([]Metric, error) {
398
+) ([]ddsnmp.Metric, error) {
399
var oidsToGet []string
400
401
for _, columns := range cachedOIDs {
@@ -411,7 +428,8 @@ func (c *Collector) collectTableWithCache(
428
}
429
}
430
414
- var metrics []Metric
431
+ var metrics []ddsnmp.Metric
432
+ var errs []error
433
434
for index, columns := range cachedOIDs {
435
rowTags := make(map[string]string)
@@ -440,7 +458,7 @@ func (c *Collector) collectTableWithCache(
458
continue
459
}
460
443
- metric := Metric{
461
+ metric := ddsnmp.Metric{
462
Name: sym.Name,
463
Value: value,
464
StaticTags: ternary(len(rowStaticTags) > 0, rowStaticTags, nil),
@@ -453,10 +471,22 @@ func (c *Collector) collectTableWithCache(
471
IsTable: true,
472
}
473
474
+ if sym.TransformCompiled != nil {
475
+ if err := applyTransform(&metric, sym); err != nil {
476
+ errs = append(errs, fmt.Errorf("failed to apply transform for metric '%s:%s': %w",
477
+ cfg.Table.Name, sym.Name, err))
478
+ continue
479
+ }
480
+ }
481
+
482
metrics = append(metrics, metric)
483
}
484
}
485
486
+ if len(errs) > 0 {
487
+ c.log.Warningf("failed to collect table metrics: %v", errors.Join(errs...))
488
+ }
489
+
490
return metrics, nil
491
}
492
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+6
-37
@@ -15,29 +15,6 @@ import (
15
16
"github.com/netdata/netdata/go/plugins/logger"
17
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
18
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
19
-)
20
-
21
-type (
22
- ProfileMetrics struct {
23
- Source string
24
- DeviceMetadata map[string]string
25
- Tags map[string]string
26
- Metrics []Metric
27
- }
28
- Metric struct {
29
- Profile *ProfileMetrics
30
- Name string
31
- Description string
32
- Family string
33
- Unit string
34
- MetricType ddprofiledefinition.ProfileMetricType
35
- StaticTags map[string]string
36
- Tags map[string]string
37
- Mappings map[int64]string
38
- IsTable bool
39
- Value int64
40
- }
18
)
19
20
func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logger) *Collector {
@@ -75,8 +52,8 @@ type (
52
}
53
)
54
78
-func (c *Collector) Collect() ([]*ProfileMetrics, error) {
79
- var metrics []*ProfileMetrics
55
+func (c *Collector) Collect() ([]*ddsnmp.ProfileMetrics, error) {
56
+ var metrics []*ddsnmp.ProfileMetrics
57
var errs []error
58
59
if expired := c.tableCache.clearExpired(); len(expired) > 0 {
@@ -103,7 +80,7 @@ func (c *Collector) Collect() ([]*ProfileMetrics, error) {
80
return metrics, nil
81
}
82
106
-func (c *Collector) collectProfile(ps *profileState) (*ProfileMetrics, error) {
83
+func (c *Collector) collectProfile(ps *profileState) (*ddsnmp.ProfileMetrics, error) {
84
if !ps.initialized {
85
globalTag, err := c.collectGlobalTags(ps.profile)
86
if err != nil {
@@ -120,7 +97,7 @@ func (c *Collector) collectProfile(ps *profileState) (*ProfileMetrics, error) {
97
ps.initialized = true
98
}
99
123
- var metrics []Metric
100
+ var metrics []ddsnmp.Metric
101
102
scalarMetrics, err := c.collectScalarMetrics(ps.profile)
103
if err != nil {
@@ -136,7 +113,7 @@ func (c *Collector) collectProfile(ps *profileState) (*ProfileMetrics, error) {
113
metrics = append(metrics, tableMetrics...)
114
}
115
139
- pm := &ProfileMetrics{
116
+ pm := &ddsnmp.ProfileMetrics{
117
Source: ps.profile.SourceFile,
118
DeviceMetadata: maps.Clone(ps.deviceMetadata),
119
Tags: maps.Clone(ps.globalTags),
@@ -149,7 +126,7 @@ func (c *Collector) collectProfile(ps *profileState) (*ProfileMetrics, error) {
126
return pm, nil
127
}
128
152
-func (c *Collector) updateMetrics(pms []*ProfileMetrics) {
129
+func (c *Collector) updateMetrics(pms []*ddsnmp.ProfileMetrics) {
130
// Find device vendor and type from any profile that has them.
131
// Multiple profiles can be loaded for a single device (e.g., base profiles, generic MIB profiles),
132
// but only device-specific profiles contain vendor/type information.
@@ -185,14 +162,6 @@ func (c *Collector) updateMetrics(pms []*ProfileMetrics) {
162
for k, v := range m.Tags {
163
m.Tags[k] = metricMetaReplacer.Replace(v)
164
}
188
- if v, ok := m.Tags["_unit"]; ok && v != "" {
189
- delete(m.Tags, "_unit")
190
- m.Unit = metricMetaReplacer.Replace(v)
191
- }
192
- if v, ok := m.Tags["_metric_suffix"]; ok && v != "" {
193
- delete(m.Tags, "_metric_suffix")
194
- m.Name += "_" + metricMetaReplacer.Replace(v)
195
- }
165
}
166
}
167
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_test.go
+654
-100
@@ -5,6 +5,7 @@ package ddsnmpcollector
5
import (
6
"errors"
7
"regexp"
8
+ "strings"
9
"testing"
10
"time"
11
@@ -24,7 +25,7 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
25
tests := map[string]struct {
26
profiles []*ddsnmp.Profile
27
setupMock func(m *snmpmock.MockHandler)
27
- expectedResult []*ProfileMetrics
28
+ expectedResult []*ddsnmp.ProfileMetrics
29
expectedError bool
30
errorContains string
31
enableCache bool // Whether to enable cache for this test
@@ -70,11 +71,11 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
71
}, nil,
72
)
73
},
73
- expectedResult: []*ProfileMetrics{
74
+ expectedResult: []*ddsnmp.ProfileMetrics{
75
{
76
Source: "test-profile.yaml",
77
DeviceMetadata: nil,
77
- Metrics: []Metric{
78
+ Metrics: []ddsnmp.Metric{
79
{
80
Name: "sysUpTime",
81
Value: 123456,
@@ -138,12 +139,12 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
139
}, nil,
140
)
141
},
141
- expectedResult: []*ProfileMetrics{
142
+ expectedResult: []*ddsnmp.ProfileMetrics{
143
{
144
Source: "test-profile.yaml",
145
DeviceMetadata: nil,
146
Tags: map[string]string{"device_vendor": "Cisco IOS"},
146
- Metrics: []Metric{
147
+ Metrics: []ddsnmp.Metric{
148
{
149
Name: "sysUpTime",
150
Value: 123456,
@@ -212,14 +213,14 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
213
}, nil,
214
)
215
},
215
- expectedResult: []*ProfileMetrics{
216
+ expectedResult: []*ddsnmp.ProfileMetrics{
217
{
218
Source: "test-profile.yaml",
219
DeviceMetadata: map[string]string{
220
"vendor": "dell",
221
"serial_number": "ABC123",
222
},
222
- Metrics: []Metric{
223
+ Metrics: []ddsnmp.Metric{
224
{
225
Name: "sysUpTime",
226
Value: 123456,
@@ -261,11 +262,11 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
262
}, nil,
263
)
264
},
264
- expectedResult: []*ProfileMetrics{
265
+ expectedResult: []*ddsnmp.ProfileMetrics{
266
{
267
Source: "test-profile.yaml",
268
DeviceMetadata: nil,
268
- Metrics: []Metric{
269
+ Metrics: []ddsnmp.Metric{
270
{
271
Name: "memoryKilobytes",
272
Value: 1024000, // 1024 * 1000
@@ -307,11 +308,11 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
308
}, nil,
309
)
310
},
310
- expectedResult: []*ProfileMetrics{
311
+ expectedResult: []*ddsnmp.ProfileMetrics{
312
{
313
Source: "test-profile.yaml",
314
DeviceMetadata: nil,
314
- Metrics: []Metric{
315
+ Metrics: []ddsnmp.Metric{
316
{
317
Name: "temperature",
318
Value: 25,
@@ -378,12 +379,12 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
379
}, nil,
380
)
381
},
381
- expectedResult: []*ProfileMetrics{
382
+ expectedResult: []*ddsnmp.ProfileMetrics{
383
{
384
Source: "test-profile.yaml",
385
DeviceMetadata: nil,
386
Tags: map[string]string{"device_type": "router"},
386
- Metrics: []Metric{
387
+ Metrics: []ddsnmp.Metric{
388
{
389
Name: "sysUpTime",
390
Value: 123456,
@@ -424,10 +425,10 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
425
}, nil,
426
)
427
},
427
- expectedResult: []*ProfileMetrics{
428
+ expectedResult: []*ddsnmp.ProfileMetrics{
429
{
430
DeviceMetadata: nil,
430
- Metrics: []Metric{},
431
+ Metrics: []ddsnmp.Metric{},
432
},
433
},
434
expectedError: false,
@@ -471,10 +472,10 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
472
setupMock: func(m *snmpmock.MockHandler) {
473
m.EXPECT().MaxOids().Return(10).AnyTimes()
474
},
474
- expectedResult: []*ProfileMetrics{
475
+ expectedResult: []*ddsnmp.ProfileMetrics{
476
{
477
DeviceMetadata: nil,
477
- Metrics: []Metric{},
478
+ Metrics: []ddsnmp.Metric{},
479
},
480
},
481
expectedError: false,
@@ -528,11 +529,11 @@ func TestCollector_Collect_ScalarMetrics(t *testing.T) {
529
errors.New("connection refused"),
530
)
531
},
531
- expectedResult: []*ProfileMetrics{
532
+ expectedResult: []*ddsnmp.ProfileMetrics{
533
{
534
Source: "profile1.yaml",
535
DeviceMetadata: nil,
535
- Metrics: []Metric{
536
+ Metrics: []ddsnmp.Metric{
537
{
538
Name: "sysUpTime",
539
Value: 123456,
@@ -599,7 +600,7 @@ func TestCollector_Collect_ValueMappings(t *testing.T) {
600
tests := map[string]struct {
601
profiles []*ddsnmp.Profile
602
setupMock func(m *snmpmock.MockHandler)
602
- expectedResult []*ProfileMetrics
603
+ expectedResult []*ddsnmp.ProfileMetrics
604
expectedError bool
605
errorContains string
606
}{
@@ -638,11 +639,11 @@ func TestCollector_Collect_ValueMappings(t *testing.T) {
639
}, nil,
640
)
641
},
641
- expectedResult: []*ProfileMetrics{
642
+ expectedResult: []*ddsnmp.ProfileMetrics{
643
{
644
Source: "test-profile.yaml",
645
DeviceMetadata: nil,
645
- Metrics: []Metric{
646
+ Metrics: []ddsnmp.Metric{
647
{
648
Name: "clusterHealth",
649
Value: 1,
@@ -695,11 +696,11 @@ func TestCollector_Collect_ValueMappings(t *testing.T) {
696
}, nil,
697
)
698
},
698
- expectedResult: []*ProfileMetrics{
699
+ expectedResult: []*ddsnmp.ProfileMetrics{
700
{
701
Source: "test-profile.yaml",
702
DeviceMetadata: nil,
702
- Metrics: []Metric{
703
+ Metrics: []ddsnmp.Metric{
704
{
705
Name: "ifOperStatus",
706
Value: 2,
@@ -753,11 +754,11 @@ func TestCollector_Collect_ValueMappings(t *testing.T) {
754
}, nil,
755
)
756
},
756
- expectedResult: []*ProfileMetrics{
757
+ expectedResult: []*ddsnmp.ProfileMetrics{
758
{
759
Source: "test-profile.yaml",
760
DeviceMetadata: nil,
760
- Metrics: []Metric{
761
+ Metrics: []ddsnmp.Metric{
762
{
763
Name: "fanStatus",
764
Value: 2,
@@ -808,11 +809,11 @@ func TestCollector_Collect_ValueMappings(t *testing.T) {
809
}, nil,
810
)
811
},
811
- expectedResult: []*ProfileMetrics{
812
+ expectedResult: []*ddsnmp.ProfileMetrics{
813
{
814
Source: "test-profile.yaml",
815
DeviceMetadata: nil,
815
- Metrics: []Metric{
816
+ Metrics: []ddsnmp.Metric{
817
{
818
Name: "ifAdminStatus",
819
Value: 0, // mapped from 2 -> 0
@@ -936,11 +937,11 @@ func TestCollector_Collect_ValueMappings(t *testing.T) {
937
}, nil,
938
)
939
},
939
- expectedResult: []*ProfileMetrics{
940
+ expectedResult: []*ddsnmp.ProfileMetrics{
941
{
942
Source: "test-profile.yaml",
943
DeviceMetadata: nil,
943
- Metrics: []Metric{
944
+ Metrics: []ddsnmp.Metric{
945
{
946
Name: "sysUpTime",
947
Value: 123456,
@@ -987,11 +988,11 @@ func TestCollector_Collect_ValueMappings(t *testing.T) {
988
}, nil,
989
)
990
},
990
- expectedResult: []*ProfileMetrics{
991
+ expectedResult: []*ddsnmp.ProfileMetrics{
992
{
993
Source: "test-profile.yaml",
994
DeviceMetadata: nil,
994
- Metrics: []Metric{
995
+ Metrics: []ddsnmp.Metric{
996
{
997
Name: "ifAdminStatus",
998
Value: 1,
@@ -1043,11 +1044,11 @@ func TestCollector_Collect_ValueMappings(t *testing.T) {
1044
}, nil,
1045
)
1046
},
1046
- expectedResult: []*ProfileMetrics{
1047
+ expectedResult: []*ddsnmp.ProfileMetrics{
1048
{
1049
Source: "test-profile.yaml",
1050
DeviceMetadata: nil,
1050
- Metrics: []Metric{
1051
+ Metrics: []ddsnmp.Metric{
1052
{
1053
Name: "upsBasicBatteryStatus",
1054
Value: 1,
@@ -1114,7 +1115,7 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1115
tests := map[string]struct {
1116
profiles []*ddsnmp.Profile
1117
setupMock func(m *snmpmock.MockHandler)
1117
- expectedResult []*ProfileMetrics
1118
+ expectedResult []*ddsnmp.ProfileMetrics
1119
expectedError bool
1120
errorContains string
1121
enableCache bool
@@ -1178,11 +1179,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1179
}, nil,
1180
)
1181
},
1181
- expectedResult: []*ProfileMetrics{
1182
+ expectedResult: []*ddsnmp.ProfileMetrics{
1183
{
1184
Source: "test-profile.yaml",
1185
DeviceMetadata: nil,
1185
- Metrics: []Metric{
1186
+ Metrics: []ddsnmp.Metric{
1187
{
1188
Name: "ifInOctets",
1189
Value: 1000,
@@ -1291,11 +1292,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1292
}, nil,
1293
)
1294
},
1294
- expectedResult: []*ProfileMetrics{
1295
+ expectedResult: []*ddsnmp.ProfileMetrics{
1296
{
1297
Source: "test-profile.yaml",
1298
DeviceMetadata: nil,
1298
- Metrics: []Metric{
1299
+ Metrics: []ddsnmp.Metric{
1300
// Row 1
1301
{
1302
Name: "ifInOctets",
@@ -1426,11 +1427,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1427
}, nil,
1428
)
1429
},
1429
- expectedResult: []*ProfileMetrics{
1430
+ expectedResult: []*ddsnmp.ProfileMetrics{
1431
{
1432
Source: "test-profile.yaml",
1433
DeviceMetadata: nil,
1433
- Metrics: []Metric{
1434
+ Metrics: []ddsnmp.Metric{
1435
{
1436
Name: "ifInErrors",
1437
Value: 10,
@@ -1531,11 +1532,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1532
}, nil,
1533
)
1534
},
1534
- expectedResult: []*ProfileMetrics{
1535
+ expectedResult: []*ddsnmp.ProfileMetrics{
1536
{
1537
Source: "test-profile.yaml",
1538
DeviceMetadata: nil,
1538
- Metrics: []Metric{
1539
+ Metrics: []ddsnmp.Metric{
1540
// Row 1 - complete
1541
{
1542
Name: "ifInOctets",
@@ -1632,11 +1633,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1633
}, nil,
1634
)
1635
},
1635
- expectedResult: []*ProfileMetrics{
1636
+ expectedResult: []*ddsnmp.ProfileMetrics{
1637
{
1638
Source: "test-profile.yaml",
1639
DeviceMetadata: nil,
1639
- Metrics: []Metric{
1640
+ Metrics: []ddsnmp.Metric{
1641
{
1642
Name: "ifInOctets",
1643
Value: 1000,
@@ -1713,11 +1714,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1714
}, nil,
1715
)
1716
},
1716
- expectedResult: []*ProfileMetrics{
1717
+ expectedResult: []*ddsnmp.ProfileMetrics{
1718
{
1719
Source: "test-profile.yaml",
1720
DeviceMetadata: nil,
1720
- Metrics: []Metric{
1721
+ Metrics: []ddsnmp.Metric{
1722
{
1723
Name: "ifInErrors",
1724
Value: 10,
@@ -1815,11 +1816,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1816
}, nil,
1817
)
1818
},
1818
- expectedResult: []*ProfileMetrics{
1819
+ expectedResult: []*ddsnmp.ProfileMetrics{
1820
{
1821
Source: "test-profile.yaml",
1822
DeviceMetadata: nil,
1822
- Metrics: []Metric{
1823
+ Metrics: []ddsnmp.Metric{
1824
{
1825
Name: "ifInOctets",
1826
Value: 1000,
@@ -1874,11 +1875,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1875
[]gosnmp.SnmpPDU{}, nil,
1876
)
1877
},
1877
- expectedResult: []*ProfileMetrics{
1878
+ expectedResult: []*ddsnmp.ProfileMetrics{
1879
{
1880
Source: "test-profile.yaml",
1881
DeviceMetadata: nil,
1881
- Metrics: []Metric{},
1882
+ Metrics: []ddsnmp.Metric{},
1883
},
1884
},
1885
expectedError: false,
@@ -1967,11 +1968,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
1968
}, nil,
1969
)
1970
},
1970
- expectedResult: []*ProfileMetrics{
1971
+ expectedResult: []*ddsnmp.ProfileMetrics{
1972
{
1973
Source: "test-profile.yaml",
1974
DeviceMetadata: nil,
1974
- Metrics: []Metric{
1975
+ Metrics: []ddsnmp.Metric{
1976
{
1977
Name: "cpmCPUMemoryUsed",
1978
Value: 2097152, // 2048 * 1024
@@ -2033,11 +2034,11 @@ func TestCollector_Collect_TableMetricsBasic(t *testing.T) {
2034
}, nil,
2035
)
2036
},
2036
- expectedResult: []*ProfileMetrics{
2037
+ expectedResult: []*ddsnmp.ProfileMetrics{
2038
{
2039
Source: "test-profile.yaml",
2040
DeviceMetadata: nil,
2040
- Metrics: []Metric{
2041
+ Metrics: []ddsnmp.Metric{
2042
{
2043
Name: "ifInOctets",
2044
Value: 1000,
@@ -2106,7 +2107,7 @@ func TestCollector_Collect_CrossTableTags(t *testing.T) {
2107
tests := map[string]struct {
2108
profiles []*ddsnmp.Profile
2109
setupMock func(m *snmpmock.MockHandler)
2109
- expectedResult []*ProfileMetrics
2110
+ expectedResult []*ddsnmp.ProfileMetrics
2111
expectedError bool
2112
errorContains string
2113
enableCache bool
@@ -2190,11 +2191,11 @@ func TestCollector_Collect_CrossTableTags(t *testing.T) {
2191
}, nil,
2192
)
2193
},
2193
- expectedResult: []*ProfileMetrics{
2194
+ expectedResult: []*ddsnmp.ProfileMetrics{
2195
{
2196
Source: "test-profile.yaml",
2197
DeviceMetadata: nil,
2197
- Metrics: []Metric{
2198
+ Metrics: []ddsnmp.Metric{
2199
{
2200
Name: "ifInErrors",
2201
Value: 10,
@@ -2298,11 +2299,11 @@ func TestCollector_Collect_CrossTableTags(t *testing.T) {
2299
}, nil,
2300
)
2301
},
2301
- expectedResult: []*ProfileMetrics{
2302
+ expectedResult: []*ddsnmp.ProfileMetrics{
2303
{
2304
Source: "test-profile.yaml",
2305
DeviceMetadata: nil,
2305
- Metrics: []Metric{
2306
+ Metrics: []ddsnmp.Metric{
2307
{
2308
Name: "ifInErrors",
2309
Value: 10,
@@ -2427,11 +2428,11 @@ func TestCollector_Collect_CrossTableTags(t *testing.T) {
2428
}, nil,
2429
)
2430
},
2430
- expectedResult: []*ProfileMetrics{
2431
+ expectedResult: []*ddsnmp.ProfileMetrics{
2432
{
2433
Source: "test-profile.yaml",
2434
DeviceMetadata: nil,
2434
- Metrics: []Metric{
2435
+ Metrics: []ddsnmp.Metric{
2436
{
2437
Name: "ifInErrors",
2438
Value: 10,
@@ -2568,11 +2569,11 @@ func TestCollector_Collect_CrossTableTags(t *testing.T) {
2569
}, nil,
2570
)
2571
},
2571
- expectedResult: []*ProfileMetrics{
2572
+ expectedResult: []*ddsnmp.ProfileMetrics{
2573
{
2574
Source: "test-profile.yaml",
2575
DeviceMetadata: nil,
2575
- Metrics: []Metric{
2576
+ Metrics: []ddsnmp.Metric{
2577
{
2578
Name: "cieIfResetCount",
2579
Value: 5,
@@ -2662,11 +2663,11 @@ func TestCollector_Collect_CrossTableTags(t *testing.T) {
2663
}, nil,
2664
)
2665
},
2665
- expectedResult: []*ProfileMetrics{
2666
+ expectedResult: []*ddsnmp.ProfileMetrics{
2667
{
2668
Source: "test-profile.yaml",
2669
DeviceMetadata: nil,
2669
- Metrics: []Metric{
2670
+ Metrics: []ddsnmp.Metric{
2671
{
2672
Name: "ifInErrors",
2673
Value: 10,
@@ -2756,11 +2757,11 @@ func TestCollector_Collect_CrossTableTags(t *testing.T) {
2757
}, nil,
2758
)
2759
},
2759
- expectedResult: []*ProfileMetrics{
2760
+ expectedResult: []*ddsnmp.ProfileMetrics{
2761
{
2762
Source: "test-profile.yaml",
2763
DeviceMetadata: nil,
2763
- Metrics: []Metric{
2764
+ Metrics: []ddsnmp.Metric{
2765
{
2766
Name: "ifInOctets",
2767
Value: 1000,
@@ -2872,11 +2873,11 @@ func TestCollector_Collect_CrossTableTags(t *testing.T) {
2873
}, nil,
2874
)
2875
},
2875
- expectedResult: []*ProfileMetrics{
2876
+ expectedResult: []*ddsnmp.ProfileMetrics{
2877
{
2878
Source: "test-profile.yaml",
2879
DeviceMetadata: nil,
2879
- Metrics: []Metric{
2880
+ Metrics: []ddsnmp.Metric{
2881
{
2882
Name: "ifInErrors",
2883
Value: 10,
@@ -2962,11 +2963,11 @@ func TestCollector_Collect_CrossTableTags(t *testing.T) {
2963
}, nil,
2964
)
2965
},
2965
- expectedResult: []*ProfileMetrics{
2966
+ expectedResult: []*ddsnmp.ProfileMetrics{
2967
{
2968
Source: "test-profile.yaml",
2969
DeviceMetadata: nil,
2969
- Metrics: []Metric{
2970
+ Metrics: []ddsnmp.Metric{
2971
{
2972
Name: "ifInErrors",
2973
Value: 10,
@@ -3036,7 +3037,7 @@ func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3037
tests := map[string]struct {
3038
profiles []*ddsnmp.Profile
3039
setupMock func(m *snmpmock.MockHandler)
3039
- expectedResult []*ProfileMetrics
3040
+ expectedResult []*ddsnmp.ProfileMetrics
3041
expectedError bool
3042
errorContains string
3043
enableCache bool
@@ -3088,11 +3089,11 @@ func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3089
}, nil,
3090
)
3091
},
3091
- expectedResult: []*ProfileMetrics{
3092
+ expectedResult: []*ddsnmp.ProfileMetrics{
3093
{
3094
Source: "test-profile.yaml",
3095
DeviceMetadata: nil,
3095
- Metrics: []Metric{
3096
+ Metrics: []ddsnmp.Metric{
3097
{
3098
Name: "ipSystemStatsHCInReceives",
3099
Value: 1000,
@@ -3167,11 +3168,11 @@ func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3168
}, nil,
3169
)
3170
},
3170
- expectedResult: []*ProfileMetrics{
3171
+ expectedResult: []*ddsnmp.ProfileMetrics{
3172
{
3173
Source: "test-profile.yaml",
3174
DeviceMetadata: nil,
3174
- Metrics: []Metric{
3175
+ Metrics: []ddsnmp.Metric{
3176
{
3177
Name: "cfwConnectionStatValue",
3178
Value: 100,
@@ -3256,11 +3257,11 @@ func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3257
}, nil,
3258
)
3259
},
3259
- expectedResult: []*ProfileMetrics{
3260
+ expectedResult: []*ddsnmp.ProfileMetrics{
3261
{
3262
Source: "test-profile.yaml",
3263
DeviceMetadata: nil,
3263
- Metrics: []Metric{
3264
+ Metrics: []ddsnmp.Metric{
3265
{
3266
Name: "ipSystemStatsHCInReceives",
3267
Value: 1000,
@@ -3337,11 +3338,11 @@ func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3338
}, nil,
3339
)
3340
},
3340
- expectedResult: []*ProfileMetrics{
3341
+ expectedResult: []*ddsnmp.ProfileMetrics{
3342
{
3343
Source: "test-profile.yaml",
3344
DeviceMetadata: nil,
3344
- Metrics: []Metric{
3345
+ Metrics: []ddsnmp.Metric{
3346
{
3347
Name: "ifInOctets",
3348
Value: 1000,
@@ -3435,11 +3436,11 @@ func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3436
}, nil,
3437
)
3438
},
3438
- expectedResult: []*ProfileMetrics{
3439
+ expectedResult: []*ddsnmp.ProfileMetrics{
3440
{
3441
Source: "test-profile.yaml",
3442
DeviceMetadata: nil,
3442
- Metrics: []Metric{
3443
+ Metrics: []ddsnmp.Metric{
3444
{
3445
Name: "cpiPduBranchCurrent",
3446
Value: 150,
@@ -3525,11 +3526,11 @@ func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3526
}, nil,
3527
)
3528
},
3528
- expectedResult: []*ProfileMetrics{
3529
+ expectedResult: []*ddsnmp.ProfileMetrics{
3530
{
3531
Source: "test-profile.yaml",
3532
DeviceMetadata: nil,
3532
- Metrics: []Metric{
3533
+ Metrics: []ddsnmp.Metric{
3534
{
3535
Name: "customMetric",
3536
Value: 500,
@@ -3619,11 +3620,11 @@ func TestCollector_Collect_IndexBasedAndTransforms(t *testing.T) {
3620
}, nil,
3621
)
3622
},
3622
- expectedResult: []*ProfileMetrics{
3623
+ expectedResult: []*ddsnmp.ProfileMetrics{
3624
{
3625
Source: "test-profile.yaml",
3626
DeviceMetadata: nil,
3626
- Metrics: []Metric{
3627
+ Metrics: []ddsnmp.Metric{
3628
{
3629
Name: "ifInOctets",
3630
Value: 1000,
@@ -3699,7 +3700,7 @@ func TestCollector_Collect_OpaqueTypes(t *testing.T) {
3700
tests := map[string]struct {
3701
profiles []*ddsnmp.Profile
3702
setupMock func(m *snmpmock.MockHandler)
3702
- expectedResult []*ProfileMetrics
3703
+ expectedResult []*ddsnmp.ProfileMetrics
3704
expectedError bool
3705
errorContains string
3706
enableCache bool
@@ -3760,11 +3761,11 @@ func TestCollector_Collect_OpaqueTypes(t *testing.T) {
3761
}, nil,
3762
)
3763
},
3763
- expectedResult: []*ProfileMetrics{
3764
+ expectedResult: []*ddsnmp.ProfileMetrics{
3765
{
3766
Source: "test-profile.yaml",
3767
DeviceMetadata: nil,
3767
- Metrics: []Metric{
3768
+ Metrics: []ddsnmp.Metric{
3769
{
3770
Name: "cpmCPULoadAvg1min",
3771
Value: 0, // 0.75 truncated to int64
@@ -3841,11 +3842,11 @@ func TestCollector_Collect_OpaqueTypes(t *testing.T) {
3842
}, nil,
3843
)
3844
},
3844
- expectedResult: []*ProfileMetrics{
3845
+ expectedResult: []*ddsnmp.ProfileMetrics{
3846
{
3847
Source: "test-profile.yaml",
3848
DeviceMetadata: nil,
3848
- Metrics: []Metric{
3849
+ Metrics: []ddsnmp.Metric{
3850
{
3851
Name: "cpqHeSysBatteryVoltage",
3852
Value: 12, // 12.6 truncated to int64
@@ -3922,7 +3923,7 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
3923
tests := map[string]struct {
3924
profiles []*ddsnmp.Profile
3925
setupMock func(m *snmpmock.MockHandler)
3925
- expectedResult []*ProfileMetrics
3926
+ expectedResult []*ddsnmp.ProfileMetrics
3927
expectedError bool
3928
errorContains string
3929
enableCache bool
@@ -4071,11 +4072,11 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
4072
}, nil,
4073
).Times(1)
4074
},
4074
- expectedResult: []*ProfileMetrics{
4075
+ expectedResult: []*ddsnmp.ProfileMetrics{
4076
{
4077
Source: "test-profile.yaml",
4078
DeviceMetadata: nil,
4078
- Metrics: []Metric{
4079
+ Metrics: []ddsnmp.Metric{
4080
{
4081
Name: "ifInOctets",
4082
Value: 1200, // Latest value
@@ -4205,11 +4206,11 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
4206
),
4207
)
4208
},
4208
- expectedResult: []*ProfileMetrics{
4209
+ expectedResult: []*ddsnmp.ProfileMetrics{
4210
{
4211
Source: "test-profile.yaml",
4212
DeviceMetadata: nil,
4212
- Metrics: []Metric{
4213
+ Metrics: []ddsnmp.Metric{
4214
{
4215
Name: "ifInOctets",
4216
Value: 1200,
@@ -4372,11 +4373,11 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
4373
}, nil,
4374
).Times(1)
4375
},
4375
- expectedResult: []*ProfileMetrics{
4376
+ expectedResult: []*ddsnmp.ProfileMetrics{
4377
{
4378
Source: "test-profile.yaml",
4379
DeviceMetadata: nil,
4379
- Metrics: []Metric{
4380
+ Metrics: []ddsnmp.Metric{
4381
// From first config
4382
{
4383
Name: "ifInOctets",
@@ -4436,7 +4437,7 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
4437
collector.tableCache.setTTL(0, 0) // Disable cache
4438
}
4439
4439
- var result []*ProfileMetrics
4440
+ var result []*ddsnmp.ProfileMetrics
4441
var err error
4442
4443
// Perform multiple collections to test caching behavior
@@ -4483,6 +4484,559 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
4484
}
4485
}
4486
4487
+func TestCollector_Collect_MetricTransforms(t *testing.T) {
4488
+ tests := map[string]struct {
4489
+ profiles []*ddsnmp.Profile
4490
+ setupMock func(m *snmpmock.MockHandler)
4491
+ expectedResult []*ddsnmp.ProfileMetrics
4492
+ expectedError bool
4493
+ errorContains string
4494
+ }{
4495
+ "basic metric name transformation": {
4496
+ profiles: []*ddsnmp.Profile{
4497
+ {
4498
+ SourceFile: "test-profile.yaml",
4499
+ Definition: &ddprofiledefinition.ProfileDefinition{
4500
+ Metrics: []ddprofiledefinition.MetricsConfig{
4501
+ {
4502
+ Symbol: ddprofiledefinition.SymbolConfig{
4503
+ OID: "1.3.6.1.4.1.12345.1.1",
4504
+ Name: "sensorValue",
4505
+ Transform: `{{- setName .Metric "customName" -}}`,
4506
+ },
4507
+ },
4508
+ },
4509
+ },
4510
+ },
4511
+ },
4512
+ setupMock: func(m *snmpmock.MockHandler) {
4513
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
4514
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.12345.1.1"}).Return(
4515
+ &gosnmp.SnmpPacket{
4516
+ Variables: []gosnmp.SnmpPDU{
4517
+ {
4518
+ Name: "1.3.6.1.4.1.12345.1.1",
4519
+ Type: gosnmp.Gauge32,
4520
+ Value: uint(100),
4521
+ },
4522
+ },
4523
+ }, nil,
4524
+ )
4525
+ },
4526
+ expectedResult: []*ddsnmp.ProfileMetrics{
4527
+ {
4528
+ Source: "test-profile.yaml",
4529
+ DeviceMetadata: nil,
4530
+ Metrics: []ddsnmp.Metric{
4531
+ {
4532
+ Name: "customName",
4533
+ Value: 100,
4534
+ MetricType: "gauge",
4535
+ },
4536
+ },
4537
+ },
4538
+ },
4539
+ expectedError: false,
4540
+ },
4541
+ "table metric with sensor type transformation": {
4542
+ profiles: []*ddsnmp.Profile{
4543
+ {
4544
+ SourceFile: "test-profile.yaml",
4545
+ Definition: &ddprofiledefinition.ProfileDefinition{
4546
+ Metrics: []ddprofiledefinition.MetricsConfig{
4547
+ {
4548
+ Table: ddprofiledefinition.SymbolConfig{
4549
+ OID: "1.3.6.1.4.1.14988.1.1.3.100",
4550
+ Name: "mtxrHlTable",
4551
+ },
4552
+ Symbols: []ddprofiledefinition.SymbolConfig{
4553
+ {
4554
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.3",
4555
+ Name: "mtxrHlSensorValue",
4556
+ Transform: `
4557
+{{- $sensorType := index .Metric.Tags "sensor_type" | default "" -}}
4558
+{{- if eq $sensorType "1" -}}
4559
+ {{- setName .Metric (printf "%s_temperature" .Metric.Name) -}}
4560
+ {{- setUnit .Metric "celsius" -}}
4561
+ {{- setFamily .Metric "Health/Temperature" -}}
4562
+{{- else if eq $sensorType "3" -}}
4563
+ {{- setName .Metric (printf "%s_voltage" .Metric.Name) -}}
4564
+ {{- setUnit .Metric "volts" -}}
4565
+ {{- setValue .Metric (int64 (div (float64 .Metric.Value) 10.0)) -}}
4566
+ {{- setFamily .Metric "Health/Power" -}}
4567
+{{- end -}}
4568
+{{- deleteTag .Metric "sensor_type" -}}`,
4569
+ },
4570
+ },
4571
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
4572
+ {
4573
+ Tag: "sensor_name",
4574
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
4575
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.2",
4576
+ Name: "mtxrHlSensorName",
4577
+ },
4578
+ },
4579
+ {
4580
+ Tag: "sensor_type",
4581
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
4582
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.4",
4583
+ Name: "mtxrHlSensorType",
4584
+ },
4585
+ },
4586
+ },
4587
+ },
4588
+ },
4589
+ },
4590
+ },
4591
+ },
4592
+ setupMock: func(m *snmpmock.MockHandler) {
4593
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
4594
+ m.EXPECT().Version().Return(gosnmp.Version2c)
4595
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.14988.1.1.3.100").Return(
4596
+ []gosnmp.SnmpPDU{
4597
+ // Temperature sensor (type 1)
4598
+ {
4599
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.1",
4600
+ Type: gosnmp.Integer,
4601
+ Value: 25,
4602
+ },
4603
+ {
4604
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.1",
4605
+ Type: gosnmp.OctetString,
4606
+ Value: []byte("cpu-temperature"),
4607
+ },
4608
+ {
4609
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.1",
4610
+ Type: gosnmp.Integer,
4611
+ Value: 1,
4612
+ },
4613
+ // Voltage sensor (type 3)
4614
+ {
4615
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.2",
4616
+ Type: gosnmp.Integer,
4617
+ Value: 120, // 12.0V in decivolts
4618
+ },
4619
+ {
4620
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.2",
4621
+ Type: gosnmp.OctetString,
4622
+ Value: []byte("input-voltage"),
4623
+ },
4624
+ {
4625
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.2",
4626
+ Type: gosnmp.Integer,
4627
+ Value: 3,
4628
+ },
4629
+ }, nil,
4630
+ )
4631
+ },
4632
+ expectedResult: []*ddsnmp.ProfileMetrics{
4633
+ {
4634
+ Source: "test-profile.yaml",
4635
+ DeviceMetadata: nil,
4636
+ Metrics: []ddsnmp.Metric{
4637
+ {
4638
+ Name: "mtxrHlSensorValue_temperature",
4639
+ Value: 25,
4640
+ Tags: map[string]string{"sensor_name": "cpu-temperature"},
4641
+ Unit: "celsius",
4642
+ Family: "Health/Temperature",
4643
+ MetricType: "gauge",
4644
+ IsTable: true,
4645
+ },
4646
+ {
4647
+ Name: "mtxrHlSensorValue_voltage",
4648
+ Value: 12, // Converted from decivolts
4649
+ Tags: map[string]string{"sensor_name": "input-voltage"},
4650
+ Unit: "volts",
4651
+ Family: "Health/Power",
4652
+ MetricType: "gauge",
4653
+ IsTable: true,
4654
+ },
4655
+ },
4656
+ },
4657
+ },
4658
+ expectedError: false,
4659
+ },
4660
+ "transformation with value mappings": {
4661
+ profiles: []*ddsnmp.Profile{
4662
+ {
4663
+ SourceFile: "test-profile.yaml",
4664
+ Definition: &ddprofiledefinition.ProfileDefinition{
4665
+ Metrics: []ddprofiledefinition.MetricsConfig{
4666
+ {
4667
+ Symbol: ddprofiledefinition.SymbolConfig{
4668
+ OID: "1.3.6.1.4.1.12345.1.1",
4669
+ Name: "deviceStatus",
4670
+ Transform: `
4671
+{{- setMappings .Metric (i64map 0 "down" 1 "up" 2 "testing") -}}`,
4672
+ },
4673
+ },
4674
+ },
4675
+ },
4676
+ },
4677
+ },
4678
+ setupMock: func(m *snmpmock.MockHandler) {
4679
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
4680
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.12345.1.1"}).Return(
4681
+ &gosnmp.SnmpPacket{
4682
+ Variables: []gosnmp.SnmpPDU{
4683
+ {
4684
+ Name: "1.3.6.1.4.1.12345.1.1",
4685
+ Type: gosnmp.Integer,
4686
+ Value: 1,
4687
+ },
4688
+ },
4689
+ }, nil,
4690
+ )
4691
+ },
4692
+ expectedResult: []*ddsnmp.ProfileMetrics{
4693
+ {
4694
+ Source: "test-profile.yaml",
4695
+ DeviceMetadata: nil,
4696
+ Metrics: []ddsnmp.Metric{
4697
+ {
4698
+ Name: "deviceStatus",
4699
+ Value: 1,
4700
+ Mappings: map[int64]string{
4701
+ 0: "down",
4702
+ 1: "up",
4703
+ 2: "testing",
4704
+ },
4705
+ MetricType: "gauge",
4706
+ },
4707
+ },
4708
+ },
4709
+ },
4710
+ expectedError: false,
4711
+ },
4712
+ "complex transformation with sprig functions": {
4713
+ profiles: []*ddsnmp.Profile{
4714
+ {
4715
+ SourceFile: "test-profile.yaml",
4716
+ Definition: &ddprofiledefinition.ProfileDefinition{
4717
+ Metrics: []ddprofiledefinition.MetricsConfig{
4718
+ {
4719
+ Table: ddprofiledefinition.SymbolConfig{
4720
+ OID: "1.3.6.1.2.1.2.2",
4721
+ Name: "ifTable",
4722
+ },
4723
+ Symbols: []ddprofiledefinition.SymbolConfig{
4724
+ {
4725
+ OID: "1.3.6.1.2.1.2.2.1.10",
4726
+ Name: "ifInOctets",
4727
+ Transform: `
4728
+{{- $ifName := index .Metric.Tags "interface" | lower -}}
4729
+{{- if contains "eth" $ifName -}}
4730
+ {{- setFamily .Metric "Interfaces/Ethernet" -}}
4731
+{{- else if contains "lo" $ifName -}}
4732
+ {{- setFamily .Metric "Interfaces/Loopback" -}}
4733
+{{- end -}}
4734
+{{- setDesc .Metric (printf "Traffic on %s interface" (index .Metric.Tags "interface")) -}}`,
4735
+ },
4736
+ },
4737
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
4738
+ {
4739
+ Tag: "interface",
4740
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
4741
+ OID: "1.3.6.1.2.1.2.2.1.2",
4742
+ Name: "ifDescr",
4743
+ },
4744
+ },
4745
+ },
4746
+ },
4747
+ },
4748
+ },
4749
+ },
4750
+ },
4751
+ setupMock: func(m *snmpmock.MockHandler) {
4752
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
4753
+ m.EXPECT().Version().Return(gosnmp.Version2c)
4754
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.2.2").Return(
4755
+ []gosnmp.SnmpPDU{
4756
+ {
4757
+ Name: "1.3.6.1.2.1.2.2.1.10.1",
4758
+ Type: gosnmp.Counter32,
4759
+ Value: uint(1000),
4760
+ },
4761
+ {
4762
+ Name: "1.3.6.1.2.1.2.2.1.2.1",
4763
+ Type: gosnmp.OctetString,
4764
+ Value: []byte("eth0"),
4765
+ },
4766
+ {
4767
+ Name: "1.3.6.1.2.1.2.2.1.10.2",
4768
+ Type: gosnmp.Counter32,
4769
+ Value: uint(2000),
4770
+ },
4771
+ {
4772
+ Name: "1.3.6.1.2.1.2.2.1.2.2",
4773
+ Type: gosnmp.OctetString,
4774
+ Value: []byte("lo0"),
4775
+ },
4776
+ }, nil,
4777
+ )
4778
+ },
4779
+ expectedResult: []*ddsnmp.ProfileMetrics{
4780
+ {
4781
+ Source: "test-profile.yaml",
4782
+ DeviceMetadata: nil,
4783
+ Metrics: []ddsnmp.Metric{
4784
+ {
4785
+ Name: "ifInOctets",
4786
+ Value: 1000,
4787
+ Tags: map[string]string{"interface": "eth0"},
4788
+ Family: "Interfaces/Ethernet",
4789
+ Description: "Traffic on eth0 interface",
4790
+ MetricType: "rate",
4791
+ IsTable: true,
4792
+ },
4793
+ {
4794
+ Name: "ifInOctets",
4795
+ Value: 2000,
4796
+ Tags: map[string]string{"interface": "lo0"},
4797
+ Family: "Interfaces/Loopback",
4798
+ Description: "Traffic on lo0 interface",
4799
+ MetricType: "rate",
4800
+ IsTable: true,
4801
+ },
4802
+ },
4803
+ },
4804
+ },
4805
+ expectedError: false,
4806
+ },
4807
+ "transformation with invalid template": {
4808
+ profiles: []*ddsnmp.Profile{
4809
+ {
4810
+ SourceFile: "test-profile.yaml",
4811
+ Definition: &ddprofiledefinition.ProfileDefinition{
4812
+ Metrics: []ddprofiledefinition.MetricsConfig{
4813
+ {
4814
+ Symbol: ddprofiledefinition.SymbolConfig{
4815
+ OID: "1.3.6.1.4.1.12345.1.1",
4816
+ Name: "testMetric",
4817
+ Transform: `{{- invalid template syntax {{`,
4818
+ TransformCompiled: nil, // Will fail compilation
4819
+ },
4820
+ },
4821
+ },
4822
+ },
4823
+ },
4824
+ },
4825
+ setupMock: func(m *snmpmock.MockHandler) {
4826
+ // This test should fail during profile validation, not during collection
4827
+ },
4828
+ expectedResult: nil,
4829
+ expectedError: true,
4830
+ errorContains: "template",
4831
+ },
4832
+ "mikrotik sensor table real-world example": {
4833
+ profiles: []*ddsnmp.Profile{
4834
+ {
4835
+ SourceFile: "mikrotik-profile.yaml",
4836
+ Definition: &ddprofiledefinition.ProfileDefinition{
4837
+ Metrics: []ddprofiledefinition.MetricsConfig{
4838
+ {
4839
+ Table: ddprofiledefinition.SymbolConfig{
4840
+ OID: "1.3.6.1.4.1.14988.1.1.3.100",
4841
+ Name: "mtxrHlTable",
4842
+ },
4843
+ Symbols: []ddprofiledefinition.SymbolConfig{
4844
+ {
4845
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.3",
4846
+ Name: "mtxrHlSensorValue",
4847
+ Transform: `
4848
+{{- $config := get (dict
4849
+ "1" (dict "name" "temperature" "unit" "celsius" "family" "Health/Temperature")
4850
+ "2" (dict "name" "fan_speed" "unit" "rpm" "family" "Health/Cooling")
4851
+ "3" (dict "name" "voltage" "unit" "volts" "family" "Health/Power" "divisor" 10.0)
4852
+ "6" (dict "name" "sensor_status" "family" "Health/Status"
4853
+ "mapping" (i64map 0 "not_ok" 1 "ok"))
4854
+) (index .Metric.Tags "sensor_type" | default "") -}}
4855
+
4856
+{{- if $config -}}
4857
+ {{- setName .Metric (printf "%s_%s" .Metric.Name (get $config "name")) -}}
4858
+ {{- setFamily .Metric (get $config "family") -}}
4859
+ {{- with get $config "unit" -}}{{- setUnit $.Metric . -}}{{- end -}}
4860
+ {{- with get $config "divisor" -}}{{- setValue $.Metric (int64 (div (float64 $.Metric.Value) .)) -}}{{- end -}}
4861
+ {{- with get $config "mapping" -}}{{- setMappings $.Metric . -}}{{- end -}}
4862
+{{- end -}}
4863
+
4864
+{{- deleteTag .Metric "sensor_type" -}}`,
4865
+ },
4866
+ },
4867
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
4868
+ {
4869
+ Tag: "sensor_name",
4870
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
4871
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.2",
4872
+ Name: "mtxrHlSensorName",
4873
+ },
4874
+ },
4875
+ {
4876
+ Tag: "sensor_type",
4877
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
4878
+ OID: "1.3.6.1.4.1.14988.1.1.3.100.1.4",
4879
+ Name: "mtxrHlSensorType",
4880
+ },
4881
+ },
4882
+ },
4883
+ },
4884
+ },
4885
+ },
4886
+ },
4887
+ },
4888
+ setupMock: func(m *snmpmock.MockHandler) {
4889
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
4890
+ m.EXPECT().Version().Return(gosnmp.Version2c)
4891
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.14988.1.1.3.100").Return(
4892
+ []gosnmp.SnmpPDU{
4893
+ // Temperature sensor
4894
+ {
4895
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.1",
4896
+ Type: gosnmp.Integer,
4897
+ Value: 45,
4898
+ },
4899
+ {
4900
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.1",
4901
+ Type: gosnmp.OctetString,
4902
+ Value: []byte("cpu-temperature"),
4903
+ },
4904
+ {
4905
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.1",
4906
+ Type: gosnmp.Integer,
4907
+ Value: 1,
4908
+ },
4909
+ // PSU status sensor
4910
+ {
4911
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.2",
4912
+ Type: gosnmp.Integer,
4913
+ Value: 1,
4914
+ },
4915
+ {
4916
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.2",
4917
+ Type: gosnmp.OctetString,
4918
+ Value: []byte("psu1-state"),
4919
+ },
4920
+ {
4921
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.2",
4922
+ Type: gosnmp.Integer,
4923
+ Value: 6,
4924
+ },
4925
+ // Voltage sensor
4926
+ {
4927
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.3.3",
4928
+ Type: gosnmp.Integer,
4929
+ Value: 240, // 24.0V
4930
+ },
4931
+ {
4932
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.2.3",
4933
+ Type: gosnmp.OctetString,
4934
+ Value: []byte("psu-voltage"),
4935
+ },
4936
+ {
4937
+ Name: "1.3.6.1.4.1.14988.1.1.3.100.1.4.3",
4938
+ Type: gosnmp.Integer,
4939
+ Value: 3,
4940
+ },
4941
+ }, nil,
4942
+ )
4943
+ },
4944
+ expectedResult: []*ddsnmp.ProfileMetrics{
4945
+ {
4946
+ Source: "mikrotik-profile.yaml",
4947
+ DeviceMetadata: nil,
4948
+ Metrics: []ddsnmp.Metric{
4949
+ {
4950
+ Name: "mtxrHlSensorValue_temperature",
4951
+ Value: 45,
4952
+ Tags: map[string]string{"sensor_name": "cpu-temperature"},
4953
+ Unit: "celsius",
4954
+ Family: "Health/Temperature",
4955
+ MetricType: "gauge",
4956
+ IsTable: true,
4957
+ },
4958
+ {
4959
+ Name: "mtxrHlSensorValue_sensor_status",
4960
+ Value: 1,
4961
+ Tags: map[string]string{"sensor_name": "psu1-state"},
4962
+ Family: "Health/Status",
4963
+ Mappings: map[int64]string{
4964
+ 0: "not_ok",
4965
+ 1: "ok",
4966
+ },
4967
+ MetricType: "gauge",
4968
+ IsTable: true,
4969
+ },
4970
+ {
4971
+ Name: "mtxrHlSensorValue_voltage",
4972
+ Value: 24,
4973
+ Tags: map[string]string{"sensor_name": "psu-voltage"},
4974
+ Unit: "volts",
4975
+ Family: "Health/Power",
4976
+ MetricType: "gauge",
4977
+ IsTable: true,
4978
+ },
4979
+ },
4980
+ },
4981
+ },
4982
+ expectedError: false,
4983
+ },
4984
+ }
4985
+
4986
+ for name, tc := range tests {
4987
+ t.Run(name, func(t *testing.T) {
4988
+ ctrl := gomock.NewController(t)
4989
+ defer ctrl.Finish()
4990
+
4991
+ mockHandler := snmpmock.NewMockHandler(ctrl)
4992
+ tc.setupMock(mockHandler)
4993
+
4994
+ // Compile transforms for profiles
4995
+ for _, profile := range tc.profiles {
4996
+ if err := ddsnmp.CompileTransforms(profile); err != nil {
4997
+ if tc.expectedError && tc.errorContains != "" && strings.Contains(err.Error(), tc.errorContains) {
4998
+ return // Expected error during compilation
4999
+ }
5000
+ t.Fatalf("Failed to compile transforms: %v", err)
5001
+ }
5002
+ }
5003
+
5004
+ collector := New(mockHandler, tc.profiles, logger.New())
5005
+ collector.DoTableMetrics = true
5006
+ collector.tableCache.setTTL(0, 0) // Disable cache
5007
+
5008
+ result, err := collector.Collect()
5009
+
5010
+ // Clear circular references
5011
+ for _, profile := range result {
5012
+ for i := range profile.Metrics {
5013
+ profile.Metrics[i].Profile = nil
5014
+ }
5015
+ }
5016
+
5017
+ if tc.expectedError {
5018
+ assert.Error(t, err)
5019
+ if tc.errorContains != "" {
5020
+ assert.Contains(t, err.Error(), tc.errorContains)
5021
+ }
5022
+ } else {
5023
+ assert.NoError(t, err)
5024
+ }
5025
+
5026
+ if tc.expectedResult != nil {
5027
+ require.Equal(t, len(tc.expectedResult), len(result))
5028
+ for i := range tc.expectedResult {
5029
+ assert.Equal(t, tc.expectedResult[i].DeviceMetadata, result[i].DeviceMetadata)
5030
+ assert.Equal(t, tc.expectedResult[i].Tags, result[i].Tags)
5031
+ assert.ElementsMatch(t, tc.expectedResult[i].Metrics, result[i].Metrics)
5032
+ }
5033
+ } else {
5034
+ assert.Nil(t, result)
5035
+ }
5036
+ })
5037
+ }
5038
+}
5039
+
5040
func mustCompileRegex(pattern string) *regexp.Regexp {
5041
re, err := regexp.Compile(pattern)
5042
if err != nil {
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/utils.go
+19
@@ -3,6 +3,7 @@
3
package ddsnmpcollector
4
5
import (
6
+ "bytes"
7
"encoding/hex"
8
"fmt"
9
"regexp"
@@ -12,6 +13,7 @@ import (
13
14
"github.com/gosnmp/gosnmp"
15
16
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
17
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
18
)
19
@@ -260,3 +262,20 @@ func mergeTagsWithEmptyFallback(dest, src map[string]string) {
262
}
263
}
264
}
265
+
266
+func applyTransform(metric *ddsnmp.Metric, sym ddprofiledefinition.SymbolConfig) error {
267
+ if sym.TransformCompiled == nil {
268
+ return nil
269
+ }
270
+
271
+ type transformCtx struct{ Metric *ddsnmp.Metric }
272
+
273
+ ctx := &transformCtx{Metric: metric}
274
+
275
+ var buf bytes.Buffer
276
+ if err := sym.TransformCompiled.Execute(&buf, ctx); err != nil {
277
+ return fmt.Errorf("failed to execute transform template: %w", err)
278
+ }
279
+
280
+ return nil
281
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/load.go
+4
@@ -93,6 +93,10 @@ func loadProfilesFromDir(dirpath string, extendsPaths multipath.MultiPath) ([]*P
93
log.Warningf("invalid profile '%s': %v", path, err)
94
return nil
95
}
96
+ if err := CompileTransforms(profile); err != nil {
97
+ log.Warningf("invalid profile '%s': %v", path, err)
98
+ return nil
99
+ }
100
101
profile.removeConstantMetrics()
102
src/go/plugin/go.d/collector/snmp/ddsnmp/metric.go
new
+26
@@ -0,0 +1,26 @@
1
+package ddsnmp
2
+
3
+import (
4
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
5
+)
6
+
7
+type ProfileMetrics struct {
8
+ Source string
9
+ DeviceMetadata map[string]string
10
+ Tags map[string]string
11
+ Metrics []Metric
12
+}
13
+
14
+type Metric struct {
15
+ Profile *ProfileMetrics
16
+ Name string
17
+ Description string
18
+ Family string
19
+ Unit string
20
+ MetricType ddprofiledefinition.ProfileMetricType
21
+ StaticTags map[string]string
22
+ Tags map[string]string
23
+ Mappings map[int64]string
24
+ IsTable bool
25
+ Value int64
26
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/transform.go
new
+122
@@ -0,0 +1,122 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmp
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "text/template"
9
+
10
+ "github.com/Masterminds/sprig/v3"
11
+)
12
+
13
+// CompileTransforms exported for testing purposes only
14
+func CompileTransforms(prof *Profile) error {
15
+ var errs []error
16
+
17
+ for i := range prof.Definition.Metrics {
18
+ metric := &prof.Definition.Metrics[i]
19
+
20
+ switch {
21
+ case metric.IsScalar():
22
+ if metric.Symbol.Transform == "" {
23
+ continue
24
+ }
25
+ tmpl, err := compileTransform(metric.Symbol.Transform)
26
+ if err != nil {
27
+ errs = append(errs, fmt.Errorf("metric_transform parsing failed for scalar metric '%s': %v",
28
+ metric.Symbol.Name, err))
29
+ continue
30
+ }
31
+ metric.Symbol.TransformCompiled = tmpl
32
+ case metric.IsColumn():
33
+ for j := range metric.Symbols {
34
+ sym := &metric.Symbols[j]
35
+ if sym.Transform == "" {
36
+ continue
37
+ }
38
+ tmpl, err := compileTransform(sym.Transform)
39
+ if err != nil {
40
+ errs = append(errs, fmt.Errorf("metric_transform parsing failed for table '%s' metric '%s': %v",
41
+ metric.Table.Name, sym.Name, err))
42
+ continue
43
+ }
44
+ sym.TransformCompiled = tmpl
45
+ }
46
+ }
47
+ }
48
+
49
+ if len(errs) > 0 {
50
+ return errors.Join(errs...)
51
+
52
+ }
53
+
54
+ return nil
55
+}
56
+
57
+func compileTransform(transform string) (*template.Template, error) {
58
+ return template.New("metric_transform").
59
+ Option("missingkey=zero").
60
+ Funcs(newMetricTransformFuncMap()).
61
+ Parse(transform)
62
+}
63
+
64
+func newMetricTransformFuncMap() template.FuncMap {
65
+ fm := sprig.TxtFuncMap()
66
+
67
+ extra := map[string]any{
68
+ "deleteTag": func(m *Metric, key string) interface{} {
69
+ delete(m.Tags, key)
70
+ return nil
71
+ },
72
+ "setName": func(m *Metric, name string) string {
73
+ m.Name = name
74
+ return ""
75
+ },
76
+ "setUnit": func(m *Metric, unit string) string {
77
+ m.Unit = unit
78
+ return ""
79
+ },
80
+ "setFamily": func(m *Metric, family string) string {
81
+ m.Family = family
82
+ return ""
83
+ },
84
+ "setDesc": func(m *Metric, desc string) string {
85
+ m.Description = desc
86
+ return ""
87
+ },
88
+ "setValue": func(m *Metric, value int64) string {
89
+ m.Value = value
90
+ return ""
91
+ },
92
+ "setMappings": func(m *Metric, mappings map[int64]string) string {
93
+ m.Mappings = mappings
94
+ return ""
95
+ },
96
+ "i64map": func(pairs ...any) map[int64]string {
97
+ m := make(map[int64]string)
98
+ for i := 0; i < len(pairs)-1; i += 2 {
99
+ key := int64(0)
100
+ switch k := pairs[i].(type) {
101
+ case int:
102
+ key = int64(k)
103
+ case int64:
104
+ key = k
105
+ case float64:
106
+ key = int64(k)
107
+ }
108
+
109
+ if val, ok := pairs[i+1].(string); ok {
110
+ m[key] = val
111
+ }
112
+ }
113
+ return m
114
+ },
115
+ }
116
+
117
+ for name, fn := range extra {
118
+ fm[name] = fn
119
+ }
120
+
121
+ return fm
122
+}
src/go/plugin/go.d/config/go.d/snmp.profiles/default/mikrotik-router.yaml
+34
-23
@@ -174,37 +174,48 @@ metrics:
174
name: mtxrHlSensorValue
175
description: Sensor value
176
family: Health/Sensors
177
+ transform: |
178
+ {{- $sensorType := index .Metric.Tags "sensor_type" | default "" -}}
179
+ {{- $config := get (dict
180
+ "1" (dict "name" "temperature" "unit" "celsius" "family" "Temperature"
181
+ "desc" "Temperature reading")
182
+ "2" (dict "name" "fan_speed" "unit" "rpm" "family" "Fan"
183
+ "desc" "Fan rotation speed")
184
+ "3" (dict "name" "voltage" "unit" "volts" "family" "Power" "divisor" 10.0
185
+ "desc" "Voltage measurement")
186
+ "4" (dict "name" "current" "unit" "amperes" "family" "Power" "divisor" 10.0
187
+ "desc" "Current draw")
188
+ "5" (dict "name" "power" "unit" "watts" "family" "Power" "divisor" 10.0
189
+ "desc" "Power consumption")
190
+ "6" (dict "name" "sensor_status" "family" "Status"
191
+ "mapping" (i64map 0 "absent_or_faulty" 1 "present_and_ok")
192
+ "desc" "Component presence and operational status")
193
+ "7" (dict "name" "sensor_state" "family" "Status"
194
+ "mapping" (i64map 0 "false" 1 "true")
195
+ "desc" "Boolean sensor state")
196
+ "8" (dict "name" "usage_percentage" "unit" "percentage" "family" "Usage"
197
+ "desc" "Resource utilization percentage")
198
+ ) $sensorType -}}
199
+
200
+ {{- if $config -}}
201
+ {{- setName .Metric (printf "%s_%s" .Metric.Name (get $config "name")) -}}
202
+ {{- setFamily .Metric (printf "Health/Sensors/%s" (get $config "family")) -}}
203
+ {{- with get $config "desc" -}}{{- setDesc $.Metric . -}}{{- end -}}
204
+ {{- with get $config "unit" -}}{{- setUnit $.Metric . -}}{{- end -}}
205
+ {{- with get $config "divisor" -}}{{- setValue $.Metric (int64 (div (float64 $.Value) .)) -}}{{- end -}}
206
+ {{- with get $config "mapping" -}}{{- setMappings $.Metric . -}}{{- end -}}
207
+ {{- end -}}
208
+
209
+ {{- deleteTag .Metric "sensor_type" -}}
210
metric_tags:
211
- tag: sensor_name
212
symbol:
213
OID: 1.3.6.1.4.1.14988.1.1.3.100.1.2
214
name: mtxrHlSensorName
182
- - tag: "_unit"
215
+ - tag: sensor_type
216
symbol:
217
OID: 1.3.6.1.4.1.14988.1.1.3.100.1.4
218
name: mtxrHlSensorUnit
186
- mapping:
187
- 1: celsius
188
- 2: rpm
189
- 3: dV # decivolts
190
- 4: dA # deciamps
191
- 5: dW # deciwatts
192
- 6: status
193
- 7: boolean
194
- 8: percentage
195
- - tag: "_metric_suffix"
196
- symbol:
197
- OID: 1.3.6.1.4.1.14988.1.1.3.100.1.4
198
- name: mtxrHlSensorType
199
- mapping:
200
- 1: temperature
201
- 2: fan_speed
202
- 3: voltage
203
- 4: current
204
- 5: power
205
- 6: state
206
- 7: boolean
207
- 8: percentage
219
220
metric_tags:
221
- OID: 1.3.6.1.4.1.14988.1.1.4.1.0