diff --git a/go.mod b/go.mod index 2c25dbf41..40321bdee 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.30.2 github.com/aws/aws-sdk-go-v2/service/codedeploy v1.20.3 github.com/aws/aws-sdk-go-v2/service/configservice v1.41.2 + github.com/aws/aws-sdk-go-v2/service/costexplorer v1.32.4 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.25.2 github.com/aws/aws-sdk-go-v2/service/ec2 v1.136.0 github.com/aws/aws-sdk-go-v2/service/ecr v1.23.0 diff --git a/go.sum b/go.sum index 96e5ba6b4..e528b712a 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/aws/aws-sdk-go-v2/service/codedeploy v1.20.3 h1:rGqIKTmugpZ7lEzXTmbiP github.com/aws/aws-sdk-go-v2/service/codedeploy v1.20.3/go.mod h1:A7i1lQClkFz09enKv5WYKb8a2lf9QeeI1s9dNiym3hg= github.com/aws/aws-sdk-go-v2/service/configservice v1.41.2 h1:WJt83aWld986AxwJpzE0eDqQi18a/PwZ36y7DqENYdk= github.com/aws/aws-sdk-go-v2/service/configservice v1.41.2/go.mod h1:wIuYBSC8G7HHXK/T6YO0t/m463ssur9aMLnycNvKXqQ= +github.com/aws/aws-sdk-go-v2/service/costexplorer v1.32.4 h1:ojxirFFJN39ar+tHiz84PuaeKA/Z3BiopdhxOGGQD4A= +github.com/aws/aws-sdk-go-v2/service/costexplorer v1.32.4/go.mod h1:1ujrFMokNtwDv3fwb9RBwdeXS+RonpIeV9uh19GJoH8= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.25.2 h1:O6ff5PwwgQ7QkL/XA0H+0U0mWwjkYaP9tHvbr0Ptqak= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.25.2/go.mod h1:kuVxCbsxbP/h6YTT2BfOj4s/bwXYsG3C/8Qn9gO5QJY= github.com/aws/aws-sdk-go-v2/service/ec2 v1.136.0 h1:nZPVFkGojUUJupKJzaCKE07LaFDO3Tto1U69F8JipsI= diff --git a/providers/aws/aws.go b/providers/aws/aws.go index 1e03cee43..2af4d122c 100644 --- a/providers/aws/aws.go +++ b/providers/aws/aws.go @@ -3,9 +3,12 @@ package aws import ( "context" "strings" + "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/costexplorer" + "github.com/aws/aws-sdk-go-v2/service/costexplorer/types" log "github.com/sirupsen/logrus" - "github.com/tailwarden/komiser/models" "github.com/tailwarden/komiser/providers" "github.com/tailwarden/komiser/providers/aws/apigateway" @@ -34,7 +37,9 @@ import ( "github.com/tailwarden/komiser/providers/aws/sns" "github.com/tailwarden/komiser/providers/aws/sqs" "github.com/tailwarden/komiser/providers/aws/systemsmanager" + awsUtils "github.com/tailwarden/komiser/providers/aws/utils" "github.com/tailwarden/komiser/utils" + "github.com/uptrace/bun" ) @@ -113,6 +118,43 @@ func FetchResources(ctx context.Context, client providers.ProviderClient, region listOfSupportedRegions = regions } + costexplorerClient := costexplorer.NewFromConfig(*client.AWSClient) + costexplorerOutputList := []*costexplorer.GetCostAndUsageOutput{} + var nextPageToken *string + for { + costexplorerOutput, err := costexplorerClient.GetCostAndUsage(ctx, &costexplorer.GetCostAndUsageInput{ + Granularity: "DAILY", + Metrics: []string{"UnblendedCost"}, + TimePeriod: &types.DateInterval{ + Start: aws.String(utils.BeginningOfMonth(time.Now()).Format("2006-01-02")), + End: aws.String(time.Now().Format("2006-01-02")), + }, + GroupBy: []types.GroupDefinition{ + { + Key: aws.String("SERVICE"), + Type: "DIMENSION", + }, + { + Key: aws.String("REGION"), + Type: "DIMENSION", + }, + }, + NextPageToken: nextPageToken, + }) + if err != nil { + log.Warn("Couldn't fetch cost and usage data:", err) + break + } + + costexplorerOutputList = append(costexplorerOutputList, costexplorerOutput) + + if aws.ToString(costexplorerOutput.NextPageToken) == "" { + break + } + + nextPageToken = costexplorerOutput.NextPageToken + } + ctxWithCostexplorerOutput := context.WithValue(ctx, awsUtils.CostexplorerKey, costexplorerOutputList) for _, region := range listOfSupportedRegions { c := client.AWSClient.Copy() c.Region = region @@ -123,7 +165,7 @@ func FetchResources(ctx context.Context, client providers.ProviderClient, region for _, fetchResources := range listOfSupportedServices() { fetchResources := fetchResources wp.SubmitTask(func() { - resources, err := fetchResources(ctx, client) + resources, err := fetchResources(ctxWithCostexplorerOutput, client) if err != nil { log.Warnf("[%s][AWS] %s", client.Name, err) } else { diff --git a/providers/aws/lambda/functions.go b/providers/aws/lambda/functions.go index 9bca7d517..7ab13e6e8 100644 --- a/providers/aws/lambda/functions.go +++ b/providers/aws/lambda/functions.go @@ -32,6 +32,11 @@ func Functions(ctx context.Context, client providers.ProviderClient) ([]models.R cloudwatchClient := cloudwatch.NewFromConfig(*client.AWSClient) lambdaClient := lambda.NewFromConfig(*client.AWSClient) + serviceCost, err := awsUtils.GetCostAndUsage(ctx, client.AWSClient.Region, "Lambda") + if err != nil { + log.Warnln("Couldn't fetch Lambda cost and usage:", err) + } + tempRegion := client.AWSClient.Region client.AWSClient.Region = "us-east-1" pricingClient := pricing.NewFromConfig(*client.AWSClient) @@ -157,7 +162,8 @@ func Functions(ctx context.Context, client providers.ProviderClient) ([]models.R Name: *o.FunctionName, Cost: monthlyCost, Metadata: map[string]string{ - "runtime": string(o.Runtime), + "runtime": string(o.Runtime), + "serviceCost": fmt.Sprint(serviceCost), }, Relations: relations, FetchedAt: time.Now(), diff --git a/providers/aws/utils/utils.go b/providers/aws/utils/utils.go index 5ed566701..bcec79e9a 100644 --- a/providers/aws/utils/utils.go +++ b/providers/aws/utils/utils.go @@ -1,13 +1,43 @@ package utils import ( + "context" "encoding/json" "fmt" "strconv" + "github.com/aws/aws-sdk-go-v2/service/costexplorer" "github.com/aws/aws-sdk-go-v2/service/pricing" ) +type AWSCtxKey uint8 + +const ( + CostexplorerKey AWSCtxKey = iota +) + +func GetCostAndUsage(ctx context.Context, region string, svcName string) (float64, error) { + total := 0.0 + costexplorerOutputList, ok := ctx.Value(CostexplorerKey).([]*costexplorer.GetCostAndUsageOutput) + if !ok || costexplorerOutputList == nil { + return 0, fmt.Errorf("incorrect costexplorerOutputList") + } + for _, costexplorerOutput := range costexplorerOutputList { + for _, group := range costexplorerOutput.ResultsByTime { + for _, v := range group.Groups { + if v.Keys[0] == svcName && v.Keys[1] == region { + amt, err := strconv.ParseFloat(*v.Metrics["UnblendedCost"].Amount, 64) + if err != nil { + return 0, err + } + total += amt + } + } + } + } + return total, nil +} + type ProductEntry struct { Product struct { Attributes struct { @@ -93,8 +123,8 @@ func GetPriceMap(pricingOutput *pricing.GetProductsOutput, field string) (map[st } func Int64PtrToFloat64(i *int64) float64 { - if i == nil { - return 0.0 // or any default value you prefer - } - return float64(*i) + if i == nil { + return 0.0 // or any default value you prefer + } + return float64(*i) }