improve(go.d/snmp): add `manual_profiles` option (#21023)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Ilya Mashchenko committed
Sep 21, 2025 at 22:16 UTC
d1c4af60ec9bf3c076e8367b751cafe744b30ece
7 files changed
+81
-25
src/go/plugin/go.d/collector/snmp/collect.go
+2
-2
@@ -168,7 +168,7 @@ func (c *Collector) setupVnode(si *snmputils.SysInfo, deviceMeta map[string]ddsn
168
}
169
170
func (c *Collector) setupProfiles(sysObjectID string) []*ddsnmp.Profile {
171
- snmpProfiles := ddsnmp.FindProfiles(sysObjectID)
171
+ snmpProfiles := ddsnmp.FindProfiles(sysObjectID, c.ManualProfiles)
172
var profInfo []string
173
174
for _, prof := range snmpProfiles {
@@ -180,7 +180,7 @@ func (c *Collector) setupProfiles(sysObjectID string) []*ddsnmp.Profile {
180
}
181
}
182
183
- c.Infof("device matched %d profile(s): %s (sysObjectID: %s)", len(snmpProfiles), strings.Join(profInfo, ", "), sysObjectID)
183
+ c.Infof("device matched %d profile(s): %s (sysObjectID: '%s')", len(snmpProfiles), strings.Join(profInfo, ", "), sysObjectID)
184
185
return snmpProfiles
186
}
src/go/plugin/go.d/collector/snmp/config.go
+10
-8
@@ -6,19 +6,21 @@ import "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/vnodes"
6
7
type (
8
Config struct {
9
- UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
10
- Hostname string `yaml:"hostname" json:"hostname"`
11
- CreateVnode bool `yaml:"create_vnode,omitempty" json:"create_vnode"`
12
- VnodeDeviceDownThreshold int `yaml:"vnode_device_down_threshold,omitempty" json:"vnode_device_down_threshold"`
13
- Vnode vnodes.VirtualNode `yaml:"vnode,omitempty" json:"vnode"`
14
- Community string `yaml:"community,omitempty" json:"community"`
15
- User User `yaml:"user,omitempty" json:"user"`
16
- Options Options `yaml:"options,omitempty" json:"options"`
9
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
10
+ Hostname string `yaml:"hostname" json:"hostname"`
11
+ CreateVnode bool `yaml:"create_vnode,omitempty" json:"create_vnode"`
12
+ VnodeDeviceDownThreshold int `yaml:"vnode_device_down_threshold,omitempty" json:"vnode_device_down_threshold"`
13
+ Vnode vnodes.VirtualNode `yaml:"vnode,omitempty" json:"vnode"`
14
+ Community string `yaml:"community,omitempty" json:"community"`
15
+ User User `yaml:"user,omitempty" json:"user"`
16
+ Options Options `yaml:"options,omitempty" json:"options"`
17
+
18
ChartsInput []ChartConfig `yaml:"charts,omitempty" json:"charts"`
19
NetworkInterfaceFilter NetworkInterfaceFilter `yaml:"network_interface_filter,omitempty" json:"network_interface_filter"`
20
EnableProfiles bool `yaml:"enable_profiles,omitempty" json:"enable_profiles"`
21
EnableProfilesTableMetrics bool `yaml:"enable_profiles_table_metrics,omitempty" json:"enable_profiles_table_metrics"`
22
DisableLegacyCollection bool `yaml:"disable_legacy_collection,omitempty" json:"disable_legacy_collection"`
23
+ ManualProfiles []string `yaml:"manual_profiles,omitempty" json:"manual_profiles"`
24
}
25
NetworkInterfaceFilter struct {
26
ByName string `yaml:"by_name,omitempty" json:"by_name"`
src/go/plugin/go.d/collector/snmp/config_schema.json
+19
-1
@@ -367,6 +367,19 @@
367
"description": "Disable the legacy SNMP collection method, forcing the collector to use only SNMP profiles (YAML-based configuration). When enabled, the collector will ignore any non-profile based collection logic.",
368
"type": "boolean",
369
"default": false
370
+ },
371
+ "manual_profiles": {
372
+ "title": "Manual SNMP Profiles",
373
+ "description": "Profiles to apply if automatic detection cannot be used.",
374
+ "type": [
375
+ "array",
376
+ "null"
377
+ ],
378
+ "items": {
379
+ "title": "Profile",
380
+ "type": "string"
381
+ },
382
+ "uniqueItems": true
383
}
384
},
385
"required": [
@@ -385,6 +398,10 @@
398
"enable_profiles_table_metrics": {
399
"ui:help": "Table metrics include interface statistics, routing tables, and other tabular data."
400
},
401
+ "manual_profiles": {
402
+ "ui:listFlavour": "list",
403
+ "ui:help": "**Profiles are always applied automatically** based on the device sysObjectID. **If no sysObjectID is provided** (rare case, e.g. some printers), the profiles listed here will be applied instead. In most cases, **leave this empty**."
404
+ },
405
"network_interface_filter": {
406
"ui:collapsible": true
407
},
@@ -499,7 +516,8 @@
516
"fields": [
517
"enable_profiles",
518
"enable_profiles_table_metrics",
502
- "disable_legacy_collection"
519
+ "disable_legacy_collection",
520
+ "manual_profiles"
521
]
522
}
523
]
src/go/plugin/go.d/collector/snmp/ddsnmp/profile.go
+34
-9
@@ -30,29 +30,54 @@ func OidMatches(sysObjId, id string) bool {
30
31
// FindProfiles returns profiles matching the given sysObjectID.
32
// Profiles are sorted by match specificity: most specific first.
33
-func FindProfiles(sysObjId string) []*Profile {
33
+func FindProfiles(sysObjID string, manualProfiles []string) []*Profile {
34
loadProfiles()
35
36
+ finalize := func(profiles []*Profile) []*Profile {
37
+ if len(profiles) == 0 {
38
+ return nil
39
+ }
40
+ enrichProfiles(profiles)
41
+ deduplicateMetricsAcrossProfiles(profiles)
42
+ return profiles
43
+ }
44
+
45
+ // Fallback/manual path (no sysObjectID)
46
+ if sysObjID == "" {
47
+ if len(manualProfiles) == 0 {
48
+ log.Warning("No sysObjectID found and no manual_profiles configured. Either ensure the device provides sysObjectID or configure manual_profiles option.")
49
+ return nil
50
+ }
51
+
52
+ var selected []*Profile
53
+ for _, prof := range ddProfiles {
54
+ name := stripFileNameExt(prof.SourceFile)
55
+ if slices.ContainsFunc(manualProfiles, func(p string) bool { return stripFileNameExt(p) == name }) {
56
+ selected = append(selected, prof.clone())
57
+ }
58
+ }
59
+
60
+ return finalize(selected)
61
+ }
62
+
63
+ // Auto-detect path
64
matchedOIDs := make(map[*Profile]string)
37
- var profiles []*Profile
65
+ var selected []*Profile
66
67
for _, prof := range ddProfiles {
68
// Use first matching OID (profile author's responsibility to order them)
69
for _, id := range prof.Definition.SysObjectIDs {
42
- if OidMatches(sysObjId, id) {
70
+ if OidMatches(sysObjID, id) {
71
cloned := prof.clone()
44
- profiles = append(profiles, cloned)
72
+ selected = append(selected, cloned)
73
matchedOIDs[cloned] = id
74
break
75
}
76
}
77
}
78
51
- sortProfilesBySpecificity(profiles, matchedOIDs)
52
-
53
- enrichProfiles(profiles)
54
- deduplicateMetricsAcrossProfiles(profiles)
55
- return profiles
79
+ sortProfilesBySpecificity(selected, matchedOIDs)
80
+ return finalize(selected)
81
}
82
83
type (
src/go/plugin/go.d/collector/snmp/ddsnmp/profile_test.go
+10
-4
@@ -44,8 +44,9 @@ func Test_loadDDSnmpProfiles(t *testing.T) {
44
45
func Test_FindProfiles(t *testing.T) {
46
test := map[string]struct {
47
- sysObjOId string
48
- wanProfiles int
47
+ sysObjOId string
48
+ manualProfiles []string
49
+ wanProfiles int
50
}{
51
"mikrotik": {
52
sysObjOId: "1.3.6.1.4.1.14988.1",
@@ -63,11 +64,16 @@ func Test_FindProfiles(t *testing.T) {
64
sysObjOId: "1.3.6.1.4.1.9.1.2170",
65
wanProfiles: 3,
66
},
67
+ "no sysObjectID, manual profile applied": {
68
+ sysObjOId: "",
69
+ manualProfiles: []string{"generic-device"},
70
+ wanProfiles: 1,
71
+ },
72
}
73
74
for name, test := range test {
75
t.Run(name, func(t *testing.T) {
70
- profiles := FindProfiles(test.sysObjOId)
76
+ profiles := FindProfiles(test.sysObjOId, test.manualProfiles)
77
78
require.Len(t, profiles, test.wanProfiles)
79
})
@@ -75,7 +81,7 @@ func Test_FindProfiles(t *testing.T) {
81
}
82
83
func Test_Profile_merge(t *testing.T) {
78
- profiles := FindProfiles("1.3.6.1.4.1.9.1.1216") // cisco-nexus
84
+ profiles := FindProfiles("1.3.6.1.4.1.9.1.1216", nil) // cisco-nexus
85
86
i := slices.IndexFunc(profiles, func(p *Profile) bool {
87
return strings.HasSuffix(p.SourceFile, "cisco-nexus.yaml")
src/go/plugin/go.d/collector/snmp/testdata/config.json
+4
-1
@@ -56,5 +56,8 @@
56
],
57
"enable_profiles": true,
58
"enable_profiles_table_metrics": true,
59
- "disable_legacy_collection": true
59
+ "disable_legacy_collection": true,
60
+ "manual_profiles": [
61
+ "ok"
62
+ ]
63
}
src/go/plugin/go.d/collector/snmp/testdata/config.yaml
+2
@@ -5,6 +5,8 @@ vnode_device_down_threshold: 123
5
enable_profiles: yes
6
enable_profiles_table_metrics: yes
7
disable_legacy_collection: yes
8
+manual_profiles:
9
+ - "ok"
10
vnode:
11
name: "ok"
12
guid: "ok"