@cryptotaxi247 / netdata-1 / commits / 0d0a0fb5c

fix(go.d/azure_monitor): ignore invalid user profile overrides (#22146)

Ilya Mashchenko committed Apr 6, 2026 at 12:57 UTC 0d0a0fb5c9e88d7361e3ade5b9f7ba8711a8fbfe
2 files changed +214 -8
src/go/plugin/go.d/collector/azure_monitor/azureprofiles/catalog.go
+19 -8
@@ -15,6 +15,7 @@ import (
15
16 "gopkg.in/yaml.v3"
17
18 + "github.com/netdata/netdata/go/plugins/logger"
19 "github.com/netdata/netdata/go/plugins/pkg/executable"
20 "github.com/netdata/netdata/go/plugins/pkg/pluginconfig"
21 )
@@ -23,6 +24,8 @@ const (
24 profilesDirName = "azure_monitor.profiles"
25 )
26
27 +var log = logger.New().With("component", "azure_monitor/azureprofiles")
28 +
29 type Catalog struct {
30 byBaseName map[string]Profile
31 stockProfileBaseNames map[string]struct{}
@@ -81,7 +84,7 @@ func LoadFromDirs(specs []DirSpec) (Catalog, error) {
84
85 err := filepath.WalkDir(spec.Path, func(path string, d fs.DirEntry, err error) error {
86 if err != nil {
84 - return err
87 + return handleProfileLoadError(spec, path, err)
88 }
89 if d.IsDir() {
90 return nil
@@ -98,12 +101,12 @@ func LoadFromDirs(specs []DirSpec) (Catalog, error) {
101
102 baseName := strings.TrimSpace(profileBaseName(path))
103 if !IsValidProfileName(baseName) {
101 - return fmt.Errorf("profile %q: basename must match %q", path, reIdentityID.String())
104 + return handleProfileLoadError(spec, path, fmt.Errorf("profile %q: basename must match %q", path, reIdentityID.String()))
105 }
106
107 cfg, err := loadProfileFile(path, baseName)
108 if err != nil {
106 - return err
109 + return handleProfileLoadError(spec, path, err)
110 }
111 if spec.IsStock {
112 catalog.stockProfileBaseNames[baseName] = struct{}{}
@@ -117,11 +120,11 @@ func LoadFromDirs(specs []DirSpec) (Catalog, error) {
120
121 switch {
122 case prev.IsStock == spec.IsStock:
120 - scope := "user"
121 - if spec.IsStock {
122 - scope = "stock"
123 + if !spec.IsStock {
124 + log.Warningf("ignoring duplicate user profile basename %q in %q; already loaded from %q", baseName, path, prev.Path)
125 + return nil
126 }
124 - return fmt.Errorf("duplicate %s profile basename %q in %q and %q", scope, baseName, prev.Path, path)
127 + return fmt.Errorf("duplicate stock profile basename %q in %q and %q", baseName, prev.Path, path)
128 case prev.IsStock && !spec.IsStock:
129 seen[baseName] = catalogEntry{Config: cfg, Path: path, BaseName: baseName, IsStock: false}
130 case !prev.IsStock && spec.IsStock:
@@ -154,7 +157,6 @@ func loadProfileFile(path, baseName string) (Profile, error) {
157
158 var cfg Profile
159 dec := yaml.NewDecoder(bytes.NewReader(data))
157 - dec.KnownFields(true)
160 if err := dec.Decode(&cfg); err != nil {
161 return Profile{}, fmt.Errorf("unmarshal profile %q: %w", path, err)
162 }
@@ -169,6 +171,15 @@ func loadProfileFile(path, baseName string) (Profile, error) {
171 return cfg, nil
172 }
173
174 +func handleProfileLoadError(spec DirSpec, path string, err error) error {
175 + if spec.IsStock {
176 + return err
177 + }
178 +
179 + log.Warningf("ignoring invalid user profile %q: %v", path, err)
180 + return nil
181 +}
182 +
183 func (c Catalog) Resolve(profileNames []string) ([]ResolvedProfile, error) {
184 if len(profileNames) == 0 {
185 return nil, errors.New("no Azure Monitor profiles selected")
src/go/plugin/go.d/collector/azure_monitor/profile_catalog_test.go
+195
@@ -137,6 +137,201 @@ template:
137 assert.Equal(t, "Azure SQL Database (User Override)", got.DisplayName)
138 }
139
140 +func TestLoadProfileCatalogFromDirs_IgnoresInvalidUserOverrideAndFallsBackToStock(t *testing.T) {
141 + dir := t.TempDir()
142 + userDir := filepath.Join(dir, "user")
143 + stockDir := filepath.Join(dir, "stock")
144 +
145 + require.NoError(t, writeProfileFile(filepath.Join(userDir, "sql_database.yaml"), `
146 +id: sql_database
147 +name: Azure SQL Database (Old User Override)
148 +resource_type: Microsoft.Sql/servers/databases
149 +metrics:
150 + - id: cpu_percent
151 + azure_name: cpu_percent
152 + time_grain: PT1M
153 + series:
154 + - aggregation: average
155 + kind: gauge
156 +template:
157 + family: Azure SQL Database (Old User Override)
158 + context_namespace: sql_database
159 + charts:
160 + - id: am_test_sql_database_cpu
161 + title: Azure SQL Database CPU
162 + context: cpu
163 + family: Utilization
164 + type: line
165 + units: percentage
166 + algorithm: absolute
167 + label_promotion: [resource_name, resource_group, region, resource_type, profile]
168 + instances:
169 + by_labels: [resource_uid]
170 + dimensions:
171 + - selector: sql_database.cpu_percent_average
172 + name: average
173 +`))
174 + require.NoError(t, writeProfileFile(filepath.Join(stockDir, "sql_database.yaml"), `
175 +display_name: Azure SQL Database (Stock)
176 +resource_type: Microsoft.Sql/servers/databases
177 +metrics:
178 + - id: cpu_percent
179 + azure_name: cpu_percent
180 + time_grain: PT1M
181 + series:
182 + - aggregation: average
183 + kind: gauge
184 +template:
185 + family: Azure SQL Database (Stock)
186 + context_namespace: sql_database
187 + charts:
188 + - id: am_test_sql_database_cpu
189 + title: Azure SQL Database CPU
190 + context: cpu
191 + family: Utilization
192 + type: line
193 + units: percentage
194 + algorithm: absolute
195 + label_promotion: [resource_name, resource_group, region, resource_type, profile]
196 + instances:
197 + by_labels: [resource_uid]
198 + dimensions:
199 + - selector: sql_database.cpu_percent_average
200 + name: average
201 +`))
202 +
203 + catalog, err := azureprofiles.LoadFromDirs([]azureprofiles.DirSpec{
204 + {Path: userDir, IsStock: false},
205 + {Path: stockDir, IsStock: true},
206 + })
207 + require.NoError(t, err)
208 +
209 + gotProfiles, err := catalog.ResolveBaseNames([]string{"sql_database"})
210 + require.NoError(t, err)
211 + require.Len(t, gotProfiles, 1)
212 + assert.Equal(t, "Azure SQL Database (Stock)", gotProfiles[0].DisplayName)
213 +}
214 +
215 +func TestLoadProfileCatalogFromDirs_IgnoresUnknownFieldsInUserProfile(t *testing.T) {
216 + dir := t.TempDir()
217 + userDir := filepath.Join(dir, "user")
218 +
219 + require.NoError(t, writeProfileFile(filepath.Join(userDir, "sql_database.yaml"), `
220 +id: sql_database
221 +name: Azure SQL Database (Ignored Legacy Field)
222 +display_name: Azure SQL Database (User Override)
223 +resource_type: Microsoft.Sql/servers/databases
224 +metrics:
225 + - id: cpu_percent
226 + azure_name: cpu_percent
227 + time_grain: PT1M
228 + series:
229 + - aggregation: average
230 + kind: gauge
231 +template:
232 + family: Azure SQL Database (User Override)
233 + context_namespace: sql_database
234 + charts:
235 + - id: am_test_sql_database_cpu
236 + title: Azure SQL Database CPU
237 + context: cpu
238 + family: Utilization
239 + type: line
240 + units: percentage
241 + algorithm: absolute
242 + label_promotion: [resource_name, resource_group, region, resource_type, profile]
243 + instances:
244 + by_labels: [resource_uid]
245 + dimensions:
246 + - selector: sql_database.cpu_percent_average
247 + name: average
248 +`))
249 +
250 + catalog, err := azureprofiles.LoadFromDirs([]azureprofiles.DirSpec{
251 + {Path: userDir, IsStock: false},
252 + })
253 + require.NoError(t, err)
254 +
255 + gotProfiles, err := catalog.ResolveBaseNames([]string{"sql_database"})
256 + require.NoError(t, err)
257 + require.Len(t, gotProfiles, 1)
258 + assert.Equal(t, "Azure SQL Database (User Override)", gotProfiles[0].DisplayName)
259 +}
260 +
261 +func TestLoadProfileCatalogFromDirs_IgnoresDuplicateUserBasename(t *testing.T) {
262 + dir := t.TempDir()
263 + userDirA := filepath.Join(dir, "user-a")
264 + userDirB := filepath.Join(dir, "user-b")
265 +
266 + require.NoError(t, writeProfileFile(filepath.Join(userDirA, "sql_database.yaml"), `
267 +display_name: Azure SQL Database (User A)
268 +resource_type: Microsoft.Sql/servers/databases
269 +metrics:
270 + - id: cpu_percent
271 + azure_name: cpu_percent
272 + time_grain: PT1M
273 + series:
274 + - aggregation: average
275 + kind: gauge
276 +template:
277 + family: Azure SQL Database (User A)
278 + context_namespace: sql_database
279 + charts:
280 + - id: am_test_sql_database_user_a_cpu
281 + title: Azure SQL Database CPU
282 + context: cpu
283 + family: Utilization
284 + type: line
285 + units: percentage
286 + algorithm: absolute
287 + label_promotion: [resource_name, resource_group, region, resource_type, profile]
288 + instances:
289 + by_labels: [resource_uid]
290 + dimensions:
291 + - selector: sql_database.cpu_percent_average
292 + name: average
293 +`))
294 + require.NoError(t, writeProfileFile(filepath.Join(userDirB, "sql_database.yaml"), `
295 +display_name: Azure SQL Database (User B)
296 +resource_type: Microsoft.Sql/servers/databases
297 +metrics:
298 + - id: cpu_percent
299 + azure_name: cpu_percent
300 + time_grain: PT1M
301 + series:
302 + - aggregation: average
303 + kind: gauge
304 +template:
305 + family: Azure SQL Database (User B)
306 + context_namespace: sql_database
307 + charts:
308 + - id: am_test_sql_database_user_b_cpu
309 + title: Azure SQL Database CPU
310 + context: cpu
311 + family: Utilization
312 + type: line
313 + units: percentage
314 + algorithm: absolute
315 + label_promotion: [resource_name, resource_group, region, resource_type, profile]
316 + instances:
317 + by_labels: [resource_uid]
318 + dimensions:
319 + - selector: sql_database.cpu_percent_average
320 + name: average
321 +`))
322 +
323 + catalog, err := azureprofiles.LoadFromDirs([]azureprofiles.DirSpec{
324 + {Path: userDirA, IsStock: false},
325 + {Path: userDirB, IsStock: false},
326 + })
327 + require.NoError(t, err)
328 +
329 + gotProfiles, err := catalog.ResolveBaseNames([]string{"sql_database"})
330 + require.NoError(t, err)
331 + require.Len(t, gotProfiles, 1)
332 + assert.Equal(t, "Azure SQL Database (User A)", gotProfiles[0].DisplayName)
333 +}
334 +
335 func TestLoadProfileCatalogFromDirs_RejectsDuplicateProfileBasenames(t *testing.T) {
336 dir := t.TempDir()
337 stockDir := filepath.Join(dir, "stock")