feat(go.d/azure_monitor): add per-profile resource filters (#22132)
Ilya Mashchenko committed
Apr 3, 2026 at 23:59 UTC
a7fd47fa2b9189c2b6ee94a38cd28e80ce42c971
57 files changed
+1689
-552
src/go/plugin/go.d/collector/azure_monitor/azureprofiles/catalog.go
+45
-99
@@ -25,10 +25,14 @@ const (
25
26
type Catalog struct {
27
byBaseName map[string]Profile
28
- byID map[string]Profile
28
stockProfileBaseNames map[string]struct{}
29
}
30
31
+type ResolvedProfile struct {
32
+ Name string
33
+ Config Profile
34
+}
35
+
36
type catalogEntry struct {
37
Config Profile
38
Path string
@@ -60,7 +64,6 @@ func LoadFromDefaultDirs() (Catalog, error) {
64
func LoadFromDirs(specs []DirSpec) (Catalog, error) {
65
catalog := Catalog{
66
byBaseName: make(map[string]Profile),
63
- byID: make(map[string]Profile),
67
stockProfileBaseNames: make(map[string]struct{}),
68
}
69
seen := make(map[string]catalogEntry)
@@ -93,14 +96,14 @@ func LoadFromDirs(specs []DirSpec) (Catalog, error) {
96
return nil
97
}
98
96
- cfg, err := loadProfileFile(path)
97
- if err != nil {
98
- return err
99
+ baseName := strings.TrimSpace(profileBaseName(path))
100
+ if !IsValidProfileName(baseName) {
101
+ return fmt.Errorf("profile %q: basename must match %q", path, reIdentityID.String())
102
}
103
101
- baseName := normalizeKey(profileBaseName(path))
102
- if baseName == "" {
103
- return fmt.Errorf("profile %q: decoded empty basename", path)
104
+ cfg, err := loadProfileFile(path, baseName)
105
+ if err != nil {
106
+ return err
107
}
108
if spec.IsStock {
109
catalog.stockProfileBaseNames[baseName] = struct{}{}
@@ -136,24 +139,14 @@ func LoadFromDirs(specs []DirSpec) (Catalog, error) {
139
return Catalog{}, errors.New("no Azure Monitor profiles were loaded")
140
}
141
139
- idToBaseName := make(map[string]string, len(seen))
140
- for _, entry := range seen {
141
- id := normalizeKey(entry.Config.ID)
142
- if id == "" {
143
- return Catalog{}, fmt.Errorf("profile %q: decoded empty id", entry.Path)
144
- }
145
- if prevBaseName, ok := idToBaseName[id]; ok {
146
- return Catalog{}, fmt.Errorf("duplicate profile id %q for basenames %q and %q", entry.Config.ID, prevBaseName, entry.BaseName)
147
- }
148
- idToBaseName[id] = entry.BaseName
149
- catalog.byID[id] = entry.Config
150
- catalog.byBaseName[entry.BaseName] = entry.Config
142
+ for baseName, entry := range seen {
143
+ catalog.byBaseName[baseName] = entry.Config
144
}
145
146
return catalog, nil
147
}
148
156
-func loadProfileFile(path string) (Profile, error) {
149
+func loadProfileFile(path, baseName string) (Profile, error) {
150
data, err := os.ReadFile(path)
151
if err != nil {
152
return Profile{}, err
@@ -166,47 +159,42 @@ func loadProfileFile(path string) (Profile, error) {
159
return Profile{}, fmt.Errorf("unmarshal profile %q: %w", path, err)
160
}
161
169
- profileID := strings.TrimSpace(cfg.ID)
170
- if profileID == "" {
171
- return Profile{}, fmt.Errorf("validate profile %q: missing required field 'id'", path)
162
+ if err := cfg.Normalize(baseName); err != nil {
163
+ return Profile{}, fmt.Errorf("normalize profile %q: %w", path, err)
164
}
173
- if err := cfg.Validate(fmt.Sprintf("profile %q", profileID)); err != nil {
165
+ if err := cfg.Validate(fmt.Sprintf("profile %q", baseName), baseName); err != nil {
166
return Profile{}, fmt.Errorf("validate profile %q: %w", path, err)
167
}
168
169
return cfg, nil
170
}
171
180
-func (c Catalog) Resolve(profileIDs []string) ([]Profile, error) {
181
- if len(profileIDs) == 0 {
172
+func (c Catalog) Resolve(profileNames []string) ([]ResolvedProfile, error) {
173
+ if len(profileNames) == 0 {
174
return nil, errors.New("no Azure Monitor profiles selected")
175
}
176
185
- profiles := make([]Profile, 0, len(profileIDs))
186
- for _, id := range profileIDs {
187
- normalizedID := normalizeKey(id)
188
- prof, ok := c.byID[normalizedID]
177
+ profiles := make([]ResolvedProfile, 0, len(profileNames))
178
+ for _, name := range profileNames {
179
+ profileName := strings.TrimSpace(name)
180
+ prof, ok := c.byBaseName[profileName]
181
if !ok {
190
- return nil, fmt.Errorf("unknown profile %q", id)
182
+ return nil, fmt.Errorf("unknown profile %q", name)
183
}
192
- profiles = append(profiles, prof)
184
+ profiles = append(profiles, ResolvedProfile{Name: profileName, Config: prof})
185
}
186
return profiles, nil
187
}
188
189
func (c Catalog) ResolveBaseNames(profileBaseNames []string) ([]Profile, error) {
198
- if len(profileBaseNames) == 0 {
199
- return nil, errors.New("no Azure Monitor profiles selected")
190
+ resolved, err := c.Resolve(profileBaseNames)
191
+ if err != nil {
192
+ return nil, err
193
}
194
202
- profiles := make([]Profile, 0, len(profileBaseNames))
203
- for _, baseName := range profileBaseNames {
204
- normalizedBaseName := normalizeKey(baseName)
205
- prof, ok := c.byBaseName[normalizedBaseName]
206
- if !ok {
207
- return nil, fmt.Errorf("unknown profile basename %q", baseName)
208
- }
209
- profiles = append(profiles, prof)
195
+ profiles := make([]Profile, 0, len(resolved))
196
+ for _, profile := range resolved {
197
+ profiles = append(profiles, profile.Config)
198
}
199
return profiles, nil
200
}
@@ -216,72 +204,30 @@ func (c Catalog) ProfilesForResourceTypes(types map[string]struct{}) []string {
204
return nil
205
}
206
219
- var ids []string
220
- for id, prof := range c.byID {
207
+ var names []string
208
+ for name, prof := range c.byBaseName {
209
rt := normalizeKey(prof.ResourceType)
210
if rt == "" {
211
continue
212
}
213
if _, ok := types[rt]; ok {
226
- ids = append(ids, id)
214
+ names = append(names, name)
215
}
216
}
229
- sort.Strings(ids)
230
- return ids
231
-}
232
-
233
-func (c Catalog) ResourceTypesForProfileIDs(profileIDs []string) ([]string, error) {
234
- if len(profileIDs) == 0 {
235
- return nil, nil
236
- }
237
-
238
- seen := make(map[string]struct{}, len(profileIDs))
239
- types := make([]string, 0, len(profileIDs))
240
- for _, id := range profileIDs {
241
- prof, ok := c.byID[normalizeKey(id)]
242
- if !ok {
243
- return nil, fmt.Errorf("unknown profile %q", id)
244
- }
245
-
246
- rt := strings.TrimSpace(prof.ResourceType)
247
- key := normalizeKey(rt)
248
- if key == "" {
249
- continue
250
- }
251
- if _, ok := seen[key]; ok {
252
- continue
253
- }
254
- seen[key] = struct{}{}
255
- types = append(types, rt)
256
- }
257
-
258
- sort.Strings(types)
259
- return types, nil
260
-}
261
-
262
-func (c Catalog) ProfileIDsForBaseNames(profileBaseNames []string) ([]string, error) {
263
- profiles, err := c.ResolveBaseNames(profileBaseNames)
264
- if err != nil {
265
- return nil, err
266
- }
267
-
268
- ids := make([]string, 0, len(profiles))
269
- for _, profile := range profiles {
270
- ids = append(ids, profile.ID)
271
- }
272
- return ids, nil
217
+ sort.Strings(names)
218
+ return names
219
}
220
221
func (c Catalog) ResourceTypesForProfileBaseNames(profileBaseNames []string) ([]string, error) {
276
- profiles, err := c.ResolveBaseNames(profileBaseNames)
222
+ resolved, err := c.Resolve(profileBaseNames)
223
if err != nil {
224
return nil, err
225
}
226
281
- seen := make(map[string]struct{}, len(profiles))
282
- types := make([]string, 0, len(profiles))
283
- for _, profile := range profiles {
284
- rt := strings.TrimSpace(profile.ResourceType)
227
+ seen := make(map[string]struct{}, len(resolved))
228
+ types := make([]string, 0, len(resolved))
229
+ for _, resolvedProfile := range resolved {
230
+ rt := strings.TrimSpace(resolvedProfile.Config.ResourceType)
231
key := normalizeKey(rt)
232
if key == "" {
233
continue
@@ -317,13 +263,13 @@ func profileBaseName(path string) string {
263
}
264
265
func (c Catalog) ResourceTypes() []string {
320
- if len(c.byID) == 0 {
266
+ if len(c.byBaseName) == 0 {
267
return nil
268
}
269
324
- seen := make(map[string]struct{}, len(c.byID))
325
- types := make([]string, 0, len(c.byID))
326
- for _, prof := range c.byID {
270
+ seen := make(map[string]struct{}, len(c.byBaseName))
271
+ types := make([]string, 0, len(c.byBaseName))
272
+ for _, prof := range c.byBaseName {
273
rt := strings.TrimSpace(prof.ResourceType)
274
key := normalizeKey(rt)
275
if key == "" {
src/go/plugin/go.d/collector/azure_monitor/azureprofiles/default_catalog_test.go
+1
-5
@@ -37,7 +37,6 @@ func TestLoadFromDefaultDirs_LoadsAllStockProfiles(t *testing.T) {
37
require.NoError(t, err)
38
39
assert.Len(t, catalog.byBaseName, want)
40
- assert.Len(t, catalog.byID, want)
40
}
41
42
func TestDefaultCatalog_CachesSuccessfulLoads(t *testing.T) {
@@ -122,14 +121,11 @@ func stubDefaultCatalog(t *testing.T, cacheEnabled func() bool, loader func() (C
121
}
122
123
func testCatalog(id string) Catalog {
125
- profile := Profile{ID: id, DisplayName: id}
124
+ profile := Profile{DisplayName: id}
125
return Catalog{
126
byBaseName: map[string]Profile{
127
normalizeKey(id): profile,
128
},
130
- byID: map[string]Profile{
131
- id: profile,
132
- },
129
stockProfileBaseNames: map[string]struct{}{
130
normalizeKey(id): {},
131
},
src/go/plugin/go.d/collector/azure_monitor/azureprofiles/profile.go
+88
-17
@@ -32,8 +32,7 @@ var SupportedTimeGrains = map[string]time.Duration{
32
}
33
34
type Profile struct {
35
- ID string `yaml:"id" json:"id,omitempty"`
36
- DisplayName string `yaml:"name" json:"name,omitempty"`
35
+ DisplayName string `yaml:"display_name" json:"display_name,omitempty"`
36
ResourceType string `yaml:"resource_type" json:"resource_type,omitempty"`
37
MetricNamespace string `yaml:"metric_namespace,omitempty" json:"metric_namespace,omitempty"`
38
Metrics []Metric `yaml:"metrics" json:"metrics,omitempty"`
@@ -52,14 +51,20 @@ type MetricSeries struct {
51
Kind string `yaml:"kind" json:"kind,omitempty"`
52
}
53
55
-func (p Profile) Validate(prefix string) error {
54
+func (p *Profile) Normalize(baseName string) error {
55
+ visibleMetrics := visibleMetricsForProfile(baseName, p.Metrics)
56
+ normalizeGroupSelectors(baseName, visibleMetrics, &p.Template)
57
+ return nil
58
+}
59
+
60
+func (p Profile) Validate(prefix, baseName string) error {
61
var errs []error
62
58
- if !IsValidIdentityID(p.ID) {
59
- errs = append(errs, fmt.Errorf("%s: 'id' must match %q", prefix, reIdentityID.String()))
63
+ if !IsValidProfileName(baseName) {
64
+ errs = append(errs, fmt.Errorf("%s: profile basename must match %q", prefix, reIdentityID.String()))
65
}
66
if strings.TrimSpace(p.DisplayName) == "" {
62
- errs = append(errs, fmt.Errorf("%s: 'name' is required", prefix))
67
+ errs = append(errs, fmt.Errorf("%s: 'display_name' is required", prefix))
68
}
69
if !IsValidResourceType(p.ResourceType) {
70
errs = append(errs, fmt.Errorf("%s: 'resource_type' is invalid", prefix))
@@ -102,7 +107,7 @@ func (p Profile) Validate(prefix string) error {
107
}
108
109
if len(errs) == 0 {
105
- if err := validateTemplate(prefix, p); err != nil {
110
+ if err := validateTemplate(prefix, baseName, p); err != nil {
111
errs = append(errs, err)
112
}
113
}
@@ -162,8 +167,8 @@ func (s MetricSeries) validate(metricPrefix string, idx int) error {
167
return errors.Join(errs...)
168
}
169
165
-func ExportedSeriesName(profileID, metricID, aggregation string) string {
166
- return strings.TrimSpace(profileID) + "." + strings.TrimSpace(metricID) + "_" + NormalizeAggregation(aggregation)
170
+func ExportedSeriesName(profileName, metricID, aggregation string) string {
171
+ return strings.TrimSpace(profileName) + "." + strings.TrimSpace(metricID) + "_" + NormalizeAggregation(aggregation)
172
}
173
174
func NormalizeAggregation(v string) string {
@@ -198,21 +203,24 @@ func IsValidIdentityID(v string) bool {
203
return reIdentityID.MatchString(strings.TrimSpace(v))
204
}
205
206
+func IsValidProfileName(v string) bool {
207
+ return reIdentityID.MatchString(strings.TrimSpace(v))
208
+}
209
+
210
func IsValidResourceType(v string) bool {
211
return reResourceType.MatchString(strings.TrimSpace(v))
212
}
213
205
-func validateTemplate(prefix string, profile Profile) error {
206
- visibleMetrics := make([]string, 0)
207
- for _, metric := range profile.Metrics {
208
- for _, series := range metric.Series {
209
- visibleMetrics = append(visibleMetrics, ExportedSeriesName(profile.ID, metric.ID, series.Aggregation))
210
- }
214
+func validateTemplate(prefix, baseName string, profile Profile) error {
215
+ visibleMetrics := visibleMetricsForProfile(baseName, profile.Metrics)
216
+ visibleList := make([]string, 0, len(visibleMetrics))
217
+ for metric := range visibleMetrics {
218
+ visibleList = append(visibleList, metric)
219
}
212
- sort.Strings(visibleMetrics)
220
+ sort.Strings(visibleList)
221
222
root := profile.Template
215
- root.Metrics = visibleMetrics
223
+ root.Metrics = visibleList
224
225
spec := charttpl.Spec{
226
Version: charttpl.VersionV1,
@@ -225,6 +233,69 @@ func validateTemplate(prefix string, profile Profile) error {
233
return nil
234
}
235
236
+func visibleMetricsForProfile(baseName string, metrics []Metric) map[string]struct{} {
237
+ visible := make(map[string]struct{})
238
+ for _, metric := range metrics {
239
+ for _, series := range metric.Series {
240
+ visible[ExportedSeriesName(baseName, metric.ID, series.Aggregation)] = struct{}{}
241
+ }
242
+ }
243
+ return visible
244
+}
245
+
246
+func normalizeGroupSelectors(baseName string, visible map[string]struct{}, group *charttpl.Group) {
247
+ if group == nil {
248
+ return
249
+ }
250
+
251
+ for i := range group.Charts {
252
+ normalizeChartSelectors(baseName, visible, &group.Charts[i])
253
+ }
254
+ for i := range group.Groups {
255
+ normalizeGroupSelectors(baseName, visible, &group.Groups[i])
256
+ }
257
+}
258
+
259
+func normalizeChartSelectors(baseName string, visible map[string]struct{}, chart *charttpl.Chart) {
260
+ if chart == nil {
261
+ return
262
+ }
263
+
264
+ for i := range chart.Dimensions {
265
+ chart.Dimensions[i].Selector = normalizeSelector(baseName, visible, chart.Dimensions[i].Selector)
266
+ }
267
+}
268
+
269
+func normalizeSelector(baseName string, visible map[string]struct{}, selector string) string {
270
+ metricName, suffix, ok := splitSelectorMetric(selector)
271
+ if !ok || strings.Contains(metricName, ".") {
272
+ return selector
273
+ }
274
+
275
+ candidate := baseName + "." + metricName
276
+ if _, ok := visible[candidate]; !ok {
277
+ return selector
278
+ }
279
+ return candidate + suffix
280
+}
281
+
282
+func splitSelectorMetric(selector string) (metricName, suffix string, ok bool) {
283
+ selector = strings.TrimSpace(selector)
284
+ if selector == "" || strings.HasPrefix(selector, "{") {
285
+ return "", "", false
286
+ }
287
+
288
+ if idx := strings.Index(selector, "{"); idx >= 0 {
289
+ metricName = strings.TrimSpace(selector[:idx])
290
+ if metricName == "" {
291
+ return "", "", false
292
+ }
293
+ return metricName, selector[idx:], true
294
+ }
295
+
296
+ return selector, "", true
297
+}
298
+
299
func countChartsInGroup(group charttpl.Group) int {
300
total := len(group.Charts)
301
for _, child := range group.Groups {
src/go/plugin/go.d/collector/azure_monitor/collect.go
+12
-8
@@ -10,16 +10,16 @@ import (
10
)
11
12
func (c *Collector) collect(ctx context.Context) error {
13
- resources, err := c.refreshCollectResources(ctx)
13
+ hasResources, err := c.refreshCollectResources(ctx)
14
if err != nil {
15
return err
16
}
17
- if len(resources) == 0 {
17
+ if !hasResources {
18
return nil
19
}
20
21
now := c.now()
22
- queryBatches := c.buildQueryBatches(resources, now)
22
+ queryBatches := c.buildQueryBatches(now)
23
24
dueInstruments := dueInstrumentsForBatches(queryBatches)
25
samples, err := c.collectQuerySamples(ctx, queryBatches, now)
@@ -33,20 +33,24 @@ func (c *Collector) collect(ctx context.Context) error {
33
return nil
34
}
35
36
-func (c *Collector) refreshCollectResources(ctx context.Context) ([]resourceInfo, error) {
36
+func (c *Collector) refreshCollectResources(ctx context.Context) (bool, error) {
37
if err := c.ensureBootstrapped(ctx); err != nil {
38
- return nil, err
38
+ return false, err
39
}
40
41
prevFetchCounter := c.discovery.FetchCounter
42
resources, err := c.refreshDiscovery(ctx, false)
43
if err != nil {
44
- return nil, fmt.Errorf("resource discovery: %w", err)
44
+ if c.discovery.FetchedAt.IsZero() {
45
+ return false, fmt.Errorf("resource discovery: %w", err)
46
+ }
47
+ c.Warningf("resource discovery refresh failed, continuing with last known discovery snapshot: %v", err)
48
+ resources = c.discovery.Resources
49
}
50
if c.discovery.FetchCounter != prevFetchCounter {
47
- c.observations.pruneStaleResources(resources)
51
+ c.observations.pruneStaleResources(c.discovery.ByProfile)
52
}
49
- return resources, nil
53
+ return len(resources) > 0, nil
54
}
55
56
func (c *Collector) collectQuerySamples(ctx context.Context, batches []queryBatch, queryNow time.Time) ([]metricSample, error) {
src/go/plugin/go.d/collector/azure_monitor/collector_runtime.go
+9
-1
@@ -17,10 +17,11 @@ type collectorRuntime struct {
17
}
18
19
type profileRuntime struct {
20
- ID string
20
Name string
21
+ DisplayName string
22
ResourceType string
23
MetricNamespace string
24
+ Filters *ResourceFiltersConfig
25
Metrics []*metricRuntime
26
Template charttpl.Group
27
}
@@ -60,11 +61,17 @@ func (i *instrumentRuntime) observe(labelValues []string, value float64) {
61
type discoveryState struct {
62
Resources []resourceInfo
63
ByType map[string][]resourceInfo
64
+ ByProfile map[string][]resourceInfo
65
ExpiresAt time.Time
66
FetchedAt time.Time
67
FetchCounter uint64
68
}
69
70
+type resourceTag struct {
71
+ Key string
72
+ Value string
73
+}
74
+
75
type resourceInfo struct {
76
SubscriptionID string
77
ID string
@@ -73,6 +80,7 @@ type resourceInfo struct {
80
Type string
81
ResourceGroup string
82
Region string
83
+ Tags []resourceTag
84
}
85
86
func (r resourceInfo) String() string {
src/go/plugin/go.d/collector/azure_monitor/collector_test.go
+487
-42
@@ -5,6 +5,7 @@ package azure_monitor
5
import (
6
"context"
7
"encoding/json"
8
+ "errors"
9
"os"
10
"path/filepath"
11
"strings"
@@ -74,12 +75,38 @@ func TestConfigSchema_RuntimeContract(t *testing.T) {
75
assert.False(t, hasIDs)
76
_, hasNames := uiProfiles["names"]
77
assert.False(t, hasNames)
78
+ _, hasModeAuto := uiProfiles["mode_auto"]
79
+ assert.True(t, hasModeAuto)
80
_, hasModeExact := uiProfiles["mode_exact"]
81
assert.True(t, hasModeExact)
82
_, hasModeCombined := uiProfiles["mode_combined"]
83
assert.True(t, hasModeCombined)
84
}
85
86
+func TestConfigSchema_ProfileTagHelpText(t *testing.T) {
87
+ raw, err := os.ReadFile("config_schema.json")
88
+ require.NoError(t, err)
89
+
90
+ var doc map[string]any
91
+ require.NoError(t, json.Unmarshal(raw, &doc))
92
+
93
+ schema := requireMapField(t, doc, "jsonSchema")
94
+ assert.NotContains(t, schema, "allOf")
95
+
96
+ uiSchema := requireMapField(t, doc, "uiSchema")
97
+ uiProfiles := requireMapField(t, uiSchema, "profiles")
98
+ for _, mode := range []string{"mode_auto", "mode_exact", "mode_combined"} {
99
+ modeUI := requireMapField(t, uiProfiles, mode)
100
+ entries := requireMapField(t, modeUI, "entries")
101
+ items := requireMapField(t, entries, "items")
102
+ filters := requireMapField(t, items, "filters")
103
+ tags := requireMapField(t, filters, "tags")
104
+ help, ok := tags["ui:help"].(string)
105
+ require.True(t, ok)
106
+ assert.Contains(t, help, "Only supported when `discovery.mode` is `filters`")
107
+ }
108
+}
109
+
110
func requireMapField(t *testing.T, m map[string]any, key string) map[string]any {
111
t.Helper()
112
@@ -107,6 +134,16 @@ func requireStringSliceField(t *testing.T, m map[string]any, key string) []strin
134
return out
135
}
136
137
+func requireArrayField(t *testing.T, m map[string]any, key string) []any {
138
+ t.Helper()
139
+
140
+ value, ok := m[key]
141
+ require.Truef(t, ok, "missing key %q", key)
142
+ items, ok := value.([]any)
143
+ require.Truef(t, ok, "key %q is not an array", key)
144
+ return items
145
+}
146
+
147
func TestCollector_ConfigurationSerialize(t *testing.T) {
148
collecttest.TestConfigurationSerialize(t, &Collector{}, dataConfigJSON, dataConfigYAML)
149
}
@@ -132,7 +169,7 @@ func TestCollector_Init(t *testing.T) {
169
Timeout: confopt.Duration(-time.Second),
170
Profiles: ProfilesConfig{
171
Mode: profilesModeExact,
135
- ModeExact: &ProfilesModeConfig{Names: []string{"postgres_flexible"}},
172
+ ModeExact: testProfilesModeEntries("postgres_flexible"),
173
},
174
Auth: cloudauth.AzureADAuthConfig{
175
Mode: cloudauth.AzureADAuthModeDefault,
@@ -425,6 +462,53 @@ func TestCollector_RefreshDiscoveryDisabledWhenRefreshEveryZero(t *testing.T) {
462
assert.Equal(t, 1, rg.calls())
463
}
464
465
+func TestCollector_RefreshDiscoveryFailureFallsBackToLastKnownSnapshot(t *testing.T) {
466
+ now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
467
+
468
+ rg := &mockResourceGraph{
469
+ resources: []map[string]any{
470
+ {
471
+ "id": "/subscriptions/sub-1/resourceGroups/rg-a/providers/Microsoft.DBforPostgreSQL/flexibleServers/pg-a",
472
+ "name": "pg-a",
473
+ "type": "Microsoft.DBforPostgreSQL/flexibleServers",
474
+ "resourceGroup": "rg-a",
475
+ "location": "eastus",
476
+ },
477
+ },
478
+ }
479
+ mx := &mockMetricsClient{
480
+ queryResponse: azmetrics.QueryResourcesResponse{MetricResults: azmetrics.MetricResults{Values: []azmetrics.MetricData{
481
+ {
482
+ ResourceID: ptrString("/subscriptions/sub-1/resourcegroups/rg-a/providers/microsoft.dbforpostgresql/flexibleservers/pg-a"),
483
+ Values: []azmetrics.Metric{
484
+ metricWithAvg("cpu_percent", now, 21.5),
485
+ },
486
+ },
487
+ }}},
488
+ }
489
+
490
+ c := newTestCollectorWithMocks(rg, mx)
491
+ c.Config = testConfig()
492
+ c.Config.Discovery.RefreshEvery = 60
493
+ c.now = func() time.Time { return now }
494
+
495
+ require.NoError(t, c.Init(context.Background()))
496
+ require.NoError(t, c.Check(context.Background()))
497
+ assert.Equal(t, 1, rg.calls())
498
+ assert.Equal(t, uint64(1), c.discovery.FetchCounter)
499
+
500
+ rg.responseErr = errors.New("refresh failed")
501
+ now = now.Add(10 * time.Minute)
502
+
503
+ series, err := collecttest.CollectScalarSeries(c, metrix.ReadRaw())
504
+ require.NoError(t, err)
505
+ assert.NotEmpty(t, series)
506
+ assert.Contains(t, strings.Join(keysFromSeries(series), "\n"), `subscription_id="sub-1"`)
507
+ assert.Equal(t, 2, rg.calls())
508
+ assert.GreaterOrEqual(t, mx.calls(), 1)
509
+ assert.Equal(t, uint64(1), c.discovery.FetchCounter)
510
+}
511
+
512
func TestCollector_CollectScenarios(t *testing.T) {
513
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
514
@@ -628,7 +712,7 @@ func TestCollector_CollectScenarios(t *testing.T) {
712
func TestCollector_RefreshDiscovery_PushesModeFiltersIntoQuery(t *testing.T) {
713
tests := map[string]struct {
714
resources []map[string]any
631
- filters *DiscoveryFiltersConfig
715
+ filters *ResourceFiltersConfig
716
wantQuery string
717
wantCounts int
718
}{
@@ -649,9 +733,9 @@ func TestCollector_RefreshDiscovery_PushesModeFiltersIntoQuery(t *testing.T) {
733
"location": "eastus",
734
},
735
},
652
- filters: &DiscoveryFiltersConfig{ResourceGroups: []string{"RG-B", " rg-a ", "rg-b"}},
736
+ filters: &ResourceFiltersConfig{ResourceGroups: []string{"RG-B", " rg-a ", "rg-b"}},
737
wantCounts: 2,
654
- wantQuery: "resources | where type in~ ('Microsoft.DBforPostgreSQL/flexibleServers') | where resourceGroup in~ ('rg-a', 'rg-b') | project id, name, type, resourceGroup, location",
738
+ wantQuery: "resources | where type in~ ('Microsoft.DBforPostgreSQL/flexibleServers') | where resourceGroup in~ ('rg-a', 'rg-b') | project id, name, type, resourceGroup, location, tags",
739
},
740
"resource groups regions and tags": {
741
resources: []map[string]any{
@@ -663,7 +747,7 @@ func TestCollector_RefreshDiscovery_PushesModeFiltersIntoQuery(t *testing.T) {
747
"location": "eastus",
748
},
749
},
666
- filters: &DiscoveryFiltersConfig{
750
+ filters: &ResourceFiltersConfig{
751
ResourceGroups: []string{"RG-B", " rg-a ", "rg-b"},
752
Regions: []string{" WestEurope ", "eastus", "EASTUS"},
753
Tags: map[string][]string{
@@ -671,7 +755,27 @@ func TestCollector_RefreshDiscovery_PushesModeFiltersIntoQuery(t *testing.T) {
755
" env ": {"prod"},
756
},
757
},
674
- wantQuery: "resources | where type in~ ('Microsoft.DBforPostgreSQL/flexibleServers') | where resourceGroup in~ ('rg-a', 'rg-b') | where location in~ ('eastus', 'westeurope') | mv-expand bagexpansion=array tags | where isnotempty(tags) | extend tagKey = tostring(tags[0]), tagValue = tostring(tags[1]) | where (tagKey =~ 'env' and tagValue == 'prod') or (tagKey =~ 'role' and tagValue in ('api', 'worker')) | summarize by id, name, type, resourceGroup, location, matchedTagKey = tolower(tagKey) | summarize matchedTagKeys = count() by id, name, type, resourceGroup, location | where matchedTagKeys == 2 | project id, name, type, resourceGroup, location",
758
+ wantQuery: "resources | where type in~ ('Microsoft.DBforPostgreSQL/flexibleServers') | where resourceGroup in~ ('rg-a', 'rg-b') | where location in~ ('eastus', 'westeurope') | extend tagsBag = tags | mv-expand bagexpansion=array tags | where isnotempty(tags) | extend tagKey = tostring(tags[0]), tagValue = tostring(tags[1]) | where (tagKey =~ 'env' and tagValue == 'prod') or (tagKey =~ 'role' and tagValue in ('api', 'worker')) | summarize tags = take_any(tagsBag), matchedTagKeys = dcount(tolower(tagKey)) by id, name, type, resourceGroup, location | where matchedTagKeys == 2 | project id, name, type, resourceGroup, location, tags",
759
+ },
760
+ "deduplicates ids case insensitively": {
761
+ resources: []map[string]any{
762
+ {
763
+ "id": "/subscriptions/sub-1/resourceGroups/rg-a/providers/Microsoft.DBforPostgreSQL/flexibleServers/pg-a",
764
+ "name": "pg-a",
765
+ "type": "Microsoft.DBforPostgreSQL/flexibleServers",
766
+ "resourceGroup": "rg-a",
767
+ "location": "eastus",
768
+ },
769
+ {
770
+ "id": "/SUBSCRIPTIONS/sub-1/resourcegroups/rg-a/providers/microsoft.dbforpostgresql/flexibleservers/pg-a",
771
+ "name": "pg-a-duplicate",
772
+ "type": "Microsoft.DBforPostgreSQL/flexibleServers",
773
+ "resourceGroup": "rg-a",
774
+ "location": "eastus",
775
+ },
776
+ },
777
+ wantCounts: 1,
778
+ wantQuery: "resources | where type in~ ('Microsoft.DBforPostgreSQL/flexibleServers') | project id, name, type, resourceGroup, location, tags",
779
},
780
}
781
@@ -720,12 +824,11 @@ func TestCollector_TimeGrainScheduling(t *testing.T) {
824
}
825
826
cfg := testConfig()
723
- cfg.Profiles.ModeExact = &ProfilesModeConfig{Names: []string{"storage_slow"}}
827
+ cfg.Profiles.ModeExact = testProfilesModeEntries("storage_slow")
828
829
catalog := mustLoadStockCatalog(t, map[string]string{
830
"storage_slow.yaml": `
727
-id: storage_slow
728
-name: Azure Storage Slow
831
+display_name: Azure Storage Slow
832
resource_type: Microsoft.Storage/storageAccounts
833
metrics:
834
- id: used_capacity
@@ -798,12 +901,11 @@ func TestCollector_QueryOffsetUsesEffectivePerBatchOffset(t *testing.T) {
901
}
902
903
cfg := testConfig()
801
- cfg.Profiles.ModeExact = &ProfilesModeConfig{Names: []string{"storage_mixed"}}
904
+ cfg.Profiles.ModeExact = testProfilesModeEntries("storage_mixed")
905
906
catalog := mustLoadStockCatalog(t, map[string]string{
907
"storage_mixed.yaml": `
805
-id: storage_mixed
806
-name: Azure Storage Mixed Grains
908
+display_name: Azure Storage Mixed Grains
909
resource_type: Microsoft.Storage/storageAccounts
910
metrics:
911
- id: transactions
@@ -908,7 +1010,7 @@ func TestCollector_CheckBootstrapProfileScenarios(t *testing.T) {
1010
},
1011
check: func(t *testing.T, c *Collector, rg *mockResourceGraph) {
1012
require.NotEmpty(t, c.runtime.Profiles)
911
- assert.Equal(t, "postgres_flexible", c.runtime.Profiles[0].ID)
1013
+ assert.Equal(t, "postgres_flexible", c.runtime.Profiles[0].Name)
1014
},
1015
},
1016
"combined with explicit profiles": {
@@ -925,12 +1027,12 @@ func TestCollector_CheckBootstrapProfileScenarios(t *testing.T) {
1027
c.Config = testConfig()
1028
c.Config.Profiles.Mode = profilesModeCombined
1029
c.Config.Profiles.ModeExact = nil
928
- c.Config.Profiles.ModeCombined = &ProfilesModeConfig{Names: []string{"cosmos_db"}}
1030
+ c.Config.Profiles.ModeCombined = testProfilesModeEntries("cosmos_db")
1031
},
1032
check: func(t *testing.T, c *Collector, rg *mockResourceGraph) {
1033
var ids []string
1034
for _, p := range c.runtime.Profiles {
933
- ids = append(ids, p.ID)
1035
+ ids = append(ids, p.Name)
1036
}
1037
assert.Contains(t, ids, "cosmos_db")
1038
assert.Contains(t, ids, "postgres_flexible")
@@ -941,11 +1043,11 @@ func TestCollector_CheckBootstrapProfileScenarios(t *testing.T) {
1043
c.Config = testConfig()
1044
c.Config.Profiles.Mode = profilesModeCombined
1045
c.Config.Profiles.ModeExact = nil
944
- c.Config.Profiles.ModeCombined = &ProfilesModeConfig{Names: []string{"cosmos_db"}}
1046
+ c.Config.Profiles.ModeCombined = testProfilesModeEntries("cosmos_db")
1047
},
1048
check: func(t *testing.T, c *Collector, rg *mockResourceGraph) {
1049
require.Len(t, c.runtime.Profiles, 1)
948
- assert.Equal(t, "cosmos_db", c.runtime.Profiles[0].ID)
1050
+ assert.Equal(t, "cosmos_db", c.runtime.Profiles[0].Name)
1051
},
1052
},
1053
"default mode resolves to auto and fails with no matches": {
@@ -1019,7 +1121,7 @@ func TestCollector_CheckBootstrapQueryModeScenarios(t *testing.T) {
1121
},
1122
check: func(t *testing.T, c *Collector, rg *mockResourceGraph) {
1123
require.NotEmpty(t, c.runtime.Profiles)
1022
- assert.Equal(t, "postgres_flexible", c.runtime.Profiles[0].ID)
1124
+ assert.Equal(t, "postgres_flexible", c.runtime.Profiles[0].Name)
1125
assert.Equal(t, kql, rg.lastQuery())
1126
},
1127
},
@@ -1062,7 +1164,7 @@ func TestCollector_CheckBootstrapQueryModeScenarios(t *testing.T) {
1164
},
1165
check: func(t *testing.T, c *Collector, rg *mockResourceGraph) {
1166
require.Len(t, c.runtime.Profiles, 1)
1065
- assert.Equal(t, "postgres_flexible", c.runtime.Profiles[0].ID)
1167
+ assert.Equal(t, "postgres_flexible", c.runtime.Profiles[0].Name)
1168
assert.Empty(t, c.discovery.Resources)
1169
},
1170
},
@@ -1159,6 +1261,258 @@ func TestCollector_InitQueryModeRejectsMalformedRows(t *testing.T) {
1261
}
1262
}
1263
1264
+func TestCollector_ProfileFilters_NarrowMatchedResourcesInFiltersMode(t *testing.T) {
1265
+ rg := &mockResourceGraph{
1266
+ resources: []map[string]any{
1267
+ {
1268
+ "id": "/subscriptions/sub-1/resourceGroups/rg-a/providers/Microsoft.Sql/servers/sql-a/databases/db-a",
1269
+ "name": "db-a",
1270
+ "type": "Microsoft.Sql/servers/databases",
1271
+ "resourceGroup": "rg-a",
1272
+ "location": "eastus",
1273
+ "tags": map[string]any{
1274
+ "env": "prod",
1275
+ },
1276
+ },
1277
+ {
1278
+ "id": "/subscriptions/sub-1/resourceGroups/rg-a/providers/Microsoft.Sql/servers/sql-a/databases/db-b",
1279
+ "name": "db-b",
1280
+ "type": "Microsoft.Sql/servers/databases",
1281
+ "resourceGroup": "rg-a",
1282
+ "location": "eastus",
1283
+ "tags": map[string]any{
1284
+ "env": "dev",
1285
+ },
1286
+ },
1287
+ },
1288
+ }
1289
+
1290
+ c := newTestCollectorWithMocks(rg, &mockMetricsClient{})
1291
+ c.Config = testConfig()
1292
+ c.Config.Profiles.ModeExact = &ProfilesModeConfig{
1293
+ Entries: []ProfileEntryConfig{{
1294
+ Name: "sql_database",
1295
+ Filters: &ResourceFiltersConfig{
1296
+ Tags: map[string][]string{
1297
+ "env": {"prod"},
1298
+ },
1299
+ },
1300
+ }},
1301
+ }
1302
+
1303
+ require.NoError(t, c.Init(context.Background()))
1304
+ require.NoError(t, c.Check(context.Background()))
1305
+
1306
+ require.Len(t, c.discovery.Resources, 2)
1307
+ require.Len(t, c.discovery.ByProfile["sql_database"], 1)
1308
+ assert.Equal(t, "db-a", c.discovery.ByProfile["sql_database"][0].Name)
1309
+}
1310
+
1311
+func TestCollector_ProfileFilters_NarrowMatchedResourcesInQueryMode(t *testing.T) {
1312
+ const kql = "resources | project id, name, type, resourceGroup, location"
1313
+
1314
+ rg := &mockResourceGraph{
1315
+ resources: []map[string]any{
1316
+ {
1317
+ "id": "/subscriptions/sub-1/resourceGroups/rg-a/providers/Microsoft.Sql/servers/sql-a/databases/db-a",
1318
+ "name": "db-a",
1319
+ "type": "Microsoft.Sql/servers/databases",
1320
+ "resourceGroup": "rg-a",
1321
+ "location": "eastus",
1322
+ },
1323
+ {
1324
+ "id": "/subscriptions/sub-1/resourceGroups/rg-a/providers/Microsoft.Sql/servers/sql-a/databases/db-b",
1325
+ "name": "db-b",
1326
+ "type": "Microsoft.Sql/servers/databases",
1327
+ "resourceGroup": "rg-a",
1328
+ "location": "westeurope",
1329
+ },
1330
+ },
1331
+ }
1332
+
1333
+ c := newTestCollectorWithMocks(rg, &mockMetricsClient{})
1334
+ c.Config = testConfig()
1335
+ c.Config.Discovery.Mode = discoveryModeQuery
1336
+ c.Config.Discovery.ModeFilters = nil
1337
+ c.Config.Discovery.ModeQuery = &DiscoveryQueryConfig{KQL: kql}
1338
+ c.Config.Profiles.ModeExact = &ProfilesModeConfig{
1339
+ Entries: []ProfileEntryConfig{{
1340
+ Name: "sql_database",
1341
+ Filters: &ResourceFiltersConfig{
1342
+ Regions: []string{"eastus"},
1343
+ },
1344
+ }},
1345
+ }
1346
+
1347
+ require.NoError(t, c.Init(context.Background()))
1348
+ require.NoError(t, c.Check(context.Background()))
1349
+
1350
+ require.Len(t, c.discovery.Resources, 2)
1351
+ require.Len(t, c.discovery.ByProfile["sql_database"], 1)
1352
+ assert.Equal(t, "db-a", c.discovery.ByProfile["sql_database"][0].Name)
1353
+}
1354
+
1355
+func TestCollector_CombinedExplicitEntryOverlaysAutoProfile(t *testing.T) {
1356
+ rg := &mockResourceGraph{
1357
+ resources: []map[string]any{
1358
+ {
1359
+ "id": "/subscriptions/sub-1/resourceGroups/rg-a/providers/Microsoft.DBforPostgreSQL/flexibleServers/pg-a",
1360
+ "name": "pg-a",
1361
+ "type": "Microsoft.DBforPostgreSQL/flexibleServers",
1362
+ "resourceGroup": "rg-a",
1363
+ "location": "eastus",
1364
+ },
1365
+ },
1366
+ }
1367
+
1368
+ c := newTestCollectorWithMocks(rg, &mockMetricsClient{})
1369
+ c.Config = testConfig()
1370
+ c.Config.Profiles.Mode = profilesModeCombined
1371
+ c.Config.Profiles.ModeExact = nil
1372
+ c.Config.Profiles.ModeCombined = &ProfilesModeConfig{
1373
+ Entries: []ProfileEntryConfig{{
1374
+ Name: "postgres_flexible",
1375
+ Filters: &ResourceFiltersConfig{
1376
+ Regions: []string{"westeurope"},
1377
+ },
1378
+ }},
1379
+ }
1380
+
1381
+ require.NoError(t, c.Init(context.Background()))
1382
+ require.NoError(t, c.Check(context.Background()))
1383
+
1384
+ require.Len(t, c.runtime.Profiles, 1)
1385
+ assert.Equal(t, "postgres_flexible", c.runtime.Profiles[0].Name)
1386
+ require.NotNil(t, c.runtime.Profiles[0].Filters)
1387
+ assert.Equal(t, []string{"westeurope"}, c.runtime.Profiles[0].Filters.Regions)
1388
+ require.Len(t, c.discovery.Resources, 1)
1389
+ assert.NotContains(t, c.discovery.ByProfile, "postgres_flexible")
1390
+}
1391
+
1392
+func TestCollector_AutoModeEntriesStayDormantAcrossRefresh(t *testing.T) {
1393
+ rg := &mockResourceGraph{
1394
+ resources: []map[string]any{
1395
+ {
1396
+ "id": "/subscriptions/sub-1/resourceGroups/rg-a/providers/Microsoft.DBforPostgreSQL/flexibleServers/pg-a",
1397
+ "name": "pg-a",
1398
+ "type": "Microsoft.DBforPostgreSQL/flexibleServers",
1399
+ "resourceGroup": "rg-a",
1400
+ "location": "eastus",
1401
+ },
1402
+ },
1403
+ }
1404
+
1405
+ c := newTestCollectorWithMocks(rg, &mockMetricsClient{})
1406
+ c.Config = testConfig()
1407
+ c.Config.Profiles.Mode = profilesModeAuto
1408
+ c.Config.Profiles.ModeExact = nil
1409
+ c.Config.Profiles.ModeCombined = nil
1410
+ c.Config.Profiles.ModeAuto = &ProfilesModeConfig{
1411
+ Entries: []ProfileEntryConfig{{
1412
+ Name: "sql_database",
1413
+ Filters: &ResourceFiltersConfig{
1414
+ ResourceGroups: []string{"prod-rg"},
1415
+ },
1416
+ }},
1417
+ }
1418
+
1419
+ require.NoError(t, c.Init(context.Background()))
1420
+ require.NoError(t, c.Check(context.Background()))
1421
+
1422
+ require.Len(t, c.runtime.Profiles, 1)
1423
+ assert.Equal(t, "postgres_flexible", c.runtime.Profiles[0].Name)
1424
+ assert.NotContains(t, c.discovery.ByProfile, "sql_database")
1425
+
1426
+ rg.setResources([]map[string]any{
1427
+ {
1428
+ "id": "/subscriptions/sub-1/resourceGroups/prod-rg/providers/Microsoft.Sql/servers/sql-a/databases/db-a",
1429
+ "name": "db-a",
1430
+ "type": "Microsoft.Sql/servers/databases",
1431
+ "resourceGroup": "prod-rg",
1432
+ "location": "eastus",
1433
+ },
1434
+ })
1435
+
1436
+ _, err := c.refreshDiscovery(context.Background(), true)
1437
+ require.NoError(t, err)
1438
+
1439
+ require.Len(t, c.runtime.Profiles, 1)
1440
+ assert.Equal(t, "postgres_flexible", c.runtime.Profiles[0].Name)
1441
+ assert.Empty(t, c.discovery.Resources)
1442
+ assert.NotContains(t, c.discovery.ByProfile, "sql_database")
1443
+ assert.Contains(t, rg.lastQuery(), "Microsoft.DBforPostgreSQL/flexibleServers")
1444
+ assert.NotContains(t, rg.lastQuery(), "Microsoft.Sql/servers/databases")
1445
+}
1446
+
1447
+func TestObservationState_PruneStaleResources_RemovesOldProfileMembership(t *testing.T) {
1448
+ resource := resourceInfo{
1449
+ SubscriptionID: "sub-1",
1450
+ UID: "uid-a",
1451
+ Name: "db-a",
1452
+ ResourceGroup: "rg-a",
1453
+ Region: "eastus",
1454
+ Type: "Microsoft.Sql/servers/databases",
1455
+ }
1456
+ labels := labelValues(resourceLabels(resource, "sql_database"))
1457
+ key := sampleObservationKey("sql.cpu", labels)
1458
+
1459
+ state := &observationState{
1460
+ accumulators: map[string]float64{
1461
+ key: 10,
1462
+ },
1463
+ lastObserved: map[string]lastObservation{
1464
+ key: {
1465
+ instrument: "sql.cpu",
1466
+ labelValues: append([]string(nil), labels...),
1467
+ value: 10,
1468
+ },
1469
+ },
1470
+ }
1471
+
1472
+ state.pruneStaleResources(map[string][]resourceInfo{
1473
+ "postgres_flexible": {resource},
1474
+ })
1475
+
1476
+ assert.Empty(t, state.lastObserved)
1477
+ assert.Empty(t, state.accumulators)
1478
+}
1479
+
1480
+func TestObservationState_PruneStaleResources_RemovesLabelChurnForSameResource(t *testing.T) {
1481
+ oldResource := resourceInfo{
1482
+ SubscriptionID: "sub-1",
1483
+ UID: "uid-a",
1484
+ Name: "db-a",
1485
+ ResourceGroup: "rg-a",
1486
+ Region: "eastus",
1487
+ Type: "Microsoft.Sql/servers/databases",
1488
+ }
1489
+ newResource := oldResource
1490
+ newResource.Name = "db-a-renamed"
1491
+
1492
+ labels := labelValues(resourceLabels(oldResource, "sql_database"))
1493
+ key := sampleObservationKey("sql.cpu", labels)
1494
+
1495
+ state := &observationState{
1496
+ accumulators: map[string]float64{
1497
+ key: 10,
1498
+ },
1499
+ lastObserved: map[string]lastObservation{
1500
+ key: {
1501
+ instrument: "sql.cpu",
1502
+ labelValues: append([]string(nil), labels...),
1503
+ value: 10,
1504
+ },
1505
+ },
1506
+ }
1507
+
1508
+ state.pruneStaleResources(map[string][]resourceInfo{
1509
+ "sql_database": {newResource},
1510
+ })
1511
+
1512
+ assert.Empty(t, state.lastObserved)
1513
+ assert.Empty(t, state.accumulators)
1514
+}
1515
+
1516
func TestConfig_ValidateDiscoveryContracts(t *testing.T) {
1517
tests := map[string]struct {
1518
cfg Config
@@ -1179,7 +1533,7 @@ func TestConfig_ValidateDiscoveryContracts(t *testing.T) {
1533
cfg := testConfig()
1534
cfg.Discovery.Mode = discoveryModeQuery
1535
cfg.Discovery.ModeQuery = &DiscoveryQueryConfig{KQL: "resources | project id, name, type, resourceGroup, location"}
1182
- cfg.Discovery.ModeFilters = &DiscoveryFiltersConfig{ResourceGroups: []string{"rg-a"}}
1536
+ cfg.Discovery.ModeFilters = &ResourceFiltersConfig{ResourceGroups: []string{"rg-a"}}
1537
return cfg
1538
}(),
1539
wantErr: false,
@@ -1199,7 +1553,7 @@ func TestConfig_ValidateDiscoveryContracts(t *testing.T) {
1553
cfg: func() Config {
1554
cfg := testConfig()
1555
cfg.Discovery.Mode = discoveryModeFilters
1202
- cfg.Discovery.ModeFilters = &DiscoveryFiltersConfig{ResourceGroups: []string{""}}
1556
+ cfg.Discovery.ModeFilters = &ResourceFiltersConfig{ResourceGroups: []string{""}}
1557
return cfg
1558
}(),
1559
wantErr: true,
@@ -1209,7 +1563,7 @@ func TestConfig_ValidateDiscoveryContracts(t *testing.T) {
1563
cfg: func() Config {
1564
cfg := testConfig()
1565
cfg.Discovery.Mode = discoveryModeFilters
1212
- cfg.Discovery.ModeFilters = &DiscoveryFiltersConfig{Regions: []string{" "}}
1566
+ cfg.Discovery.ModeFilters = &ResourceFiltersConfig{Regions: []string{" "}}
1567
return cfg
1568
}(),
1569
wantErr: true,
@@ -1219,7 +1573,7 @@ func TestConfig_ValidateDiscoveryContracts(t *testing.T) {
1573
cfg: func() Config {
1574
cfg := testConfig()
1575
cfg.Profiles.Mode = profilesModeAuto
1222
- cfg.Profiles.ModeCombined = &ProfilesModeConfig{Names: []string{"cosmos_db"}}
1576
+ cfg.Profiles.ModeCombined = testProfilesModeEntries("cosmos_db")
1577
return cfg
1578
}(),
1579
wantErr: false,
@@ -1228,20 +1582,48 @@ func TestConfig_ValidateDiscoveryContracts(t *testing.T) {
1582
cfg: func() Config {
1583
cfg := testConfig()
1584
cfg.Profiles.Mode = profilesModeExact
1231
- cfg.Profiles.ModeCombined = &ProfilesModeConfig{Names: []string{"cosmos_db"}}
1585
+ cfg.Profiles.ModeCombined = testProfilesModeEntries("cosmos_db")
1586
return cfg
1587
}(),
1588
wantErr: false,
1589
},
1236
- "exact mode rejects duplicate names ignoring case": {
1590
+ "exact mode rejects duplicate entry names": {
1591
+ cfg: func() Config {
1592
+ cfg := testConfig()
1593
+ cfg.Profiles.Mode = profilesModeExact
1594
+ cfg.Profiles.ModeExact = &ProfilesModeConfig{Entries: []ProfileEntryConfig{{Name: "postgres_flexible"}, {Name: "postgres_flexible"}}}
1595
+ return cfg
1596
+ }(),
1597
+ wantErr: true,
1598
+ wantErrContain: "'profiles.mode_exact.entries' contains duplicate entry name 'postgres_flexible'",
1599
+ },
1600
+ "exact mode rejects non-lowercase entry names": {
1601
cfg: func() Config {
1602
cfg := testConfig()
1603
cfg.Profiles.Mode = profilesModeExact
1240
- cfg.Profiles.ModeExact = &ProfilesModeConfig{Names: []string{"POSTGRES_FLEXIBLE", "postgres_flexible"}}
1604
+ cfg.Profiles.ModeExact = &ProfilesModeConfig{Entries: []ProfileEntryConfig{{Name: "POSTGRES_FLEXIBLE"}}}
1605
return cfg
1606
}(),
1607
wantErr: true,
1244
- wantErrContain: "'profiles.mode_exact.names' contains duplicate value 'postgres_flexible'",
1608
+ wantErrContain: "'profiles.mode_exact.entries[0].name' must match",
1609
+ },
1610
+ "query mode ignores profile tag filters during validation": {
1611
+ cfg: func() Config {
1612
+ cfg := testConfig()
1613
+ cfg.Discovery.Mode = discoveryModeQuery
1614
+ cfg.Discovery.ModeFilters = nil
1615
+ cfg.Discovery.ModeQuery = &DiscoveryQueryConfig{KQL: "resources | project id, name, type, resourceGroup, location"}
1616
+ cfg.Profiles.ModeExact = &ProfilesModeConfig{
1617
+ Entries: []ProfileEntryConfig{{
1618
+ Name: "postgres_flexible",
1619
+ Filters: &ResourceFiltersConfig{
1620
+ Tags: map[string][]string{"env": {"prod"}},
1621
+ },
1622
+ }},
1623
+ }
1624
+ return cfg
1625
+ }(),
1626
+ wantErr: false,
1627
},
1628
}
1629
@@ -1258,6 +1640,53 @@ func TestConfig_ValidateDiscoveryContracts(t *testing.T) {
1640
}
1641
}
1642
1643
+func TestSanitizeIgnoredProfileTagFilters(t *testing.T) {
1644
+ cfg := testConfig()
1645
+ cfg.Discovery.Mode = discoveryModeQuery
1646
+ cfg.Discovery.ModeFilters = nil
1647
+ cfg.Discovery.ModeQuery = &DiscoveryQueryConfig{KQL: "resources | project id, name, type, resourceGroup, location"}
1648
+ cfg.Profiles.Mode = profilesModeExact
1649
+ cfg.Profiles.ModeAuto = nil
1650
+ cfg.Profiles.ModeCombined = nil
1651
+ cfg.Profiles.ModeExact = &ProfilesModeConfig{
1652
+ Entries: []ProfileEntryConfig{
1653
+ {
1654
+ Name: "postgres_flexible",
1655
+ Filters: &ResourceFiltersConfig{
1656
+ ResourceGroups: []string{"rg-a"},
1657
+ Tags: map[string][]string{"env": {"prod"}},
1658
+ },
1659
+ },
1660
+ {
1661
+ Name: "sql_database",
1662
+ Filters: &ResourceFiltersConfig{
1663
+ Tags: map[string][]string{"tier": {"critical"}},
1664
+ },
1665
+ },
1666
+ },
1667
+ }
1668
+
1669
+ sanitized, warnings := sanitizeIgnoredProfileTagFilters(cfg)
1670
+
1671
+ assert.Equal(t, []string{
1672
+ "profiles.mode_exact.entries[0].filters.tags",
1673
+ "profiles.mode_exact.entries[1].filters.tags",
1674
+ }, warnings)
1675
+ require.NotNil(t, sanitized.Profiles.ModeExact)
1676
+ require.Len(t, sanitized.Profiles.ModeExact.Entries, 2)
1677
+ require.NotNil(t, sanitized.Profiles.ModeExact.Entries[0].Filters)
1678
+ assert.Equal(t, []string{"rg-a"}, sanitized.Profiles.ModeExact.Entries[0].Filters.ResourceGroups)
1679
+ assert.Nil(t, sanitized.Profiles.ModeExact.Entries[0].Filters.Tags)
1680
+ assert.Nil(t, sanitized.Profiles.ModeExact.Entries[1].Filters)
1681
+
1682
+ require.NotNil(t, cfg.Profiles.ModeExact)
1683
+ require.Len(t, cfg.Profiles.ModeExact.Entries, 2)
1684
+ require.NotNil(t, cfg.Profiles.ModeExact.Entries[0].Filters)
1685
+ assert.Equal(t, map[string][]string{"env": {"prod"}}, cfg.Profiles.ModeExact.Entries[0].Filters.Tags)
1686
+ require.NotNil(t, cfg.Profiles.ModeExact.Entries[1].Filters)
1687
+ assert.Equal(t, map[string][]string{"tier": {"critical"}}, cfg.Profiles.ModeExact.Entries[1].Filters.Tags)
1688
+}
1689
+
1690
func TestMergeProfileIDs(t *testing.T) {
1691
tests := map[string]struct {
1692
explicit []string
@@ -1283,19 +1712,18 @@ func TestMergeProfileIDs(t *testing.T) {
1712
1713
for name, tc := range tests {
1714
t.Run(name, func(t *testing.T) {
1286
- got := mergeProfileIDs(tc.explicit, tc.discovered)
1715
+ got := mergeProfileNames(tc.explicit, tc.discovered)
1716
assert.Equal(t, tc.want, got)
1717
})
1718
}
1719
}
1720
1721
func TestBuildCollectorRuntime_DetectsChartIDCollision(t *testing.T) {
1293
- profileIDs := []string{"redis_upper", "redis_lower"}
1722
+ profileNames := []string{"redis_upper", "redis_lower"}
1723
1724
catalog := mustLoadStockCatalog(t, map[string]string{
1725
"redis_upper.yaml": `
1297
-id: redis_upper
1298
-name: Azure Redis Cache Upper
1726
+display_name: Azure Redis Cache Upper
1727
resource_type: Microsoft.Cache/Redis
1728
metrics:
1729
- id: connectedclients
@@ -1320,8 +1748,7 @@ template:
1748
name: average
1749
`,
1750
"redis_lower.yaml": `
1323
-id: redis_lower
1324
-name: Azure Redis Cache Lower
1751
+display_name: Azure Redis Cache Lower
1752
resource_type: Microsoft.Cache/Redis
1753
metrics:
1754
- id: cachehits
@@ -1347,7 +1774,7 @@ template:
1774
`,
1775
})
1776
1350
- _, err := buildCollectorRuntimeFromConfig(profileIDs, catalog)
1777
+ _, err := buildCollectorRuntimeFromConfig(profileNames, nil, catalog)
1778
require.Error(t, err)
1779
}
1780
@@ -1363,7 +1790,7 @@ func testConfig() Config {
1790
},
1791
Profiles: ProfilesConfig{
1792
Mode: profilesModeExact,
1366
- ModeExact: &ProfilesModeConfig{Names: []string{"postgres_flexible"}},
1793
+ ModeExact: testProfilesModeEntries("postgres_flexible"),
1794
},
1795
QueryOffset: 180,
1796
Timeout: defaultTimeout,
@@ -1393,6 +1820,14 @@ func mustLoadStockCatalog(t *testing.T, files map[string]string) azureprofiles.C
1820
return catalog
1821
}
1822
1823
+func testProfilesModeEntries(names ...string) *ProfilesModeConfig {
1824
+ entries := make([]ProfileEntryConfig, 0, len(names))
1825
+ for _, name := range names {
1826
+ entries = append(entries, ProfileEntryConfig{Name: name})
1827
+ }
1828
+ return &ProfilesModeConfig{Entries: entries}
1829
+}
1830
+
1831
func newTestCollectorWithMocks(rg *mockResourceGraph, mx *mockMetricsClient) *Collector {
1832
c := New()
1833
c.newResourceGraph = func(string, azcore.TokenCredential, azcloud.Configuration) (resourceGraphClient, error) {
@@ -1405,13 +1840,14 @@ func newTestCollectorWithMocks(rg *mockResourceGraph, mx *mockMetricsClient) *Co
1840
}
1841
1842
type mockResourceGraph struct {
1408
- mu sync.Mutex
1409
- resources []map[string]any
1410
- count int
1411
- query string
1412
- subs []string
1413
- timeout time.Duration
1414
- hasDL bool
1843
+ mu sync.Mutex
1844
+ resources []map[string]any
1845
+ responseErr error
1846
+ count int
1847
+ query string
1848
+ subs []string
1849
+ timeout time.Duration
1850
+ hasDL bool
1851
}
1852
1853
func (m *mockResourceGraph) Resources(ctx context.Context, req armresourcegraph.QueryRequest, _ *armresourcegraph.ClientResourcesOptions) (armresourcegraph.ClientResourcesResponse, error) {
@@ -1438,6 +1874,9 @@ func (m *mockResourceGraph) Resources(ctx context.Context, req armresourcegraph.
1874
} else {
1875
m.timeout = 0
1876
}
1877
+ if m.responseErr != nil {
1878
+ return armresourcegraph.ClientResourcesResponse{}, m.responseErr
1879
+ }
1880
1881
rows := make([]any, 0, len(m.resources))
1882
for _, r := range m.resources {
@@ -1470,6 +1909,12 @@ func (m *mockResourceGraph) lastSubscriptions() []string {
1909
return append([]string(nil), m.subs...)
1910
}
1911
1912
+func (m *mockResourceGraph) setResources(resources []map[string]any) {
1913
+ m.mu.Lock()
1914
+ defer m.mu.Unlock()
1915
+ m.resources = append([]map[string]any(nil), resources...)
1916
+}
1917
+
1918
type mockMetricsClient struct {
1919
mu sync.Mutex
1920
queryResponse azmetrics.QueryResourcesResponse
src/go/plugin/go.d/collector/azure_monitor/config.go
+173
-49
@@ -58,13 +58,13 @@ type Config struct {
58
}
59
60
type DiscoveryConfig struct {
61
- RefreshEvery int `yaml:"refresh_every,omitempty" json:"refresh_every"`
62
- Mode string `yaml:"mode,omitempty" json:"mode"`
63
- ModeFilters *DiscoveryFiltersConfig `yaml:"mode_filters,omitempty" json:"mode_filters,omitempty"`
64
- ModeQuery *DiscoveryQueryConfig `yaml:"mode_query,omitempty" json:"mode_query,omitempty"`
61
+ RefreshEvery int `yaml:"refresh_every,omitempty" json:"refresh_every"`
62
+ Mode string `yaml:"mode,omitempty" json:"mode"`
63
+ ModeFilters *ResourceFiltersConfig `yaml:"mode_filters,omitempty" json:"mode_filters,omitempty"`
64
+ ModeQuery *DiscoveryQueryConfig `yaml:"mode_query,omitempty" json:"mode_query,omitempty"`
65
}
66
67
-type DiscoveryFiltersConfig struct {
67
+type ResourceFiltersConfig struct {
68
ResourceGroups []string `yaml:"resource_groups,omitempty" json:"resource_groups,omitempty"`
69
Regions []string `yaml:"regions,omitempty" json:"regions,omitempty"`
70
Tags map[string][]string `yaml:"tags,omitempty" json:"tags,omitempty"`
@@ -74,12 +74,18 @@ type DiscoveryQueryConfig struct {
74
KQL string `yaml:"kql" json:"kql"`
75
}
76
77
+type ProfileEntryConfig struct {
78
+ Name string `yaml:"name" json:"name"`
79
+ Filters *ResourceFiltersConfig `yaml:"filters,omitempty" json:"filters,omitempty"`
80
+}
81
+
82
type ProfilesModeConfig struct {
78
- Names []string `yaml:"names,omitempty" json:"names,omitempty"`
83
+ Entries []ProfileEntryConfig `yaml:"entries,omitempty" json:"entries,omitempty"`
84
}
85
86
type ProfilesConfig struct {
87
Mode string `yaml:"mode,omitempty" json:"mode"`
88
+ ModeAuto *ProfilesModeConfig `yaml:"mode_auto,omitempty" json:"mode_auto,omitempty"`
89
ModeExact *ProfilesModeConfig `yaml:"mode_exact,omitempty" json:"mode_exact,omitempty"`
90
ModeCombined *ProfilesModeConfig `yaml:"mode_combined,omitempty" json:"mode_combined,omitempty"`
91
}
@@ -160,7 +166,7 @@ func (c Config) validate() error {
166
errs = append(errs, errors.New("'limits.max_metrics_per_query' must be between 1 and 20"))
167
}
168
163
- switch strings.ToLower(strings.TrimSpace(c.Cloud)) {
169
+ switch stringsLowerTrim(c.Cloud) {
170
case cloudPublic, cloudGovernment, cloudChina:
171
default:
172
errs = append(errs, fmt.Errorf("'cloud' must be one of: %s, %s, %s", cloudPublic, cloudGovernment, cloudChina))
@@ -170,9 +176,11 @@ func (c Config) validate() error {
176
errs = append(errs, err)
177
}
178
173
- switch strings.ToLower(strings.TrimSpace(c.Discovery.Mode)) {
179
+ validateProfileTags := false
180
+ switch stringsLowerTrim(c.Discovery.Mode) {
181
case discoveryModeFilters:
175
- errs = append(errs, validateDiscoveryFilters(c.Discovery.ModeFilters)...)
182
+ validateProfileTags = true
183
+ errs = append(errs, validateResourceFilters("discovery.mode_filters", c.Discovery.ModeFilters, true)...)
184
case discoveryModeQuery:
185
if c.Discovery.ModeQuery == nil || strings.TrimSpace(c.Discovery.ModeQuery.KQL) == "" {
186
errs = append(errs, errors.New("'discovery.mode_query.kql' must not be empty when discovery.mode is 'query'"))
@@ -181,19 +189,22 @@ func (c Config) validate() error {
189
errs = append(errs, fmt.Errorf("'discovery.mode' must be one of: %s, %s", discoveryModeFilters, discoveryModeQuery))
190
}
191
184
- switch strings.ToLower(strings.TrimSpace(c.Profiles.Mode)) {
192
+ switch stringsLowerTrim(c.Profiles.Mode) {
193
case profilesModeAuto:
194
+ errs = append(errs, validateProfileEntries("profiles.mode_auto.entries", modeEntries(c.Profiles.ModeAuto), validateProfileTags)...)
195
case profilesModeExact:
187
- if c.Profiles.ModeExact == nil || len(c.Profiles.ModeExact.Names) == 0 {
188
- errs = append(errs, fmt.Errorf("'profiles.mode_exact.names' must not be empty when profiles.mode is '%s'", c.Profiles.Mode))
196
+ entries := modeEntries(c.Profiles.ModeExact)
197
+ if len(entries) == 0 {
198
+ errs = append(errs, fmt.Errorf("'profiles.mode_exact.entries' must not be empty when profiles.mode is '%s'", c.Profiles.Mode))
199
} else {
190
- errs = append(errs, validateProfilesList("profiles.mode_exact.names", c.Profiles.ModeExact.Names)...)
200
+ errs = append(errs, validateProfileEntries("profiles.mode_exact.entries", entries, validateProfileTags)...)
201
}
202
case profilesModeCombined:
193
- if c.Profiles.ModeCombined == nil || len(c.Profiles.ModeCombined.Names) == 0 {
194
- errs = append(errs, fmt.Errorf("'profiles.mode_combined.names' must not be empty when profiles.mode is '%s'", c.Profiles.Mode))
203
+ entries := modeEntries(c.Profiles.ModeCombined)
204
+ if len(entries) == 0 {
205
+ errs = append(errs, fmt.Errorf("'profiles.mode_combined.entries' must not be empty when profiles.mode is '%s'", c.Profiles.Mode))
206
} else {
196
- errs = append(errs, validateProfilesList("profiles.mode_combined.names", c.Profiles.ModeCombined.Names)...)
207
+ errs = append(errs, validateProfileEntries("profiles.mode_combined.entries", entries, validateProfileTags)...)
208
}
209
default:
210
errs = append(errs, fmt.Errorf("'profiles.mode' must be one of: %s, %s, %s",
@@ -203,75 +214,188 @@ func (c Config) validate() error {
214
return errors.Join(errs...)
215
}
216
206
-func validateDiscoveryFilters(filters *DiscoveryFiltersConfig) []error {
217
+func validateProfileEntries(path string, entries []ProfileEntryConfig, validateTags bool) []error {
218
+ if len(entries) == 0 {
219
+ return nil
220
+ }
221
+
222
+ var errs []error
223
+ seen := map[string]struct{}{}
224
+ for i, entry := range entries {
225
+ entryPath := fmt.Sprintf("%s[%d]", path, i)
226
+ name := stringsTrim(entry.Name)
227
+ if !isValidProfileName(name) {
228
+ errs = append(errs, fmt.Errorf("'%s.name' must match %q", entryPath, profileNamePattern))
229
+ } else {
230
+ if _, ok := seen[name]; ok {
231
+ errs = append(errs, fmt.Errorf("'%s' contains duplicate entry name '%s'", path, name))
232
+ }
233
+ seen[name] = struct{}{}
234
+ }
235
+ errs = append(errs, validateResourceFilters(entryPath+".filters", entry.Filters, validateTags)...)
236
+ }
237
+ return errs
238
+}
239
+
240
+func validateResourceFilters(path string, filters *ResourceFiltersConfig, validateTags bool) []error {
241
if filters == nil {
242
return nil
243
}
244
245
var errs []error
246
for i, v := range filters.ResourceGroups {
213
- if strings.TrimSpace(v) == "" {
214
- errs = append(errs, fmt.Errorf("'discovery.mode_filters.resource_groups[%d]' must not be empty", i))
247
+ if stringsTrim(v) == "" {
248
+ errs = append(errs, fmt.Errorf("'%s.resource_groups[%d]' must not be empty", path, i))
249
}
250
}
251
for i, v := range filters.Regions {
218
- if strings.TrimSpace(v) == "" {
219
- errs = append(errs, fmt.Errorf("'discovery.mode_filters.regions[%d]' must not be empty", i))
252
+ if stringsTrim(v) == "" {
253
+ errs = append(errs, fmt.Errorf("'%s.regions[%d]' must not be empty", path, i))
254
}
255
}
256
+ if !validateTags && len(filters.Tags) > 0 {
257
+ return errs
258
+ }
259
for key, values := range filters.Tags {
223
- if strings.TrimSpace(key) == "" {
224
- errs = append(errs, errors.New("'discovery.mode_filters.tags' contains an empty key"))
260
+ if stringsTrim(key) == "" {
261
+ errs = append(errs, fmt.Errorf("'%s.tags' contains an empty key", path))
262
continue
263
}
264
if len(values) == 0 {
228
- errs = append(errs, fmt.Errorf("'discovery.mode_filters.tags.%s' must contain at least one value", key))
265
+ errs = append(errs, fmt.Errorf("'%s.tags.%s' must contain at least one value", path, key))
266
continue
267
}
268
for i, v := range values {
232
- if strings.TrimSpace(v) == "" {
233
- errs = append(errs, fmt.Errorf("'discovery.mode_filters.tags.%s[%d]' must not be empty", key, i))
269
+ if stringsTrim(v) == "" {
270
+ errs = append(errs, fmt.Errorf("'%s.tags.%s[%d]' must not be empty", path, key, i))
271
}
272
}
273
}
274
return errs
275
}
276
240
-func validateProfilesList(path string, profiles []string) []error {
241
- var errs []error
242
- seen := map[string]struct{}{}
243
- for _, name := range profiles {
244
- n := strings.TrimSpace(name)
245
- if n == "" {
246
- errs = append(errs, fmt.Errorf("'%s' contains an empty value", path))
277
+func sanitizeIgnoredProfileTagFilters(cfg Config) (Config, []string) {
278
+ if stringsLowerTrim(cfg.Discovery.Mode) != discoveryModeQuery {
279
+ return cfg, nil
280
+ }
281
+
282
+ switch stringsLowerTrim(cfg.Profiles.Mode) {
283
+ case profilesModeAuto:
284
+ cfg.Profiles.ModeAuto = cloneProfilesModeConfig(cfg.Profiles.ModeAuto)
285
+ warnings := stripIgnoredProfileTagFilters("profiles.mode_auto.entries", cfg.Profiles.ModeAuto)
286
+ return cfg, warnings
287
+ case profilesModeExact:
288
+ cfg.Profiles.ModeExact = cloneProfilesModeConfig(cfg.Profiles.ModeExact)
289
+ warnings := stripIgnoredProfileTagFilters("profiles.mode_exact.entries", cfg.Profiles.ModeExact)
290
+ return cfg, warnings
291
+ case profilesModeCombined:
292
+ cfg.Profiles.ModeCombined = cloneProfilesModeConfig(cfg.Profiles.ModeCombined)
293
+ warnings := stripIgnoredProfileTagFilters("profiles.mode_combined.entries", cfg.Profiles.ModeCombined)
294
+ return cfg, warnings
295
+ default:
296
+ return cfg, nil
297
+ }
298
+}
299
+
300
+func cloneProfilesModeConfig(src *ProfilesModeConfig) *ProfilesModeConfig {
301
+ if src == nil {
302
+ return nil
303
+ }
304
+
305
+ out := &ProfilesModeConfig{
306
+ Entries: make([]ProfileEntryConfig, len(src.Entries)),
307
+ }
308
+ for i, entry := range src.Entries {
309
+ out.Entries[i] = ProfileEntryConfig{
310
+ Name: entry.Name,
311
+ Filters: cloneResourceFilters(entry.Filters),
312
+ }
313
+ }
314
+ return out
315
+}
316
+
317
+func stripIgnoredProfileTagFilters(path string, cfg *ProfilesModeConfig) []string {
318
+ if cfg == nil {
319
+ return nil
320
+ }
321
+
322
+ var warnings []string
323
+ for i := range cfg.Entries {
324
+ filters := cfg.Entries[i].Filters
325
+ if filters == nil || len(filters.Tags) == 0 {
326
continue
327
}
249
- norm := stringsLowerTrim(n)
250
- if _, ok := seen[norm]; ok {
251
- errs = append(errs, fmt.Errorf("'%s' contains duplicate value '%s'", path, n))
328
+
329
+ warnings = append(warnings, fmt.Sprintf("%s[%d].filters.tags", path, i))
330
+ filters.Tags = nil
331
+ if len(filters.ResourceGroups) == 0 && len(filters.Regions) == 0 {
332
+ cfg.Entries[i].Filters = nil
333
}
253
- seen[norm] = struct{}{}
334
}
255
- return errs
335
+ return warnings
336
}
337
258
-func (p ProfilesConfig) explicitBaseNames() []string {
259
- switch stringsLowerTrim(p.Mode) {
260
- case profilesModeExact:
261
- if p.ModeExact != nil {
262
- return p.ModeExact.Names
338
+func modeEntries(cfg *ProfilesModeConfig) []ProfileEntryConfig {
339
+ if cfg == nil {
340
+ return nil
341
+ }
342
+ return cfg.Entries
343
+}
344
+
345
+func entryNames(entries []ProfileEntryConfig) []string {
346
+ if len(entries) == 0 {
347
+ return nil
348
+ }
349
+
350
+ names := make([]string, 0, len(entries))
351
+ for _, entry := range entries {
352
+ if name := stringsTrim(entry.Name); name != "" {
353
+ names = append(names, name)
354
}
264
- case profilesModeCombined:
265
- if p.ModeCombined != nil {
266
- return p.ModeCombined.Names
355
+ }
356
+ return names
357
+}
358
+
359
+func entryMap(entries []ProfileEntryConfig) map[string]ProfileEntryConfig {
360
+ if len(entries) == 0 {
361
+ return nil
362
+ }
363
+
364
+ out := make(map[string]ProfileEntryConfig, len(entries))
365
+ for _, entry := range entries {
366
+ name := stringsTrim(entry.Name)
367
+ if name == "" {
368
+ continue
369
+ }
370
+ out[name] = ProfileEntryConfig{
371
+ Name: name,
372
+ Filters: cloneResourceFilters(entry.Filters),
373
+ }
374
+ }
375
+ return out
376
+}
377
+
378
+func cloneResourceFilters(src *ResourceFiltersConfig) *ResourceFiltersConfig {
379
+ if src == nil {
380
+ return nil
381
+ }
382
+
383
+ dst := &ResourceFiltersConfig{
384
+ ResourceGroups: append([]string(nil), src.ResourceGroups...),
385
+ Regions: append([]string(nil), src.Regions...),
386
+ }
387
+ if len(src.Tags) > 0 {
388
+ dst.Tags = make(map[string][]string, len(src.Tags))
389
+ for key, values := range src.Tags {
390
+ dst.Tags[key] = append([]string(nil), values...)
391
}
392
}
269
- return nil
393
+ return dst
394
}
395
396
func (c Config) primarySubscriptionID() string {
397
for _, id := range c.SubscriptionIDs {
274
- if v := strings.TrimSpace(id); v != "" {
398
+ if v := stringsTrim(id); v != "" {
399
return v
400
}
401
}
@@ -281,7 +405,7 @@ func (c Config) primarySubscriptionID() string {
405
func (c Config) subscriptionIDs() []string {
406
out := make([]string, 0, len(c.SubscriptionIDs))
407
for _, id := range c.SubscriptionIDs {
284
- if v := strings.TrimSpace(id); v != "" {
408
+ if v := stringsTrim(id); v != "" {
409
out = append(out, v)
410
}
411
}
src/go/plugin/go.d/collector/azure_monitor/config_schema.json
+232
-19
@@ -158,6 +158,67 @@
158
"properties": {
159
"mode": {
160
"const": "auto"
161
+ },
162
+ "mode_auto": {
163
+ "title": "Auto profile overrides",
164
+ "type": "object",
165
+ "properties": {
166
+ "entries": {
167
+ "title": "Profile entries",
168
+ "description": "Optional per-profile overrides applied only to profiles that auto-activate at bootstrap. Unmatched entries stay dormant.",
169
+ "type": "array",
170
+ "items": {
171
+ "type": "object",
172
+ "properties": {
173
+ "name": {
174
+ "title": "Profile basename",
175
+ "description": "Canonical profile basename. Must be lowercase letters, digits, or underscores and start with a letter.",
176
+ "type": "string",
177
+ "pattern": "^[a-z][a-z0-9_]*$"
178
+ },
179
+ "filters": {
180
+ "title": "Filters",
181
+ "description": "Optional per-profile resource filters that narrow the globally discovered set for this profile.",
182
+ "type": "object",
183
+ "properties": {
184
+ "resource_groups": {
185
+ "title": "Resource groups",
186
+ "type": "array",
187
+ "items": {
188
+ "type": "string",
189
+ "minLength": 1
190
+ }
191
+ },
192
+ "regions": {
193
+ "title": "Regions",
194
+ "type": "array",
195
+ "items": {
196
+ "type": "string",
197
+ "minLength": 1
198
+ }
199
+ },
200
+ "tags": {
201
+ "title": "Tags",
202
+ "description": "Optional tag filters for this profile. Supported only when `discovery.mode` is `filters`; in `query` mode, encode per-profile tag filtering in the KQL.",
203
+ "type": "object",
204
+ "additionalProperties": {
205
+ "type": "array",
206
+ "items": {
207
+ "type": "string",
208
+ "minLength": 1
209
+ },
210
+ "minItems": 1
211
+ }
212
+ }
213
+ }
214
+ }
215
+ },
216
+ "required": [
217
+ "name"
218
+ ]
219
+ }
220
+ }
221
+ }
222
}
223
}
224
},
@@ -170,17 +231,64 @@
231
"title": "Exact profiles",
232
"type": "object",
233
"properties": {
173
- "names": {
174
- "title": "Profile basenames",
175
- "description": "Explicit profile file basenames used by `exact` mode. Matching is case-insensitive.",
234
+ "entries": {
235
+ "title": "Profile entries",
236
+ "description": "Explicit profile entries used by `exact` mode.",
237
"type": "array",
238
"items": {
178
- "type": "string"
239
+ "type": "object",
240
+ "properties": {
241
+ "name": {
242
+ "title": "Profile basename",
243
+ "description": "Canonical profile basename. Must be lowercase letters, digits, or underscores and start with a letter.",
244
+ "type": "string",
245
+ "pattern": "^[a-z][a-z0-9_]*$"
246
+ },
247
+ "filters": {
248
+ "title": "Filters",
249
+ "description": "Optional per-profile resource filters that narrow the globally discovered set for this profile.",
250
+ "type": "object",
251
+ "properties": {
252
+ "resource_groups": {
253
+ "title": "Resource groups",
254
+ "type": "array",
255
+ "items": {
256
+ "type": "string",
257
+ "minLength": 1
258
+ }
259
+ },
260
+ "regions": {
261
+ "title": "Regions",
262
+ "type": "array",
263
+ "items": {
264
+ "type": "string",
265
+ "minLength": 1
266
+ }
267
+ },
268
+ "tags": {
269
+ "title": "Tags",
270
+ "description": "Optional tag filters for this profile. Supported only when `discovery.mode` is `filters`; in `query` mode, encode per-profile tag filtering in the KQL.",
271
+ "type": "object",
272
+ "additionalProperties": {
273
+ "type": "array",
274
+ "items": {
275
+ "type": "string",
276
+ "minLength": 1
277
+ },
278
+ "minItems": 1
279
+ }
280
+ }
281
+ }
282
+ }
283
+ },
284
+ "required": [
285
+ "name"
286
+ ]
287
}
288
}
289
},
290
"required": [
183
- "names"
291
+ "entries"
292
]
293
}
294
},
@@ -197,17 +305,64 @@
305
"title": "Combined profiles",
306
"type": "object",
307
"properties": {
200
- "names": {
201
- "title": "Profile basenames",
202
- "description": "Explicit profile file basenames merged with auto-discovered profiles in `combined` mode. Matching is case-insensitive.",
308
+ "entries": {
309
+ "title": "Profile entries",
310
+ "description": "Explicit profile entries merged with auto-discovered profiles in `combined` mode.",
311
"type": "array",
312
"items": {
205
- "type": "string"
313
+ "type": "object",
314
+ "properties": {
315
+ "name": {
316
+ "title": "Profile basename",
317
+ "description": "Canonical profile basename. Must be lowercase letters, digits, or underscores and start with a letter.",
318
+ "type": "string",
319
+ "pattern": "^[a-z][a-z0-9_]*$"
320
+ },
321
+ "filters": {
322
+ "title": "Filters",
323
+ "description": "Optional per-profile resource filters that narrow the globally discovered set for this profile.",
324
+ "type": "object",
325
+ "properties": {
326
+ "resource_groups": {
327
+ "title": "Resource groups",
328
+ "type": "array",
329
+ "items": {
330
+ "type": "string",
331
+ "minLength": 1
332
+ }
333
+ },
334
+ "regions": {
335
+ "title": "Regions",
336
+ "type": "array",
337
+ "items": {
338
+ "type": "string",
339
+ "minLength": 1
340
+ }
341
+ },
342
+ "tags": {
343
+ "title": "Tags",
344
+ "description": "Optional tag filters for this profile. Supported only when `discovery.mode` is `filters`; in `query` mode, encode per-profile tag filtering in the KQL.",
345
+ "type": "object",
346
+ "additionalProperties": {
347
+ "type": "array",
348
+ "items": {
349
+ "type": "string",
350
+ "minLength": 1
351
+ },
352
+ "minItems": 1
353
+ }
354
+ }
355
+ }
356
+ }
357
+ },
358
+ "required": [
359
+ "name"
360
+ ]
361
}
362
}
363
},
364
"required": [
210
- "names"
365
+ "entries"
366
]
367
}
368
},
@@ -431,29 +586,87 @@
586
},
587
"profiles": {
588
"mode": {
434
- "ui:help": "Choose how metric profiles are activated: `auto` enables profiles for discovered resource types, `exact` uses only the listed profile basenames, and `combined` merges both.",
589
+ "ui:help": "Choose how metric profiles are activated: `auto` enables profiles for discovered resource types, `exact` uses only the listed profile entries, and `combined` merges both.",
590
"ui:widget": "radio",
591
"ui:options": {
592
"inline": true
593
}
594
},
595
+ "mode_auto": {
596
+ "entries": {
597
+ "ui:listFlavour": "list",
598
+ "ui:help": "Optional per-profile overrides applied only to profiles that auto-activate at bootstrap.",
599
+ "items": {
600
+ "name": {
601
+ "ui:help": "Canonical profile basename.",
602
+ "ui:placeholder": "sql_database"
603
+ },
604
+ "filters": {
605
+ "resource_groups": {
606
+ "ui:listFlavour": "list"
607
+ },
608
+ "regions": {
609
+ "ui:listFlavour": "list"
610
+ },
611
+ "tags": {
612
+ "ui:help": "Only supported when `discovery.mode` is `filters`. In `query` mode, encode per-profile tag filtering in the KQL.",
613
+ "additionalProperties": {
614
+ "ui:listFlavour": "list"
615
+ }
616
+ }
617
+ }
618
+ }
619
+ }
620
+ },
621
"mode_exact": {
441
- "names": {
622
+ "entries": {
623
"ui:listFlavour": "list",
443
- "ui:help": "Add Azure Monitor profile file basenames to enable explicitly in `exact` mode. Matching is case-insensitive.",
624
+ "ui:help": "Add explicit profile entries for `exact` mode.",
625
"items": {
445
- "ui:help": "Profile file basename from the Azure Monitor profile catalog.",
446
- "ui:placeholder": "sql_database"
626
+ "name": {
627
+ "ui:help": "Canonical profile basename.",
628
+ "ui:placeholder": "sql_database"
629
+ },
630
+ "filters": {
631
+ "resource_groups": {
632
+ "ui:listFlavour": "list"
633
+ },
634
+ "regions": {
635
+ "ui:listFlavour": "list"
636
+ },
637
+ "tags": {
638
+ "ui:help": "Only supported when `discovery.mode` is `filters`. In `query` mode, encode per-profile tag filtering in the KQL.",
639
+ "additionalProperties": {
640
+ "ui:listFlavour": "list"
641
+ }
642
+ }
643
+ }
644
}
645
}
646
},
647
"mode_combined": {
451
- "names": {
648
+ "entries": {
649
"ui:listFlavour": "list",
453
- "ui:help": "Add profile file basenames to merge with auto-discovered profiles in `combined` mode. Matching is case-insensitive.",
650
+ "ui:help": "Add explicit profile entries to merge with auto-discovered profiles in `combined` mode.",
651
"items": {
455
- "ui:help": "Profile file basename from the Azure Monitor profile catalog.",
456
- "ui:placeholder": "sql_database"
652
+ "name": {
653
+ "ui:help": "Canonical profile basename.",
654
+ "ui:placeholder": "sql_database"
655
+ },
656
+ "filters": {
657
+ "resource_groups": {
658
+ "ui:listFlavour": "list"
659
+ },
660
+ "regions": {
661
+ "ui:listFlavour": "list"
662
+ },
663
+ "tags": {
664
+ "ui:help": "Only supported when `discovery.mode` is `filters`. In `query` mode, encode per-profile tag filtering in the KQL.",
665
+ "additionalProperties": {
666
+ "ui:listFlavour": "list"
667
+ }
668
+ }
669
+ }
670
}
671
}
672
}
src/go/plugin/go.d/collector/azure_monitor/discover.go
+241
-56
@@ -20,6 +20,19 @@ type discoveryFetchResult struct {
20
UnsupportedTypes []string
21
}
22
23
+type normalizedTagFilter struct {
24
+ Key string
25
+ Values []string
26
+}
27
+
28
+type profileResourceMatcher struct {
29
+ profileName string
30
+ resourceType string
31
+ resourceGroups map[string]struct{}
32
+ regions map[string]struct{}
33
+ tagFilters []normalizedTagFilter
34
+}
35
+
36
func (c *Collector) refreshDiscovery(ctx context.Context, force bool) ([]resourceInfo, error) {
37
now := c.now()
38
if !force && !c.discovery.FetchedAt.IsZero() {
@@ -36,21 +49,25 @@ func (c *Collector) refreshDiscovery(ctx context.Context, force bool) ([]resourc
49
c.Warningf("ignoring unsupported discovered resource types: %v", fetched.UnsupportedTypes)
50
}
51
39
- resources, byType := filterDiscoveryResourcesByTypes(fetched.Resources, runtimeResourceTypes(c.runtime))
40
-
41
- if !slices.Equal(resources, c.discovery.Resources) {
42
- c.Infof("discovered %d resources: %v", len(resources), resources)
52
+ state := buildDiscoveryState(fetched.Resources, c.runtime, now, c.Discovery.RefreshEvery, c.discovery.FetchCounter+1)
53
+ if !equalResourceSlices(state.Resources, c.discovery.Resources) {
54
+ c.Infof("discovered %d resources: %v", len(state.Resources), state.Resources)
55
}
56
45
- c.discovery = discoveryState{
46
- Resources: resources,
57
+ c.discovery = state
58
+ return state.Resources, nil
59
+}
60
+
61
+func buildDiscoveryState(resources []resourceInfo, runtime *collectorRuntime, now time.Time, refreshEvery int, fetchCounter uint64) discoveryState {
62
+ filteredResources, byType := filterDiscoveryResourcesByTypes(resources, runtimeResourceTypes(runtime))
63
+ return discoveryState{
64
+ Resources: filteredResources,
65
ByType: byType,
66
+ ByProfile: filterDiscoveryResourcesByProfiles(filteredResources, runtime),
67
FetchedAt: now,
49
- ExpiresAt: discoveryExpiresAt(now, c.Discovery.RefreshEvery),
50
- FetchCounter: c.discovery.FetchCounter + 1,
68
+ ExpiresAt: discoveryExpiresAt(now, refreshEvery),
69
+ FetchCounter: fetchCounter,
70
}
52
-
53
- return resources, nil
71
}
72
73
func discoveryExpiresAt(now time.Time, refreshEvery int) time.Time {
@@ -109,7 +126,7 @@ func runtimeResourceTypes(runtime *collectorRuntime) []string {
126
return resourceTypes
127
}
128
112
-func discoverResources(ctx context.Context, subscriptionIDs []string, timeout time.Duration, resourceGraph resourceGraphClient, resourceTypes []string, filters *DiscoveryFiltersConfig) ([]resourceInfo, map[string][]resourceInfo, error) {
129
+func discoverResources(ctx context.Context, subscriptionIDs []string, timeout time.Duration, resourceGraph resourceGraphClient, resourceTypes []string, filters *ResourceFiltersConfig) ([]resourceInfo, map[string][]resourceInfo, error) {
130
if len(resourceTypes) == 0 {
131
return nil, map[string][]resourceInfo{}, nil
132
}
@@ -119,11 +136,11 @@ func discoverResources(ctx context.Context, subscriptionIDs []string, timeout ti
136
return nil, nil, fmt.Errorf("failed to build resource discovery query")
137
}
138
122
- resourceGroupsFilter := normalizedDiscoveryFilterSet(nil)
123
- regionsFilter := normalizedDiscoveryFilterSet(nil)
139
+ resourceGroupsFilter := normalizedFilterSet(nil)
140
+ regionsFilter := normalizedFilterSet(nil)
141
if filters != nil {
125
- resourceGroupsFilter = normalizedDiscoveryFilterSet(filters.ResourceGroups)
126
- regionsFilter = normalizedDiscoveryFilterSet(filters.Regions)
142
+ resourceGroupsFilter = normalizedFilterSet(filters.ResourceGroups)
143
+ regionsFilter = normalizedFilterSet(filters.Regions)
144
}
145
146
result := make([]resourceInfo, 0, 256)
@@ -149,15 +166,15 @@ func discoverResources(ctx context.Context, subscriptionIDs []string, timeout ti
166
if id == "" {
167
continue
168
}
152
- if _, ok := seenIDs[id]; ok {
169
+ idKey := stringsLowerTrim(id)
170
+ if _, ok := seenIDs[idKey]; ok {
171
continue
172
}
155
- seenIDs[id] = struct{}{}
173
+ seenIDs[idKey] = struct{}{}
174
175
rg := stringsTrim(asString(row["resourceGroup"]))
158
- rgLower := stringsLowerTrim(rg)
176
if len(resourceGroupsFilter) > 0 {
160
- if _, ok := resourceGroupsFilter[rgLower]; !ok {
177
+ if _, ok := resourceGroupsFilter[stringsLowerTrim(rg)]; !ok {
178
continue
179
}
180
}
@@ -167,6 +184,7 @@ func discoverResources(ctx context.Context, subscriptionIDs []string, timeout ti
184
if resourceType == "" || !ok {
185
continue
186
}
187
+
188
region := stringsLowerTrim(asString(row["location"]))
189
if region == "" {
190
region = "global"
@@ -185,6 +203,7 @@ func discoverResources(ctx context.Context, subscriptionIDs []string, timeout ti
203
Type: resourceType,
204
ResourceGroup: rg,
205
Region: region,
206
+ Tags: normalizeResourceTags(row["tags"]),
207
})
208
}
209
@@ -195,13 +214,8 @@ func discoverResources(ctx context.Context, subscriptionIDs []string, timeout ti
214
skipToken = &token
215
}
216
198
- byType := make(map[string][]resourceInfo)
199
- for _, r := range result {
200
- key := stringsLowerTrim(r.Type)
201
- byType[key] = append(byType[key], r)
202
- }
203
-
204
- return result, byType, nil
217
+ sortResourceInfos(result)
218
+ return result, indexResourcesByType(result), nil
219
}
220
221
func discoverResourcesFromQuery(ctx context.Context, subscriptionIDs []string, timeout time.Duration, resourceGraph resourceGraphClient, kql string, supportedTypes map[string]struct{}) (discoveryFetchResult, error) {
@@ -211,7 +225,6 @@ func discoverResourcesFromQuery(ctx context.Context, subscriptionIDs []string, t
225
}
226
227
result := make([]resourceInfo, 0, 256)
214
- byType := make(map[string][]resourceInfo)
228
unsupported := make(map[string]struct{})
229
seenIDs := make(map[string]struct{})
230
@@ -244,7 +257,6 @@ func discoverResourcesFromQuery(ctx context.Context, subscriptionIDs []string, t
257
258
result = append(result, resource)
259
typeKey := stringsLowerTrim(resource.Type)
247
- byType[typeKey] = append(byType[typeKey], resource)
260
if _, ok := supportedTypes[typeKey]; !ok {
261
unsupported[typeKey] = struct{}{}
262
}
@@ -257,6 +269,8 @@ func discoverResourcesFromQuery(ctx context.Context, subscriptionIDs []string, t
269
skipToken = &token
270
}
271
272
+ sortResourceInfos(result)
273
+
274
unsupportedTypes := make([]string, 0, len(unsupported))
275
for resourceType := range unsupported {
276
unsupportedTypes = append(unsupportedTypes, resourceType)
@@ -265,7 +279,7 @@ func discoverResourcesFromQuery(ctx context.Context, subscriptionIDs []string, t
279
280
return discoveryFetchResult{
281
Resources: result,
268
- ByType: byType,
282
+ ByType: indexResourcesByType(result),
283
UnsupportedTypes: unsupportedTypes,
284
}, nil
285
}
@@ -329,10 +343,11 @@ func parseARMResourceID(resourceID string) (string, bool) {
343
return stringsTrim(parts[1]), true
344
}
345
332
-func buildDiscoveryQuery(resourceTypes []string, filters *DiscoveryFiltersConfig) string {
346
+func buildDiscoveryQuery(resourceTypes []string, filters *ResourceFiltersConfig) string {
347
if len(resourceTypes) == 0 {
348
return ""
349
}
350
+
351
quotedTypes := make([]string, 0, len(resourceTypes))
352
for _, rt := range resourceTypes {
353
rt = stringsTrim(rt)
@@ -351,10 +366,10 @@ func buildDiscoveryQuery(resourceTypes []string, filters *DiscoveryFiltersConfig
366
query := "resources | where type in~ (" + strings.Join(quotedTypes, ", ") + ")"
367
368
if filters == nil {
354
- return query + " | project id, name, type, resourceGroup, location"
369
+ return query + " | project id, name, type, resourceGroup, location, tags"
370
}
371
357
- if groups := normalizeDiscoveryFilterValues(filters.ResourceGroups); len(groups) > 0 {
372
+ if groups := normalizeFilterValues(filters.ResourceGroups); len(groups) > 0 {
373
quotedGroups := make([]string, 0, len(groups))
374
for _, rg := range groups {
375
quotedGroups = append(quotedGroups, quoteKQLString(rg))
@@ -362,7 +377,7 @@ func buildDiscoveryQuery(resourceTypes []string, filters *DiscoveryFiltersConfig
377
query += " | where resourceGroup in~ (" + strings.Join(quotedGroups, ", ") + ")"
378
}
379
365
- if regions := normalizeDiscoveryFilterValues(filters.Regions); len(regions) > 0 {
380
+ if regions := normalizeFilterValues(filters.Regions); len(regions) > 0 {
381
quotedRegions := make([]string, 0, len(regions))
382
for _, region := range regions {
383
quotedRegions = append(quotedRegions, quoteKQLString(region))
@@ -370,20 +385,20 @@ func buildDiscoveryQuery(resourceTypes []string, filters *DiscoveryFiltersConfig
385
query += " | where location in~ (" + strings.Join(quotedRegions, ", ") + ")"
386
}
387
373
- if tagFilters := normalizeDiscoveryTagFilters(filters.Tags); len(tagFilters) > 0 {
388
+ if tagFilters := normalizeTagFilters(filters.Tags); len(tagFilters) > 0 {
389
+ query += " | extend tagsBag = tags"
390
query += " | mv-expand bagexpansion=array tags"
391
query += " | where isnotempty(tags)"
392
query += " | extend tagKey = tostring(tags[0]), tagValue = tostring(tags[1])"
377
- query += " | where " + buildDiscoveryTagPredicate(tagFilters)
378
- query += " | summarize by id, name, type, resourceGroup, location, matchedTagKey = tolower(tagKey)"
379
- query += " | summarize matchedTagKeys = count() by id, name, type, resourceGroup, location"
393
+ query += " | where " + buildTagPredicate(tagFilters)
394
+ query += " | summarize tags = take_any(tagsBag), matchedTagKeys = dcount(tolower(tagKey)) by id, name, type, resourceGroup, location"
395
query += fmt.Sprintf(" | where matchedTagKeys == %d", len(tagFilters))
396
}
397
383
- return query + " | project id, name, type, resourceGroup, location"
398
+ return query + " | project id, name, type, resourceGroup, location, tags"
399
}
400
386
-func normalizeDiscoveryFilterValues(values []string) []string {
401
+func normalizeFilterValues(values []string) []string {
402
seen := make(map[string]struct{}, len(values))
403
out := make([]string, 0, len(values))
404
@@ -403,17 +418,12 @@ func normalizeDiscoveryFilterValues(values []string) []string {
418
return out
419
}
420
406
-type discoveryTagFilter struct {
407
- Key string
408
- Values []string
409
-}
410
-
411
-func normalizeDiscoveryTagFilters(tags map[string][]string) []discoveryTagFilter {
421
+func normalizeTagFilters(tags map[string][]string) []normalizedTagFilter {
422
if len(tags) == 0 {
423
return nil
424
}
425
416
- out := make([]discoveryTagFilter, 0, len(tags))
426
+ out := make([]normalizedTagFilter, 0, len(tags))
427
for key, values := range tags {
428
normalizedKey := stringsLowerTrim(key)
429
if normalizedKey == "" {
@@ -438,10 +448,10 @@ func normalizeDiscoveryTagFilters(tags map[string][]string) []discoveryTagFilter
448
}
449
450
slices.Sort(normalizedValues)
441
- out = append(out, discoveryTagFilter{Key: normalizedKey, Values: normalizedValues})
451
+ out = append(out, normalizedTagFilter{Key: normalizedKey, Values: normalizedValues})
452
}
453
444
- slices.SortFunc(out, func(a, b discoveryTagFilter) int {
454
+ slices.SortFunc(out, func(a, b normalizedTagFilter) int {
455
switch {
456
case a.Key < b.Key:
457
return -1
@@ -454,17 +464,17 @@ func normalizeDiscoveryTagFilters(tags map[string][]string) []discoveryTagFilter
464
return out
465
}
466
457
-func buildDiscoveryTagPredicate(filters []discoveryTagFilter) string {
467
+func buildTagPredicate(filters []normalizedTagFilter) string {
468
clauses := make([]string, 0, len(filters))
469
for _, filter := range filters {
470
keyClause := "tagKey =~ " + quoteKQLString(filter.Key)
461
- valueClause := buildDiscoveryTagValueClause(filter.Values)
471
+ valueClause := buildTagValueClause(filter.Values)
472
clauses = append(clauses, "("+keyClause+" and "+valueClause+")")
473
}
474
return strings.Join(clauses, " or ")
475
}
476
467
-func buildDiscoveryTagValueClause(values []string) string {
477
+func buildTagValueClause(values []string) string {
478
if len(values) == 1 {
479
return "tagValue == " + quoteKQLString(values[0])
480
}
@@ -476,13 +486,13 @@ func buildDiscoveryTagValueClause(values []string) string {
486
return "tagValue in (" + strings.Join(quoted, ", ") + ")"
487
}
488
479
-func normalizedDiscoveryFilterSet(values []string) map[string]struct{} {
489
+func normalizedFilterSet(values []string) map[string]struct{} {
490
if len(values) == 0 {
491
return nil
492
}
493
494
set := make(map[string]struct{}, len(values))
485
- for _, value := range normalizeDiscoveryFilterValues(values) {
495
+ for _, value := range normalizeFilterValues(values) {
496
set[value] = struct{}{}
497
}
498
return set
@@ -561,17 +571,89 @@ func filterDiscoveryResourcesByTypes(resources []resourceInfo, allowedTypes []st
571
}
572
573
filtered := make([]resourceInfo, 0, len(resources))
564
- byType := make(map[string][]resourceInfo)
574
for _, resource := range resources {
575
typeKey := stringsLowerTrim(resource.Type)
576
if _, ok := allowed[typeKey]; !ok {
577
continue
578
}
579
filtered = append(filtered, resource)
571
- byType[typeKey] = append(byType[typeKey], resource)
580
}
581
574
- return filtered, byType
582
+ return filtered, indexResourcesByType(filtered)
583
+}
584
+
585
+func filterDiscoveryResourcesByProfiles(resources []resourceInfo, runtime *collectorRuntime) map[string][]resourceInfo {
586
+ if runtime == nil || len(runtime.Profiles) == 0 {
587
+ return map[string][]resourceInfo{}
588
+ }
589
+
590
+ result := make(map[string][]resourceInfo, len(runtime.Profiles))
591
+ for _, profile := range runtime.Profiles {
592
+ matcher := newProfileResourceMatcher(profile)
593
+ for _, resource := range resources {
594
+ if !matcher.matches(resource) {
595
+ continue
596
+ }
597
+ result[profile.Name] = append(result[profile.Name], resource)
598
+ }
599
+ }
600
+ return result
601
+}
602
+
603
+func newProfileResourceMatcher(profile *profileRuntime) profileResourceMatcher {
604
+ matcher := profileResourceMatcher{
605
+ resourceType: stringsLowerTrim(profile.ResourceType),
606
+ profileName: profile.Name,
607
+ }
608
+ if profile.Filters == nil {
609
+ return matcher
610
+ }
611
+
612
+ matcher.resourceGroups = normalizedFilterSet(profile.Filters.ResourceGroups)
613
+ matcher.regions = normalizedFilterSet(profile.Filters.Regions)
614
+ matcher.tagFilters = normalizeTagFilters(profile.Filters.Tags)
615
+ return matcher
616
+}
617
+
618
+func (m profileResourceMatcher) matches(resource resourceInfo) bool {
619
+ if stringsLowerTrim(resource.Type) != m.resourceType {
620
+ return false
621
+ }
622
+ if len(m.resourceGroups) > 0 {
623
+ if _, ok := m.resourceGroups[stringsLowerTrim(resource.ResourceGroup)]; !ok {
624
+ return false
625
+ }
626
+ }
627
+ if len(m.regions) > 0 {
628
+ if _, ok := m.regions[normalizeRegion(resource.Region)]; !ok {
629
+ return false
630
+ }
631
+ }
632
+ for _, tagFilter := range m.tagFilters {
633
+ if !resourceMatchesTag(resource, tagFilter) {
634
+ return false
635
+ }
636
+ }
637
+ return true
638
+}
639
+
640
+func resourceMatchesTag(resource resourceInfo, filter normalizedTagFilter) bool {
641
+ for _, tag := range resource.Tags {
642
+ if tag.Key != filter.Key {
643
+ continue
644
+ }
645
+ return slices.Contains(filter.Values, tag.Value)
646
+ }
647
+ return false
648
+}
649
+
650
+func indexResourcesByType(resources []resourceInfo) map[string][]resourceInfo {
651
+ result := make(map[string][]resourceInfo)
652
+ for _, resource := range resources {
653
+ key := stringsLowerTrim(resource.Type)
654
+ result[key] = append(result[key], resource)
655
+ }
656
+ return result
657
}
658
659
func catalogResourceTypeSet(catalog azureprofiles.Catalog) map[string]struct{} {
@@ -612,3 +694,106 @@ func asString(v any) string {
694
return ""
695
}
696
}
697
+
698
+func normalizeResourceTags(v any) []resourceTag {
699
+ rawTags, ok := v.(map[string]any)
700
+ if !ok {
701
+ if typed, ok := v.(map[string]string); ok {
702
+ rawTags = make(map[string]any, len(typed))
703
+ for key, value := range typed {
704
+ rawTags[key] = value
705
+ }
706
+ } else {
707
+ return nil
708
+ }
709
+ }
710
+
711
+ tags := make([]resourceTag, 0, len(rawTags))
712
+ for key, value := range rawTags {
713
+ tagKey := stringsLowerTrim(key)
714
+ if tagKey == "" {
715
+ continue
716
+ }
717
+ tags = append(tags, resourceTag{
718
+ Key: tagKey,
719
+ Value: normalizeResourceTagValue(value),
720
+ })
721
+ }
722
+
723
+ slices.SortFunc(tags, func(a, b resourceTag) int {
724
+ switch {
725
+ case a.Key < b.Key:
726
+ return -1
727
+ case a.Key > b.Key:
728
+ return 1
729
+ case a.Value < b.Value:
730
+ return -1
731
+ case a.Value > b.Value:
732
+ return 1
733
+ default:
734
+ return 0
735
+ }
736
+ })
737
+ return tags
738
+}
739
+
740
+func normalizeResourceTagValue(v any) string {
741
+ switch x := v.(type) {
742
+ case nil:
743
+ return ""
744
+ case string:
745
+ return stringsTrim(x)
746
+ case fmt.Stringer:
747
+ return stringsTrim(x.String())
748
+ default:
749
+ return stringsTrim(fmt.Sprint(x))
750
+ }
751
+}
752
+
753
+func sortResourceInfos(resources []resourceInfo) {
754
+ slices.SortFunc(resources, func(a, b resourceInfo) int {
755
+ switch {
756
+ case stringsLowerTrim(a.ID) < stringsLowerTrim(b.ID):
757
+ return -1
758
+ case stringsLowerTrim(a.ID) > stringsLowerTrim(b.ID):
759
+ return 1
760
+ default:
761
+ return 0
762
+ }
763
+ })
764
+}
765
+
766
+func equalResourceSlices(a, b []resourceInfo) bool {
767
+ if len(a) != len(b) {
768
+ return false
769
+ }
770
+ for i := range a {
771
+ if !equalResourceInfo(a[i], b[i]) {
772
+ return false
773
+ }
774
+ }
775
+ return true
776
+}
777
+
778
+func equalResourceInfo(a, b resourceInfo) bool {
779
+ return a.SubscriptionID == b.SubscriptionID &&
780
+ a.ID == b.ID &&
781
+ a.UID == b.UID &&
782
+ a.Name == b.Name &&
783
+ a.Type == b.Type &&
784
+ a.ResourceGroup == b.ResourceGroup &&
785
+ a.Region == b.Region &&
786
+ equalResourceTags(a.Tags, b.Tags)
787
+}
788
+
789
+func equalResourceTags(a, b []resourceTag) bool {
790
+ if len(a) != len(b) {
791
+ return false
792
+ }
793
+ for i := range a {
794
+ if a[i] != b[i] {
795
+ return false
796
+ }
797
+ }
798
+ return true
799
+}
src/go/plugin/go.d/collector/azure_monitor/helpers.go
+20
@@ -10,6 +10,8 @@ import (
10
"time"
11
)
12
13
+const profileNamePattern = "^[a-z][a-z0-9_]*$"
14
+
15
func stringsLowerTrim(v string) string {
16
return strings.ToLower(strings.TrimSpace(v))
17
}
@@ -29,3 +31,21 @@ func withOptionalTimeout(ctx context.Context, timeout time.Duration) (context.Co
31
}
32
return context.WithTimeout(ctx, timeout)
33
}
34
+
35
+func isValidProfileName(v string) bool {
36
+ v = stringsTrim(v)
37
+ if len(v) == 0 {
38
+ return false
39
+ }
40
+ if v[0] < 'a' || v[0] > 'z' {
41
+ return false
42
+ }
43
+ for i := 1; i < len(v); i++ {
44
+ c := v[i]
45
+ if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' {
46
+ continue
47
+ }
48
+ return false
49
+ }
50
+ return true
51
+}
src/go/plugin/go.d/collector/azure_monitor/init.go
+66
-35
@@ -6,6 +6,7 @@ import (
6
"context"
7
"errors"
8
"fmt"
9
+ "strings"
10
11
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
12
azcloud "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
@@ -23,6 +24,12 @@ type initResult struct {
24
supportedResourceTypes map[string]struct{}
25
}
26
27
+type initProfileSelection struct {
28
+ Names []string
29
+ AutoNames []string
30
+ Entries map[string]ProfileEntryConfig
31
+}
32
+
33
func (c *Collector) initInstruments(runtime *collectorRuntime) error {
34
if runtime == nil {
35
return errors.New("nil collector runtime")
@@ -96,12 +103,12 @@ func (c *Collector) ensureBootstrapped(ctx context.Context) error {
103
c.Warningf("ignoring unsupported discovered resource types: %v", fetched.UnsupportedTypes)
104
}
105
99
- profileIDs, autoProfiles, err := resolveInitProfileIDs(c.Config, c.profileCatalog, fetched.ByType)
106
+ selection, err := resolveInitProfiles(c.Config, c.profileCatalog, fetched.ByType)
107
if err != nil {
108
return err
109
}
103
- if len(autoProfiles) > 0 {
104
- c.Infof("auto-discovery resolved profiles: %v", autoProfiles)
110
+ if len(selection.AutoNames) > 0 {
111
+ c.Infof("auto-discovery resolved profiles: %v", selection.AutoNames)
112
}
113
114
// TODO(azure_monitor): Insert bootstrap-only metric-definition validation here.
@@ -110,26 +117,18 @@ func (c *Collector) ensureBootstrapped(ctx context.Context) error {
117
// lookup errors, and prune unsupported metrics/aggregations/time grains
118
// before final runtime build. Because runtime is currently global per job,
119
// multi-subscription capability differences need an explicit merge rule first.
113
- runtime, err := buildCollectorRuntimeFromConfig(profileIDs, c.profileCatalog)
120
+ runtime, err := buildCollectorRuntimeFromConfig(selection.Names, selection.Entries, c.profileCatalog)
121
if err != nil {
122
return fmt.Errorf("build collector runtime: %w", err)
123
}
124
if err := c.initInstruments(runtime); err != nil {
125
return err
126
}
120
-
121
- resources, byType := filterDiscoveryResourcesByTypes(fetched.Resources, runtimeResourceTypes(runtime))
127
now := c.now()
128
129
c.runtime = runtime
130
c.observations = newObservationState(runtime.Instruments)
126
- c.discovery = discoveryState{
127
- Resources: resources,
128
- ByType: byType,
129
- FetchedAt: now,
130
- ExpiresAt: discoveryExpiresAt(now, c.Discovery.RefreshEvery),
131
- FetchCounter: 1,
132
- }
131
+ c.discovery = buildDiscoveryState(fetched.Resources, runtime, now, c.Discovery.RefreshEvery, 1)
132
133
return nil
134
}
@@ -146,6 +145,14 @@ func (c *Collector) prepareInitConfig() (Config, azureprofiles.Catalog, error) {
145
if err := cfg.validate(); err != nil {
146
return Config{}, azureprofiles.Catalog{}, fmt.Errorf("config validation: %w", err)
147
}
148
+ cfg, ignoredTagPaths := sanitizeIgnoredProfileTagFilters(cfg)
149
+ if len(ignoredTagPaths) > 0 {
150
+ c.Warningf(
151
+ "ignoring profile tag filters in discovery.mode %q; encode per-profile tag filtering in discovery.mode_query.kql: %s",
152
+ discoveryModeQuery,
153
+ strings.Join(ignoredTagPaths, ", "),
154
+ )
155
+ }
156
157
return cfg, catalog, nil
158
}
@@ -211,13 +218,13 @@ func initDiscoveryResourceTypes(cfg Config, catalog azureprofiles.Catalog) ([]st
218
case profilesModeAuto, profilesModeCombined:
219
return catalog.ResourceTypes(), nil
220
case profilesModeExact:
214
- return catalog.ResourceTypesForProfileBaseNames(cfg.Profiles.explicitBaseNames())
221
+ return catalog.ResourceTypesForProfileBaseNames(entryNames(modeEntries(cfg.Profiles.ModeExact)))
222
default:
223
return nil, fmt.Errorf("unsupported profiles.mode %q", cfg.Profiles.Mode)
224
}
225
}
226
220
-func resolveInitProfileIDs(cfg Config, catalog azureprofiles.Catalog, byType map[string][]resourceInfo) ([]string, []string, error) {
227
+func resolveInitProfiles(cfg Config, catalog azureprofiles.Catalog, byType map[string][]resourceInfo) (initProfileSelection, error) {
228
discoveredTypes := make(map[string]struct{}, len(byType))
229
for key := range byType {
230
discoveredTypes[key] = struct{}{}
@@ -227,23 +234,28 @@ func resolveInitProfileIDs(cfg Config, catalog azureprofiles.Catalog, byType map
234
switch stringsLowerTrim(cfg.Profiles.Mode) {
235
case profilesModeAuto:
236
if len(autoProfiles) == 0 {
230
- return nil, nil, errors.New("auto-discovery found no Azure resources matching any known profile")
237
+ return initProfileSelection{}, errors.New("auto-discovery found no Azure resources matching any known profile")
238
}
232
- return autoProfiles, autoProfiles, nil
239
+ return initProfileSelection{
240
+ Names: autoProfiles,
241
+ AutoNames: autoProfiles,
242
+ Entries: filterEntryMap(entryMap(modeEntries(cfg.Profiles.ModeAuto)), autoProfiles),
243
+ }, nil
244
case profilesModeExact:
234
- explicitProfileIDs, err := catalog.ProfileIDsForBaseNames(cfg.Profiles.explicitBaseNames())
235
- if err != nil {
236
- return nil, nil, err
237
- }
238
- return explicitProfileIDs, nil, nil
245
+ explicitNames := entryNames(modeEntries(cfg.Profiles.ModeExact))
246
+ return initProfileSelection{
247
+ Names: explicitNames,
248
+ Entries: entryMap(modeEntries(cfg.Profiles.ModeExact)),
249
+ }, nil
250
case profilesModeCombined:
240
- explicitProfileIDs, err := catalog.ProfileIDsForBaseNames(cfg.Profiles.explicitBaseNames())
241
- if err != nil {
242
- return nil, nil, err
243
- }
244
- return mergeProfileIDs(explicitProfileIDs, autoProfiles), autoProfiles, nil
251
+ explicitNames := entryNames(modeEntries(cfg.Profiles.ModeCombined))
252
+ return initProfileSelection{
253
+ Names: mergeProfileNames(explicitNames, autoProfiles),
254
+ AutoNames: autoProfiles,
255
+ Entries: entryMap(modeEntries(cfg.Profiles.ModeCombined)),
256
+ }, nil
257
default:
246
- return nil, nil, fmt.Errorf("unsupported profiles.mode %q", cfg.Profiles.Mode)
258
+ return initProfileSelection{}, fmt.Errorf("unsupported profiles.mode %q", cfg.Profiles.Mode)
259
}
260
}
261
@@ -257,28 +269,47 @@ func createCredential(auth cloudauth.AzureADAuthConfig, cloudCfg azcloud.Configu
269
})
270
}
271
260
-func mergeProfileIDs(explicit, discovered []string) []string {
272
+func mergeProfileNames(explicit, discovered []string) []string {
273
seen := make(map[string]struct{}, len(explicit)+len(discovered))
274
merged := make([]string, 0, len(explicit)+len(discovered))
263
- for _, id := range explicit {
264
- key := stringsLowerTrim(id)
275
+ for _, name := range explicit {
276
+ key := stringsLowerTrim(name)
277
if _, ok := seen[key]; ok {
278
continue
279
}
280
seen[key] = struct{}{}
269
- merged = append(merged, id)
281
+ merged = append(merged, name)
282
}
271
- for _, id := range discovered {
272
- key := stringsLowerTrim(id)
283
+ for _, name := range discovered {
284
+ key := stringsLowerTrim(name)
285
if _, ok := seen[key]; ok {
286
continue
287
}
288
seen[key] = struct{}{}
277
- merged = append(merged, id)
289
+ merged = append(merged, name)
290
}
291
return merged
292
}
293
294
+func filterEntryMap(entries map[string]ProfileEntryConfig, active []string) map[string]ProfileEntryConfig {
295
+ if len(entries) == 0 || len(active) == 0 {
296
+ return nil
297
+ }
298
+
299
+ filtered := make(map[string]ProfileEntryConfig)
300
+ for _, name := range active {
301
+ entry, ok := entries[name]
302
+ if !ok {
303
+ continue
304
+ }
305
+ filtered[name] = entry
306
+ }
307
+ if len(filtered) == 0 {
308
+ return nil
309
+ }
310
+ return filtered
311
+}
312
+
313
func defaultNewResourceGraphClient(subscriptionID string, cred azcore.TokenCredential, cloudCfg azcloud.Configuration) (resourceGraphClient, error) {
314
_ = subscriptionID
315
client, err := armresourcegraph.NewClient(cred, armClientOptions{Cloud: cloudCfg}.toARM())
src/go/plugin/go.d/collector/azure_monitor/metadata.yaml
+144
-55
@@ -29,7 +29,7 @@ modules:
29
30
- **Multi-subscription** -- monitor resources across one or more Azure subscriptions in a single job
31
- **Automatic service detection** -- discovers resources and enables matching metric profiles without manual configuration
32
- - **38 built-in service profiles** -- covers databases, compute, networking, storage, AI, analytics, and more
32
+ - **[38 built-in service profiles](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default)** -- covers databases, compute, networking, storage, AI, analytics, and more
33
- **Flexible discovery** -- use structured filters (resource groups, regions, tags) or a custom Azure Resource Graph KQL query
34
method_description: &method_description |
35
It uses the [Azure Monitor Metrics batch API](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/migrate-to-batch-api) to collect metrics, grouping requests by subscription, region, and time grain. Resources are discovered via [Azure Resource Graph](https://learn.microsoft.com/en-us/azure/governance/resource-graph/overview) queries at startup and refreshed periodically. Authentication is handled through [Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity/) (service principal, managed identity, or default credentials).
@@ -53,7 +53,7 @@ modules:
53
**Bootstrap (first run)**
54
55
- With the default `profiles.mode: auto`, the collector queries Azure Resource Graph within the configured `subscription_ids` to find candidate resources.
56
- - It matches discovered resource types against built-in profiles and automatically enables the relevant ones.
56
+ - It matches discovered resource types against [built-in profiles](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default) and automatically enables the relevant ones.
57
- Discovery scope can be narrowed using `discovery.mode: filters` (resource groups, regions, tags) or replaced entirely with `discovery.mode: query` for a custom KQL query.
58
- A single job can monitor multiple subscriptions.
59
@@ -131,7 +131,7 @@ modules:
131
| Stock profiles | `/usr/lib/netdata/conf.d/go.d/azure_monitor.profiles/default/` |
132
| User overrides | `/etc/netdata/go.d/azure_monitor.profiles/` |
133
134
- User profile files with the same `id` as a stock profile override it.
134
+ User profile files with the same basename as a [stock profile](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default) override it.
135
Custom profiles extend the collector's catalog -- they do not replace the discovery mechanism.
136
folding:
137
title: Config options
@@ -248,6 +248,7 @@ modules:
248
group: Discovery
249
detailed_description: |
250
A raw Azure Resource Graph KQL query used when `discovery.mode` is `query`.
251
+ See the [Azure Resource Graph query language documentation](https://learn.microsoft.com/en-us/azure/governance/resource-graph/concepts/query-language) for syntax and supported operators.
252
253
The query **must** project these five columns:
254
@@ -259,6 +260,14 @@ modules:
260
| `resourceGroup` | Resource group name |
261
| `location` | Azure region |
262
263
+ :::info
264
+
265
+ - Returned resource `type` values should match the Azure resource types expected by the active profiles.
266
+ - Resources with unsupported or non-matching types are ignored and do not activate profiles.
267
+ - For the stock catalog, see [built-in profiles](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default).
268
+
269
+ :::
270
+
271
Example:
272
273
```
@@ -276,21 +285,70 @@ modules:
285
286
| Mode | Behavior |
287
|:-----|:---------|
279
- | `auto` | Discovers resource types in your subscriptions and enables matching built-in profiles automatically. This is the default. |
280
- | `exact` | Uses only the profile basenames listed under `profiles.mode_exact.names`. No auto-discovery. |
281
- | `combined` | Merges auto-discovered profiles with the basenames listed under `profiles.mode_combined.names`. |
288
+ | `auto` | Discovers resource types in your subscriptions and enables matching [built-in profiles](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default) automatically. This is the default. |
289
+ | `exact` | Uses only the profile entries listed under `profiles.mode_exact.entries`. No auto-discovery. |
290
+ | `combined` | Merges auto-discovered profiles with the explicit entries listed under `profiles.mode_combined.entries`. |
291
+
292
+ Each profile entry uses `name` as the canonical profile basename. The basename is the profile filename without the `.yaml` / `.yml` suffix and must be lowercase.
293
283
- Profile basename matching is case-insensitive. A basename is the profile filename without the `.yaml` / `.yml` suffix.
284
- - name: profiles.mode_exact.names
285
- description: Explicit profile file basenames used by `exact` mode. Matching is case-insensitive.
294
+ Filter layering:
295
+
296
+ - `discovery.mode_filters.*` defines the job-wide discovery scope.
297
+ - `profiles.mode_*.entries[].filters.*` narrows resources for one profile only.
298
+ - The effective resource set for a profile is the intersection of both filter sets.
299
+ - Per-profile filters never widen or bypass the global discovery scope.
300
+ - In `discovery.mode: query`, per-profile filters can use only `resource_groups` and `regions`. If you need per-profile tag filtering there, encode it in the KQL.
301
+ - name: profiles.mode_auto.entries
302
+ description: Optional per-profile overrides applied only to profiles that auto-activate at bootstrap.
303
+ default_value: "[]"
304
+ required: false
305
+ group: Profiles
306
+ detailed_description: |
307
+ Each entry has:
308
+
309
+ | Field | Description |
310
+ |:------|:------------|
311
+ | `name` | Canonical profile basename. |
312
+ | `filters.resource_groups` | Optional resource-group narrowing for this profile. |
313
+ | `filters.regions` | Optional region narrowing for this profile. |
314
+ | `filters.tags` | Optional tag narrowing for this profile in `discovery.mode: filters`. |
315
+
316
+ Auto-mode entries do not activate profiles on their own. They only override filtering for profiles that were auto-selected at bootstrap.
317
+ In `discovery.mode: query`, only `filters.resource_groups` and `filters.regions` apply.
318
+ - name: profiles.mode_exact.entries
319
+ description: Explicit profile entries used by `exact` mode.
320
default_value: "[]"
321
required: false
322
group: Profiles
289
- - name: profiles.mode_combined.names
290
- description: Explicit profile file basenames merged with auto-discovered profiles in `combined` mode. Matching is case-insensitive.
323
+ detailed_description: |
324
+ Each entry has:
325
+
326
+ | Field | Description |
327
+ |:------|:------------|
328
+ | `name` | Canonical profile basename. |
329
+ | `filters.resource_groups` | Optional resource-group narrowing for this profile. |
330
+ | `filters.regions` | Optional region narrowing for this profile. |
331
+ | `filters.tags` | Optional tag narrowing for this profile in `discovery.mode: filters`. |
332
+
333
+ Per-profile filters only narrow the globally discovered resource set. They never widen it.
334
+ In `discovery.mode: query`, only `filters.resource_groups` and `filters.regions` apply.
335
+ - name: profiles.mode_combined.entries
336
+ description: Explicit profile entries merged with auto-discovered profiles in `combined` mode.
337
default_value: "[]"
338
required: false
339
group: Profiles
340
+ detailed_description: |
341
+ Each entry has:
342
+
343
+ | Field | Description |
344
+ |:------|:------------|
345
+ | `name` | Canonical profile basename. |
346
+ | `filters.resource_groups` | Optional resource-group narrowing for this profile. |
347
+ | `filters.regions` | Optional region narrowing for this profile. |
348
+ | `filters.tags` | Optional tag narrowing for this profile in `discovery.mode: filters`. |
349
+
350
+ If an explicit `combined` entry matches an auto-selected profile, the collector keeps one runtime profile and overlays the explicit entry filters onto it.
351
+ In `discovery.mode: query`, only `filters.resource_groups` and `filters.regions` apply.
352
- name: limits.max_concurrency
353
description: Maximum concurrent batch queries to Azure Monitor.
354
default_value: 4
@@ -354,9 +412,40 @@ modules:
412
profiles:
413
mode: exact
414
mode_exact:
357
- names:
358
- - sql_database
359
- - postgres_flexible
415
+ entries:
416
+ - name: sql_database
417
+ - name: postgres_flexible
418
+ auth:
419
+ mode: managed_identity
420
+ - name: Global and per-profile filters together
421
+ description: Apply a global discovery boundary to the whole job, then narrow only the SQL Database profile further with a per-profile tag filter.
422
+ config: |
423
+ jobs:
424
+ - name: prod-databases
425
+ subscription_ids:
426
+ - "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
427
+ discovery:
428
+ mode: filters
429
+ mode_filters:
430
+ resource_groups:
431
+ - production-rg
432
+ regions:
433
+ - eastus
434
+ profiles:
435
+ mode: combined
436
+ mode_combined:
437
+ entries:
438
+ - name: sql_database
439
+ filters:
440
+ tags:
441
+ env:
442
+ - prod
443
+ # Effective scope:
444
+ # 1. Discovery first keeps only resources in production-rg and eastus.
445
+ # 2. Auto-selected profiles use that global scope as-is.
446
+ # 3. The sql_database profile is narrowed further to resources tagged env=prod.
447
+ # 4. The sql_database profile cannot see resources outside production-rg/eastus,
448
+ # because per-profile filters only narrow the globally discovered set.
449
auth:
450
mode: managed_identity
451
- name: Custom Azure Resource Graph KQL
@@ -412,8 +501,8 @@ modules:
501
description: |
502
Profiles are matched by Azure resource type. If a resource type exists but metrics are missing:
503
415
- - **Check profile mode** -- Ensure `profiles.mode: auto` (default), or explicitly list the profile basename under `profiles.mode_exact.names` or `profiles.mode_combined.names`.
416
- - **Verify a built-in profile exists** -- List available profiles:
504
+ - **Check profile mode** -- Ensure `profiles.mode: auto` (default), or explicitly list the profile basename under `profiles.mode_exact.entries` or `profiles.mode_combined.entries`.
505
+ - **Verify a [built-in profile](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default) exists** -- List available profiles:
506
```bash
507
ls /usr/lib/netdata/conf.d/go.d/azure_monitor.profiles/default/
508
```
@@ -512,7 +601,7 @@ modules:
601
- name: resource_type
602
description: "The Azure resource type identifier."
603
- name: profile
515
- description: "The Azure Monitor profile id."
604
+ description: "The Azure Monitor profile basename."
605
- name: subscription_id
606
description: "The Azure subscription identifier."
607
- name: resource_uid
@@ -683,7 +772,7 @@ modules:
772
- name: resource_type
773
description: "The Azure resource type identifier."
774
- name: profile
686
- description: "The Azure Monitor profile id."
775
+ description: "The Azure Monitor profile basename."
776
- name: subscription_id
777
description: "The Azure subscription identifier."
778
- name: resource_uid
@@ -969,7 +1058,7 @@ modules:
1058
- name: resource_type
1059
description: "The Azure resource type identifier."
1060
- name: profile
972
- description: "The Azure Monitor profile id."
1061
+ description: "The Azure Monitor profile basename."
1062
- name: subscription_id
1063
description: "The Azure subscription identifier."
1064
- name: resource_uid
@@ -1357,7 +1446,7 @@ modules:
1446
- name: resource_type
1447
description: "The Azure resource type identifier."
1448
- name: profile
1360
- description: "The Azure Monitor profile id."
1449
+ description: "The Azure Monitor profile basename."
1450
- name: subscription_id
1451
description: "The Azure subscription identifier."
1452
- name: resource_uid
@@ -1597,7 +1686,7 @@ modules:
1686
- name: resource_type
1687
description: "The Azure resource type identifier."
1688
- name: profile
1600
- description: "The Azure Monitor profile id."
1689
+ description: "The Azure Monitor profile basename."
1690
- name: subscription_id
1691
description: "The Azure subscription identifier."
1692
- name: resource_uid
@@ -1859,7 +1948,7 @@ modules:
1948
- name: resource_type
1949
description: "The Azure resource type identifier."
1950
- name: profile
1862
- description: "The Azure Monitor profile id."
1951
+ description: "The Azure Monitor profile basename."
1952
- name: subscription_id
1953
description: "The Azure subscription identifier."
1954
- name: resource_uid
@@ -2209,7 +2298,7 @@ modules:
2298
- name: resource_type
2299
description: "The Azure resource type identifier."
2300
- name: profile
2212
- description: "The Azure Monitor profile id."
2301
+ description: "The Azure Monitor profile basename."
2302
- name: subscription_id
2303
description: "The Azure subscription identifier."
2304
- name: resource_uid
@@ -2444,7 +2533,7 @@ modules:
2533
- name: resource_type
2534
description: "The Azure resource type identifier."
2535
- name: profile
2447
- description: "The Azure Monitor profile id."
2536
+ description: "The Azure Monitor profile basename."
2537
- name: subscription_id
2538
description: "The Azure subscription identifier."
2539
- name: resource_uid
@@ -2558,7 +2647,7 @@ modules:
2647
- name: resource_type
2648
description: "The Azure resource type identifier."
2649
- name: profile
2561
- description: "The Azure Monitor profile id."
2650
+ description: "The Azure Monitor profile basename."
2651
- name: subscription_id
2652
description: "The Azure subscription identifier."
2653
- name: resource_uid
@@ -2704,7 +2793,7 @@ modules:
2793
- name: resource_type
2794
description: "The Azure resource type identifier."
2795
- name: profile
2707
- description: "The Azure Monitor profile id."
2796
+ description: "The Azure Monitor profile basename."
2797
- name: subscription_id
2798
description: "The Azure subscription identifier."
2799
- name: resource_uid
@@ -2938,7 +3027,7 @@ modules:
3027
- name: resource_type
3028
description: "The Azure resource type identifier."
3029
- name: profile
2941
- description: "The Azure Monitor profile id."
3030
+ description: "The Azure Monitor profile basename."
3031
- name: subscription_id
3032
description: "The Azure subscription identifier."
3033
- name: resource_uid
@@ -3219,7 +3308,7 @@ modules:
3308
- name: resource_type
3309
description: "The Azure resource type identifier."
3310
- name: profile
3222
- description: "The Azure Monitor profile id."
3311
+ description: "The Azure Monitor profile basename."
3312
- name: subscription_id
3313
description: "The Azure subscription identifier."
3314
- name: resource_uid
@@ -3528,7 +3617,7 @@ modules:
3617
- name: resource_type
3618
description: "The Azure resource type identifier."
3619
- name: profile
3531
- description: "The Azure Monitor profile id."
3620
+ description: "The Azure Monitor profile basename."
3621
- name: subscription_id
3622
description: "The Azure subscription identifier."
3623
- name: resource_uid
@@ -3726,7 +3815,7 @@ modules:
3815
- name: resource_type
3816
description: "The Azure resource type identifier."
3817
- name: profile
3729
- description: "The Azure Monitor profile id."
3818
+ description: "The Azure Monitor profile basename."
3819
- name: subscription_id
3820
description: "The Azure subscription identifier."
3821
- name: resource_uid
@@ -3922,7 +4011,7 @@ modules:
4011
- name: resource_type
4012
description: "The Azure resource type identifier."
4013
- name: profile
3925
- description: "The Azure Monitor profile id."
4014
+ description: "The Azure Monitor profile basename."
4015
- name: subscription_id
4016
description: "The Azure subscription identifier."
4017
- name: resource_uid
@@ -4128,7 +4217,7 @@ modules:
4217
- name: resource_type
4218
description: "The Azure resource type identifier."
4219
- name: profile
4131
- description: "The Azure Monitor profile id."
4220
+ description: "The Azure Monitor profile basename."
4221
- name: subscription_id
4222
description: "The Azure subscription identifier."
4223
- name: resource_uid
@@ -4259,7 +4348,7 @@ modules:
4348
- name: resource_type
4349
description: "The Azure resource type identifier."
4350
- name: profile
4262
- description: "The Azure Monitor profile id."
4351
+ description: "The Azure Monitor profile basename."
4352
- name: subscription_id
4353
description: "The Azure subscription identifier."
4354
- name: resource_uid
@@ -4417,7 +4506,7 @@ modules:
4506
- name: resource_type
4507
description: "The Azure resource type identifier."
4508
- name: profile
4420
- description: "The Azure Monitor profile id."
4509
+ description: "The Azure Monitor profile basename."
4510
- name: subscription_id
4511
description: "The Azure subscription identifier."
4512
- name: resource_uid
@@ -4668,7 +4757,7 @@ modules:
4757
- name: resource_type
4758
description: "The Azure resource type identifier."
4759
- name: profile
4671
- description: "The Azure Monitor profile id."
4760
+ description: "The Azure Monitor profile basename."
4761
- name: subscription_id
4762
description: "The Azure subscription identifier."
4763
- name: resource_uid
@@ -5030,7 +5119,7 @@ modules:
5119
- name: resource_type
5120
description: "The Azure resource type identifier."
5121
- name: profile
5033
- description: "The Azure Monitor profile id."
5122
+ description: "The Azure Monitor profile basename."
5123
- name: subscription_id
5124
description: "The Azure subscription identifier."
5125
- name: resource_uid
@@ -5275,7 +5364,7 @@ modules:
5364
- name: resource_type
5365
description: "The Azure resource type identifier."
5366
- name: profile
5278
- description: "The Azure Monitor profile id."
5367
+ description: "The Azure Monitor profile basename."
5368
- name: subscription_id
5369
description: "The Azure subscription identifier."
5370
- name: resource_uid
@@ -5601,7 +5690,7 @@ modules:
5690
- name: resource_type
5691
description: "The Azure resource type identifier."
5692
- name: profile
5604
- description: "The Azure Monitor profile id."
5693
+ description: "The Azure Monitor profile basename."
5694
- name: subscription_id
5695
description: "The Azure subscription identifier."
5696
- name: resource_uid
@@ -5820,7 +5909,7 @@ modules:
5909
- name: resource_type
5910
description: "The Azure resource type identifier."
5911
- name: profile
5823
- description: "The Azure Monitor profile id."
5912
+ description: "The Azure Monitor profile basename."
5913
- name: subscription_id
5914
description: "The Azure subscription identifier."
5915
- name: resource_uid
@@ -5999,7 +6088,7 @@ modules:
6088
- name: resource_type
6089
description: "The Azure resource type identifier."
6090
- name: profile
6002
- description: "The Azure Monitor profile id."
6091
+ description: "The Azure Monitor profile basename."
6092
- name: subscription_id
6093
description: "The Azure subscription identifier."
6094
- name: resource_uid
@@ -6620,7 +6709,7 @@ modules:
6709
- name: resource_type
6710
description: "The Azure resource type identifier."
6711
- name: profile
6623
- description: "The Azure Monitor profile id."
6712
+ description: "The Azure Monitor profile basename."
6713
- name: subscription_id
6714
description: "The Azure subscription identifier."
6715
- name: resource_uid
@@ -6948,7 +7037,7 @@ modules:
7037
- name: resource_type
7038
description: "The Azure resource type identifier."
7039
- name: profile
6951
- description: "The Azure Monitor profile id."
7040
+ description: "The Azure Monitor profile basename."
7041
- name: subscription_id
7042
description: "The Azure subscription identifier."
7043
- name: resource_uid
@@ -7122,7 +7211,7 @@ modules:
7211
- name: resource_type
7212
description: "The Azure resource type identifier."
7213
- name: profile
7125
- description: "The Azure Monitor profile id."
7214
+ description: "The Azure Monitor profile basename."
7215
- name: subscription_id
7216
description: "The Azure subscription identifier."
7217
- name: resource_uid
@@ -7552,7 +7641,7 @@ modules:
7641
- name: resource_type
7642
description: "The Azure resource type identifier."
7643
- name: profile
7555
- description: "The Azure Monitor profile id."
7644
+ description: "The Azure Monitor profile basename."
7645
- name: subscription_id
7646
description: "The Azure subscription identifier."
7647
- name: resource_uid
@@ -7960,7 +8049,7 @@ modules:
8049
- name: resource_type
8050
description: "The Azure resource type identifier."
8051
- name: profile
7963
- description: "The Azure Monitor profile id."
8052
+ description: "The Azure Monitor profile basename."
8053
- name: subscription_id
8054
description: "The Azure subscription identifier."
8055
- name: resource_uid
@@ -8070,7 +8159,7 @@ modules:
8159
- name: resource_type
8160
description: "The Azure resource type identifier."
8161
- name: profile
8073
- description: "The Azure Monitor profile id."
8162
+ description: "The Azure Monitor profile basename."
8163
- name: subscription_id
8164
description: "The Azure subscription identifier."
8165
- name: resource_uid
@@ -8158,7 +8247,7 @@ modules:
8247
- name: resource_type
8248
description: "The Azure resource type identifier."
8249
- name: profile
8161
- description: "The Azure Monitor profile id."
8250
+ description: "The Azure Monitor profile basename."
8251
- name: subscription_id
8252
description: "The Azure subscription identifier."
8253
- name: resource_uid
@@ -8310,7 +8399,7 @@ modules:
8399
- name: resource_type
8400
description: "The Azure resource type identifier."
8401
- name: profile
8313
- description: "The Azure Monitor profile id."
8402
+ description: "The Azure Monitor profile basename."
8403
- name: subscription_id
8404
description: "The Azure subscription identifier."
8405
- name: resource_uid
@@ -8529,7 +8618,7 @@ modules:
8618
- name: resource_type
8619
description: "The Azure resource type identifier."
8620
- name: profile
8532
- description: "The Azure Monitor profile id."
8621
+ description: "The Azure Monitor profile basename."
8622
- name: subscription_id
8623
description: "The Azure subscription identifier."
8624
- name: resource_uid
@@ -8700,7 +8789,7 @@ modules:
8789
- name: resource_type
8790
description: "The Azure resource type identifier."
8791
- name: profile
8703
- description: "The Azure Monitor profile id."
8792
+ description: "The Azure Monitor profile basename."
8793
- name: subscription_id
8794
description: "The Azure subscription identifier."
8795
- name: resource_uid
@@ -9065,7 +9154,7 @@ modules:
9154
- name: resource_type
9155
description: "The Azure resource type identifier."
9156
- name: profile
9068
- description: "The Azure Monitor profile id."
9157
+ description: "The Azure Monitor profile basename."
9158
- name: subscription_id
9159
description: "The Azure subscription identifier."
9160
- name: resource_uid
@@ -9439,7 +9528,7 @@ modules:
9528
- name: resource_type
9529
description: "The Azure resource type identifier."
9530
- name: profile
9442
- description: "The Azure Monitor profile id."
9531
+ description: "The Azure Monitor profile basename."
9532
- name: subscription_id
9533
description: "The Azure subscription identifier."
9534
- name: resource_uid
@@ -9599,7 +9688,7 @@ modules:
9688
- name: resource_type
9689
description: "The Azure resource type identifier."
9690
- name: profile
9602
- description: "The Azure Monitor profile id."
9691
+ description: "The Azure Monitor profile basename."
9692
- name: subscription_id
9693
description: "The Azure subscription identifier."
9694
- name: resource_uid
@@ -9829,7 +9918,7 @@ modules:
9918
- name: resource_type
9919
description: "The Azure resource type identifier."
9920
- name: profile
9832
- description: "The Azure Monitor profile id."
9921
+ description: "The Azure Monitor profile basename."
9922
- name: subscription_id
9923
description: "The Azure subscription identifier."
9924
- name: resource_uid
@@ -10167,7 +10256,7 @@ modules:
10256
- name: resource_type
10257
description: "The Azure resource type identifier."
10258
- name: profile
10170
- description: "The Azure Monitor profile id."
10259
+ description: "The Azure Monitor profile basename."
10260
- name: subscription_id
10261
description: "The Azure subscription identifier."
10262
- name: resource_uid
src/go/plugin/go.d/collector/azure_monitor/observation_state.go
+16
-13
@@ -93,29 +93,28 @@ func (s *observationState) reobserveCachedObservations(dueInstruments, observedT
93
}
94
}
95
96
-// pruneStaleResources removes cache entries for resources no longer in the discovery set.
97
-func (s *observationState) pruneStaleResources(current []resourceInfo) {
98
- activeUIDs := make(map[string]struct{}, len(current))
99
- for _, r := range current {
100
- activeUIDs[r.UID] = struct{}{}
96
+// pruneStaleResources removes cache entries for resources that are no longer
97
+// active for a specific profile/label identity.
98
+func (s *observationState) pruneStaleResources(current map[string][]resourceInfo) {
99
+ activeLabels := make(map[string]struct{})
100
+ for profileName, resources := range current {
101
+ for _, resource := range resources {
102
+ activeLabels[labelIdentity(labelValues(resourceLabels(resource, profileName)))] = struct{}{}
103
+ }
104
}
105
106
for key, obs := range s.lastObserved {
107
if len(obs.labelValues) == 0 {
108
continue
109
}
107
- if _, ok := activeUIDs[obs.labelValues[0]]; ok {
110
+ if _, ok := activeLabels[labelIdentity(obs.labelValues)]; ok {
111
continue
112
}
113
delete(s.lastObserved, key)
114
}
115
116
for key := range s.accumulators {
114
- uid := accumulatorResourceUID(key)
115
- if uid == "" {
116
- continue
117
- }
118
- if _, ok := activeUIDs[uid]; ok {
117
+ if _, ok := activeLabels[labelIdentityFromObservationKey(key)]; ok {
118
continue
119
}
120
delete(s.accumulators, key)
@@ -126,8 +125,12 @@ func sampleObservationKey(instrument string, values []string) string {
125
return instrument + "\x00" + strings.Join(values, "\x00")
126
}
127
129
-func accumulatorResourceUID(key string) string {
130
- parts := strings.SplitN(key, "\x00", 3)
128
+func labelIdentity(values []string) string {
129
+ return strings.Join(values, "\x00")
130
+}
131
+
132
+func labelIdentityFromObservationKey(key string) string {
133
+ parts := strings.SplitN(key, "\x00", 2)
134
if len(parts) < 2 {
135
return ""
136
}
src/go/plugin/go.d/collector/azure_monitor/plan.go
+24
-23
@@ -12,8 +12,8 @@ import (
12
"gopkg.in/yaml.v3"
13
)
14
15
-func buildCollectorRuntimeFromConfig(profileIDs []string, catalog azureprofiles.Catalog) (*collectorRuntime, error) {
16
- profiles, err := catalog.Resolve(profileIDs)
15
+func buildCollectorRuntimeFromConfig(profileNames []string, profileEntries map[string]ProfileEntryConfig, catalog azureprofiles.Catalog) (*collectorRuntime, error) {
16
+ profiles, err := catalog.Resolve(profileNames)
17
if err != nil {
18
return nil, err
19
}
@@ -22,18 +22,18 @@ func buildCollectorRuntimeFromConfig(profileIDs []string, catalog azureprofiles.
22
Profiles: make([]*profileRuntime, 0, len(profiles)),
23
}
24
25
- seenProfileIDs := make(map[string]struct{}, len(profiles))
25
+ seenProfileNames := make(map[string]struct{}, len(profiles))
26
seenChartIDs := make(map[string]struct{})
27
28
for _, src := range profiles {
29
- p, err := buildProfileRuntime(src)
29
+ p, err := buildProfileRuntime(src, profileEntries[src.Name])
30
if err != nil {
31
return nil, err
32
}
33
- if _, ok := seenProfileIDs[p.ID]; ok {
34
- return nil, fmt.Errorf("profile id collision for profile %q", src.ID)
33
+ if _, ok := seenProfileNames[p.Name]; ok {
34
+ return nil, fmt.Errorf("profile name collision for profile %q", src.Name)
35
}
36
- seenProfileIDs[p.ID] = struct{}{}
36
+ seenProfileNames[p.Name] = struct{}{}
37
38
if err := walkCharts(p.Template, func(chart charttpl.Chart) error {
39
if _, ok := seenChartIDs[chart.ID]; ok {
@@ -57,39 +57,40 @@ func buildCollectorRuntimeFromConfig(profileIDs []string, catalog azureprofiles.
57
return runtime, nil
58
}
59
60
-func buildProfileRuntime(p azureprofiles.Profile) (*profileRuntime, error) {
61
- profileID := stringsTrim(p.ID)
62
- if profileID == "" {
63
- return nil, fmt.Errorf("profile has empty id")
60
+func buildProfileRuntime(resolved azureprofiles.ResolvedProfile, entry ProfileEntryConfig) (*profileRuntime, error) {
61
+ profileName := stringsTrim(resolved.Name)
62
+ if profileName == "" {
63
+ return nil, fmt.Errorf("profile has empty name")
64
}
65
66
- name := stringsTrim(p.DisplayName)
67
- if name == "" {
68
- return nil, fmt.Errorf("profile %q has empty name", profileID)
66
+ displayName := stringsTrim(resolved.Config.DisplayName)
67
+ if displayName == "" {
68
+ return nil, fmt.Errorf("profile %q has empty display_name", profileName)
69
}
70
71
- resourceType := stringsTrim(p.ResourceType)
72
- metricNamespace := stringsTrim(p.MetricNamespace)
71
+ resourceType := stringsTrim(resolved.Config.ResourceType)
72
+ metricNamespace := stringsTrim(resolved.Config.MetricNamespace)
73
if metricNamespace == "" {
74
metricNamespace = resourceType
75
}
76
77
out := &profileRuntime{
78
- ID: profileID,
79
- Name: name,
78
+ Name: profileName,
79
+ DisplayName: displayName,
80
ResourceType: resourceType,
81
MetricNamespace: metricNamespace,
82
- Metrics: make([]*metricRuntime, 0, len(p.Metrics)),
82
+ Filters: cloneResourceFilters(entry.Filters),
83
+ Metrics: make([]*metricRuntime, 0, len(resolved.Config.Metrics)),
84
}
85
85
- for _, m := range p.Metrics {
86
+ for _, m := range resolved.Config.Metrics {
87
grain := strings.ToUpper(stringsTrim(m.TimeGrain))
88
if grain == "" {
89
grain = "PT1M"
90
}
91
grainEvery, ok := azureprofiles.SupportedTimeGrains[grain]
92
if !ok {
92
- return nil, fmt.Errorf("profile %q metric %q has unsupported time grain %q", profileID, m.ID, grain)
93
+ return nil, fmt.Errorf("profile %q metric %q has unsupported time grain %q", profileName, m.ID, grain)
94
}
95
96
mr := &metricRuntime{
@@ -105,7 +106,7 @@ func buildProfileRuntime(p azureprofiles.Profile) (*profileRuntime, error) {
106
mr.Series = append(mr.Series, &seriesRuntime{
107
Aggregation: aggregation,
108
Kind: azureprofiles.NormalizeSeriesKind(s.Kind),
108
- Instrument: azureprofiles.ExportedSeriesName(profileID, m.ID, aggregation),
109
+ Instrument: azureprofiles.ExportedSeriesName(profileName, m.ID, aggregation),
110
})
111
}
112
@@ -119,7 +120,7 @@ func buildProfileRuntime(p azureprofiles.Profile) (*profileRuntime, error) {
120
return out.Metrics[i].ID < out.Metrics[j].ID
121
})
122
122
- out.Template = p.Template
123
+ out.Template = resolved.Config.Template
124
out.Template.Metrics = profileMetricsList(out)
125
return out, nil
126
}
src/go/plugin/go.d/collector/azure_monitor/profile_catalog_test.go
+80
-30
@@ -40,8 +40,7 @@ func TestLoadProfileCatalogFromDirs_UserOverridesStock(t *testing.T) {
40
stockDir := filepath.Join(dir, "stock")
41
42
require.NoError(t, writeProfileFile(filepath.Join(userDir, "sql_database.yaml"), `
43
-id: sql_database
44
-name: Azure SQL Database (User Override)
43
+display_name: Azure SQL Database (User Override)
44
resource_type: Microsoft.Sql/servers/databases
45
metrics:
46
- id: cpu_percent
@@ -69,8 +68,7 @@ template:
68
name: average
69
`))
70
require.NoError(t, writeProfileFile(filepath.Join(stockDir, "sql_database.yaml"), `
72
-id: sql_database
73
-name: Azure SQL Database (Stock)
71
+display_name: Azure SQL Database (Stock)
72
resource_type: Microsoft.Sql/servers/databases
73
metrics:
74
- id: cpu_percent
@@ -98,8 +96,7 @@ template:
96
name: average
97
`))
98
require.NoError(t, writeProfileFile(filepath.Join(stockDir, "postgres_flexible.yaml"), `
101
-id: postgres_flexible
102
-name: Azure PostgreSQL Flexible Server
99
+display_name: Azure PostgreSQL Flexible Server
100
resource_type: Microsoft.DBforPostgreSQL/flexibleServers
101
metrics:
102
- id: cpu_percent
@@ -133,7 +130,7 @@ template:
130
})
131
require.NoError(t, err)
132
136
- gotProfiles, err := catalog.ResolveBaseNames([]string{"SQL_DATABASE"})
133
+ gotProfiles, err := catalog.ResolveBaseNames([]string{"sql_database"})
134
require.NoError(t, err)
135
require.Len(t, gotProfiles, 1)
136
got := gotProfiles[0]
@@ -145,8 +142,7 @@ func TestLoadProfileCatalogFromDirs_RejectsDuplicateProfileBasenames(t *testing.
142
stockDir := filepath.Join(dir, "stock")
143
144
require.NoError(t, writeProfileFile(filepath.Join(stockDir, "sql_database.yaml"), `
148
-id: sql_database
149
-name: Azure SQL Database
145
+display_name: Azure SQL Database
146
resource_type: Microsoft.Sql/servers/databases
147
metrics:
148
- id: cpu_percent
@@ -174,8 +170,7 @@ template:
170
name: average
171
`))
172
require.NoError(t, writeProfileFile(filepath.Join(stockDir, "nested", "sql_database.yml"), `
177
-id: sql_database_copy
178
-name: azure sql database
173
+display_name: azure sql database
174
resource_type: Microsoft.Sql/servers/databases
175
metrics:
176
- id: cpu_percent
@@ -199,7 +194,7 @@ template:
194
instances:
195
by_labels: [resource_uid]
196
dimensions:
202
- - selector: sql_database_copy.cpu_percent_average
197
+ - selector: sql_database.cpu_percent_average
198
name: average
199
`))
200
@@ -211,13 +206,53 @@ template:
206
assert.Contains(t, err.Error(), `"sql_database"`)
207
}
208
214
-func TestLoadProfileCatalogFromDirs_RejectsDuplicateProfileIDsAcrossBasenames(t *testing.T) {
209
+func TestLoadProfileCatalogFromDirs_RejectsInvalidProfileBasename(t *testing.T) {
210
+ dir := t.TempDir()
211
+ stockDir := filepath.Join(dir, "stock")
212
+
213
+ require.NoError(t, writeProfileFile(filepath.Join(stockDir, "SQL_DATABASE.yaml"), `
214
+display_name: Azure SQL Database Copy
215
+resource_type: Microsoft.Sql/servers/databases
216
+metrics:
217
+ - id: cpu_percent
218
+ azure_name: cpu_percent
219
+ time_grain: PT1M
220
+ series:
221
+ - aggregation: average
222
+ kind: gauge
223
+template:
224
+ family: Azure SQL Database Copy
225
+ context_namespace: sql_database_copy
226
+ charts:
227
+ - id: am_test_sql_database_copy_cpu
228
+ title: Azure SQL Database Copy CPU
229
+ context: cpu_copy
230
+ family: Utilization
231
+ type: line
232
+ units: percentage
233
+ algorithm: absolute
234
+ label_promotion: [resource_name, resource_group, region, resource_type, profile]
235
+ instances:
236
+ by_labels: [resource_uid]
237
+ dimensions:
238
+ - selector: sql_database.cpu_percent_average
239
+ name: average
240
+`))
241
+
242
+ _, err := azureprofiles.LoadFromDirs([]azureprofiles.DirSpec{
243
+ {Path: stockDir, IsStock: true},
244
+ })
245
+ require.Error(t, err)
246
+ assert.Contains(t, err.Error(), `basename must match`)
247
+ assert.Contains(t, err.Error(), `SQL_DATABASE.yaml`)
248
+}
249
+
250
+func TestLoadProfileCatalogFromDirs_NormalizesLocalSelectorShorthand(t *testing.T) {
251
dir := t.TempDir()
252
stockDir := filepath.Join(dir, "stock")
253
254
require.NoError(t, writeProfileFile(filepath.Join(stockDir, "sql_database.yaml"), `
219
-id: sql_database
220
-name: Azure SQL Database
255
+display_name: Azure SQL Database
256
resource_type: Microsoft.Sql/servers/databases
257
metrics:
258
- id: cpu_percent
@@ -241,12 +276,29 @@ template:
276
instances:
277
by_labels: [resource_uid]
278
dimensions:
244
- - selector: sql_database.cpu_percent_average
279
+ - selector: cpu_percent_average
280
name: average
281
`))
247
- require.NoError(t, writeProfileFile(filepath.Join(stockDir, "sql_database_copy.yaml"), `
248
-id: sql_database
249
-name: Azure SQL Database Copy
282
+
283
+ catalog, err := azureprofiles.LoadFromDirs([]azureprofiles.DirSpec{
284
+ {Path: stockDir, IsStock: true},
285
+ })
286
+ require.NoError(t, err)
287
+
288
+ profiles, err := catalog.ResolveBaseNames([]string{"sql_database"})
289
+ require.NoError(t, err)
290
+ require.Len(t, profiles, 1)
291
+ require.Len(t, profiles[0].Template.Charts, 1)
292
+ require.Len(t, profiles[0].Template.Charts[0].Dimensions, 1)
293
+ assert.Equal(t, "sql_database.cpu_percent_average", profiles[0].Template.Charts[0].Dimensions[0].Selector)
294
+}
295
+
296
+func TestLoadProfileCatalogFromDirs_RejectsUnknownSelectorShorthand(t *testing.T) {
297
+ dir := t.TempDir()
298
+ stockDir := filepath.Join(dir, "stock")
299
+
300
+ require.NoError(t, writeProfileFile(filepath.Join(stockDir, "sql_database.yaml"), `
301
+display_name: Azure SQL Database
302
resource_type: Microsoft.Sql/servers/databases
303
metrics:
304
- id: cpu_percent
@@ -256,12 +308,12 @@ metrics:
308
- aggregation: average
309
kind: gauge
310
template:
259
- family: Azure SQL Database Copy
260
- context_namespace: sql_database_copy
311
+ family: Azure SQL Database
312
+ context_namespace: sql_database
313
charts:
262
- - id: am_test_sql_database_copy_cpu
263
- title: Azure SQL Database Copy CPU
264
- context: cpu_copy
314
+ - id: am_test_sql_database_cpu
315
+ title: Azure SQL Database CPU
316
+ context: cpu
317
family: Utilization
318
type: line
319
units: percentage
@@ -270,7 +322,7 @@ template:
322
instances:
323
by_labels: [resource_uid]
324
dimensions:
273
- - selector: sql_database.cpu_percent_average
325
+ - selector: missing_average
326
name: average
327
`))
328
@@ -278,9 +330,8 @@ template:
330
{Path: stockDir, IsStock: true},
331
})
332
require.Error(t, err)
281
- assert.Contains(t, err.Error(), `duplicate profile id`)
282
- assert.Contains(t, err.Error(), `"sql_database"`)
283
- assert.Contains(t, err.Error(), `"sql_database_copy"`)
333
+ assert.Contains(t, err.Error(), "template")
334
+ assert.Contains(t, err.Error(), "missing_average")
335
}
336
337
func writeProfileFile(path, data string) error {
@@ -295,8 +346,7 @@ func TestLoadProfileCatalogFromDirs_RejectsLegacyProfileWithoutCharts(t *testing
346
stockDir := filepath.Join(dir, "stock")
347
348
require.NoError(t, writeProfileFile(filepath.Join(stockDir, "legacy.yaml"), `
298
-id: legacy
299
-name: Azure Legacy
349
+display_name: Azure Legacy
350
resource_type: Microsoft.Storage/storageAccounts
351
metrics:
352
- id: used_capacity
src/go/plugin/go.d/collector/azure_monitor/query_executor.go
+5
-5
@@ -103,7 +103,7 @@ func (e *queryExecutor) executeQueryBatch(ctx context.Context, batch queryBatch,
103
return nil, err
104
}
105
106
- return samplesFromQueryResponse(resp.Values, batch.Profile.ID, queryBatchMetricIndex(batch.Metrics), resourceByID), nil
106
+ return samplesFromQueryResponse(resp.Values, batch.Profile.Name, queryBatchMetricIndex(batch.Metrics), resourceByID), nil
107
}
108
109
func queryBatchMetricIndex(metrics []*metricRuntime) map[string]*metricRuntime {
@@ -148,14 +148,14 @@ func effectiveQueryOffset(queryOffsetSeconds int, batchTimeGrainEvery time.Durat
148
return offset
149
}
150
151
-func samplesFromQueryResponse(metricData []azmetrics.MetricData, profileID string, metricToRuntime map[string]*metricRuntime, resourceByID map[string]resourceInfo) []metricSample {
151
+func samplesFromQueryResponse(metricData []azmetrics.MetricData, profileName string, metricToRuntime map[string]*metricRuntime, resourceByID map[string]resourceInfo) []metricSample {
152
samples := make([]metricSample, 0, len(metricData))
153
for _, data := range metricData {
154
resource, ok := resourceByID[stringsLowerTrim(derefOrZero(data.ResourceID))]
155
if !ok {
156
continue
157
}
158
- samples = append(samples, samplesFromMetricValues(data.Values, resourceLabels(resource, profileID), metricToRuntime)...)
158
+ samples = append(samples, samplesFromMetricValues(data.Values, resourceLabels(resource, profileName), metricToRuntime)...)
159
}
160
return samples
161
}
@@ -183,7 +183,7 @@ func samplesFromMetricValues(metrics []azmetrics.Metric, labels metrix.Labels, m
183
return samples
184
}
185
186
-func resourceLabels(resource resourceInfo, profileID string) metrix.Labels {
186
+func resourceLabels(resource resourceInfo, profileName string) metrix.Labels {
187
return metrix.Labels{
188
"resource_uid": resource.UID,
189
"subscription_id": resource.SubscriptionID,
@@ -191,7 +191,7 @@ func resourceLabels(resource resourceInfo, profileID string) metrix.Labels {
191
"resource_group": resource.ResourceGroup,
192
"region": resource.Region,
193
"resource_type": resource.Type,
194
- "profile": profileID,
194
+ "profile": profileName,
195
}
196
}
197
src/go/plugin/go.d/collector/azure_monitor/query_schedule.go
+2
-15
@@ -10,30 +10,17 @@ import (
10
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/azure_monitor/azureprofiles"
11
)
12
13
-func (c *Collector) buildQueryBatches(resources []resourceInfo, now time.Time) []queryBatch {
13
+func (c *Collector) buildQueryBatches(now time.Time) []queryBatch {
14
dueByGrain := make(map[string]bool)
15
- resourcesByType := c.indexResourcesByType(resources)
15
var batches []queryBatch
16
17
for _, profile := range c.runtime.Profiles {
19
- batches = append(batches, c.buildProfileQueryBatches(profile, resourcesByType[stringsLowerTrim(profile.ResourceType)], dueByGrain, now)...)
18
+ batches = append(batches, c.buildProfileQueryBatches(profile, c.discovery.ByProfile[profile.Name], dueByGrain, now)...)
19
}
20
21
return batches
22
}
23
25
-func (c *Collector) indexResourcesByType(resources []resourceInfo) map[string][]resourceInfo {
26
- if len(c.discovery.ByType) > 0 {
27
- return c.discovery.ByType
28
- }
29
-
30
- result := make(map[string][]resourceInfo)
31
- for _, resource := range resources {
32
- result[stringsLowerTrim(resource.Type)] = append(result[stringsLowerTrim(resource.Type)], resource)
33
- }
34
- return result
35
-}
36
-
24
func (c *Collector) buildProfileQueryBatches(profile *profileRuntime, resources []resourceInfo, dueByGrain map[string]bool, now time.Time) []queryBatch {
25
if len(resources) == 0 {
26
return nil
src/go/plugin/go.d/collector/azure_monitor/testdata/config.json
+4
-2
@@ -16,8 +16,10 @@
16
"profiles": {
17
"mode": "exact",
18
"mode_exact": {
19
- "names": [
20
- "sql_managed_instance"
19
+ "entries": [
20
+ {
21
+ "name": "sql_managed_instance"
22
+ }
23
]
24
}
25
},
src/go/plugin/go.d/collector/azure_monitor/testdata/config.yaml
+2
-2
@@ -11,8 +11,8 @@ discovery:
11
profiles:
12
mode: exact
13
mode_exact:
14
- names:
15
- - sql_managed_instance
14
+ entries:
15
+ - name: sql_managed_instance
16
query_offset: 180
17
timeout: 30
18
limits:
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/aks.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: aks
3
-name: Azure Kubernetes Service Cluster
2
+display_name: Azure Kubernetes Service Cluster
3
resource_type: Microsoft.ContainerService/managedClusters
4
metrics:
5
- id: apiserver_cpu_usage_percentage
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/api_management.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: api_management
3
-name: Azure API Management
2
+display_name: Azure API Management
3
resource_type: Microsoft.ApiManagement/service
4
metrics:
5
- id: capacity
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/app_service.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: app_service
3
-name: Azure App Service
2
+display_name: Azure App Service
3
resource_type: Microsoft.Web/sites
4
metrics:
5
- id: requests
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/application_gateway.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: application_gateway
3
-name: Azure Application Gateway
2
+display_name: Azure Application Gateway
3
resource_type: Microsoft.Network/applicationGateways
4
metrics:
5
- id: throughput
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/application_insights.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: application_insights
3
-name: Azure Application Insights
2
+display_name: Azure Application Insights
3
resource_type: Microsoft.Insights/components
4
metrics:
5
- id: availability_results_availability_percentage
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/cognitive_services.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: cognitive_services
3
-name: Azure Cognitive Services
2
+display_name: Azure Cognitive Services
3
resource_type: Microsoft.CognitiveServices/accounts
4
metrics:
5
- id: success_rate
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/container_apps.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: container_apps
3
-name: Azure Container Apps
2
+display_name: Azure Container Apps
3
resource_type: Microsoft.App/containerApps
4
metrics:
5
- id: usage_nano_cores
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/container_instances.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: container_instances
3
-name: Azure Container Instances
2
+display_name: Azure Container Instances
3
resource_type: Microsoft.ContainerInstance/containerGroups
4
metrics:
5
- id: cpu_usage
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/container_registry.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: container_registry
3
-name: Azure Container Registry
2
+display_name: Azure Container Registry
3
resource_type: Microsoft.ContainerRegistry/registries
4
metrics:
5
- id: storage_used
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/cosmos_db.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: cosmos_db
3
-name: Azure Cosmos DB Account
2
+display_name: Azure Cosmos DB Account
3
resource_type: Microsoft.DocumentDB/databaseAccounts
4
metrics:
5
- id: total_requests
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/data_explorer.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: data_explorer
3
-name: Azure Data Explorer Cluster
2
+display_name: Azure Data Explorer Cluster
3
resource_type: Microsoft.Kusto/clusters
4
metrics:
5
- id: cpu
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/data_factory.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: data_factory
3
-name: Azure Data Factory
2
+display_name: Azure Data Factory
3
resource_type: Microsoft.DataFactory/factories
4
metrics:
5
- id: pipeline_succeeded_runs
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/event_grid.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: event_grid
3
-name: Azure Event Grid Topic
2
+display_name: Azure Event Grid Topic
3
resource_type: Microsoft.EventGrid/topics
4
metrics:
5
- id: publish_success_count
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/event_hubs.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: event_hubs
3
-name: Azure Event Hubs Namespace
2
+display_name: Azure Event Hubs Namespace
3
resource_type: Microsoft.EventHub/Namespaces
4
metrics:
5
- id: incoming_messages
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/express_route_circuit.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: express_route_circuit
3
-name: Azure ExpressRoute Circuit
2
+display_name: Azure ExpressRoute Circuit
3
resource_type: Microsoft.Network/expressRouteCircuits
4
metrics:
5
- id: arp_availability
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/express_route_gateway.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: express_route_gateway
3
-name: Azure ExpressRoute Gateway
2
+display_name: Azure ExpressRoute Gateway
3
resource_type: Microsoft.Network/expressRouteGateways
4
metrics:
5
- id: er_gateway_connection_bits_in_per_second
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/firewall.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: firewall
3
-name: Azure Firewall
2
+display_name: Azure Firewall
3
resource_type: Microsoft.Network/azureFirewalls
4
metrics:
5
- id: firewall_health
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/front_door.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: front_door
3
-name: Azure Front Door
2
+display_name: Azure Front Door
3
resource_type: Microsoft.Cdn/profiles
4
metrics:
5
- id: total_latency
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/iot_hub.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: iot_hub
3
-name: Azure IoT Hub
2
+display_name: Azure IoT Hub
3
resource_type: Microsoft.Devices/IotHubs
4
metrics:
5
- id: c2d_commands_egress_abandon_success
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/key_vault.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: key_vault
3
-name: Azure Key Vault
2
+display_name: Azure Key Vault
3
resource_type: Microsoft.KeyVault/vaults
4
metrics:
5
- id: availability
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/load_balancers.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: load_balancers
3
-name: Azure Load Balancer
2
+display_name: Azure Load Balancer
3
resource_type: Microsoft.Network/loadBalancers
4
metrics:
5
- id: vip_availability
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/log_analytics.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: log_analytics
3
-name: Azure Log Analytics Workspace
2
+display_name: Azure Log Analytics Workspace
3
resource_type: Microsoft.OperationalInsights/workspaces
4
metrics:
5
- id: availability_rate_query
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/logic_apps.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: logic_apps
3
-name: Azure Logic Apps Workflow
2
+display_name: Azure Logic Apps Workflow
3
resource_type: Microsoft.Logic/workflows
4
metrics:
5
- id: runs_started
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/machine_learning.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: machine_learning
3
-name: Azure Machine Learning Workspace
2
+display_name: Azure Machine Learning Workspace
3
resource_type: Microsoft.MachineLearningServices/workspaces
4
metrics:
5
- id: agents
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/mysql_flexible.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: mysql_flexible
3
-name: Azure MySQL Flexible Server
2
+display_name: Azure MySQL Flexible Server
3
resource_type: Microsoft.DBforMySQL/flexibleServers
4
metrics:
5
- id: ha_io_status
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/nat_gateway.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: nat_gateway
3
-name: Azure NAT Gateway
2
+display_name: Azure NAT Gateway
3
resource_type: Microsoft.Network/natGateways
4
metrics:
5
- id: datapath_availability
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/postgres_flexible.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: postgres_flexible
3
-name: Azure PostgreSQL Flexible Server
2
+display_name: Azure PostgreSQL Flexible Server
3
resource_type: Microsoft.DBforPostgreSQL/flexibleServers
4
metrics:
5
- id: cpu_percent
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/redis_cache.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: redis_cache
3
-name: Azure Cache for Redis
2
+display_name: Azure Cache for Redis
3
resource_type: Microsoft.Cache/redis
4
metrics:
5
- id: cachehits
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/service_bus.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: service_bus
3
-name: Azure Service Bus Namespace
2
+display_name: Azure Service Bus Namespace
3
resource_type: Microsoft.ServiceBus/Namespaces
4
metrics:
5
- id: incoming_messages
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/sql_database.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: sql_database
3
-name: Azure SQL Database
2
+display_name: Azure SQL Database
3
resource_type: Microsoft.Sql/servers/databases
4
metrics:
5
- id: cpu_percent
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/sql_elastic_pool.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: sql_elastic_pool
3
-name: Azure SQL Elastic Pool
2
+display_name: Azure SQL Elastic Pool
3
resource_type: Microsoft.Sql/servers/elasticPools
4
metrics:
5
- id: cpu_percent
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/sql_managed_instance.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: sql_managed_instance
3
-name: Azure SQL Managed Instance
2
+display_name: Azure SQL Managed Instance
3
resource_type: Microsoft.Sql/managedInstances
4
metrics:
5
- id: avg_cpu_percent
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/storage_accounts.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: storage_accounts
3
-name: Azure Storage Account
2
+display_name: Azure Storage Account
3
resource_type: Microsoft.Storage/storageAccounts
4
metrics:
5
- id: availability
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/stream_analytics.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: stream_analytics
3
-name: Azure Stream Analytics Job
2
+display_name: Azure Stream Analytics Job
3
resource_type: Microsoft.StreamAnalytics/streamingjobs
4
metrics:
5
- id: input_events
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/synapse.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: synapse
3
-name: Azure Synapse Analytics Workspace
2
+display_name: Azure Synapse Analytics Workspace
3
resource_type: Microsoft.Synapse/workspaces
4
metrics:
5
- id: builtin_sql_pool_data_processed_bytes
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/virtual_machines.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: virtual_machines
3
-name: Azure Virtual Machine
2
+display_name: Azure Virtual Machine
3
resource_type: Microsoft.Compute/virtualMachines
4
metrics:
5
- id: percentage_cpu
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/vmss.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: vmss
3
-name: Azure Virtual Machine Scale Set
2
+display_name: Azure Virtual Machine Scale Set
3
resource_type: Microsoft.Compute/virtualMachineScaleSets
4
metrics:
5
- id: percentage_cpu
src/go/plugin/go.d/config/go.d/azure_monitor.profiles/default/vpn_gateway.yaml
+1
-2
@@ -1,6 +1,5 @@
1
---
2
-id: vpn_gateway
3
-name: Azure VPN Gateway
2
+display_name: Azure VPN Gateway
3
resource_type: Microsoft.Network/virtualNetworkGateways
4
metrics:
5
- id: average_bandwidth