Skip to content

Commit

Permalink
Merge pull request #13 from opencost/atmdebug-bad-costs
Browse files Browse the repository at this point in the history
cost unit testing and fixes
  • Loading branch information
ameijer authored Mar 13, 2024
2 parents e2af694 + 9b2c5dc commit eda3aa7
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 50 deletions.
57 changes: 10 additions & 47 deletions datadog/cmd/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func main() {
if err != nil {
log.Fatalf("error building DD config: %v", err)
}

log.SetLogLevel(ddConfig.DDLogLevel)
// datadog usage APIs allow 10 requests every 30 seconds
rateLimiter := rate.NewLimiter(0.25, 5)
ddCostSrc := DatadogCostSource{
Expand Down Expand Up @@ -132,11 +132,12 @@ func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPric
if nextPageId != "init" {
params.PageNextRecordId = &nextPageId
}

if d.rateLimiter.Tokens() < 1.0 {
log.Infof("datadog rate limit reached. holding request until rate capacity is back")
}

err := d.rateLimiter.WaitN(context.TODO(), 2)
err := d.rateLimiter.WaitN(context.TODO(), 1)
if err != nil {
log.Errorf("error waiting on rate limiter`: %v\n", err)
ccResp.Errors = append(ccResp.Errors, err.Error())
Expand All @@ -151,32 +152,12 @@ func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPric
ccResp.Errors = append(ccResp.Errors, err.Error())
}

// many datadog usages are given in terms of a cumulative month to date usage
// therefore, make a call for the hour before this hour to get a comparison
// where needed
params.FilterTimestampEnd = window.Start()
toSub := window.End().Sub(*window.Start())
respPriorWindow, r, err := d.usageApi.GetHourlyUsage(d.ddCtx, (*window.Start()).Add(-toSub), "all", *params)
if err != nil {
log.Errorf("Error when calling `UsageMeteringApi.GetHourlyUsage`: %v\n", err)
log.Errorf("Full HTTP response: %v\n", r)
ccResp.Errors = append(ccResp.Errors, err.Error())
}

for index := range resp.Data {
for indexMeas := range resp.Data[index].Attributes.Measurements {
usageQty := float32(0.0)

if resp.Data[index].Attributes.Measurements[indexMeas].Value.IsSet() {
var prior *datadogV2.HourlyUsageMeasurement
if len(respPriorWindow.Data) > index {
prior = &respPriorWindow.Data[index].Attributes.Measurements[indexMeas]
} else {
// then this is an out of bound access
log.Warnf("could not get prior window data from timeframe %v, and measurement %v", window, resp.Data[index].Attributes.Measurements[indexMeas])
log.Warnf("passing in nil prior window data")
}
usageQty = GetUsageQuantity(*resp.Data[index].Attributes.ProductFamily, &resp.Data[index].Attributes.Measurements[indexMeas], prior)
usageQty = float32(resp.Data[index].Attributes.Measurements[indexMeas].GetValue())
}

if usageQty == 0.0 {
Expand Down Expand Up @@ -215,28 +196,6 @@ func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPric
return &ccResp
}

// we have two basic types usages: cumulative and rate
// rate usages are e.g., number of infra hosts, that have fixed costs per hour
// cumulative usages are e.g., number of logs ingested, that have a fixed cost per unit
// if a usage is cumulative, then suptract the usage in the hour prior to get the incremental usage
// if a usage is a rate, then just return the usage
func GetUsageQuantity(productFamily string, currentPeriodUsage, previousPeriodUsage *datadogV2.HourlyUsageMeasurement) float32 {
curUsage := currentPeriodUsage.GetValue()
if _, found := rateFamilies[productFamily]; found {
// this family is a rate family, so just return the usage
return float32(curUsage)
}

prevUsage := int64(0)
if previousPeriodUsage == nil {
log.Warnf("previous period usage was nil, assuming 0 usage for that timeframe for family %s", productFamily)
} else {
prevUsage = previousPeriodUsage.GetValue()
}

return float32(curUsage - prevUsage)
}

// the public pricing used in the pricing list doesn't always match the usage reports
// therefore, we maintain a list of aliases
var usageToPricingMap = map[string]string{
Expand Down Expand Up @@ -375,6 +334,10 @@ func getDatadogConfig(configFilePath string) (*datadogplugin.DatadogConfig, erro
return nil, fmt.Errorf("error marshaling json into DD config %v", err)
}

if result.DDLogLevel == "" {
result.DDLogLevel = "info"
}

return &result, nil
}

Expand Down Expand Up @@ -412,13 +375,13 @@ func scrapeDatadogPrices(url string) (*datadogplugin.PricingInformation, error)
}
res := datadogplugin.DatadogProJSON{}
r := regexp.MustCompile(`var productDetailData = \s*(.*?)\s*;`)
log.Debugf("got response: %s", string(b))
log.Tracef("got response: %s", string(b))
matches := r.FindAllStringSubmatch(string(b), -1)
if len(matches) != 1 {
return nil, fmt.Errorf("requires exactly 1 product detail data, got %d", len(matches))
}

log.Debugf("matches[0][1]:" + matches[0][1])
log.Tracef("matches[0][1]:" + matches[0][1])
err = json.Unmarshal([]byte(matches[0][1]), &res)
if err != nil {
return nil, fmt.Errorf("failed to read pricing page body: %v", err)
Expand Down
64 changes: 64 additions & 0 deletions datadog/cmd/main/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ package main

import (
"fmt"
"os"
"testing"
"time"

datadogplugin "github.com/opencost/opencost-plugins/datadog/datadogplugin"
"github.com/opencost/opencost/core/pkg/log"
"github.com/opencost/opencost/core/pkg/model/pb"
"github.com/opencost/opencost/core/pkg/util/timeutil"
"golang.org/x/time/rate"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
)

func TestPricingFetch(t *testing.T) {
Expand All @@ -15,3 +25,57 @@ func TestPricingFetch(t *testing.T) {
t.Fatalf("expected non zero pricing details")
}
}

func TestGetCustomCosts(t *testing.T) {
// read necessary env vars. If any are missing, log warning and skip test
ddSite := os.Getenv("DD_SITE")
ddApiKey := os.Getenv("DD_API_KEY")
ddAppKey := os.Getenv("DD_APPLICATION_KEY")

if ddSite == "" {
log.Warnf("DD_SITE undefined, this needs to have the URL of your DD instance, skipping test")
t.Skip()
return
}

if ddApiKey == "" {
log.Warnf("DD_API_KEY undefined, skipping test")
t.Skip()
return
}

if ddAppKey == "" {
log.Warnf("DD_APPLICATION_KEY undefined, skipping test")
t.Skip()
return
}

// write out config to temp file using contents of env vars
config := datadogplugin.DatadogConfig{
DDSite: ddSite,
DDAPIKey: ddApiKey,
DDAppKey: ddAppKey,
}

rateLimiter := rate.NewLimiter(0.25, 5)
ddCostSrc := DatadogCostSource{
rateLimiter: rateLimiter,
}
ddCostSrc.ddCtx, ddCostSrc.usageApi = getDatadogClients(config)
windowStart := time.Date(2024, 3, 11, 0, 0, 0, 0, time.UTC)
// query for qty 2 of 1 hour windows
windowEnd := time.Date(2024, 3, 12, 0, 0, 0, 0, time.UTC)

req := &pb.CustomCostRequest{
Start: timestamppb.New(windowStart),
End: timestamppb.New(windowEnd),
Resolution: durationpb.New(timeutil.Day),
}

log.SetLogLevel("debug")
resp := ddCostSrc.GetCustomCosts(req)

if len(resp) == 0 {
t.Fatalf("empty response")
}
}
7 changes: 4 additions & 3 deletions datadog/datadogplugin/datadogconfig.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package datadog

type DatadogConfig struct {
DDSite string `json:"datadog_site"`
DDAPIKey string `json:"datadog_api_key"`
DDAppKey string `json:"datadog_app_key"`
DDSite string `json:"datadog_site"`
DDAPIKey string `json:"datadog_api_key"`
DDAppKey string `json:"datadog_app_key"`
DDLogLevel string `json:"log_level"`
}

0 comments on commit eda3aa7

Please sign in to comment.