master
go 329 lines 10.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package azure_monitor
4
5 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"
13 "github.com/Azure/azure-sdk-for-go/sdk/monitor/query/azmetrics"
14 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph"
15 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/azure_monitor/azureprofiles"
16 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth"
17 )
18
19 type initResult struct {
20 config Config
21 profileCatalog azureprofiles.Catalog
22 resourceGraph resourceGraphClient
23 queryExecutor *queryExecutor
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")
36 }
37
38 var labelKeys = []string{"resource_uid", "subscription_id", "resource_name", "resource_group", "region", "resource_type", "profile"}
39
40 vec := c.store.Write().SnapshotMeter("").Vec(labelKeys...)
41 if runtime.Instruments == nil {
42 runtime.Instruments = make(map[string]*instrumentRuntime)
43 }
44
45 for _, p := range runtime.Profiles {
46 for _, m := range p.Metrics {
47 for _, series := range m.Series {
48 name := series.Instrument
49 if _, ok := runtime.Instruments[name]; ok {
50 continue
51 }
52 inst := &instrumentRuntime{Kind: series.Kind}
53 if series.Kind == azureprofiles.SeriesKindCounter {
54 inst.Counter = vec.Counter(name)
55 } else {
56 inst.Gauge = vec.Gauge(name)
57 }
58 runtime.Instruments[name] = inst
59 }
60 }
61 }
62
63 return nil
64 }
65
66 func (c *Collector) prepareInitResult() (*initResult, error) {
67 cfg, catalog, err := c.prepareInitConfig()
68 if err != nil {
69 return nil, err
70 }
71
72 resourceGraph, queryExecutor, err := c.prepareInitClients(cfg)
73 if err != nil {
74 return nil, err
75 }
76
77 supportedResourceTypes := catalogResourceTypeSet(catalog)
78 return &initResult{
79 config: cfg,
80 profileCatalog: catalog,
81 resourceGraph: resourceGraph,
82 queryExecutor: queryExecutor,
83 supportedResourceTypes: supportedResourceTypes,
84 }, nil
85 }
86
87 func (c *Collector) ensureBootstrapped(ctx context.Context) error {
88 if c.runtime != nil {
89 return nil
90 }
91 if c.resourceGraph == nil || c.queryExecutor == nil {
92 return errors.New("collector is not initialized")
93 }
94 if len(c.supportedResourceTypes) == 0 && len(c.profileCatalog.ResourceTypes()) == 0 {
95 return errors.New("collector profile catalog is not initialized")
96 }
97
98 fetched, err := fetchInitDiscovery(ctx, c.Config, c.profileCatalog, c.resourceGraph, c.supportedResourceTypes)
99 if err != nil {
100 return err
101 }
102 if len(fetched.UnsupportedTypes) > 0 {
103 c.Warningf("ignoring unsupported discovered resource types: %v", fetched.UnsupportedTypes)
104 }
105
106 selection, err := resolveInitProfiles(c.Config, c.profileCatalog, fetched.ByType)
107 if err != nil {
108 return err
109 }
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.
115 // Resolve selected profiles, fetch one representative resource per
116 // (subscription_id, resource_type, metric_namespace), fail open per key on
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.
120 runtime, err := buildCollectorRuntimeFromConfig(selection.Names, selection.Entries, c.profileCatalog, c.Config.workloadResourceTagKey())
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 }
127 now := c.now()
128
129 c.runtime = runtime
130 c.observations = newObservationState(runtime.Instruments)
131 c.discovery = buildDiscoveryState(fetched.Resources, runtime, now, c.Discovery.RefreshEvery, 1, fetched)
132 c.warnDiscoveryScopeFallbacks(c.discovery, runtime)
133
134 return nil
135 }
136
137 func (c *Collector) prepareInitConfig() (Config, azureprofiles.Catalog, error) {
138 cfg := c.Config
139 cfg.applyDefaults()
140
141 catalog, err := c.loadProfileCatalog()
142 if err != nil {
143 return Config{}, azureprofiles.Catalog{}, fmt.Errorf("load profiles catalog: %w", err)
144 }
145
146 if err := cfg.validate(); err != nil {
147 return Config{}, azureprofiles.Catalog{}, fmt.Errorf("config validation: %w", err)
148 }
149 cfg, ignoredTagPaths := sanitizeIgnoredProfileTagFilters(cfg)
150 if len(ignoredTagPaths) > 0 {
151 c.Warningf(
152 "ignoring profile tag filters in discovery.mode %q; encode per-profile tag filtering in discovery.mode_query.kql: %s",
153 discoveryModeQuery,
154 strings.Join(ignoredTagPaths, ", "),
155 )
156 }
157
158 return cfg, catalog, nil
159 }
160
161 func (c *Collector) prepareInitClients(cfg Config) (resourceGraphClient, *queryExecutor, error) {
162 cloudCfg, err := cloudConfigFromName(cfg.Cloud)
163 if err != nil {
164 return nil, nil, err
165 }
166
167 credential, err := createCredential(cfg.Auth, cloudCfg)
168 if err != nil {
169 return nil, nil, fmt.Errorf("create azure credential: %w", err)
170 }
171
172 subscriptionID := cfg.primarySubscriptionID()
173 resourceGraph, err := c.newResourceGraph(subscriptionID, credential, cloudCfg)
174 if err != nil {
175 return nil, nil, fmt.Errorf("create resource graph client: %w", err)
176 }
177
178 return resourceGraph, newQueryExecutor(cfg.Limits.MaxConcurrency, cfg.Timeout.Duration(), credential, cloudCfg, c.newMetricsClient), nil
179 }
180
181 func fetchInitDiscovery(ctx context.Context, cfg Config, catalog azureprofiles.Catalog, resourceGraph resourceGraphClient, supportedResourceTypes map[string]struct{}) (discoveryFetchResult, error) {
182 if stringsLowerTrim(cfg.Discovery.Mode) == discoveryModeQuery {
183 fetched, err := discoverResourcesFromQuery(
184 ctx,
185 cfg.subscriptionIDs(),
186 cfg.Timeout.Duration(),
187 resourceGraph,
188 cfg.Discovery.ModeQuery.KQL,
189 supportedResourceTypes,
190 )
191 if err != nil {
192 return discoveryFetchResult{}, fmt.Errorf("discover candidate resources: %w", err)
193 }
194 return fetched, nil
195 }
196
197 discoveryTypes, err := initDiscoveryResourceTypes(cfg, catalog)
198 if err != nil {
199 return discoveryFetchResult{}, fmt.Errorf("prepare discovery scope: %w", err)
200 }
201
202 resources, byType, err := discoverResources(
203 ctx,
204 cfg.subscriptionIDs(),
205 cfg.Timeout.Duration(),
206 resourceGraph,
207 discoveryTypes,
208 cfg.Discovery.ModeFilters,
209 )
210 if err != nil {
211 return discoveryFetchResult{}, fmt.Errorf("discover candidate resources: %w", err)
212 }
213
214 return discoveryFetchResult{Resources: resources, ByType: byType}, nil
215 }
216
217 func initDiscoveryResourceTypes(cfg Config, catalog azureprofiles.Catalog) ([]string, error) {
218 switch stringsLowerTrim(cfg.Profiles.Mode) {
219 case profilesModeAuto, profilesModeCombined:
220 return catalog.ResourceTypes(), nil
221 case profilesModeExact:
222 return catalog.ResourceTypesForProfileBaseNames(entryNames(modeEntries(cfg.Profiles.ModeExact)))
223 default:
224 return nil, fmt.Errorf("unsupported profiles.mode %q", cfg.Profiles.Mode)
225 }
226 }
227
228 func resolveInitProfiles(cfg Config, catalog azureprofiles.Catalog, byType map[string][]resourceInfo) (initProfileSelection, error) {
229 discoveredTypes := make(map[string]struct{}, len(byType))
230 for key := range byType {
231 discoveredTypes[key] = struct{}{}
232 }
233
234 autoProfiles := catalog.ProfilesForResourceTypes(discoveredTypes)
235 switch stringsLowerTrim(cfg.Profiles.Mode) {
236 case profilesModeAuto:
237 if len(autoProfiles) == 0 {
238 return initProfileSelection{}, errors.New("auto-discovery found no Azure resources matching any known profile")
239 }
240 return initProfileSelection{
241 Names: autoProfiles,
242 AutoNames: autoProfiles,
243 Entries: filterEntryMap(entryMap(modeEntries(cfg.Profiles.ModeAuto)), autoProfiles),
244 }, nil
245 case profilesModeExact:
246 explicitNames := entryNames(modeEntries(cfg.Profiles.ModeExact))
247 return initProfileSelection{
248 Names: explicitNames,
249 Entries: entryMap(modeEntries(cfg.Profiles.ModeExact)),
250 }, nil
251 case profilesModeCombined:
252 explicitNames := entryNames(modeEntries(cfg.Profiles.ModeCombined))
253 return initProfileSelection{
254 Names: mergeProfileNames(explicitNames, autoProfiles),
255 AutoNames: autoProfiles,
256 Entries: entryMap(modeEntries(cfg.Profiles.ModeCombined)),
257 }, nil
258 default:
259 return initProfileSelection{}, fmt.Errorf("unsupported profiles.mode %q", cfg.Profiles.Mode)
260 }
261 }
262
263 func createCredential(auth cloudauth.AzureADAuthConfig, cloudCfg azcloud.Configuration) (azcore.TokenCredential, error) {
264 if err := auth.ValidateWithPath("auth"); err != nil {
265 return nil, err
266 }
267
268 return auth.NewCredentialWithOptions(&cloudauth.AzureADCredentialOptions{
269 ClientOptions: azcore.ClientOptions{Cloud: cloudCfg},
270 })
271 }
272
273 func mergeProfileNames(explicit, discovered []string) []string {
274 seen := make(map[string]struct{}, len(explicit)+len(discovered))
275 merged := make([]string, 0, len(explicit)+len(discovered))
276 for _, name := range explicit {
277 key := stringsLowerTrim(name)
278 if _, ok := seen[key]; ok {
279 continue
280 }
281 seen[key] = struct{}{}
282 merged = append(merged, name)
283 }
284 for _, name := range discovered {
285 key := stringsLowerTrim(name)
286 if _, ok := seen[key]; ok {
287 continue
288 }
289 seen[key] = struct{}{}
290 merged = append(merged, name)
291 }
292 return merged
293 }
294
295 func filterEntryMap(entries map[string]ProfileEntryConfig, active []string) map[string]ProfileEntryConfig {
296 if len(entries) == 0 || len(active) == 0 {
297 return nil
298 }
299
300 filtered := make(map[string]ProfileEntryConfig)
301 for _, name := range active {
302 entry, ok := entries[name]
303 if !ok {
304 continue
305 }
306 filtered[name] = entry
307 }
308 if len(filtered) == 0 {
309 return nil
310 }
311 return filtered
312 }
313
314 func defaultNewResourceGraphClient(subscriptionID string, cred azcore.TokenCredential, cloudCfg azcloud.Configuration) (resourceGraphClient, error) {
315 _ = subscriptionID
316 client, err := armresourcegraph.NewClient(cred, armClientOptions{Cloud: cloudCfg}.toARM())
317 if err != nil {
318 return nil, err
319 }
320 return client, nil
321 }
322
323 func defaultNewMetricsClient(endpoint string, cred azcore.TokenCredential, cloudCfg azcloud.Configuration) (metricsQueryClient, error) {
324 client, err := azmetrics.NewClient(endpoint, cred, &azmetrics.ClientOptions{ClientOptions: azcore.ClientOptions{Cloud: cloudCfg}})
325 if err != nil {
326 return nil, err
327 }
328 return client, nil
329 }