Skip to content

Commit

Permalink
add budget metrics by scope (subscriptions/billing account) and budge…
Browse files Browse the repository at this point in the history
…t forecast metric (#102)

* add budget metrics by scope

* remove unused field in budget prometheus struc

* Fix lint error prometheus field unused

* re-add MetricsCollectorAzureRmCosts collector

* Fix retry with header

---------

Co-authored-by: PaulPowershell <116181531+PaulPowershell@users.noreply.github.com>
  • Loading branch information
kevindelmont and PaulPowershell committed Sep 14, 2024
1 parent 94bcf94 commit f3e517e
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 157 deletions.
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type (
Iam CollectorBase `yaml:"iam"`
Graph CollectorGraph `yaml:"graph"`
Costs CollectorCosts `yaml:"costs"`
Budgets CollectorBudgets `yaml:"budgets"`
Reservation CollectorReservation `yaml:"reservation"`
Portscan CollectorPortscan `yaml:"portscan"`
} `yaml:"collectors"`
Expand Down
9 changes: 9 additions & 0 deletions config/config_budget.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package config

type (
CollectorBudgets struct {
CollectorBase `yaml:",inline"`

Scopes []string `yaml:"scopes"`
}
)
2 changes: 2 additions & 0 deletions default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ collectors:

costs: {}

budgets: {}

reservation: {}

portscan:
Expand Down
19 changes: 18 additions & 1 deletion example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ collectors:
application: ""
servicePrincipal: ""

# Azure cost metrics (cost queries, budgets)
# Azure cost metrics (cost queries)
# needs queries below
costs:
scrapeTime: 60m
Expand Down Expand Up @@ -104,6 +104,23 @@ collectors:
# optional, additional static labels
labels: {}

# Azure budget metrics
budgets:
scrapeTime: 1h

# optional, see https://learn.microsoft.com/en-us/rest/api/cost-management/query/usage?tabs=HTTP
# will disable fetching by subscription and will enable fetching by scope
#scopes: [...]
# '/subscriptions/{subscriptionId}/' for subscription scope
# '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}' for resourceGroup scope
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}' for Billing Account scope
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/departments/{departmentId}' for Department scope
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/enrollmentAccounts/{enrollmentAccountId}' for EnrollmentAccount scope
# '/providers/Microsoft.Management/managementGroups/{managementGroupId} for Management Group scope
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/billingProfiles/{billingProfileId}' for billingProfile scope
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/billingProfiles/{billingProfileId}/invoiceSections/{invoiceSectionId}' for invoiceSection scope
# '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/customers/{customerId}' specific for partners

reservation:
scrapeTime: 1h

Expand Down
15 changes: 15 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,21 @@ func initMetricCollector() {
logger.With(zap.String("collector", collectorName)).Infof("collector disabled")
}

collectorName = "budgets"
if Config.Collectors.Budgets.IsEnabled() {
c := collector.New(collectorName, &MetricsCollectorAzureRmBudgets{}, logger)
c.SetScapeTime(*Config.Collectors.Budgets.ScrapeTime)
c.SetCache(
Opts.GetCachePath(collectorName+".json"),
collector.BuildCacheTag(cacheTag, Config.Azure, Config.Collectors.Budgets),
)
if err := c.Start(); err != nil {
logger.Fatal(err.Error())
}
} else {
logger.With(zap.String("collector", collectorName)).Infof("collector disabled")
}

