Skip to content

Commit 1df60b1

Browse files
committed
switch to resourceproviders usages api for quotas
the quota api seems to be stricly rate limited, maybe not a good choice for this api Signed-off-by: Markus Blaschke <mblaschke82@gmail.com>
1 parent a7f8e45 commit 1df60b1

5 files changed

Lines changed: 133 additions & 111 deletions

File tree

config/config_quota.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ type (
44
CollectorQuota struct {
55
*CollectorBase `yaml:",inline"`
66

7-
ResourceProviders []string `json:"resourceProviders"`
7+
ResourceProviders []CollectorQuotaResourceProvider `json:"resourceProviders"`
8+
}
9+
10+
CollectorQuotaResourceProvider struct {
11+
Provider string `json:"provider"`
12+
ApiVersion string `json:"apiVersion"`
813
}
914
)

example.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ collectors:
3535
scrapeTime: 5m
3636

3737
resourceProviders:
38-
- Microsoft.App
39-
- Microsoft.Compute
40-
- Microsoft.Network
41-
- Microsoft.Storage
42-
- Microsoft.MachineLearningServices
38+
- {provider: Microsoft.App, apiVersion: "2025-06-01"}
39+
- {provider: Microsoft.Compute, apiVersion: "2025-04-01"}
40+
- {provider: Microsoft.Network, apiVersion: "2025-05-01"}
41+
- {provider: Microsoft.Storage, apiVersion: "2025-06-01"}
42+
- {provider: Microsoft.MachineLearningServices, apiVersion: "2025-06-01"}
4343

4444
# Azure Advisor recommendations
4545
advisor:

metrics_azurerm_quota.go

Lines changed: 78 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package main
22

