Skip to content

Commit b2e2c35

Browse files
committed
add budget metrics by scope
1 parent 32960d7 commit b2e2c35

File tree

7 files changed

+267
-147
lines changed

7 files changed

+267
-147
lines changed

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type (
1717
Iam CollectorBase `yaml:"iam"`
1818
Graph CollectorGraph `yaml:"graph"`
1919
Costs CollectorCosts `yaml:"costs"`
20+
Budgets CollectorBudgets `yaml:"budgets"`
2021
Reservation CollectorReservation `yaml:"reservation"`
2122
Portscan CollectorPortscan `yaml:"portscan"`
2223
} `yaml:"collectors"`

config/config_budget.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package config
2+
3+
type (
4+
CollectorBudgets struct {
5+
CollectorBase `yaml:",inline"`
6+
7+
Scopes []string `yaml:"scopes"`
8+
}
9+
)

default.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ collectors:
2323

2424
costs: {}
2525

26+
budgets: {}
27+
2628
reservation: {}
2729

2830
portscan:

example.yaml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ collectors:
5555
application: ""
5656
servicePrincipal: ""
5757

58-
# Azure cost metrics (cost queries, budgets)
58+
# Azure cost metrics (cost queries)
5959
# needs queries below
6060
costs:
6161
scrapeTime: 60m
@@ -104,6 +104,23 @@ collectors:
104104
# optional, additional static labels
105105
labels: {}
106106

107+
# Azure budget metrics
108+
budgets:
109+
scrapeTime: 1h
110+
111+
# optional, see https://learn.microsoft.com/en-us/rest/api/cost-management/query/usage?tabs=HTTP
112+
# will disable fetching by subscription and will enable fetching by scope
113+
#scopes: [...]
114+
# '/subscriptions/{subscriptionId}/' for subscription scope
115+
# '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}' for resourceGroup scope
116+
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}' for Billing Account scope
117+
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/departments/{departmentId}' for Department scope
118+
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/enrollmentAccounts/{enrollmentAccountId}' for EnrollmentAccount scope
119+
# '/providers/Microsoft.Management/managementGroups/{managementGroupId} for Management Group scope
120+
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/billingProfiles/{billingProfileId}' for billingProfile scope
121+
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/billingProfiles/{billingProfileId}/invoiceSections/{invoiceSectionId}' for invoiceSection scope
122+
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/customers/{customerId}' specific for partners
123+
107124
reservation:
108125
scrapeTime: 1h
109126

main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,21 @@ func initMetricCollector() {
258258
logger.With(zap.String("collector", collectorName)).Infof("collector disabled")
259259
}
260260