collectorName = "defender"
if Config.Collectors.Defender.IsEnabled() {
c := collector.New(collectorName, &MetricsCollectorAzureRmDefender{}, logger)
Expand Down
219 changes: 219 additions & 0 deletions metrics_azurerm_budgets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package main

import (
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
"github.com/prometheus/client_golang/prometheus"
"github.com/webdevops/go-common/azuresdk/armclient"
"github.com/webdevops/go-common/prometheus/collector"
"github.com/webdevops/go-common/utils/to"
"go.uber.org/zap"
)

// Define MetricsCollectorAzureRmBudgets struct
type MetricsCollectorAzureRmBudgets struct {
collector.Processor

prometheus struct {
consumptionBudgetInfo *prometheus.GaugeVec
consumptionBudgetLimit *prometheus.GaugeVec
consumptionBudgetCurrent *prometheus.GaugeVec
consumptionBudgetForecast *prometheus.GaugeVec
consumptionBudgetUsage *prometheus.GaugeVec
}
}

// Setup method to initialize Prometheus metrics
func (m *MetricsCollectorAzureRmBudgets) Setup(collector *collector.Collector) {
m.Processor.Setup(collector)

// ----------------------------------------------------
// Budget
m.prometheus.consumptionBudgetInfo = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "azurerm_budgets_info",
Help: "Azure ResourceManager consumption budget info",
},
[]string{
"scope",
"resourceID",
"subscriptionID",
"budgetName",
"resourceGroup",
"category",
"timeGrain",
},
)
m.Collector.RegisterMetricList("consumptionBudgetInfo", m.prometheus.consumptionBudgetInfo, true)

m.prometheus.consumptionBudgetLimit = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "azurerm_budgets_limit",
Help: "Azure ResourceManager consumption budget limit",
},
[]string{
"scope",
"resourceID",
"subscriptionID",
"resourceGroup",
"budgetName",
},
)
m.Collector.RegisterMetricList("consumptionBudgetLimit", m.prometheus.consumptionBudgetLimit, true)

m.prometheus.consumptionBudgetUsage = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "azurerm_budgets_usage",
Help: "Azure ResourceManager consumption budget usage percentage",
},
[]string{
"scope",
"resourceID",
"subscriptionID",
"resourceGroup",
"budgetName",
},
)
m.Collector.RegisterMetricList("consumptionBudgetUsage", m.prometheus.consumptionBudgetUsage, true)

m.prometheus.consumptionBudgetCurrent = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "azurerm_budgets_current",
Help: "Azure ResourceManager consumption budget current",
},
[]string{
"scope",
"resourceID",
"subscriptionID",
"resourceGroup",
"budgetName",
"unit",
},
)
m.Collector.RegisterMetricList("consumptionBudgetCurrent", m.prometheus.consumptionBudgetCurrent, true)

m.prometheus.consumptionBudgetForecast = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "azurerm_budgets_forecast",
Help: "Azure ResourceManager consumption budget forecast",
},
[]string{
"scope",
"resourceID",
"subscriptionID",
"resourceGroup",
"budgetName",
"unit",
},
)
m.Collector.RegisterMetricList("consumptionBudgetForecast", m.prometheus.consumptionBudgetForecast, true)
}

func (m *MetricsCollectorAzureRmBudgets) Reset() {}

func (m *MetricsCollectorAzureRmBudgets) Collect(callback chan<- func()) {
if Config.Collectors.Budgets.Scopes != nil && len(Config.Collectors.Budgets.Scopes) > 0 {

Check failure on line 115 in metrics_azurerm_budgets.go

View workflow job for this annotation

GitHub Actions / release / lint

S1009: should omit nil check; len() for []string is defined as zero (gosimple)

Check failure on line 115 in metrics_azurerm_budgets.go

View workflow job for this annotation

GitHub Actions / release / lint

S1009: should omit nil check; len() for []string is defined as zero (gosimple)

Check failure on line 115 in metrics_azurerm_budgets.go

View workflow job for this annotation

GitHub Actions / schedule / lint

S1009: should omit nil check; len() for []string is defined as zero (gosimple)

Check failure on line 115 in metrics_azurerm_budgets.go

View workflow job for this annotation

GitHub Actions / schedule / lint

S1009: should omit nil check; len() for []string is defined as zero (gosimple)

Check failure on line 115 in metrics_azurerm_budgets.go

View workflow job for this annotation

GitHub Actions / schedule / lint

S1009: should omit nil check; len() for []string is defined as zero (gosimple)
for _, scope := range Config.Collectors.Budgets.Scopes {
// Run the budget query for the current scope
m.collectBudgetMetrics(logger, scope, callback)
}
} else {
// using subscription iterator
iterator := AzureSubscriptionsIterator

err := iterator.ForEach(m.Logger(), func(subscription *armsubscriptions.Subscription, logger *zap.SugaredLogger) {
m.collectBudgetMetrics(
logger,
*subscription.ID,
callback,
)
})
if err != nil {
m.Logger().Panic(err)
}
}
}

