Skip to content

Commit

Permalink
Merge pull request #837 from siddarthkay/estimate-aws-cloudwatch-cost
Browse files Browse the repository at this point in the history
feat(aws): estimate aws cloudwatch costs
  • Loading branch information
mlabouardy authored Jun 6, 2023
2 parents d750465 + 908f897 commit 4fb8faa
Showing 1 changed file with 78 additions and 1 deletion.
79 changes: 78 additions & 1 deletion providers/aws/cloudwatch/alarms.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package cloudwatch

import (
"context"
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/pricing"
"github.com/aws/aws-sdk-go-v2/service/pricing/types"
"strconv"
"time"

log "github.com/sirupsen/logrus"
Expand All @@ -13,9 +17,21 @@ import (
. "github.com/tailwarden/komiser/providers"
)

const (
RateCode = "JRTCKXETXF.6YS6EN2CT7"
AverageHoursPerMonth = 730
)

func Alarms(ctx context.Context, client ProviderClient) ([]Resource, error) {
resources := make([]Resource, 0)
var config cloudwatch.DescribeAlarmsInput
// This code temporarily changes the region to "us-east-1" and creates a new Pricing client
// then changes the region back to what it was before.
// This is necessary because the Pricing client needs to operate in the "us-east-1" region
oldRegion := client.AWSClient.Region
client.AWSClient.Region = "us-east-1"
pricingClient := pricing.NewFromConfig(*client.AWSClient)
client.AWSClient.Region = oldRegion
cloudWatchClient := cloudwatch.NewFromConfig(*client.AWSClient)
for {
output, err := cloudWatchClient.DescribeAlarms(ctx, &config)
Expand All @@ -39,14 +55,41 @@ func Alarms(ctx context.Context, client ProviderClient) ([]Resource, error) {
}
}

pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{
ServiceCode: aws.String("AmazonCloudWatch"),
Filters: []types.Filter{
{
Field: aws.String("regionCode"),
Value: aws.String(client.AWSClient.Region),
Type: types.FilterTypeTermMatch,
},
{
Field: aws.String("group"),
Value: aws.String("Alarm"),
Type: types.FilterTypeTermMatch,
},
},
MaxResults: aws.Int32(1),
})
if err != nil {
log.Printf("ERROR: Couldn't fetch pricing info for alarm %s: %v", *alarm.AlarmName, err)
continue
}

costPerMonth, err := calculateCostPerMonth(pricingOutput)
if err != nil {
log.Printf("ERROR: Failed to calculate cost per month: %v", err)
continue
}

resources = append(resources, Resource{
Provider: "AWS",
Account: client.Name,
Service: "CloudWatch",
ResourceId: *alarm.AlarmArn,
Region: client.AWSClient.Region,
Name: *alarm.AlarmName,
Cost: 0,
Cost: costPerMonth,
Tags: tags,
FetchedAt: time.Now(),
Link: fmt.Sprintf("https://%s.console.aws.amazon.com/cloudwatch/home?region=%s#alarmsV2:alarm/%s", client.AWSClient.Region, client.AWSClient.Region, *alarm.AlarmName),
Expand All @@ -68,3 +111,37 @@ func Alarms(ctx context.Context, client ProviderClient) ([]Resource, error) {

return resources, nil
}

func calculateCostPerMonth(pricingOutput *pricing.GetProductsOutput) (float64, error) {
costPerMonth := 0.0

if pricingOutput != nil && len(pricingOutput.PriceList) > 0 {
var priceList interface{}
err := json.Unmarshal([]byte(pricingOutput.PriceList[0]), &priceList)
if err != nil {
return 0, fmt.Errorf("failed to unmarshal JSON: %w", err)
}

priceListMap := priceList.(map[string]interface{})
if onDemand, ok := priceListMap["terms"].(map[string]interface{})["OnDemand"]; ok {
for _, details := range onDemand.(map[string]interface{}) {
if priceDetails, ok := details.(map[string]interface{})["priceDimensions"].(map[string]interface{}); ok {
for _, price := range priceDetails {
rateCode := price.(map[string]interface{})["rateCode"].(string)
if rateCode == RateCode {
usdPrice := price.(map[string]interface{})["pricePerUnit"].(map[string]interface{})["USD"].(string)
cost, err := strconv.ParseFloat(usdPrice, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse cost per month: %w", err)
}
costPerMonth = cost * AverageHoursPerMonth
break
}
}
}
}
}
}

return costPerMonth, nil
}

0 comments on commit 4fb8faa

Please sign in to comment.