diff --git a/providers/aws/cloudwatch/alarms.go b/providers/aws/cloudwatch/alarms.go index 7b9b6a82f..bddf753d1 100644 --- a/providers/aws/cloudwatch/alarms.go +++ b/providers/aws/cloudwatch/alarms.go @@ -4,11 +4,12 @@ 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" + "github.com/aws/aws-sdk-go-v2/service/pricing" + "github.com/aws/aws-sdk-go-v2/service/pricing/types" + log "github.com/sirupsen/logrus" "github.com/aws/aws-sdk-go-v2/aws" @@ -18,7 +19,6 @@ import ( ) const ( - RateCode = "JRTCKXETXF.6YS6EN2CT7" AverageHoursPerMonth = 730 ) @@ -127,16 +127,14 @@ func calculateCostPerMonth(pricingOutput *pricing.GetProductsOutput) (float64, e 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 + 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 + } } } diff --git a/providers/aws/cloudwatch/log_metrics.go b/providers/aws/cloudwatch/log_metrics.go index 440ce6c45..a261614f4 100644 --- a/providers/aws/cloudwatch/log_metrics.go +++ b/providers/aws/cloudwatch/log_metrics.go @@ -2,20 +2,85 @@ package cloudwatch import ( "context" + "encoding/json" "fmt" + "strconv" "time" log "github.com/sirupsen/logrus" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + cwTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" + + "github.com/aws/aws-sdk-go-v2/service/pricing" + "github.com/aws/aws-sdk-go-v2/service/pricing/types" + "github.com/tailwarden/komiser/models" "github.com/tailwarden/komiser/providers" ) +func getRate(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 { + usdPrice := price.(map[string]interface{})["pricePerUnit"].(map[string]interface{})["USD"].(string) + costPerMonth, err = strconv.ParseFloat(usdPrice, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse cost per month: %w", err) + } + break + } + } + } + } + } + + return costPerMonth, nil +} + func MetricStreams(ctx context.Context, client providers.ProviderClient) ([]models.Resource, error) { resources := make([]models.Resource, 0) cloudWatchMetricsClient := cloudwatch.NewFromConfig(*client.AWSClient) + pricingClient := pricing.NewFromConfig(*client.AWSClient) + + 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("operation"), + Value: aws.String("MetricUpdate"), + Type: types.FilterTypeTermMatch, + }, + }, + MaxResults: aws.Int32(1), + }) + if err != nil { + log.Printf("ERROR: Couldn't fetch pricing info for Metric Streams: %v", err) + return resources, err + } + + costPerUpdate, err := getRate(pricingOutput) + if err != nil { + log.Printf("ERROR: Failed to calculate cost per month: %v", err) + return resources, err + } input := &cloudwatch.ListMetricStreamsInput{} for { @@ -42,6 +107,23 @@ func MetricStreams(ctx context.Context, client providers.ProviderClient) ([]mode } } + statisticsOutput, err := cloudWatchMetricsClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{ + MetricName: aws.String("MetricUpdate"), + Namespace: aws.String("AWS/CloudWatch/MetricStreams"), + Dimensions: []cwTypes.Dimension{ + { + Name: aws.String("MetricStreamName"), + Value: stream.Name, + }, + }, + }) + if err != nil { + return resources, err + } + + updateCount := *statisticsOutput.Datapoints[0].Sum + monthlyCost := costPerUpdate * updateCount + resources = append(resources, models.Resource{ Provider: "AWS", Account: client.Name, @@ -49,7 +131,7 @@ func MetricStreams(ctx context.Context, client providers.ProviderClient) ([]mode ResourceId: streamArn, Region: client.AWSClient.Region, Name: aws.ToString(stream.Name), - Cost: 0, + Cost: monthlyCost, Tags: tags, FetchedAt: time.Now(), Link: fmt.Sprintf("https://%s.console.aws.amazon.com/cloudwatch/home?region=%s#metric-streams:streamsList/%s", client.AWSClient.Region, client.AWSClient.Region, aws.ToString(stream.Name)),