33
import (
4-
"context"
4+
"errors"
5+
"fmt"
6+
"io"
57
"log/slog"
68
"net/http"
79
"net/url"
@@ -10,11 +12,13 @@ import (
1012
armruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/runtime"
1113
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
1214
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
13-
armquota "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/quota/armquota/v2"
1415
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
1516
"github.com/prometheus/client_golang/prometheus"
1617
"github.com/webdevops/go-common/prometheus/collector"
1718
"github.com/webdevops/go-common/utils/to"
19+
20+
"github.com/webdevops/azure-resourcemanager-exporter/config"
21+
"github.com/webdevops/azure-resourcemanager-exporter/models/quota"
1822
)
1923

2024
type MetricsCollectorAzureRmQuota struct {
@@ -105,10 +109,15 @@ func (m *MetricsCollectorAzureRmQuota) Collect(callback chan<- func()) {
105109

106110
if registered, err := AzureClient.IsResourceProviderRegistered(m.Context(), *subscription.SubscriptionID, "Microsoft.Capacity"); registered {
107111
for _, provider := range Config.Collectors.Quota.ResourceProviders {
108-
if registered, err := AzureClient.IsResourceProviderRegistered(m.Context(), *subscription.SubscriptionID, provider); registered {
109-
m.collectQuotaUsage(subscription, provider, logger, callback)
112+
if registered, err := AzureClient.IsResourceProviderRegistered(m.Context(), *subscription.SubscriptionID, provider.Provider); registered {
113+
114+
for _, location := range Config.Azure.Locations {
115+
quotaLogger := logger.With(slog.String("provider", provider.Provider), slog.String("location", location))
116+
m.collectQuotaUsage(subscription, provider, location, quotaLogger, callback)
117+
}
118+
110119
} else if err != nil {
111-
logger.Error("quota for resourceProvider requested, but not registered", slog.String("resourceProvider", provider), slog.Any("error", err))
120+
logger.Error("quota for resourceProvider requested, but not registered", slog.String("resourceProvider", provider.Provider), slog.Any("error", err))
112121
}
113122
}
114123
} else if err != nil {
@@ -138,15 +147,14 @@ func (m *MetricsCollectorAzureRmQuota) collectAuthorizationUsage(subscription *a
138147
quotaLimitMetric := m.Collector.GetMetricList("quotaLimit")
139148
quotaUsageMetric := m.Collector.GetMetricList("quotaUsage")
140149

141-
ctx := context.Background()
142-
143150
urlPath := "/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/roleassignmentsusagemetrics"
144151
urlPath = strings.ReplaceAll(urlPath, "{subscriptionId}", url.PathEscape(*subscription.SubscriptionID))
145152

146-
req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(ep, urlPath))
153+
req, err := runtime.NewRequest(m.Context(), http.MethodGet, runtime.JoinPaths(ep, urlPath))
147154
if err != nil {
148155
panic(err)
149156
}
157+
defer req.Close()
150158
reqQP := req.Raw().URL.Query()
151159
reqQP.Set("api-version", "2019-08-01-preview")
152160
req.Raw().URL.RawQuery = reqQP.Encode()
@@ -159,11 +167,7 @@ func (m *MetricsCollectorAzureRmQuota) collectAuthorizationUsage(subscription *a
159167
defer resp.Body.Close()
160168

161169
if runtime.HasStatusCode(resp, http.StatusOK) {
162-
result := struct {
163-
RoleAssignmentsLimit float64 `json:"roleAssignmentsLimit"`
164-
RoleAssignmentsCurrentCount float64 `json:"roleAssignmentsCurrentCount"`
165-
RoleAssignmentsRemainingCount float64 `json:"roleAssignmentsRemainingCount"`
166-
}{}
170+
result := quota.RoleAssignmentUsage{}
167171

168172
if err := runtime.UnmarshalAsJSON(resp, &result); err == nil {
169173
currentValue := result.RoleAssignmentsCurrentCount
@@ -187,121 +191,90 @@ func (m *MetricsCollectorAzureRmQuota) collectAuthorizationUsage(subscription *a
187191
}
188192
}
189193

190-
type (
191-
Quota struct {
192-
Labels prometheus.Labels
193-
Limit *float64
194-
Current *float64
195-
}
196-
197-
QuotaList map[string]*Quota
198-
)
199-
200-
func (q *QuotaList) Get(name string) *Quota {
201-
name = strings.ToLower(name)
202-
if _, ok := (*q)[name]; !ok {
203-
(*q)[name] = &Quota{}
204-
}
205-
206-
return (*q)[name]
207-
}
208-
209194
// collectQuotaUsage collect generic quota usages
210-
func (m *MetricsCollectorAzureRmQuota) collectQuotaUsage(subscription *armsubscriptions.Subscription, provider string, logger *slog.Logger, callback chan<- func()) {
211-
quotaClient, err := armquota.NewClient(AzureClient.GetCred(), AzureClient.NewArmClientOptions())
212-
if err != nil {
213-
panic(err)
214-
}
215-
216-
usageClient, err := armquota.NewUsagesClient(AzureClient.GetCred(), AzureClient.NewArmClientOptions())
217-
if err != nil {
218-
panic(err)
219-
}
220-
195+
func (m *MetricsCollectorAzureRmQuota) collectQuotaUsage(subscription *armsubscriptions.Subscription, provider config.CollectorQuotaResourceProvider, location string, logger *slog.Logger, callback chan<- func()) {
221196
quotaMetric := m.Collector.GetMetricList("quota")
222197
quotaCurrentMetric := m.Collector.GetMetricList("quotaCurrent")
223198
quotaLimitMetric := m.Collector.GetMetricList("quotaLimit")
224199
quotaUsageMetric := m.Collector.GetMetricList("quotaUsage")
225200

226-
for _, location := range Config.Azure.Locations {
227-
scope := "/subscriptions/{subscriptionId}/providers/{provider}/locations/{location}"
228-
scope = strings.ReplaceAll(scope, "{subscriptionId}", url.PathEscape(*subscription.SubscriptionID))
229-
scope = strings.ReplaceAll(scope, "{provider}", url.PathEscape(provider))
230-
scope = strings.ReplaceAll(scope, "{location}", url.PathEscape(location))
231-
232-
quotaList := QuotaList{}
233-
234-
// -----------------------
235-
// Quotas
236-
quotaPager := quotaClient.NewListPager(scope, nil)
237-
for quotaPager.More() {
238-
result, err := quotaPager.NextPage(m.Context())
239-
if err != nil {
240-
panic(err)
241-
}
201+
options := AzureClient.NewArmClientOptions()
202+
ep := cloud.AzurePublic.Services[cloud.ResourceManager].Endpoint
203+
if c, ok := options.Cloud.Services[cloud.ResourceManager]; ok {
204+
ep = c.Endpoint
205+
}
242206

243-
if result.Value == nil {
244-
continue
245-
}
207+
pl, err := armruntime.NewPipeline("azurerm-quota", gitTag, AzureClient.GetCred(), runtime.PipelineOptions{}, options)
208+
if err != nil {
209+
logger.Error("failed to create arm client", slog.Any("error", err))
210+
panic(err)
211+
}
246212

247-
for _, row := range result.Value {
248-
switch v := row.Properties.Limit.(type) {
249-
case *armquota.LimitObject:
250-
if v.Value != nil {
251-
quotaName := to.String(row.Name)
252-
253-
labels := prometheus.Labels{
254-
"subscriptionID": to.StringLower(subscription.SubscriptionID),
255-
"location": strings.ToLower(location),
256-
"provider": provider,
257-
"quota": to.String(row.Properties.Name.Value),
258-
"quotaName": to.String(row.Properties.Name.LocalizedValue),
259-
}
260-
261-
quotaList.Get(quotaName).Limit = to.Ptr(float64(to.Number(v.Value)))
262-
quotaList.Get(quotaName).Labels = labels
263-
}
264-
}
265-
}
213+
urlPath := "/subscriptions/{subscriptionId}/providers/{provider}/locations/{location}/usages"
214+
urlPath = strings.ReplaceAll(urlPath, "{subscriptionId}", url.PathEscape(*subscription.SubscriptionID))
215+
urlPath = strings.ReplaceAll(urlPath, "{provider}", url.PathEscape(provider.Provider))
216+
urlPath = strings.ReplaceAll(urlPath, "{location}", url.PathEscape(location))
217+
218+
requestUrl := runtime.JoinPaths(ep, urlPath)
219+
fmt.Println(requestUrl)
220+
for {
221+
req, err := runtime.NewRequest(m.Context(), http.MethodGet, requestUrl)
222+
if err != nil {
223+
logger.Error("failed to create request", slog.Any("error", err))
224+
panic(err)
225+
}
226+
defer req.Close()
227+
reqQP := req.Raw().URL.Query()
228+
reqQP.Set("api-version", provider.ApiVersion)
229+
req.Raw().URL.RawQuery = reqQP.Encode()
230+
req.Raw().Header["Accept"] = []string{"application/json"}
231+
232+
resp, err := pl.Do(req)
233+
if err != nil {
234+
logger.Error("failed to send request", slog.Any("error", err))
235+
panic(err)
266236
}
237+
defer resp.Body.Close()
267238

268-
// -----------------------
269-
// Usages
270-
usagePager := usageClient.NewListPager(scope, nil)
271-
for usagePager.More() {
272-
result, err := usagePager.NextPage(m.Context())
239+
if !runtime.HasStatusCode(resp, http.StatusOK) {
240+
buf := new(strings.Builder)
241+
_, err := io.Copy(buf, resp.Body)
273242
if err != nil {
274243
panic(err)
275244
}
245+
logger.Error("request failed", slog.String("statusCode", resp.Status), slog.Any("error", errors.New(buf.String())))
246+
panic("request failed")
247+
}
276248

277-
if result.Value == nil {
278-
continue
279-
}
280-
281-
for _, row := range result.Value {
282-
quotaName := to.String(row.Name)
283-
value := float64(to.Number(row.Properties.Usages.Value))
249+
result := quota.ListUsageResult{}
250+
if err := runtime.UnmarshalAsJSON(resp, &result); err == nil {
251+
for _, quotaUsage := range result.Value {
284252

285253
labels := prometheus.Labels{
286254
"subscriptionID": to.StringLower(subscription.SubscriptionID),
287255
"location": strings.ToLower(location),
288-
"provider": provider,
289-
"quota": to.String(row.Properties.Name.Value),
290-
"quotaName": to.String(row.Properties.Name.LocalizedValue),
256+
"provider": provider.Provider,
257+
"quota": to.String(quotaUsage.Name.Value),
258+
"quotaName": to.String(quotaUsage.Name.LocalizedValue),
291259
}
292260

293-
quotaList.Get(quotaName).Current = to.Ptr(value)
294-
quotaList.Get(quotaName).Labels = labels
261+
quotaMetric.Add(labels, 1)
262+
quotaCurrentMetric.AddIfNotNil(labels, quotaUsage.CurrentValue)
263+
quotaLimitMetric.AddIfNotNil(labels, quotaUsage.Limit)
264+
if quotaUsage.CurrentValue != nil && quotaUsage.Limit != nil && *quotaUsage.Limit != 0 {
265+
quotaUsageMetric.Add(labels, *quotaUsage.CurrentValue / *quotaUsage.Limit)
266+
}
295267
}
268+
} else {
269+
logger.Error("failed to parse request", slog.Any("error", err))
270+
panic("parsing failed")
296271
}
297272

298-
for _, quota := range quotaList {
299-
quotaMetric.Add(quota.Labels, 1)
300-
quotaCurrentMetric.AddIfNotNil(quota.Labels, quota.Current)
301-
quotaLimitMetric.AddIfNotNil(quota.Labels, quota.Limit)
302-
if quota.Current != nil && quota.Limit != nil && *quota.Limit != 0 {
303-
quotaUsageMetric.Add(quota.Labels, *quota.Current / *quota.Limit)
304-
}
273+
if result.NextLink != nil && *result.NextLink != "" {
274+
requestUrl = *result.NextLink
275+
continue
305276
}
277+
278+
break
306279
}
307280
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package quota
2+
3+
type (
4+
RoleAssignmentUsage struct {
5+
RoleAssignmentsLimit float64 `json:"roleAssignmentsLimit"`
6+
RoleAssignmentsCurrentCount float64 `json:"roleAssignmentsCurrentCount"`
7+
RoleAssignmentsRemainingCount float64 `json:"roleAssignmentsRemainingCount"`
8+
}
9+
)

models/quota/usages.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package quota
2+
3+
type (
4+
ListUsageResult struct {
5+
// REQUIRED; The list of compute resource usages.
6+
Value []*Usage
7+
8+
// The URI to fetch the next page of compute resource usage information. Call ListNext() with this to fetch the next page
9+
// of compute resource usage information.
10+
NextLink *string
11+
}
12+
13+
Usage struct {
14+
// REQUIRED; The current usage of the resource.
15+
CurrentValue *float64
16+
17+
// REQUIRED; The maximum permitted usage of the resource.
18+
Limit *float64
19+
20+
// REQUIRED; The name of the type of usage.
21+
Name *UsageName
22+
23+
// REQUIRED; An enum describing the unit of usage measurement.
24+
Unit *string
25+
}
26+
27+
// UsageName - The Usage Names.
28+
UsageName struct {
29+
// The localized name of the resource.
30+
LocalizedValue *string
31+
32+
// The name of the resource.
33+
Value *string
34+
}
35+
)

0 commit comments

Comments
 (0)