261+
collectorName = "budgets"
262+
if Config.Collectors.Budgets.IsEnabled() {
263+
c := collector.New(collectorName, &MetricsCollectorAzureRmBudgets{}, logger)
264+
c.SetScapeTime(*Config.Collectors.Budgets.ScrapeTime)
265+
c.SetCache(
266+
Opts.GetCachePath(collectorName+".json"),
267+
collector.BuildCacheTag(cacheTag, Config.Azure, Config.Collectors.Budgets),
268+
)
269+
if err := c.Start(); err != nil {
270+
logger.Fatal(err.Error())
271+
}
272+
} else {
273+
logger.With(zap.String("collector", collectorName)).Infof("collector disabled")
274+
}
275+
261276
collectorName = "defender"
262277
if Config.Collectors.Defender.IsEnabled() {
263278
c := collector.New(collectorName, &MetricsCollectorAzureRmDefender{}, logger)

metrics_azurerm_budgets.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package main
2+
3+
import (
4+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
5+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
6+
"github.com/prometheus/client_golang/prometheus"
7+
"github.com/webdevops/go-common/azuresdk/armclient"
8+
"github.com/webdevops/go-common/prometheus/collector"
9+
"github.com/webdevops/go-common/utils/to"
10+
"go.uber.org/zap"
11+
)
12+
13+
// Define MetricsCollectorAzureRmBudgets struct
14+
type MetricsCollectorAzureRmBudgets struct {
15+
collector.Processor
16+
17+
prometheus struct {
18+
consumptionBudgetInfo *prometheus.GaugeVec
19+
consumptionBudgetLimit *prometheus.GaugeVec
20+
consumptionBudgetCurrent *prometheus.GaugeVec
21+
consumptionBudgetForecast *prometheus.GaugeVec
22+
consumptionBudgetUsage *prometheus.GaugeVec
23+
24+
costmanagementOverallUsage *prometheus.GaugeVec
25+
costmanagementOverallActualCost *prometheus.GaugeVec
26+
}
27+
}
28+
29+
// Setup method to initialize Prometheus metrics
30+
func (m *MetricsCollectorAzureRmBudgets) Setup(collector *collector.Collector) {
31+
m.Processor.Setup(collector)
32+
33+
// ----------------------------------------------------
34+
// Budget
35+
m.prometheus.consumptionBudgetInfo = prometheus.NewGaugeVec(
36+
prometheus.GaugeOpts{
37+
Name: "azurerm_budgets_info",
38+
Help: "Azure ResourceManager consumption budget info",
39+
},
40+
[]string{
41+
"scope",
42+
"resourceID",
43+
"subscriptionID",
44+
"budgetName",
45+
"resourceGroup",
46+
"category",
47+
"timeGrain",
48+
},
49+
)
50+
m.Collector.RegisterMetricList("consumptionBudgetInfo", m.prometheus.consumptionBudgetInfo, true)
51+
52+
m.prometheus.consumptionBudgetLimit = prometheus.NewGaugeVec(
53+
prometheus.GaugeOpts{
54+
Name: "azurerm_budgets_limit",
55+
Help: "Azure ResourceManager consumption budget limit",
56+
},
57+
[]string{
58+
"scope",
59+
"resourceID",
60+
"subscriptionID",
61+
"resourceGroup",
62+
"budgetName",
63+
},
64+
)
65+
m.Collector.RegisterMetricList("consumptionBudgetLimit", m.prometheus.consumptionBudgetLimit, true)
66+
67+
m.prometheus.consumptionBudgetUsage = prometheus.NewGaugeVec(
68+
prometheus.GaugeOpts{
69+
Name: "azurerm_budgets_usage",
70+
Help: "Azure ResourceManager consumption budget usage percentage",
71+
},
72+
[]string{
73+
"scope",
74+
"resourceID",
75+
"subscriptionID",
76+
"resourceGroup",
77+
"budgetName",
78+
},
79+
)
80+
m.Collector.RegisterMetricList("consumptionBudgetUsage", m.prometheus.consumptionBudgetUsage, true)
81+
82+
m.prometheus.consumptionBudgetCurrent = prometheus.NewGaugeVec(
83+
prometheus.GaugeOpts{
84+
Name: "azurerm_budgets_current",
85+
Help: "Azure ResourceManager consumption budget current",
86+
},
87+
[]string{
88+
"scope",
89+
"resourceID",
90+
"subscriptionID",
91+
"resourceGroup",
92+
"budgetName",
93+
"unit",
94+
},
95+
)
96+
m.Collector.RegisterMetricList("consumptionBudgetCurrent", m.prometheus.consumptionBudgetCurrent, true)
97+
98+
m.prometheus.consumptionBudgetForecast = prometheus.NewGaugeVec(
99+
prometheus.GaugeOpts{
100+
Name: "azurerm_budgets_forecast",
101+
Help: "Azure ResourceManager consumption budget forecast",
102+
},
103+
[]string{
104+
"scope",
105+
"resourceID",
106+
"subscriptionID",
107+
"resourceGroup",
108+
"budgetName",
109+
"unit",
110+
},
111+
)
112+
m.Collector.RegisterMetricList("consumptionBudgetForecast", m.prometheus.consumptionBudgetForecast, true)
113+
}
114+
115+
func (m *MetricsCollectorAzureRmBudgets) Reset() {}
116+
117+
func (m *MetricsCollectorAzureRmBudgets) Collect(callback chan<- func()) {
118+
if Config.Collectors.Budgets.Scopes != nil && len(Config.Collectors.Budgets.Scopes) > 0 {
119+
for _, scope := range Config.Collectors.Budgets.Scopes {
120+
// Run the budget query for the current scope
121+
m.collectBudgetMetrics(logger, scope, callback)
122+
}
123+
} else {
124+
// using subscription iterator
125+
iterator := AzureSubscriptionsIterator
126+
127+
err := iterator.ForEach(m.Logger(), func(subscription *armsubscriptions.Subscription, logger *zap.SugaredLogger) {
128+
m.collectBudgetMetrics(
129+
logger,
130+
*subscription.ID,
131+
callback,
132+
)
133+
})
134+
if err != nil {
135+
m.Logger().Panic(err)
136+
}
137+
}
138+
}
139+
140+
func (m *MetricsCollectorAzureRmBudgets) collectBudgetMetrics(logger *zap.SugaredLogger, scope string, callback chan<- func()) {
141+
clientFactory, err := armconsumption.NewClientFactory("<subscription-id>", AzureClient.GetCred(), AzureClient.NewArmClientOptions())
142+
if err != nil {
143+
logger.Panic(err)
144+
}
145+
146+
infoMetric := m.Collector.GetMetricList("consumptionBudgetInfo")
147+
usageMetric := m.Collector.GetMetricList("consumptionBudgetUsage")
148+
limitMetric := m.Collector.GetMetricList("consumptionBudgetLimit")
149+
currentMetric := m.Collector.GetMetricList("consumptionBudgetCurrent")
150+
forecastMetric := m.Collector.GetMetricList("consumptionBudgetForecast")
151+
152+
pager := clientFactory.NewBudgetsClient().NewListPager(scope, nil)
153+
154+
for pager.More() {
155+
result, err := pager.NextPage(m.Context())
156+
if err != nil {
157+
logger.Panic(err)
158+
}
159+
160+
if result.Value == nil {
161+
continue
162+
}
163+
164+
for _, budget := range result.Value {
165+
resourceId := to.String(budget.ID)
166+
167+
azureResource, _ := armclient.ParseResourceId(resourceId)
168+
169+
infoMetric.AddInfo(prometheus.Labels{
170+
"scope": scope,
171+
"resourceID": stringToStringLower(resourceId),
172+
"subscriptionID": azureResource.Subscription,
173+
"resourceGroup": azureResource.ResourceGroup,
174+
"budgetName": to.String(budget.Name),
175+
"category": stringToStringLower(string(*budget.Properties.Category)),
176+
"timeGrain": string(*budget.Properties.TimeGrain),
177+
})
178+
179+
if budget.Properties.Amount != nil {
180+
limitMetric.Add(prometheus.Labels{
181+
"scope": scope,
182+
"resourceID": stringToStringLower(resourceId),
183+
"subscriptionID": azureResource.Subscription,
184+
"resourceGroup": azureResource.ResourceGroup,
185+
"budgetName": to.String(budget.Name),
186+
}, *budget.Properties.Amount)
187+
}
188+
189+
if budget.Properties.CurrentSpend != nil {
190+
currentMetric.Add(prometheus.Labels{
191+
"scope": scope,
192+
"resourceID": stringToStringLower(resourceId),
193+
"subscriptionID": azureResource.Subscription,
194+
"resourceGroup": azureResource.ResourceGroup,
195+
"budgetName": to.String(budget.Name),
196+
"unit": to.StringLower(budget.Properties.CurrentSpend.Unit),
197+
}, *budget.Properties.CurrentSpend.Amount)
198+
}
199+
200+
if budget.Properties.ForecastSpend != nil {
201+
forecastMetric.Add(prometheus.Labels{
202+
"scope": scope,
203+
"resourceID": stringToStringLower(resourceId),
204+
"subscriptionID": azureResource.Subscription,
205+
"resourceGroup": azureResource.ResourceGroup,
206+
"budgetName": to.String(budget.Name),
207+
"unit": to.StringLower(budget.Properties.ForecastSpend.Unit),
208+
}, *budget.Properties.ForecastSpend.Amount)
209+
}
210+
211+
if budget.Properties.Amount != nil && budget.Properties.CurrentSpend != nil {
212+
usageMetric.Add(prometheus.Labels{
213+
"scope": scope,
214+
"resourceID": stringToStringLower(resourceId),
215+
"subscriptionID": azureResource.Subscription,
216+
"resourceGroup": azureResource.ResourceGroup,
217+
"budgetName": to.String(budget.Name),
218+
}, *budget.Properties.CurrentSpend.Amount / *budget.Properties.Amount)
219+
}
220+
}
221+
}
222+
}

0 commit comments

Comments
 (0)