func (m *MetricsCollectorAzureRmBudgets) collectBudgetMetrics(logger *zap.SugaredLogger, scope string, callback chan<- func()) {
clientFactory, err := armconsumption.NewClientFactory("<subscription-id>", AzureClient.GetCred(), AzureClient.NewArmClientOptions())
if err != nil {
logger.Panic(err)
}

infoMetric := m.Collector.GetMetricList("consumptionBudgetInfo")
usageMetric := m.Collector.GetMetricList("consumptionBudgetUsage")
limitMetric := m.Collector.GetMetricList("consumptionBudgetLimit")
currentMetric := m.Collector.GetMetricList("consumptionBudgetCurrent")
forecastMetric := m.Collector.GetMetricList("consumptionBudgetForecast")

pager := clientFactory.NewBudgetsClient().NewListPager(scope, nil)

for pager.More() {
result, err := pager.NextPage(m.Context())
if err != nil {
logger.Panic(err)
}

if result.Value == nil {
continue
}

for _, budget := range result.Value {
resourceId := to.String(budget.ID)

azureResource, _ := armclient.ParseResourceId(resourceId)

infoMetric.AddInfo(prometheus.Labels{
"scope": scope,
"resourceID": stringToStringLower(resourceId),
"subscriptionID": azureResource.Subscription,
"resourceGroup": azureResource.ResourceGroup,
"budgetName": to.String(budget.Name),
"category": stringToStringLower(string(*budget.Properties.Category)),
"timeGrain": string(*budget.Properties.TimeGrain),
})

if budget.Properties.Amount != nil {
limitMetric.Add(prometheus.Labels{
"scope": scope,
"resourceID": stringToStringLower(resourceId),
"subscriptionID": azureResource.Subscription,
"resourceGroup": azureResource.ResourceGroup,
"budgetName": to.String(budget.Name),
}, *budget.Properties.Amount)
}

if budget.Properties.CurrentSpend != nil {
currentMetric.Add(prometheus.Labels{
"scope": scope,
"resourceID": stringToStringLower(resourceId),
"subscriptionID": azureResource.Subscription,
"resourceGroup": azureResource.ResourceGroup,
"budgetName": to.String(budget.Name),
"unit": to.StringLower(budget.Properties.CurrentSpend.Unit),
}, *budget.Properties.CurrentSpend.Amount)
}

if budget.Properties.ForecastSpend != nil {
forecastMetric.Add(prometheus.Labels{
"scope": scope,
"resourceID": stringToStringLower(resourceId),
"subscriptionID": azureResource.Subscription,
"resourceGroup": azureResource.ResourceGroup,
"budgetName": to.String(budget.Name),
"unit": to.StringLower(budget.Properties.ForecastSpend.Unit),
}, *budget.Properties.ForecastSpend.Amount)
}

if budget.Properties.Amount != nil && budget.Properties.CurrentSpend != nil {
usageMetric.Add(prometheus.Labels{
"scope": scope,
"resourceID": stringToStringLower(resourceId),
"subscriptionID": azureResource.Subscription,
"resourceGroup": azureResource.ResourceGroup,
"budgetName": to.String(budget.Name),
}, *budget.Properties.CurrentSpend.Amount / *budget.Properties.Amount)
}
}
}
}
Loading

0 comments on commit f3e517e

Please sign in to comment.