@cryptotaxi247 / netdata / commits / 525a28886

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