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")