fix(go.d/ddsnmp): correct profile matching, metadata precedence, and OID handling (#20819)
Ilya Mashchenko committed
Aug 14, 2025 at 06:10 UTC
525a28886c3438cbb011a8e1aa935a7ee8d1146a
11 files changed
+401
-296
src/go/plugin/go.d/collector/snmp/collect.go
+2
-4
@@ -104,7 +104,7 @@ func (c *Collector) walkAll(rootOid string) ([]gosnmp.SnmpPDU, error) {
104
return c.snmpClient.BulkWalkAll(rootOid)
105
}
106
107
-func (c *Collector) setupVnode(si *snmpsd.SysInfo, deviceMeta map[string]map[string]string) *vnodes.VirtualNode {
107
+func (c *Collector) setupVnode(si *snmpsd.SysInfo, deviceMeta map[string]string) *vnodes.VirtualNode {
108
if c.Vnode.GUID == "" {
109
c.Vnode.GUID = uuid.NewSHA1(uuid.NameSpaceDNS, []byte(c.Hostname)).String()
110
}
@@ -130,9 +130,7 @@ func (c *Collector) setupVnode(si *snmpsd.SysInfo, deviceMeta map[string]map[str
130
}
131
132
maps.Copy(labels, c.Vnode.Labels)
133
- for _, meta := range deviceMeta {
134
- maps.Copy(labels, meta)
135
- }
133
+ maps.Copy(labels, deviceMeta)
134
135
if _, ok := labels["sys_object_id"]; !ok {
136
labels["sys_object_id"] = si.SysObjectID
src/go/plugin/go.d/collector/snmp/collector.go
+1
@@ -38,6 +38,7 @@ func New() *Collector {
38
EnableProfiles: true,
39
EnableProfilesTableMetrics: true,
40
VnodeDeviceDownThreshold: 3,
41
+ Community: "public",
42
Options: Options{
43
Port: 161,
44
Retries: 1,
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+5
-4
@@ -66,15 +66,16 @@ type (
66
}
67
)
68
69
-func (c *Collector) CollectDeviceMetadata() (map[string]map[string]string, error) {
70
- meta := make(map[string]map[string]string)
69
+func (c *Collector) CollectDeviceMetadata() (map[string]string, error) {
70
+ meta := make(map[string]string)
71
72
for _, prof := range c.profiles {
73
- deviceMeta, err := c.deviceMetadataCollector.Collect(prof.profile)
73
+ profDeviceMeta, err := c.deviceMetadataCollector.Collect(prof.profile)
74
if err != nil {
75
return nil, err
76
}
77
- meta[prof.profile.SourceFile] = deviceMeta
77
+
78
+ mergeTagsIfAbsent(meta, profDeviceMeta)
79
}
80
81
return meta, nil
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collectors_meta.go
+20
-34
@@ -10,7 +10,6 @@ import (
10
"github.com/gosnmp/gosnmp"
11
12
"github.com/netdata/netdata/go/plugins/logger"
13
- "github.com/netdata/netdata/go/plugins/pkg/matcher"
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
)
@@ -50,7 +49,7 @@ func (gc *globalTagsCollector) Collect(prof *ddsnmp.Profile) (map[string]string,
49
}
50
51
func (gc *globalTagsCollector) processStaticTags(staticTags []string, globalTags map[string]string) {
53
- mergeTagsWithEmptyFallback(globalTags, parseStaticTags(staticTags))
52
+ mergeTagsIfAbsent(globalTags, parseStaticTags(staticTags))
53
}
54
55
// processDynamicTags processes tags that require SNMP fetching
@@ -171,43 +170,30 @@ func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]str
170
171
meta := make(map[string]string)
172
174
- if err := dc.processMetadataFields(cfg.Fields, meta); err != nil {
175
- return ternary(len(meta) > 0, meta, nil), fmt.Errorf("failed to process metadata resource '%s': %w", resName, err)
176
- }
177
-
178
- if dc.sysobjectid == "" || len(prof.Definition.SysobjectIDMetadata) == 0 {
179
- return meta, nil
180
- }
181
-
182
- for i, entry := range prof.Definition.SysobjectIDMetadata {
183
- m, err := matcher.NewRegExpMatcher(entry.SysobjectID)
184
- if err != nil {
185
- dc.log.Warningf("sysobjectid_metadata[%d]: failed to compile regular expression from sysobjectid '%s': %v",
186
- i, entry.SysobjectID, err)
187
- continue
188
- }
189
- if m.MatchString(dc.sysobjectid) {
190
- if err := dc.processMetadataFields(entry.Metadata, meta); err != nil {
191
- dc.log.Warningf("sysobjectid_metadata[%d]: failed to process metadata fields for sysobjectid '%s': %v",
192
- i, entry.SysobjectID, err)
193
- continue
173
+ if dc.sysobjectid != "" {
174
+ for i, entry := range prof.Definition.SysobjectIDMetadata {
175
+ if ddsnmp.OidMatches(dc.sysobjectid, entry.SysobjectID) {
176
+ if err := dc.processMetadataFields(entry.Metadata, meta); err != nil {
177
+ dc.log.Warningf("sysobjectid_metadata[%d]: failed to process metadata fields for sysobjectid '%s': %v",
178
+ i, entry.SysobjectID, err)
179
+ continue
180
+ }
181
+ dc.log.Debugf("sysobjectid_metadata[%d]: matched sysobjectid '%s' with device OID '%s', applying metadata overrides",
182
+ i, entry.SysobjectID, dc.sysobjectid)
183
}
195
- dc.log.Debugf("sysobjectid_metadata[%d]: matched sysobjectid '%s' with device OID '%s', applying metadata overrides",
196
- i, entry.SysobjectID, dc.sysobjectid)
184
}
185
}
186
187
+ if err := dc.processMetadataFields(cfg.Fields, meta); err != nil {
188
+ return ternary(len(meta) > 0, meta, nil), fmt.Errorf("failed to process metadata resource '%s': %w", resName, err)
189
+ }
190
+
191
return meta, nil
192
}
193
194
// processMetadataFields processes a single metadata resource
195
func (dc *deviceMetadataCollector) processMetadataFields(fields map[string]ddprofiledefinition.MetadataField, metadata map[string]string) error {
205
- staticValues := make(map[string]string)
206
- oids := dc.collectStaticAndIdentifyOIDs(fields, staticValues)
207
-
208
- for k, v := range staticValues {
209
- metadata[k] = v
210
- }
196
+ oids := dc.collectStaticAndIdentifyOIDs(fields, metadata)
197
198
if len(oids) == 0 {
199
return nil
@@ -222,13 +208,13 @@ func (dc *deviceMetadataCollector) processMetadataFields(fields map[string]ddpro
208
}
209
210
// collectStaticAndIdentifyOIDs collects static values and returns OIDs to fetch
225
-func (dc *deviceMetadataCollector) collectStaticAndIdentifyOIDs(fields map[string]ddprofiledefinition.MetadataField, staticValues map[string]string) []string {
211
+func (dc *deviceMetadataCollector) collectStaticAndIdentifyOIDs(fields map[string]ddprofiledefinition.MetadataField, metadata map[string]string) []string {
212
var oids []string
213
214
for name, field := range fields {
215
switch {
216
case field.Value != "":
231
- staticValues[name] = field.Value
217
+ mergeTagsIfAbsent(metadata, map[string]string{name: field.Value})
218
case field.Symbol.OID != "":
219
if !dc.missingOIDs[trimOID(field.Symbol.OID)] {
220
oids = append(oids, field.Symbol.OID)
@@ -284,7 +270,7 @@ func (dc *deviceMetadataCollector) processDynamicFields(fields map[string]ddprof
270
continue
271
}
272
if v != "" {
287
- mergeTagsWithEmptyFallback(metadata, map[string]string{name: v})
273
+ mergeTagsIfAbsent(metadata, map[string]string{name: v})
274
}
275
case len(field.Symbols) > 0:
276
// Multiple symbols - try each until one succeeds
@@ -296,7 +282,7 @@ func (dc *deviceMetadataCollector) processDynamicFields(fields map[string]ddprof
282
continue
283
}
284
if v != "" {
299
- mergeTagsWithEmptyFallback(metadata, map[string]string{name: v})
285
+ mergeTagsIfAbsent(metadata, map[string]string{name: v})
286
break // Use first successful value
287
}
288
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collectors_meta_test.go
+14
-8
@@ -947,6 +947,9 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
947
"vendor": {
948
Value: "Cisco",
949
},
950
+ "platform": {
951
+ Value: "Default Platform", // Will be overridden
952
+ },
953
},
954
},
955
},
@@ -955,7 +958,7 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
958
SysobjectID: "1.3.6.1.4.1.9.*", // All Cisco devices
959
Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
960
"platform": {
958
- Value: "Enterprise",
961
+ Value: "Enterprise", // Overrides base metadata
962
},
963
"support": {
964
Value: "Premium",
@@ -971,6 +974,9 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
974
"series": {
975
Value: "ASA",
976
},
977
+ "support": {
978
+ Value: "Standard", // Won't override "Premium" - first wins
979
+ },
980
},
981
},
982
{
@@ -980,7 +986,7 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
986
Value: "ASA5510",
987
},
988
"series": {
983
- Value: "ASA5500", // Override the previous series value
989
+ Value: "ASA5500", // Won't override "ASA" - first wins
990
},
991
},
992
},
@@ -990,12 +996,12 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
996
setupMock: func(m *snmpmock.MockHandler) {},
997
sysobjectid: "1.3.6.1.4.1.9.1.669",
998
expectedResult: map[string]string{
993
- "vendor": "Cisco",
994
- "platform": "Enterprise",
995
- "support": "Premium",
996
- "type": "Firewall",
997
- "series": "ASA5500", // Last match wins
998
- "model": "ASA5510",
999
+ "vendor": "Cisco", // From base metadata (not overridden)
1000
+ "platform": "Enterprise", // From first sysobjectid match (overrides base "Default Platform")
1001
+ "support": "Premium", // From first sysobjectid match (second match can't override)
1002
+ "type": "Firewall", // From second sysobjectid match
1003
+ "series": "ASA", // From second match (third match can't override)
1004
+ "model": "ASA5510", // From third sysobjectid match
1005
},
1006
expectedError: false,
1007
},
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/utils.go
+1
-1
@@ -214,7 +214,7 @@ func isInt(s string) bool {
214
return err == nil
215
}
216
217
-func mergeTagsWithEmptyFallback(dest, src map[string]string) {
217
+func mergeTagsIfAbsent(dest, src map[string]string) {
218
for k, v := range src {
219
if existing, ok := dest[k]; !ok || existing == "" {
220
dest[k] = v
src/go/plugin/go.d/collector/snmp/ddsnmp/profile.go
+69
-43
@@ -6,38 +6,52 @@ import (
6
"errors"
7
"fmt"
8
"path/filepath"
9
+ "regexp"
10
"slices"
11
"sort"
12
"strings"
13
13
- "github.com/netdata/netdata/go/plugins/pkg/matcher"
14
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
)
16
17
+var oidOnly = regexp.MustCompile(`^[0-9]+(\.[0-9]+)*$`)
18
+
19
+func OidMatches(sysObjId, id string) bool {
20
+ if oidOnly.MatchString(id) {
21
+ return sysObjId == id
22
+ }
23
+ re, err := regexp.Compile(id)
24
+ if err != nil {
25
+ log.Warningf("invalid regex %q: %v", id, err)
26
+ return false
27
+ }
28
+ return re.MatchString(sysObjId)
29
+}
30
+
31
// FindProfiles returns profiles matching the given sysObjectID.
18
-// Profiles are loaded once on the first call and cached globally.
32
+// Profiles are sorted by match specificity: most specific first.
33
func FindProfiles(sysObjId string) []*Profile {
34
loadProfiles()
35
36
+ matchedOIDs := make(map[*Profile]string)
37
var profiles []*Profile
38
39
for _, prof := range ddProfiles {
40
+ // Use first matching OID (profile author's responsibility to order them)
41
for _, id := range prof.Definition.SysObjectIDs {
26
- m, err := matcher.NewRegExpMatcher(id)
27
- if err != nil {
28
- log.Warningf("failed to compile regular expression from '%s': %v", id, err)
29
- continue
30
- }
31
- if m.MatchString(sysObjId) {
32
- profiles = append(profiles, prof.clone())
42
+ if OidMatches(sysObjId, id) {
43
+ cloned := prof.clone()
44
+ profiles = append(profiles, cloned)
45
+ matchedOIDs[cloned] = id
46
break
47
}
48
}
49
}
50
51
+ sortProfilesBySpecificity(profiles, matchedOIDs)
52
+
53
enrichProfiles(profiles)
54
deduplicateMetricsAcrossProfiles(profiles)
40
-
55
return profiles
56
}
57
@@ -114,11 +128,11 @@ func cloneExtensionHierarchy(extensions []*extensionInfo) []*extensionInfo {
128
}
129
130
func (p *Profile) merge(base *Profile) {
131
+ p.mergeMetadata(base)
132
p.mergeMetrics(base)
133
// Append other fields as before (these likely don't need deduplication)
134
p.Definition.MetricTags = append(p.Definition.MetricTags, base.Definition.MetricTags...)
135
p.Definition.StaticTags = append(p.Definition.StaticTags, base.Definition.StaticTags...)
121
- p.mergeMetadata(base)
136
}
137
138
func (p *Profile) mergeMetrics(base *Profile) {
@@ -193,6 +207,19 @@ func (p *Profile) mergeMetadata(base *Profile) {
207
208
p.Definition.Metadata[resName] = targetRes
209
}
210
+
211
+ if len(base.Definition.SysobjectIDMetadata) > 0 {
212
+ existingOIDs := make(map[string]bool)
213
+ for _, entry := range p.Definition.SysobjectIDMetadata {
214
+ existingOIDs[entry.SysobjectID] = true
215
+ }
216
+
217
+ for _, baseEntry := range base.Definition.SysobjectIDMetadata {
218
+ if !existingOIDs[baseEntry.SysobjectID] {
219
+ p.Definition.SysobjectIDMetadata = append(p.Definition.SysobjectIDMetadata, baseEntry)
220
+ }
221
+ }
222
+ }
223
}
224
225
func (p *Profile) validate() error {
@@ -239,6 +266,34 @@ func (p *Profile) removeConstantMetrics() {
266
})
267
}
268
269
+// sortProfilesBySpecificity sorts profiles by their match specificity.
270
+// More specific profiles (longer OIDs, exact matches) come first.
271
+// The matchedOIDs map contains the OID that matched for each profile.
272
+func sortProfilesBySpecificity(profiles []*Profile, matchedOIDs map[*Profile]string) {
273
+ slices.SortStableFunc(profiles, func(a, b *Profile) int {
274
+ aOID := matchedOIDs[a]
275
+ bOID := matchedOIDs[b]
276
+
277
+ // 1. Longer OIDs first (more specific)
278
+ if diff := len(bOID) - len(aOID); diff != 0 {
279
+ return diff
280
+ }
281
+
282
+ // 2. Same length: exact OIDs before patterns
283
+ aIsExact := oidOnly.MatchString(aOID)
284
+ bIsExact := oidOnly.MatchString(bOID)
285
+ if aIsExact != bIsExact {
286
+ if aIsExact {
287
+ return -1
288
+ }
289
+ return 1
290
+ }
291
+
292
+ // 3. Same type: lexicographic order for stability
293
+ return strings.Compare(aOID, bOID)
294
+ })
295
+}
296
+
297
func enrichProfiles(profiles []*Profile) {
298
for _, prof := range profiles {
299
if prof.Definition == nil {
@@ -268,38 +323,8 @@ func deduplicateMetricsAcrossProfiles(profiles []*Profile) {
323
return
324
}
325
271
- // Create a slice of indices sorted by priority (non-generic first)
272
- type indexedProfile struct {
273
- idx int
274
- isGeneric bool
275
- }
276
-
277
- indexed := make([]indexedProfile, len(profiles))
278
- for i, prof := range profiles {
279
- indexed[i] = indexedProfile{
280
- idx: i,
281
- isGeneric: strings.Contains(strings.ToLower(prof.SourceFile), "generic"),
282
- }
283
- }
284
-
285
- slices.SortFunc(indexed, func(a, b indexedProfile) int {
286
- if a.isGeneric && !b.isGeneric {
287
- return 1 // a comes after b
288
- }
289
- if !a.isGeneric && b.isGeneric {
290
- return -1 // a comes before b
291
- }
292
- // If both are generic or both are non-generic, maintain original order
293
- return a.idx - b.idx
294
- })
295
-
296
- // Reorder profiles slice according to deduplication priority
297
- sortedProfiles := make([]*Profile, len(profiles))
298
- for i, ip := range indexed {
299
- sortedProfiles[i] = profiles[ip.idx]
300
- }
301
- copy(profiles, sortedProfiles)
302
-
326
+ // Profiles are already sorted by specificity from FindProfiles
327
+ // Just deduplicate metrics, keeping the first occurrence (most specific)
328
seenMetrics := make(map[string]bool)
329
seenVmetrics := make(map[string]bool)
330
@@ -319,6 +344,7 @@ func deduplicateMetricsAcrossProfiles(profiles []*Profile) {
344
return false
345
},
346
)
347
+
348
prof.Definition.VirtualMetrics = slices.DeleteFunc(
349
prof.Definition.VirtualMetrics,
350
func(vm ddprofiledefinition.VirtualMetricConfig) bool {
src/go/plugin/go.d/collector/snmp/ddsnmp/profile_test.go
+228
-199
@@ -59,6 +59,10 @@ func Test_FindProfiles(t *testing.T) {
59
sysObjOId: "0.1.2.3",
60
wanProfiles: 0,
61
},
62
+ "Cisco-WLC-5520": {
63
+ sysObjOId: "1.3.6.1.4.1.9.1.2170",
64
+ wanProfiles: 3,
65
+ },
66
}
67
68
for name, test := range test {
@@ -141,10 +145,10 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
145
},
146
},
147
},
144
- "duplicate scalar metrics across profiles": {
148
+ "duplicate scalar metrics across profiles - first wins": {
149
profiles: []*Profile{
150
{
147
- SourceFile: "profile1.yaml",
151
+ SourceFile: "specific-profile.yaml", // Most specific, comes first
152
Definition: &ddprofiledefinition.ProfileDefinition{
153
Metrics: []ddprofiledefinition.MetricsConfig{
154
{
@@ -157,7 +161,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
161
},
162
},
163
{
160
- SourceFile: "profile2.yaml",
164
+ SourceFile: "generic-profile.yaml", // Less specific, comes second
165
Definition: &ddprofiledefinition.ProfileDefinition{
166
Metrics: []ddprofiledefinition.MetricsConfig{
167
{
@@ -178,7 +182,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
182
},
183
expected: []*Profile{
184
{
181
- SourceFile: "profile1.yaml",
185
+ SourceFile: "specific-profile.yaml",
186
Definition: &ddprofiledefinition.ProfileDefinition{
187
Metrics: []ddprofiledefinition.MetricsConfig{
188
{
@@ -191,7 +195,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
195
},
196
},
197
{
194
- SourceFile: "profile2.yaml",
198
+ SourceFile: "generic-profile.yaml",
199
Definition: &ddprofiledefinition.ProfileDefinition{
200
Metrics: []ddprofiledefinition.MetricsConfig{
201
{
@@ -208,7 +212,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
212
"duplicate table metrics - exact same symbols": {
213
profiles: []*Profile{
214
{
211
- SourceFile: "profile1.yaml",
215
+ SourceFile: "specific-profile.yaml",
216
Definition: &ddprofiledefinition.ProfileDefinition{
217
Metrics: []ddprofiledefinition.MetricsConfig{
218
{
@@ -240,7 +244,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
244
},
245
},
246
{
243
- SourceFile: "profile2.yaml",
247
+ SourceFile: "generic-profile.yaml",
248
Definition: &ddprofiledefinition.ProfileDefinition{
249
Metrics: []ddprofiledefinition.MetricsConfig{
250
{
@@ -275,7 +279,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
279
},
280
expected: []*Profile{
281
{
278
- SourceFile: "profile1.yaml",
282
+ SourceFile: "specific-profile.yaml",
283
Definition: &ddprofiledefinition.ProfileDefinition{
284
Metrics: []ddprofiledefinition.MetricsConfig{
285
{
@@ -307,7 +311,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
311
},
312
},
313
{
310
- SourceFile: "profile2.yaml",
314
+ SourceFile: "generic-profile.yaml",
315
Definition: &ddprofiledefinition.ProfileDefinition{
316
Metrics: []ddprofiledefinition.MetricsConfig{}, // Empty because it's a duplicate
317
},
@@ -477,10 +481,11 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
481
},
482
},
483
},
480
- "multiple duplicates across three profiles": {
484
+ "order matters - specific profiles first": {
485
+ // Profiles are already sorted by FindProfiles, most specific first
486
profiles: []*Profile{
487
{
483
- SourceFile: "generic-device.yaml",
488
+ SourceFile: "cisco-nexus-9000.yaml", // Most specific
489
Definition: &ddprofiledefinition.ProfileDefinition{
490
Metrics: []ddprofiledefinition.MetricsConfig{
491
{
@@ -490,22 +495,16 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
495
},
496
},
497
{
493
- Table: ddprofiledefinition.SymbolConfig{
494
- OID: "1.3.6.1.2.1.2.2",
495
- Name: "ifTable",
496
- },
497
- Symbols: []ddprofiledefinition.SymbolConfig{
498
- {
499
- OID: "1.3.6.1.2.1.2.2.1.10",
500
- Name: "ifInOctets",
501
- },
498
+ Symbol: ddprofiledefinition.SymbolConfig{
499
+ OID: "1.3.6.1.4.1.9.9.305.1.1.1.0",
500
+ Name: "cempMemPoolUsed",
501
},
502
},
503
},
504
},
505
},
506
{
508
- SourceFile: "vendor-specific.yaml",
507
+ SourceFile: "cisco-nexus.yaml", // Less specific
508
Definition: &ddprofiledefinition.ProfileDefinition{
509
Metrics: []ddprofiledefinition.MetricsConfig{
510
{
@@ -524,9 +523,15 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
523
},
524
},
525
{
527
- SourceFile: "extended.yaml",
526
+ SourceFile: "generic-device.yaml", // Least specific
527
Definition: &ddprofiledefinition.ProfileDefinition{
528
Metrics: []ddprofiledefinition.MetricsConfig{
529
+ {
530
+ Symbol: ddprofiledefinition.SymbolConfig{
531
+ OID: "1.3.6.1.2.1.1.3.0",
532
+ Name: "sysUpTime",
533
+ },
534
+ },
535
{
536
Table: ddprofiledefinition.SymbolConfig{
537
OID: "1.3.6.1.2.1.2.2",
@@ -545,7 +550,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
550
},
551
expected: []*Profile{
552
{
548
- SourceFile: "vendor-specific.yaml",
553
+ SourceFile: "cisco-nexus-9000.yaml",
554
Definition: &ddprofiledefinition.ProfileDefinition{
555
Metrics: []ddprofiledefinition.MetricsConfig{
556
{
@@ -554,6 +559,19 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
559
Name: "sysUpTime",
560
},
561
},
562
+ {
563
+ Symbol: ddprofiledefinition.SymbolConfig{
564
+ OID: "1.3.6.1.4.1.9.9.305.1.1.1.0",
565
+ Name: "cempMemPoolUsed",
566
+ },
567
+ },
568
+ },
569
+ },
570
+ },
571
+ {
572
+ SourceFile: "cisco-nexus.yaml",
573
+ Definition: &ddprofiledefinition.ProfileDefinition{
574
+ Metrics: []ddprofiledefinition.MetricsConfig{
575
{
576
Symbol: ddprofiledefinition.SymbolConfig{
577
OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.7",
@@ -564,7 +582,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
582
},
583
},
584
{
567
- SourceFile: "extended.yaml",
585
+ SourceFile: "generic-device.yaml",
586
Definition: &ddprofiledefinition.ProfileDefinition{
587
Metrics: []ddprofiledefinition.MetricsConfig{
588
{
@@ -582,12 +600,6 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
600
},
601
},
602
},
585
- {
586
- SourceFile: "generic-device.yaml",
587
- Definition: &ddprofiledefinition.ProfileDefinition{
588
- Metrics: []ddprofiledefinition.MetricsConfig{}, // All removed as duplicates
589
- },
590
- },
603
},
604
},
605
"empty metrics in some profiles": {
@@ -648,175 +660,7 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
660
{
661
SourceFile: "profile3.yaml",
662
Definition: &ddprofiledefinition.ProfileDefinition{
651
- Metrics: []ddprofiledefinition.MetricsConfig{},
652
- },
653
- },
654
- },
655
- },
656
- "generic vs non-generic priority": {
657
- profiles: []*Profile{
658
- {
659
- SourceFile: "generic-device.yaml",
660
- Definition: &ddprofiledefinition.ProfileDefinition{
661
- Metrics: []ddprofiledefinition.MetricsConfig{
662
- {
663
- Symbol: ddprofiledefinition.SymbolConfig{
664
- OID: "1.3.6.1.2.1.1.3.0",
665
- Name: "sysUpTime",
666
- },
667
- },
668
- {
669
- Table: ddprofiledefinition.SymbolConfig{
670
- OID: "1.3.6.1.2.1.2.2",
671
- Name: "ifTable",
672
- },
673
- Symbols: []ddprofiledefinition.SymbolConfig{
674
- {
675
- OID: "1.3.6.1.2.1.2.2.1.10",
676
- Name: "ifInOctets",
677
- },
678
- {
679
- OID: "1.3.6.1.2.1.2.2.1.16",
680
- Name: "ifOutOctets",
681
- },
682
- },
683
- },
684
- {
685
- Symbol: ddprofiledefinition.SymbolConfig{
686
- OID: "1.3.6.1.2.1.1.1.0",
687
- Name: "sysDescr",
688
- },
689
- },
690
- },
691
- },
692
- },
693
- {
694
- SourceFile: "mikrotik-router.yaml",
695
- Definition: &ddprofiledefinition.ProfileDefinition{
696
- Metrics: []ddprofiledefinition.MetricsConfig{
697
- {
698
- Symbol: ddprofiledefinition.SymbolConfig{
699
- OID: "1.3.6.1.2.1.1.3.0",
700
- Name: "sysUpTime",
701
- },
702
- },
703
- {
704
- Table: ddprofiledefinition.SymbolConfig{
705
- OID: "1.3.6.1.2.1.2.2",
706
- Name: "ifTable",
707
- },
708
- Symbols: []ddprofiledefinition.SymbolConfig{
709
- {
710
- OID: "1.3.6.1.2.1.2.2.1.10",
711
- Name: "ifInOctets",
712
- },
713
- {
714
- OID: "1.3.6.1.2.1.2.2.1.16",
715
- Name: "ifOutOctets",
716
- },
717
- },
718
- },
719
- {
720
- Symbol: ddprofiledefinition.SymbolConfig{
721
- OID: "1.3.6.1.4.1.14988.1.1.1.3.0",
722
- Name: "mtxrHlCpuTemperature",
723
- },
724
- },
725
- },
726
- },
727
- },
728
- {
729
- SourceFile: "generic-if.yaml",
730
- Definition: &ddprofiledefinition.ProfileDefinition{
731
- Metrics: []ddprofiledefinition.MetricsConfig{
732
- {
733
- Symbol: ddprofiledefinition.SymbolConfig{
734
- OID: "1.3.6.1.2.1.1.1.0",
735
- Name: "sysDescr",
736
- },
737
- },
738
- {
739
- Table: ddprofiledefinition.SymbolConfig{
740
- OID: "1.3.6.1.2.1.31.1.1",
741
- Name: "ifXTable",
742
- },
743
- Symbols: []ddprofiledefinition.SymbolConfig{
744
- {
745
- OID: "1.3.6.1.2.1.31.1.1.1.6",
746
- Name: "ifHCInOctets",
747
- },
748
- },
749
- },
750
- },
751
- },
752
- },
753
- },
754
- expected: []*Profile{
755
- {
756
- SourceFile: "mikrotik-router.yaml",
757
- Definition: &ddprofiledefinition.ProfileDefinition{
758
- Metrics: []ddprofiledefinition.MetricsConfig{
759
- {
760
- Symbol: ddprofiledefinition.SymbolConfig{
761
- OID: "1.3.6.1.2.1.1.3.0",
762
- Name: "sysUpTime",
763
- },
764
- },
765
- {
766
- Table: ddprofiledefinition.SymbolConfig{
767
- OID: "1.3.6.1.2.1.2.2",
768
- Name: "ifTable",
769
- },
770
- Symbols: []ddprofiledefinition.SymbolConfig{
771
- {
772
- OID: "1.3.6.1.2.1.2.2.1.10",
773
- Name: "ifInOctets",
774
- },
775
- {
776
- OID: "1.3.6.1.2.1.2.2.1.16",
777
- Name: "ifOutOctets",
778
- },
779
- },
780
- },
781
- {
782
- Symbol: ddprofiledefinition.SymbolConfig{
783
- OID: "1.3.6.1.4.1.14988.1.1.1.3.0",
784
- Name: "mtxrHlCpuTemperature",
785
- },
786
- },
787
- },
788
- },
789
- },
790
- {
791
- SourceFile: "generic-device.yaml",
792
- Definition: &ddprofiledefinition.ProfileDefinition{
793
- Metrics: []ddprofiledefinition.MetricsConfig{
794
- {
795
- Symbol: ddprofiledefinition.SymbolConfig{
796
- OID: "1.3.6.1.2.1.1.1.0",
797
- Name: "sysDescr",
798
- },
799
- },
800
- },
801
- },
802
- },
803
- {
804
- SourceFile: "generic-if.yaml",
805
- Definition: &ddprofiledefinition.ProfileDefinition{
806
- Metrics: []ddprofiledefinition.MetricsConfig{
807
- {
808
- Table: ddprofiledefinition.SymbolConfig{
809
- OID: "1.3.6.1.2.1.31.1.1",
810
- Name: "ifXTable",
811
- },
812
- Symbols: []ddprofiledefinition.SymbolConfig{
813
- {
814
- OID: "1.3.6.1.2.1.31.1.1.1.6",
815
- Name: "ifHCInOctets",
816
- },
817
- },
818
- },
819
- },
663
+ Metrics: []ddprofiledefinition.MetricsConfig{}, // Removed as duplicate
664
},
665
},
666
},
@@ -857,6 +701,191 @@ func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
701
}
702
}
703
704
+func TestSortProfilesBySpecificity(t *testing.T) {
705
+ tests := map[string]struct {
706
+ profiles []string // Source file names
707
+ matchedOIDs []string // OIDs in the same order as profiles
708
+ expected []string // Expected order of profile source files
709
+ }{
710
+ "empty profiles": {
711
+ profiles: []string{},
712
+ matchedOIDs: []string{},
713
+ expected: []string{},
714
+ },
715
+ "single profile": {
716
+ profiles: []string{"cisco.yaml"},
717
+ matchedOIDs: []string{"1.3.6.1.4.1.9.1.669"},
718
+ expected: []string{"cisco.yaml"},
719
+ },
720
+ "exact OID vs pattern - same length": {
721
+ profiles: []string{"pattern.yaml", "exact.yaml"},
722
+ matchedOIDs: []string{"1.3.6.1.4.1.9.1.66*", "1.3.6.1.4.1.9.1.669"},
723
+ expected: []string{"exact.yaml", "pattern.yaml"},
724
+ },
725
+ "longer OID wins": {
726
+ profiles: []string{"short.yaml", "long.yaml", "medium.yaml"},
727
+ matchedOIDs: []string{"1.3.6.1.4.1.9.*", "1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1.*"},
728
+ expected: []string{"long.yaml", "medium.yaml", "short.yaml"},
729
+ },
730
+ "complex mix - length, exact, and patterns": {
731
+ profiles: []string{
732
+ "generic-wildcard.yaml",
733
+ "specific-exact.yaml",
734
+ "specific-pattern.yaml",
735
+ "mid-exact.yaml",
736
+ "mid-pattern.yaml",
737
+ },
738
+ matchedOIDs: []string{
739
+ "1.3.6.1.4.1.9.*",
740
+ "1.3.6.1.4.1.9.1.669",
741
+ "1.3.6.1.4.1.9.1.66*",
742
+ "1.3.6.1.4.1.9.1",
743
+ "1.3.6.1.4.1.9.*",
744
+ },
745
+ expected: []string{
746
+ "specific-exact.yaml", // Longest + exact
747
+ "specific-pattern.yaml", // Longest + pattern
748
+ "mid-exact.yaml", // Medium + exact
749
+ "generic-wildcard.yaml", // Short + pattern (first in input order)
750
+ "mid-pattern.yaml", // Short + pattern (second in input order)
751
+ },
752
+ },
753
+ "same OID different profiles - stable sort": {
754
+ profiles: []string{"profile-b.yaml", "profile-a.yaml", "profile-c.yaml"},
755
+ matchedOIDs: []string{"1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1.669"},
756
+ expected: []string{"profile-b.yaml", "profile-a.yaml", "profile-c.yaml"}, // Maintains input order
757
+ },
758
+ "regex patterns with special characters": {
759
+ profiles: []string{"exact.yaml", "regex-dots.yaml", "regex-plus.yaml", "regex-question.yaml"},
760
+ matchedOIDs: []string{"1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1...", "1.3.6.1.4.1.9.1.6+", "1.3.6.1.4.1.9.1.66?"},
761
+ expected: []string{
762
+ "exact.yaml", // Length 21, exact
763
+ "regex-question.yaml", // Length 20, pattern
764
+ "regex-dots.yaml", // Length 19, pattern ("..." < ".6+" lexicographically)
765
+ "regex-plus.yaml", // Length 19, pattern
766
+ },
767
+ },
768
+ "all exact OIDs - sort by OID value": {
769
+ profiles: []string{"oid-670.yaml", "oid-669.yaml", "oid-671.yaml"},
770
+ matchedOIDs: []string{"1.3.6.1.4.1.9.1.670", "1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1.671"},
771
+ expected: []string{"oid-669.yaml", "oid-670.yaml", "oid-671.yaml"},
772
+ },
773
+ "all patterns - sort by pattern value": {
774
+ profiles: []string{"pattern-b.yaml", "pattern-a.yaml", "pattern-c.yaml"},
775
+ matchedOIDs: []string{"1.3.6.1.4.1.9.1.b*", "1.3.6.1.4.1.9.1.a*", "1.3.6.1.4.1.9.1.c*"},
776
+ expected: []string{"pattern-a.yaml", "pattern-b.yaml", "pattern-c.yaml"},
777
+ },
778
+ "real-world scenario": {
779
+ profiles: []string{
780
+ "generic-device.yaml",
781
+ "cisco.yaml",
782
+ "cisco-asa.yaml",
783
+ "cisco-asa-5510.yaml",
784
+ },
785
+ matchedOIDs: []string{
786
+ "1.3.6.1.4.1.*",
787
+ "1.3.6.1.4.1.9.*",
788
+ "1.3.6.1.4.1.9.1.*",
789
+ "1.3.6.1.4.1.9.1.669",
790
+ },
791
+ expected: []string{
792
+ "cisco-asa-5510.yaml",
793
+ "cisco-asa.yaml",
794
+ "cisco.yaml",
795
+ "generic-device.yaml",
796
+ },
797
+ },
798
+ "mixed vendor OIDs": {
799
+ profiles: []string{
800
+ "dell.yaml",
801
+ "cisco.yaml",
802
+ "hp.yaml",
803
+ "generic.yaml",
804
+ },
805
+ matchedOIDs: []string{
806
+ "1.3.6.1.4.1.674.10892.5",
807
+ "1.3.6.1.4.1.9.1.669",
808
+ "1.3.6.1.4.1.11.2.3.7.11",
809
+ "1.3.6.1.4.1.*",
810
+ },
811
+ expected: []string{
812
+ "hp.yaml", // Longest OID (24 chars)
813
+ "dell.yaml", // Second longest (24 chars, but "11" < "674" lexicographically)
814
+ "cisco.yaml", // Third longest (21 chars)
815
+ "generic.yaml", // Shortest (pattern, 13 chars)
816
+ },
817
+ },
818
+ "edge case - empty OID": {
819
+ profiles: []string{"profile1.yaml", "profile2.yaml"},
820
+ matchedOIDs: []string{"", "1.3.6.1.4.1.9.1.669"},
821
+ expected: []string{"profile2.yaml", "profile1.yaml"},
822
+ },
823
+ "patterns with same prefix": {
824
+ profiles: []string{
825
+ "generic.yaml",
826
+ "specific.yaml",
827
+ "more-specific.yaml",
828
+ },
829
+ matchedOIDs: []string{
830
+ "1.3.6.1.4.1.9.*",
831
+ "1.3.6.1.4.1.9.1.*",
832
+ "1.3.6.1.4.1.9.1.6*",
833
+ },
834
+ expected: []string{
835
+ "more-specific.yaml",
836
+ "specific.yaml",
837
+ "generic.yaml",
838
+ },
839
+ },
840
+ }
841
+
842
+ for name, tc := range tests {
843
+ t.Run(name, func(t *testing.T) {
844
+ // Create profiles and matchedOIDs map
845
+ profiles := make([]*Profile, len(tc.profiles))
846
+ matchedOIDs := make(map[*Profile]string)
847
+
848
+ for i, sourceFile := range tc.profiles {
849
+ profiles[i] = &Profile{SourceFile: sourceFile}
850
+ if i < len(tc.matchedOIDs) {
851
+ matchedOIDs[profiles[i]] = tc.matchedOIDs[i]
852
+ }
853
+ }
854
+
855
+ sortProfilesBySpecificity(profiles, matchedOIDs)
856
+
857
+ actual := make([]string, len(profiles))
858
+ for i, p := range profiles {
859
+ actual[i] = p.SourceFile
860
+ }
861
+
862
+ assert.Equal(t, tc.expected, actual, "Profile order mismatch")
863
+ })
864
+ }
865
+}
866
+
867
+func TestSortProfilesBySpecificity_Stable(t *testing.T) {
868
+ // Create many profiles with the same OID
869
+ numProfiles := 100
870
+ profiles := make([]*Profile, numProfiles)
871
+ matchedOIDs := make(map[*Profile]string)
872
+
873
+ for i := 0; i < numProfiles; i++ {
874
+ profiles[i] = &Profile{
875
+ SourceFile: fmt.Sprintf("profile-%03d.yaml", i),
876
+ }
877
+ matchedOIDs[profiles[i]] = "1.3.6.1.4.1.9.1.669"
878
+ }
879
+
880
+ sortProfilesBySpecificity(profiles, matchedOIDs)
881
+
882
+ // Verify order is preserved (lexicographic due to same OID)
883
+ for i := 0; i < numProfiles; i++ {
884
+ expected := fmt.Sprintf("profile-%03d.yaml", i)
885
+ assert.Equal(t, expected, profiles[i].SourceFile)
886
+ }
887
+}
888
+
889
func Test_ProfileExtends_CircularReference(t *testing.T) {
890
tmp := t.TempDir()
891
src/go/plugin/go.d/config/go.d/snmp.profiles/default/aruba-mobility-controller.yaml
+1
-1
@@ -13,4 +13,4 @@ metadata:
13
device:
14
fields:
15
type:
16
- value: "WLC"
16
+ value: "Wireless LAN Controller"
src/go/plugin/go.d/config/go.d/snmp.profiles/default/cisco-legacy-wlc.yaml
+59
-1
@@ -17,7 +17,7 @@ metadata:
17
OID: 1.3.6.1.4.1.14179.1.1.1.4
18
name: agentInventorySerialNumber
19
type:
20
- value: "WLC"
20
+ value: "Wireless LAN Controller"
21
vendor:
22
value: "Cisco"
23
@@ -37,6 +37,64 @@ sysobjectid:
37
- 1.3.6.1.4.1.9.1.2171 # cisco8540Wlc
38
- 1.3.6.1.4.1.9.1.2427 # cisco3504WLC
39
40
+sysobjectid_metadata:
41
+ - sysobjectid: 1.3.6.1.4.1.9.1.818
42
+ metadata:
43
+ model:
44
+ value: NM-WLC-E
45
+ - sysobjectid: 1.3.6.1.4.1.9.1.828
46
+ metadata:
47
+ model:
48
+ value: AIR-WLC2106-K9
49
+ - sysobjectid: 1.3.6.1.4.1.9.1.926
50
+ metadata:
51
+ model:
52
+ value: 520-WLC
53
+ - sysobjectid: 1.3.6.1.4.1.9.1.1069
54
+ metadata:
55
+ model:
56
+ value: 5500-WLC
57
+ - sysobjectid: 1.3.6.1.4.1.9.1.1279
58
+ metadata:
59
+ model:
60
+ value: AIR-CT2504-K9
61
+ - sysobjectid: 1.3.6.1.4.1.9.1.1295
62
+ metadata:
63
+ model:
64
+ value: 7500-WLC
65
+ - sysobjectid: 1.3.6.1.4.1.9.1.1615
66
+ metadata:
67
+ model:
68
+ value: 8500-WLC
69
+ - sysobjectid: 1.3.6.1.4.1.9.1.1631
70
+ metadata:
71
+ model:
72
+ value: Virtual-WLC
73
+ - sysobjectid: 1.3.6.1.4.1.9.1.1645
74
+ metadata:
75
+ model:
76
+ value: 5760-WLC
77
+ - sysobjectid: 1.3.6.1.4.1.9.1.1926
78
+ metadata:
79
+ model:
80
+ value: WLC-CT5508-K9
81
+ - sysobjectid: 1.3.6.1.4.1.9.1.1927
82
+ metadata:
83
+ model:
84
+ value: WLC-CT2504-K9
85
+ - sysobjectid: 1.3.6.1.4.1.9.1.2170
86
+ metadata:
87
+ model:
88
+ value: 5520-WLC
89
+ - sysobjectid: 1.3.6.1.4.1.9.1.2171
90
+ metadata:
91
+ model:
92
+ value: 8540-WLC
93
+ - sysobjectid: 1.3.6.1.4.1.9.1.2427
94
+ metadata:
95
+ model:
96
+ value: 3504-WLC
97
+
98
metrics:
99
- MIB: AIRESPACE-SWITCHING-MIB
100
symbol:
src/go/plugin/go.d/config/go.d/snmp.profiles/default/ruckus-unleashed.yaml
+1
-1
@@ -13,7 +13,7 @@ metadata:
13
device:
14
fields:
15
type:
16
- value: "WLC"
16
+ value: "Wireless LAN Controller"
17
18
metric_tags:
19
- tag: ruckus_unleashed_system_model