Skip to content

Commit

Permalink
feat: add support for billing currency metric along with USD (#7)
Browse files Browse the repository at this point in the history
* Added support for billing currency and USD daily cost metrics

* Added billing currency metric to sample metrics

* Timestamp output when performing fetch

* Correctly reference currency property in grouped Azure response
Hardcode USD currency label
Updated README sample output
  • Loading branch information
cvaldemar authored May 6, 2024
1 parent f664d4b commit cab1ece
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 16 deletions.
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ On Azure we usually use the Cost Management portal to analyze costs, which is a
## Sample Output

```
# HELP azure_daily_cost Daily cost of an Azure account in billing currency
# TYPE azure_daily_cost gauge
azure_daily_cost{ChargeType="ActualCost",Currency="<the billing currency such as EUR>"EnvironmentName="prod",ProjectName="myproject",ServiceName="Virtual Network",Subscription="<subscription_id_1>",TenantId="<tenant_id_1>"} 0.439500230497108
azure_daily_cost{ChargeType="ActualCost",Currency="<the billing currency such as EUR>"EnvironmentName="prod",ProjectName="myproject",ServiceName="VPN Gateway",Subscription="<subscription_id_1>",TenantId="<tenant_id_1>"} 4.191000368311904
azure_daily_cost{ChargeType="ActualCost",Currency="<the billing currency such as EUR>"EnvironmentName="dev",ProjectName="myproject",ServiceName="Azure Container Apps",Subscription="<subscription_id_2>",TenantId="<tenant_id_2>"} 0.371846875438938
azure_daily_cost{ChargeType="ActualCost",Currency="<the billing currency such as EUR>"EnvironmentName="dev",ProjectName="myproject",ServiceName="Service Bus",Subscription="<subscription_id_2>",TenantId="<tenant_id_2>"} 13.99512952148999
# HELP azure_daily_cost_usd Daily cost of an Azure account in USD
# TYPE azure_daily_cost_usd gauge
azure_daily_cost_usd{ChargeType="ActualCost",EnvironmentName="prod",ProjectName="myproject",ServiceName="Virtual Network",Subscription="<subscription_id_1>",TenantId="<tenant_id_1>"} 0.439500230497108
azure_daily_cost_usd{ChargeType="ActualCost",EnvironmentName="prod",ProjectName="myproject",ServiceName="VPN Gateway",Subscription="<subscription_id_1>",TenantId="<tenant_id_1>"} 4.191000368311904
azure_daily_cost_usd{ChargeType="ActualCost",EnvironmentName="dev",ProjectName="myproject",ServiceName="Azure Container Apps",Subscription="<subscription_id_2>",TenantId="<tenant_id_2>"} 0.371846875438938
azure_daily_cost_usd{ChargeType="ActualCost",EnvironmentName="dev",ProjectName="myproject",ServiceName="Service Bus",Subscription="<subscription_id_2>",TenantId="<tenant_id_2>"} 13.99512952148999
azure_daily_cost_usd{ChargeType="ActualCost",Currency="USD",EnvironmentName="prod",ProjectName="myproject",ServiceName="Virtual Network",Subscription="<subscription_id_1>",TenantId="<tenant_id_1>"} 0.439500230497108
azure_daily_cost_usd{ChargeType="ActualCost",Currency="USD",EnvironmentName="prod",ProjectName="myproject",ServiceName="VPN Gateway",Subscription="<subscription_id_1>",TenantId="<tenant_id_1>"} 4.191000368311904
azure_daily_cost_usd{ChargeType="ActualCost",Currency="USD",EnvironmentName="dev",ProjectName="myproject",ServiceName="Azure Container Apps",Subscription="<subscription_id_2>",TenantId="<tenant_id_2>"} 0.371846875438938
azure_daily_cost_usd{ChargeType="ActualCost",Currency="USD",EnvironmentName="dev",ProjectName="myproject",ServiceName="Service Bus",Subscription="<subscription_id_2>",TenantId="<tenant_id_2>"} 13.99512952148999
...
```

*ps: As the metric name indicate, the metric shows the daily costs in USD. `Daily` is based a fixed 24h time window, from UTC 00:00 to UTC 24:00. `EnvironmentName` and `ProjectName` are the custom labels that can be configured. `ServiceName` is a label based on `group_by` configuration.*
*ps: As the metric names indicate, the metrics show the daily costs in both billing currency and USD. `Daily` is based a fixed 24h time window, from UTC 00:00 to UTC 24:00. `EnvironmentName` and `ProjectName` are the custom labels that can be configured. `ServiceName` is a label based on `group_by` configuration.*

## How Does This Work

Expand Down
36 changes: 26 additions & 10 deletions app/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,28 @@


class MetricExporter:
def __init__(self, polling_interval_seconds, metric_name, group_by, targets, secrets):
def __init__(self, polling_interval_seconds, metric_name, metric_name_usd, group_by, targets, secrets):
self.polling_interval_seconds = polling_interval_seconds
self.metric_name = metric_name
self.metric_name_usd = metric_name_usd
self.group_by = group_by
self.targets = targets
self.secrets = secrets
# we have verified that there is at least one target
self.labels = set(targets[0].keys())
# for now we only support exporting one type of cost (ActualCost)
self.labels.add("ChargeType")
self.labels.add("Currency")
if group_by["enabled"]:
for group in group_by["groups"]:
self.labels.add(group["label_name"])
self.azure_daily_cost_usd = Gauge(self.metric_name, "Daily cost of an Azure account in USD", self.labels)
self.azure_daily_cost = Gauge(self.metric_name, "Daily cost of an Azure account in billing currency", self.labels)
self.azure_daily_cost_usd = Gauge(self.metric_name_usd, "Daily cost of an Azure account in USD", self.labels)

def run_metrics_loop(self):
while True:
# every time we clear up all the existing labels before setting new ones
self.azure_daily_cost.clear()
self.azure_daily_cost_usd.clear()

self.fetch()
Expand Down Expand Up @@ -61,7 +65,10 @@ def query_azure_cost_explorer(self, azure_client, subscription, group_by, start_
type="ActualCost",
dataset={
"granularity": "Daily",
"aggregation": {"totalCostUSD": {"name": "CostUSD", "function": "Sum"}},
"aggregation": {
"totalCost": {"name": "Cost", "function": "Sum"},
"totalCostUSD": {"name": "CostUSD", "function": "Sum"}
},
"grouping": groups,
},
timeframe="Custom",
Expand All @@ -73,35 +80,44 @@ def query_azure_cost_explorer(self, azure_client, subscription, group_by, start_
result = azure_client.query.usage(scope, query)
return result.as_dict()

def expose_metrics(self, azure_account, result):
def expose_metrics(self, azure_account, result):
cost = float(result[0])
costUsd = float(result[1])

if not self.group_by["enabled"]:
self.azure_daily_cost_usd.labels(**azure_account, ChargeType="ActualCost").set(cost)
self.azure_daily_cost.labels(**azure_account, ChargeType="ActualCost", Currency=result[3]).set(cost)
self.azure_daily_cost_usd.labels(**azure_account, ChargeType="ActualCost", Currency="USD").set(costUsd)
else:
merged_minor_cost = 0
merged_minor_cost_usd = 0
group_key_values = dict()
for i in range(len(self.group_by["groups"])):
value = result[i + 2]
value = result[i + 3]
group_key_values.update({self.group_by["groups"][i]["label_name"]: value})

if self.group_by["merge_minor_cost"]["enabled"] and cost < self.group_by["merge_minor_cost"]["threshold"]:
merged_minor_cost += cost
merged_minor_cost_usd += costUsd
else:
self.azure_daily_cost_usd.labels(**azure_account, **group_key_values, ChargeType="ActualCost").set(cost)
self.azure_daily_cost.labels(**azure_account, **group_key_values, ChargeType="ActualCost", Currency=result[len(self.group_by["groups"]) + 3]).set(cost)
self.azure_daily_cost_usd.labels(**azure_account, **group_key_values, ChargeType="ActualCost", Currency="USD").set(costUsd)

if merged_minor_cost > 0:
group_key_values = dict()
for i in range(len(self.group_by["groups"])):
group_key_values.update(
{self.group_by["groups"][i]["label_name"]: self.group_by["merge_minor_cost"]["tag_value"]}
)
self.azure_daily_cost_usd.labels(**azure_account, **group_key_values, ChargeType="ActualCost").set(
self.azure_daily_cost.labels(**azure_account, **group_key_values, ChargeType="ActualCost").set(
merged_minor_cost
)
self.azure_daily_cost_usd.labels(**azure_account, **group_key_values, ChargeType="ActualCost").set(
merged_minor_cost_usd
)

def fetch(self):
for azure_account in self.targets:
print("querying cost data for Azure tenant %s" % azure_account["TenantId"])
print("[%s] Querying cost data for Azure tenant %s" % (datetime.now(), azure_account["TenantId"]))
azure_client = self.init_azure_client(azure_account["TenantId"])

try:
Expand All @@ -115,7 +131,7 @@ def fetch(self):
continue

for result in cost_response["rows"]:
if result[1] != int(start_date.strftime("%Y%m%d")):
if result[2] != int(start_date.strftime("%Y%m%d")):
# it is possible that Azure returns cost data which is different than the specified date
# for example, the query time period is 2023-07-10 00:00:00+00:00 to 2023-07-11 00:00:00+00:00
# Azure still returns some records for date 2023-07-11
Expand Down
3 changes: 2 additions & 1 deletion exporter_config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
exporter_port: $EXPORTER_PORT|9090 # the port that exposes cost metrics
polling_interval_seconds: $POLLING_INTERVAL_SECONDS|28800 # by default it is 8 hours
metric_name: azure_daily_cost_usd # change the metric name if needed
metric_name: azure_daily_cost # change the metric name if needed
metric_name_usd: azure_daily_cost_usd # change the metric name if needed

group_by:
enabled: true
Expand Down
1 change: 1 addition & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def main(config, secrets):
app_metrics = MetricExporter(
polling_interval_seconds=config["polling_interval_seconds"],
metric_name=config["metric_name"],
metric_name_usd=config["metric_name_usd"],
group_by=config["group_by"],
targets=config["target_azure_accounts"],
secrets=secrets
Expand Down

0 comments on commit cab1ece

Please sign in to comment.