Compare commits

...

6 Commits

Author SHA1 Message Date
Christian Petersen d8eaf32582
Fix broken link to Datadog plugin in README.md (#63)
Signed-off-by: Christian Petersen <Christian.Petersen2@ibm.com>
2025-04-03 10:22:49 -04:00
Alex Meijer 42fa6c2496
skip empty plugins (#57)
Signed-off-by: Alex Meijer <ameijer@kubecost.com>
2024-10-29 20:18:54 +00:00
Alex Meijer 4c5d648738
overhaul DD plugin to use billed costs only (#56)
* overhaul DD plugin to use billed costs only

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* add missing import

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

---------

Signed-off-by: Alex Meijer <ameijer@kubecost.com>
2024-10-29 16:02:22 -04:00
Alex Meijer b60c571ab9
get openAI and mongo integration tests working (#55)
Signed-off-by: Alex Meijer <ameijer@kubecost.com>
2024-10-23 10:30:08 -04:00
Sajit Mathew Kunnumkal 5ebd41de6d
Clean up code; add more tests (#44)
Signed-off-by: Sajit Kunnumkal <sajit@kunnumkal.com>
Co-authored-by: sajit <sajit@kunnumkal.com>
2024-10-17 09:25:33 -04:00
Alex Meijer 39e7d175b0
add contributors list (#43)
* add contributors list

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* tuning

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

---------

Signed-off-by: Alex Meijer <ameijer@kubecost.com>
2024-10-16 16:17:07 -04:00
12 changed files with 420 additions and 422 deletions

View File

@ -25,20 +25,20 @@ At the most basic level, all a plugin needs to do is gather cost data given a ti
## Plugin setup
- Pull the repo locally.
- Create a directory within the repo for the new plugin. Check out the [Datadog plugin](https://github.com/opencost/opencost-plugins/tree/main/datadog) for a reference to follow along with.
- Create a directory within the repo for the new plugin. Check out the [Datadog plugin](https://github.com/opencost/opencost-plugins/tree/main/pkg/plugins/datadog) for a reference to follow along with.
- Create the plugin subdirectories:
- `<repo>/<plugin>/cmd/main/`
- `<repo>/pkg/plugins/<plugin>/cmd/main/`
- This will contain the actual logic of the plugin.
- `<repo>/<plugin>/<plugin>plugin/`
- `<repo>/pkg/plugins/<plugin>/<plugin>plugin/`
- This will contain the plugin config struct and any other structs for handling requests/responses in the plugin.
- `<repo>/<plugin>/tests/`
- `<repo>/pkg/plugins/<plugin>/tests/`
- Highly recommended, this will contain tests to validate the functionality of the plugin.
- Initialize the subproject:
- Within `<repo>/<plugin>/`, run `go mod init github.com/opencost/opencost-plugins/<plugin>` and `go get github.com/hashicorp/go-plugin`.
- Within `<<repo>/pkg/plugins/<plugin>/`, run `go mod init github.com/opencost/opencost-plugins/pkg/plugins/<plugin>` and `go get github.com/hashicorp/go-plugin`.
## Design the configuration
All plugins require a configuration. For example, the [Datadog plugin configuration](https://github.com/opencost/opencost-plugins/blob/main/datadog/datadogplugin/datadogconfig.go) takes in some information required to authenticate with the Datadog API. This configuration will be defined by a struct inside `<repo>/<plugin>/<plugin>plugin/`.
All plugins require a configuration. For example, the [Datadog plugin configuration](https://github.com/opencost/opencost-plugins/blob/main/pkg/plugins/datadog/datadogplugin/datadogconfig.go) takes in some information required to authenticate with the Datadog API. This configuration will be defined by a struct inside `<repo>/pkg/plugins/<plugin>/<plugin>plugin/`.
## Implement the plugin
@ -51,13 +51,13 @@ Once the configuration is designed, it's time to write the plugin. Within `<repo
- Optionally (but recommended), implement your response data such that the custom cost extended attributes are utilized as much as possible.
- [Finally](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L87), we return the retrieved cost data.
- Implement the `main` function:
- Find the config file ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/main/datadog/cmd/main/main.go#L92)).
- Load the config file ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/main/datadog/cmd/main/main.go#L97)).
- Find the config file ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/main/pkg/plugins/datadog/cmd/main/main.go#L92)).
- Load the config file ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/main/pkg/plugins/datadog/cmd/main/main.go#L97)).
- Instantiate the plugin source ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L104-L106)).
- Serve the plugin for consumption by OpenCost ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L110-L118)).
## Implement tests (highly recommended)
Write some unit tests to validate the functionality of your new plugin. See the [Datadog unit tests](https://github.com/opencost/opencost-plugins/blob/main/datadog/tests/datadog_test.go) for reference.
Write some unit tests to validate the functionality of your new plugin. See the [Datadog unit tests](https://github.com/opencost/opencost-plugins/blob/main/pkg/plugins/datadog/tests/datadog_test.go) for reference.
## Submit it!
Now that your plugin is implemented and tested, all that's left is to get it submitted for review. Create a PR based off your branch and submit it, and an OpenCost developer will review it for you.
@ -65,4 +65,22 @@ Now that your plugin is implemented and tested, all that's left is to get it sub
## Plugin system limitations
- OpenCost stores the plugin responses in an in-memory repository, which necessitates that OpenCost queries the plugins again for cost data upon pod restart.
- Many cost sources have API rate limits, such as Datadog. As such, a rate limiter may be necessary.
- If you want a plugin embedded in your OpenCost image, you will have to build the image yourself.
- If you want a plugin embedded in your OpenCost image, you will have to build the image yourself.
## Contributors
Thanks to all the individuals who have given their time and effort towards creating and maintaining these plugins:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/sajit"><img src="https://avatars.githubusercontent.com/u/675316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sajit Mathew Kunnumkal</b></sub></a><br /><a href="https://github.com/opencost/opencost-plugins/commits?author=sajit" title="Code">💻</a><br /><sub><b>MongoDB Atlas Plugin</b></sub></a><br /</td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->

View File

@ -4,20 +4,19 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
_nethttp "net/http"
"os"
"regexp"
"strconv"
"reflect"
"strings"
"time"
"github.com/DataDog/datadog-api-client-go/v2/api/datadog"
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
"github.com/agnivade/levenshtein"
"golang.org/x/time/rate"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
"github.com/hashicorp/go-plugin"
commonconfig "github.com/opencost/opencost-plugins/pkg/common/config"
datadogplugin "github.com/opencost/opencost-plugins/pkg/plugins/datadog/datadogplugin"
@ -44,6 +43,7 @@ var handshakeConfig = plugin.HandshakeConfig{
type DatadogCostSource struct {
ddCtx context.Context
usageApi *datadogV2.UsageMeteringApi
v1UsageApi *datadogV1.UsageMeteringApi
rateLimiter *rate.Limiter
}
@ -60,20 +60,19 @@ func (d *DatadogCostSource) GetCustomCosts(req *pb.CustomCostRequest) []*pb.Cust
return results
}
// Call the function to scrape prices
listPricing, err := scrapeDatadogPrices(url)
if err != nil {
log.Errorf("error getting dd pricing: %v", err)
errResp := pb.CustomCostResponse{
Errors: []string{fmt.Sprintf("error getting dd pricing: %v", err)},
}
results = append(results, &errResp)
return results
} else {
log.Debugf("got list pricing: %v", listPricing.Details)
}
for _, target := range targets {
// Call the function to scrape prices
unitPricing, err := d.GetDDUnitPrices(target.Start().UTC())
if err != nil {
log.Errorf("error getting dd pricing: %v", err)
errResp := pb.CustomCostResponse{
Errors: []string{fmt.Sprintf("error getting dd pricing: %v", err)},
}
results = append(results, &errResp)
return results
} else {
log.Debugf("got unit pricing: %v", unitPricing)
}
// DataDog gets mad if we ask them to tell the future
if target.Start().After(time.Now().UTC()) {
log.Debugf("skipping future window %v", target)
@ -81,7 +80,7 @@ func (d *DatadogCostSource) GetCustomCosts(req *pb.CustomCostRequest) []*pb.Cust
}
log.Debugf("fetching DD costs for window %v", target)
result := d.getDDCostsForWindow(target, listPricing)
result := d.getDDCostsForWindow(target, unitPricing)
results = append(results, result)
}
@ -105,7 +104,7 @@ func main() {
ddCostSrc := DatadogCostSource{
rateLimiter: rateLimiter,
}
ddCostSrc.ddCtx, ddCostSrc.usageApi = getDatadogClients(*ddConfig)
ddCostSrc.ddCtx, ddCostSrc.usageApi, ddCostSrc.v1UsageApi = getDatadogClients(*ddConfig)
// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
@ -132,7 +131,7 @@ func boilerplateDDCustomCost(win opencost.Window) pb.CustomCostResponse {
Costs: []*pb.CustomCost{},
}
}
func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPricing *datadogplugin.PricingInformation) *pb.CustomCostResponse {
func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPricing map[string]billableCost) *pb.CustomCostResponse {
ccResp := boilerplateDDCustomCost(window)
costs := map[string]*pb.CustomCost{}
nextPageId := "init"
@ -196,34 +195,42 @@ func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPric
continue
}
desc, usageUnit, pricing, currency := getListingInfo(window, *resp.Data[index].Attributes.ProductFamily, *resp.Data[index].Attributes.Measurements[indexMeas].UsageType, listPricing)
ccResp.Currency = currency
matched, pricing := matchUsageToPricing(*resp.Data[index].Attributes.Measurements[indexMeas].UsageType, listPricing)
log.Infof("matched %s to %s", *resp.Data[index].Attributes.Measurements[indexMeas].UsageType, matched)
provId := *resp.Data[index].Attributes.PublicId + "/" + *resp.Data[index].Attributes.Measurements[indexMeas].UsageType
if cost, found := costs[provId]; found {
// we already have this cost type for the window, so just update the usages and costs
cost.UsageQuantity += usageQty
cost.ListCost += usageQty * pricing
if matched == "" {
log.Infof("no pricing found for %s", *resp.Data[index].Attributes.Measurements[indexMeas].UsageType)
continue
}
billedCost := float32(pricing.Cost) * usageQty
if _, found := costs[provId]; found {
// we have already encountered this cost type for this window, so add to the existing cost entry
costs[provId].UsageQuantity += usageQty
costs[provId].BilledCost += billedCost
} else {
// we have not encountered this cost type for this window yet, so create a new cost entry
cost := pb.CustomCost{
Zone: *resp.Data[index].Attributes.Region,
AccountName: *resp.Data[index].Attributes.OrgName,
ChargeCategory: "usage",
Description: desc,
Description: "nil",
ResourceName: *resp.Data[index].Attributes.Measurements[indexMeas].UsageType,
ResourceType: *resp.Data[index].Attributes.ProductFamily,
Id: *resp.Data[index].Id,
ProviderId: provId,
Labels: map[string]string{},
ListCost: usageQty * pricing,
ListUnitPrice: pricing,
ListCost: 0,
ListUnitPrice: 0,
BilledCost: billedCost,
UsageQuantity: usageQty,
UsageUnit: usageUnit,
UsageUnit: pricing.unit,
ExtendedAttributes: nil,
}
costs[provId] = &cost
}
}
}
if resp.Meta != nil && resp.Meta.Pagination != nil && resp.Meta.Pagination.NextRecordId.IsSet() {
@ -243,92 +250,73 @@ func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPric
// this post processing stage de-duplicates those usages and costs
postProcess(&ccResp)
// query from the first of the window's month until the window end's day so that we can properly adjust for the
// cumulative nature of the response
startDate := time.Date(window.Start().UTC().Year(), window.Start().UTC().Month(), 1, 0, 0, 0, 0, time.UTC)
endDate := time.Date(window.End().UTC().Year(), window.End().UTC().Month(), window.End().UTC().Day(), 0, 0, 0, 0, time.UTC)
view := "sub-org"
params := datadogV2.NewGetEstimatedCostByOrgOptionalParameters()
params.StartDate = &startDate
params.EndDate = &endDate
params.View = &view
resp, r, err := d.usageApi.GetEstimatedCostByOrg(d.ddCtx, *params)
if err != nil {
log.Errorf("Error when calling `UsageMeteringApi.GetEstimatedCostByOrg`: %v\n", err)
log.Errorf("Full HTTP response: %v\n", r)
ccResp.Errors = append(ccResp.Errors, err.Error())
}
previousChargeCosts := make(map[string]float32)
// estimated costs from datadog are per-day, so we scale in the event that we want hourly costs
var costFactor float32
switch window.Duration().Hours() {
case 24:
costFactor = 1.0
case 1:
costFactor = 1.0 / 24.0
default:
err = fmt.Errorf("unsupported window duration: %v hours", window.Duration().Hours())
log.Errorf("%v\n", err)
ccResp.Errors = append(ccResp.Errors, err.Error())
return &ccResp
}
costs = map[string]*pb.CustomCost{}
for _, costResp := range resp.Data {
attributes := costResp.Attributes
for _, charge := range attributes.Charges {
chargeCost := float32(*charge.Cost)
// we only care about non-zero totals. by filtering out non-totals, we avoid duplicate costs from the
// datadog response
if (chargeCost == 0) || (*charge.ChargeType != "total") {
continue
}
// adjust the charge cost, as the charges are cumulative throughout the response
adjustedChargeCost := chargeCost
if _, ok := previousChargeCosts[*charge.ProductName]; ok {
adjustedChargeCost -= previousChargeCosts[*charge.ProductName]
}
previousChargeCosts[*charge.ProductName] = chargeCost
adjustedChargeCost *= costFactor
if attributes.Date.Day() != window.Start().Day() {
continue
}
provId := *attributes.PublicId + "/" + *charge.ProductName
if cost, found := costs[provId]; found {
// we already have this cost type for the window, so just update the billed cost
cost.BilledCost += adjustedChargeCost
} else {
// we have not encountered this cost type for this window yet, so create a new cost entry
cost := pb.CustomCost{
Zone: *attributes.Region,
AccountName: *attributes.OrgName,
ChargeCategory: "billing",
ResourceName: *charge.ProductName,
Id: *costResp.Id,
ProviderId: provId,
Labels: map[string]string{},
BilledCost: adjustedChargeCost,
ExtendedAttributes: nil,
}
costs[provId] = &cost
}
}
}
for _, cost := range costs {
ccResp.Costs = append(ccResp.Costs, cost)
}
return &ccResp
}
func matchUsageToPricing(usageType string, pricing map[string]billableCost) (string, *billableCost) {
// for the usage, remove _count from the end of the usage type
usageType = strings.TrimSuffix(usageType, "_count")
// not specific enough to match on
if usageType == "host" {
return "", nil
}
// if the usage type is in the pricing map, use that
if _, found := pricing[usageType]; found {
entry := pricing[usageType]
return usageType, &entry
}
// break up the usage on _
tokens := strings.Split(usageType, "_")
// find the first pricing key that contains all tokens
for key, price := range pricing {
matchesAll := true
for _, token := range tokens {
if !strings.Contains(key, token) {
matchesAll = false
break
}
}
if matchesAll {
return key, &price
}
}
// try replacing agent with infra and checking that
agentAsInfra := strings.ReplaceAll(usageType, "agent", "infra")
tokens = strings.Split(agentAsInfra, "_")
// find the first pricing key that contains all tokens
for key, price := range pricing {
matchesAll := true
for _, token := range tokens {
if !strings.Contains(key, token) {
matchesAll = false
break
}
}
if matchesAll {
return key, &price
}
}
// if still no pricing key is found, compute the levenshtein distance between the usage type and the pricing key
// and use the one with the smallest distance
smallestDist := 4000000000
var closestKey string
for key := range pricing {
distance := levenshtein.ComputeDistance(usageType, key)
if distance < smallestDist {
smallestDist = distance
closestKey = key
}
}
// remember the pricing keys we have already matched. if we have already matched a pricing key, don't match it again
entry := pricing[closestKey]
return closestKey, &entry
}
func postProcess(ccResp *pb.CustomCostResponse) {
if ccResp == nil {
return
@ -338,11 +326,11 @@ func postProcess(ccResp *pb.CustomCostResponse) {
ccResp.Costs = processLogUsage(ccResp.Costs)
// removes any items that have 0 usage, either because of post processing or otherwise
ccResp.Costs = removeZeroUsages(ccResp.Costs)
// DBM queries have 200 * number of hosts included. We need to adjust the costs to reflect this
ccResp.Costs = adjustDBMQueries(ccResp.Costs)
// removes any items that have 0 usage, either because of post processing or otherwise
ccResp.Costs = removeZeroUsages(ccResp.Costs)
}
// as per https://www.datadoghq.com/pricing/?product=database-monitoring#database-monitoring-can-i-still-use-dbm-if-i-have-additional-normalized-queries-past-the-a-hrefpricingallotmentsallotteda-amount
@ -383,75 +371,36 @@ func adjustDBMQueries(costs []*pb.CustomCost) []*pb.CustomCost {
return costs
}
// removes any items that have 0 usage, either because of post processing or otherwise
// removes any items that have 0 usage or cost, either because of post processing or otherwise
func removeZeroUsages(costs []*pb.CustomCost) []*pb.CustomCost {
log.Tracef("POST -costs length before post processing: %d", len(costs))
for index := 0; index < len(costs); index++ {
if costs[index].UsageQuantity < 0.001 {
log.Debugf("removing cost %s because it has 0 usage", costs[index].ProviderId)
log.Tracef("POST - looking at cost %s with usage %f", costs[index].ResourceName, costs[index].UsageQuantity)
if costs[index].UsageQuantity < 0.001 && costs[index].ListCost == 0.0 && costs[index].BilledCost == 0.0 {
if costs[index].ResourceName == "dbm_queries_count" {
log.Tracef("leaving dbm queries cost in place")
continue
}
log.Tracef("POST -removing cost %s because it has 0 usage", costs[index].ProviderId)
costs = append(costs[:index], costs[index+1:]...)
index = 0
log.Tracef("POST - costs is now %d", len(costs))
index = -1
}
}
log.Tracef("POST -costs length after post processing: %d", len(costs))
return costs
}
func processInfraHosts(costs []*pb.CustomCost) []*pb.CustomCost {
// remove the container_count_excl_agent item
// subtract the container_count_excl_agent from the container_count
// re-add as a synthetic 'agent container' item
var cc *pb.CustomCost
for index := range costs {
// remove the container_count item
for index := 0; index < len(costs); index++ {
if costs[index].ResourceName == "container_count" {
cc = costs[index]
costs = append(costs[:index], costs[index+1:]...)
break
index = 0
}
}
if cc != nil {
numAgents := float32(0.0)
for index := range costs {
if costs[index].ResourceName == "container_count_excl_agent" {
numAgents = float32(cc.UsageQuantity) - float32(costs[index].UsageQuantity)
break
}
}
cc.Description = "agent container"
cc.UsageQuantity = numAgents
cc.ResourceName = "agent_container"
cc.ListCost = numAgents * cc.ListUnitPrice
costs = append(costs, cc)
}
// remove the host_count item
// subtract the agent_cost_count from host_count item
// remaining gets put into a 'other hosts' item count
var hc *pb.CustomCost
for index := range costs {
if costs[index].ResourceName == "host_count" {
hc = costs[index]
costs = append(costs[:index], costs[index+1:]...)
break
}
}
if hc != nil {
otherHosts := float32(0.0)
for index := range costs {
if costs[index].ResourceName == "agent_host_count" {
otherHosts = float32(hc.UsageQuantity) - float32(costs[index].UsageQuantity)
break
}
}
hc.Description = "other hosts"
hc.UsageQuantity = otherHosts
hc.ResourceName = "other_hosts"
hc.ListCost = otherHosts * hc.ListUnitPrice
costs = append(costs, hc)
}
return costs
}
@ -499,117 +448,7 @@ func processLogUsage(costs []*pb.CustomCost) []*pb.CustomCost {
return costs
}
// 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{
"timeseries": "custom_metrics",
"apm_uncategorized_host_count": "apm_hosts",
"apm_host_count_incl_usm": "apm_hosts",
"apm_azure_app_service_host_count": "apm_hosts",
"apm_devsecops_host_count": "apm_hosts",
"apm_host_count": "apm_hosts",
"opentelemetry_apm_host_count": "apm_hosts",
"apm_fargate_count": "apm_hosts",
"dbm_host_count": "dbm",
"dbm_queries_count": "dbm_queries",
"container_count": "containers",
"container_count_excl_agent": "containers",
"billable_ingested_bytes": "ingested_logs",
"ingested_events_bytes": "ingested_logs",
"logs_live_ingested_bytes": "ingested_logs",
"logs_rehydrated_ingested_bytes": "ingested_logs",
"indexed_events_count": "indexed_logs",
"logs_live_indexed_count": "indexed_logs",
"synthetics_api": "api_tests",
"synthetics_browser": "browser_checks",
"tasks_count": "fargate_tasks",
"rum": "rum_events",
"analyzed_logs": "security_logs",
"snmp": "snmp_device",
"invocations_sum": "serverless_inv",
}
var pricingMap = map[string]float64{
"custom_metrics": 100.0,
"indexed_logs": 1000000.0,
"ingested_logs": 1024.0 * 1024.0 * 1024.0 * 1024.0,
"api_tests": 10000.0,
"browser_checks": 1000.0,
"rum_events": 10000.0,
"security_logs": 1024.0 * 1024.0 * 1024.0 * 1024.0,
"serverless_inv": 1000000.0,
}
var rateFamilies = map[string]int{
"infra_hosts": 730.0,
"apm_hosts": 730.0,
"containers": 730.0,
"dbm": 730.0,
}
func getListingInfo(window opencost.Window, productfamily string, usageType string, listPricing *datadogplugin.PricingInformation) (description string, usageUnit string, pricing float32, currency string) {
pricingKey := ""
var found bool
// first, check if the usage type is mapped to a pricing key
if pricingKey, found = usageToPricingMap[usageType]; found {
log.Debugf("usage type %s was mapped to pricing key %s", usageType, pricingKey)
} else if pricingKey, found = usageToPricingMap[productfamily]; found {
// if it isn't then check if the family is mapped to a pricing key
log.Debugf("product family %s was mapped to pricing key %s", productfamily, pricingKey)
} else {
// if it isn't, then the family is the pricing key
pricingKey = productfamily
}
matchedPrice := false
// search through the pricing for the right key
for _, detail := range listPricing.Details {
if pricingKey == detail.Name {
matchedPrice = true
description = detail.DetailDescription
usageUnit = detail.Units
currency = detail.OneMonths.Currency
pricingFloat, err := strconv.ParseFloat(detail.OneMonths.Rate, 32)
if err != nil {
log.Errorf("error converting string to float for rate: %s", detail.OneMonths.Rate)
}
// if the family is a rate family, then the pricing is per hour
if hourlyPriceDenominator, found := rateFamilies[pricingKey]; found {
// adjust the pricing to fit the window duration
pricingPerHour := float32(pricingFloat) / float32(hourlyPriceDenominator)
pricingPerWindow := pricingPerHour
usageUnit = strings.TrimSuffix(usageUnit, "s")
usageUnit += " - hours"
pricing = pricingPerWindow
return
} else {
// if the family is a cumulative family, then the pricing is per unit
// check for a scale factor on the pricing
if scalefactor, found := pricingMap[pricingKey]; found {
pricing = float32(pricingFloat) / float32(scalefactor)
} else {
pricing = float32(pricingFloat)
}
return
}
}
}
if !matchedPrice {
log.Warnf("unable to find pricing for product %s/%s. going to set to 0 price", productfamily, usageType)
usageType = "PRICING UNAVAILABLE"
description = productfamily + " " + usageType
pricing = 0.0
currency = ""
}
// return the data from the usage entry
return
}
func getDatadogClients(config datadogplugin.DatadogConfig) (context.Context, *datadogV2.UsageMeteringApi) {
func getDatadogClients(config datadogplugin.DatadogConfig) (context.Context, *datadogV2.UsageMeteringApi, *datadogV1.UsageMeteringApi) {
ddctx := datadog.NewDefaultContext(context.Background())
ddctx = context.WithValue(
ddctx,
@ -631,7 +470,8 @@ func getDatadogClients(config datadogplugin.DatadogConfig) (context.Context, *da
configuration := datadog.NewConfiguration()
apiClient := datadog.NewAPIClient(configuration)
usageAPI := datadogV2.NewUsageMeteringApi(apiClient)
return ddctx, usageAPI
v1UsageAPI := datadogV1.NewUsageMeteringApi(apiClient)
return ddctx, usageAPI, v1UsageAPI
}
func getDatadogConfig(configFilePath string) (*datadogplugin.DatadogConfig, error) {
@ -652,60 +492,143 @@ func getDatadogConfig(configFilePath string) (*datadogplugin.DatadogConfig, erro
return &result, nil
}
func scrapeDatadogPrices(url string) (*datadogplugin.PricingInformation, error) {
maxTries := 5
var result *datadogplugin.PricingInformation
var errTry error
for try := 1; try <= maxTries; try++ {
var response *http.Response
// Send a GET request to the URL
response, errTry = http.Get(url)
if errTry != nil || response.StatusCode != http.StatusOK {
log.Errorf("failed to fetch the page: %v", errTry)
time.Sleep(30 * time.Second)
response.Body.Close()
continue
}
func (d *DatadogCostSource) GetDDUnitPrices(windowStart time.Time) (map[string]billableCost, error) {
b, err := io.ReadAll(response.Body)
if err != nil {
errTry = err
response.Body.Close()
log.Errorf("failed to read pricing page body: %v", err)
time.Sleep(30 * time.Second)
continue
}
response.Body.Close()
res := datadogplugin.DatadogProJSON{}
r := regexp.MustCompile(`var productDetailData = \s*(.*?)\s*};`)
log.Tracef("got response: %s", string(b))
matches := r.FindAllStringSubmatch(string(b), -1)
if len(matches) != 1 {
errTry = err
log.Errorf("requires exactly 1 product detail data, got %d", len(matches))
time.Sleep(30 * time.Second)
continue
}
// DD estimated costs can be delayed 72 hours
// so ensure we are going far enough back
stableTimeframe := time.Now().UTC().Add(-3 * 24 * time.Hour)
log.Tracef("matches[0][1]:" + matches[0][1])
// add back in the closing curly brace that was used to pattern match
err = json.Unmarshal([]byte(matches[0][1]+"}"), &res)
if err != nil {
errTry = err
log.Errorf("failed to read pricing page body: %v", err)
time.Sleep(30 * time.Second)
continue
}
if errTry == nil {
result = &res.OfferData.PricingInformation
targetMonth := time.Date(stableTimeframe.Year(), stableTimeframe.Month(), 1, 0, 0, 0, 0, time.UTC)
targetMonthEnd := time.Date(stableTimeframe.Year(), stableTimeframe.Month()+1, 1, 0, 0, 0, 0, time.UTC)
// first, get the billable usage for the month
opts := datadogV1.GetUsageBillableSummaryOptionalParameters{
Month: &targetMonth,
}
var respBillableUsage datadogV1.UsageBillableSummaryResponse
var err error
for try := 1; try <= 5; {
respBillableUsage, _, err = d.v1UsageApi.GetUsageBillableSummary(d.ddCtx, opts)
if err == nil {
break
} else {
if strings.Contains(err.Error(), "429") {
log.Errorf("rate limit reached, retrying...")
} else {
break
}
time.Sleep(30 * time.Second)
try++
}
}
if errTry != nil {
return nil, fmt.Errorf("failed to fetch the page after %d tries: %w", maxTries, errTry)
if err != nil {
return nil, fmt.Errorf("error getting usage billable usage summary: %v", err)
}
// then, get the estimated cost for the month
// the start date should be the beginning of the month
// the end date should be the end of the last month, or the stable time frame, depending on if we are in the first 3 days of the new month or not
endDateToUse := targetMonthEnd
if time.Now().Before(targetMonthEnd) {
endDateToUse = stableTimeframe
}
costOpts := datadogV2.GetEstimatedCostByOrgOptionalParameters{
StartDate: &targetMonth,
EndDate: &endDateToUse,
}
var respEstimatedCost datadogV2.CostByOrgResponse
for try := 1; try <= 5; {
respEstimatedCost, _, err = d.usageApi.GetEstimatedCostByOrg(d.ddCtx, costOpts)
if err == nil {
break
} else {
if strings.Contains(err.Error(), "429") {
log.Errorf("rate limit reached, retrying...")
} else {
break
}
time.Sleep(30 * time.Second)
try++
}
}
if err != nil {
return nil, fmt.Errorf("after calling `UsageMeteringApi.GetEstimatedCostByOrg` %d times, still getting error: %v", 5, err)
}
// now, we need to calculate the unit prices
// the unit price is the estimated cost divided by the billable usage
// we need to do this for each product family
costsByFamily := make(map[string]float64)
latestCosts := respEstimatedCost.Data[len(respEstimatedCost.Data)-1]
attrs := latestCosts.Attributes
for _, charge := range attrs.Charges {
if *charge.ChargeType != "total" {
continue
}
costsByFamily[*charge.ProductName] = float64(*charge.Cost)
}
result := make(map[string]billableCost)
for _, usage := range respBillableUsage.Usage {
log.Debugf("usage: %v", usage)
for productName, cost := range costsByFamily {
usageAmount, unit := GetAccountBillableUsage(productName, usage.Usage)
if usageAmount == 0 {
continue
}
// if the product family has 'hosts' in it, then the usage is per month
// so we need to adjust the cost to be per hour
isRated := false
if strings.Contains(productName, "host") {
isRated = true
cost /= float64(730)
}
result[productName] = billableCost{
ProductName: productName,
Cost: cost / float64(usageAmount),
isRated: isRated,
unit: unit,
}
}
}
return result, nil
}
type billableCost struct {
ProductName string
Cost float64
isRated bool
unit string
}
// CheckAccountBillableUsage checks if any AccountBillableUsage equals one.
func GetAccountBillableUsage(billingDimension string, o *datadogV1.UsageBillableSummaryKeys) (int64, string) {
v := reflect.ValueOf(o).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Ptr && !field.IsNil() {
usage := field.Interface().(*datadogV1.UsageBillableSummaryBody)
if len(usage.AdditionalProperties) > 0 {
if usage.AdditionalProperties["billing_dimension"] == billingDimension {
return *usage.AccountBillableUsage, *usage.UsageUnit
}
}
}
}
// if not in the reflected fields, check the AdditionalProperties
for name, usage := range o.AdditionalProperties {
if strings.Contains(name, billingDimension) {
untypedUsage := usage.(map[string]interface{})
untyped := untypedUsage["account_billable_usage"]
typed := int64(untyped.(float64))
return typed, untypedUsage["usage_unit"].(string)
}
}
log.Warnf("no AccountBillableUsage found for billing dimension %s", billingDimension)
return 0, ""
}

View File

@ -1,7 +1,6 @@
package main
import (
"fmt"
"os"
"testing"
"time"
@ -15,17 +14,6 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestPricingFetch(t *testing.T) {
listPricing, err := scrapeDatadogPrices(url)
if err != nil {
t.Fatalf("failed to get pricing: %v", err)
}
fmt.Printf("got response: %v", listPricing)
if len(listPricing.Details) == 0 {
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")
@ -61,10 +49,10 @@ func TestGetCustomCosts(t *testing.T) {
ddCostSrc := DatadogCostSource{
rateLimiter: rateLimiter,
}
ddCostSrc.ddCtx, ddCostSrc.usageApi = getDatadogClients(config)
windowStart := time.Date(2024, 10, 6, 0, 0, 0, 0, time.UTC)
ddCostSrc.ddCtx, ddCostSrc.usageApi, ddCostSrc.v1UsageApi = getDatadogClients(config)
windowStart := time.Date(2024, 10, 16, 0, 0, 0, 0, time.UTC)
// query for qty 2 of 1 hour windows
windowEnd := time.Date(2024, 10, 7, 0, 0, 0, 0, time.UTC)
windowEnd := time.Date(2024, 10, 17, 0, 0, 0, 0, time.UTC)
req := &pb.CustomCostRequest{
Start: timestamppb.New(windowStart),
@ -72,7 +60,7 @@ func TestGetCustomCosts(t *testing.T) {
Resolution: durationpb.New(timeutil.Day),
}
log.SetLogLevel("debug")
log.SetLogLevel("trace")
resp := ddCostSrc.GetCustomCosts(req)
if len(resp) == 0 {

View File

@ -104,6 +104,7 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
}
dbmCostsInRange := 0
seenCosts := map[string]bool{}
//verify that the returned costs are non zero
for _, resp := range respDaily {
if len(resp.Costs) == 0 && resp.Start.AsTime().After(time.Now().Truncate(24*time.Hour).Add(-1*time.Minute)) {
@ -112,12 +113,12 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
}
var costSum float32
for _, cost := range resp.Costs {
costSum += cost.GetListCost()
if cost.GetListCost() == 0 {
costSum += cost.GetBilledCost()
seenCosts[cost.GetResourceName()] = true
if cost.GetBilledCost() == 0 {
log.Debugf("got zero cost for %v", cost)
}
if cost.GetListCost() > 100 {
if cost.GetBilledCost() > 100 {
log.Errorf("daily cost returned by plugin datadog for %v is greater than 100", cost)
return false
}
@ -126,7 +127,7 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
// range
if cost.GetResourceName() == "dbm_host_count" {
// filter out recent costs since those might not be full days worth
if cost.GetListCost() > 2.5 && cost.GetListCost() < 3.0 {
if cost.GetBilledCost() > 2.5 && cost.GetBilledCost() < 3.0 {
dbmCostsInRange++
}
}
@ -143,41 +144,26 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
return false
}
seenCosts := map[string]bool{}
for _, resp := range respHourly {
var costSum float32
for _, cost := range resp.Costs {
seenCosts[cost.GetResourceName()] = true
costSum += cost.GetListCost()
}
if costSum == 0 {
log.Errorf("hourly cost returned by plugin datadog is zero")
return false
}
}
expectedCosts := []string{
"agent_host_count",
"logs_indexed_events_15_day_count",
"container_count_excl_agent",
"agent_container",
"dbm_host_count",
}
for _, cost := range expectedCosts {
if !seenCosts[cost] {
log.Errorf("hourly cost %s not found in plugin datadog response", cost)
log.Errorf("daily cost %s not found in plugin datadog response", cost)
return false
}
}
if len(seenCosts) != len(expectedCosts) {
log.Errorf("hourly costs returned by plugin datadog do not equal expected costs")
log.Errorf("daily costs returned by plugin datadog do not equal expected costs")
log.Errorf("seen costs: %v", seenCosts)
log.Errorf("expected costs: %v", expectedCosts)
log.Errorf("response: %v", respHourly)
log.Errorf("response: %v", respDaily)
return false
}
@ -190,18 +176,22 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
}
seenCosts = map[string]bool{}
sumCosts := float32(0.0)
for _, resp := range respHourly {
for _, cost := range resp.Costs {
seenCosts[cost.GetResourceName()] = true
if cost.GetListCost() == 0 {
log.Errorf("hourly cost returned by plugin datadog is zero")
return false
}
if cost.GetListCost() > 100 {
sumCosts += cost.GetBilledCost()
if cost.GetBilledCost() > 100 {
log.Errorf("hourly cost returned by plugin datadog for %v is greater than 100", cost)
return false
}
}
}
if sumCosts == 0 {
log.Errorf("hourly costs returned by datadog plugin are zero")
return false
}
for _, cost := range expectedCosts {

View File

@ -18,6 +18,7 @@ require (
require (
github.com/DataDog/zstd v1.5.5 // indirect
github.com/agnivade/levenshtein v1.2.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect

View File

@ -2,6 +2,8 @@ github.com/DataDog/datadog-api-client-go/v2 v2.23.0 h1:1ziqo+mhG8GSuxsxLVBNNe/SX
github.com/DataDog/datadog-api-client-go/v2 v2.23.0/go.mod h1:QKOu6vscsh87fMY1lHfLEmNSunyXImj8BUaUWJXOehc=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=

View File

@ -31,7 +31,7 @@ var handshakeConfig = plugin.HandshakeConfig{
MagicCookieValue: "mongodb-atlas",
}
const costExplorerPendingInvoices = "https://cloud.mongodb.com/api/atlas/v2/orgs/%s/invoices/pending"
const costExplorerPendingInvoicesURL = "https://cloud.mongodb.com/api/atlas/v2/orgs/%s/invoices/pending"
func main() {
log.Debug("Initializing Mongo plugin")
@ -155,15 +155,10 @@ func (a *AtlasCostSource) GetCustomCosts(req *pb.CustomCostRequest) []*pb.Custom
}
log.Debugf("fetching atlas costs for window %v", target)
result, err := a.getAtlasCostsForWindow(&target, lineItems)
if err != nil {
log.Errorf("error getting costs for window %v: %v", target, err)
errResp := pb.CustomCostResponse{}
errResp.Errors = append(errResp.Errors, fmt.Sprintf("error getting costs for window %v: %v", target, err))
results = append(results, &errResp)
} else {
results = append(results, result)
}
result := a.getAtlasCostsForWindow(&target, lineItems)
results = append(results, result)
}
return results
@ -183,12 +178,15 @@ func filterLineItemsByWindow(win *opencost.Window, lineItems []atlasplugin.LineI
if err1 != nil || err2 != nil {
// If parsing fails, skip this item
if err1 != nil {
log.Warnf("%s", err1)
}
if err2 != nil {
log.Warnf("%s", err2)
}
continue
}
// // Iterate over the UsageDetails in CostResponse
// for _, lineItem := range pendingInvoicesResponse.LineItems {
// Create a new pb.CustomCost for each LineItem
//log.Debugf("Line item %v", item)
customCost := &pb.CustomCost{
AccountName: item.GroupName,
@ -217,9 +215,9 @@ func filterLineItemsByWindow(win *opencost.Window, lineItems []atlasplugin.LineI
}
func (a *AtlasCostSource) getAtlasCostsForWindow(win *opencost.Window, lineItems []atlasplugin.LineItem) (*pb.CustomCostResponse, error) {
func (a *AtlasCostSource) getAtlasCostsForWindow(win *opencost.Window, lineItems []atlasplugin.LineItem) *pb.CustomCostResponse {
//filter responses between
//filter responses between the win start and win end dates
costsInWindow := filterLineItemsByWindow(win, lineItems)
@ -234,11 +232,11 @@ func (a *AtlasCostSource) getAtlasCostsForWindow(win *opencost.Window, lineItems
Errors: []string{},
Costs: costsInWindow,
}
return &resp, nil
return &resp
}
func GetPendingInvoices(org string, client HTTPClient) ([]atlasplugin.LineItem, error) {
request, _ := http.NewRequest("GET", fmt.Sprintf(costExplorerPendingInvoices, org), nil)
request, _ := http.NewRequest("GET", fmt.Sprintf(costExplorerPendingInvoicesURL, org), nil)
request.Header.Set("Accept", "application/vnd.atlas.2023-01-01+json")
request.Header.Set("Content-Type", "application/vnd.atlas.2023-01-01+json")

View File

@ -124,7 +124,7 @@ func TestGetCostsPendingInvoices(t *testing.T) {
if req.Method != http.MethodGet {
t.Errorf("expected GET request, got %s", req.Method)
}
expectedURL := fmt.Sprintf(costExplorerPendingInvoices, "myOrg")
expectedURL := fmt.Sprintf(costExplorerPendingInvoicesURL, "myOrg")
if req.URL.String() != expectedURL {
t.Errorf("expected URL %s, got %s", expectedURL, req.URL.String())
}
@ -154,10 +154,7 @@ func TestGetCostErrorFromServer(t *testing.T) {
DoFunc: func(req *http.Request) (*http.Response, error) {
// Return a mock response with status 200 and mock JSON body
return &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(bytes.NewBufferString("")),
}, nil
return nil, fmt.Errorf("mock error: failed to execute request")
},
}
costs, err := GetPendingInvoices("myOrg", mockClient)
@ -226,8 +223,7 @@ func TestGetAtlasCostsForWindow(t *testing.T) {
// Create a new Window instance
window := opencost.NewWindow(&day2, &day3)
resp, error := atlasCostSource.getAtlasCostsForWindow(&window, lineItems)
assert.Nil(t, error)
resp := atlasCostSource.getAtlasCostsForWindow(&window, lineItems)
assert.True(t, resp != nil)
assert.Equal(t, "data_storage", resp.CostSource)
assert.Equal(t, "mongodb-atlas", resp.Domain)
@ -414,3 +410,82 @@ func TestFilterInvoicesOnWindow(t *testing.T) {
assert.Equal(t, lineItems[0].Quantity, filteredItems[0].UsageQuantity)
assert.Equal(t, filteredItems[0].UsageUnit, lineItems[0].Unit)
}
func TestFilterInvoicesOnWindowBadResponse(t *testing.T) {
//setup a window between october 1st and october 31st 2024
windowStart := time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC)
windowEnd := time.Date(2024, time.October, 31, 0, 0, 0, 0, time.UTC)
window := opencost.NewWindow(&windowStart, &windowEnd)
//lineItems has bad startdate and bad endDate
lineItems := []atlasplugin.LineItem{
{StartDate: "Bar", EndDate: "Foo", UnitPriceDollars: 1.0, GroupName: "kubecost0",
SKU: "0", ClusterName: "cluster-0", GroupId: "A", TotalPriceCents: 45, Quantity: 2, Unit: "GB"}, // Within window
// Partially in window
}
filteredItems := filterLineItemsByWindow(&window, lineItems)
assert.Equal(t, 0, len(filteredItems))
}
func TestBadWindow(t *testing.T) {
pendingInvoiceResponse := atlasplugin.PendingInvoice{}
mockResponseJson, _ := json.Marshal(pendingInvoiceResponse)
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
//return costs
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(mockResponseJson)),
}, nil
},
}
atlasCostSource := AtlasCostSource{
orgID: "myOrg",
atlasClient: mockClient,
}
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
customCostRequest := pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart), // Start in current month
End: timestamppb.New(currentMonthStart.Add(5 * time.Hour)), // End in 5 hours
Resolution: durationpb.New(24 * time.Hour), // 1 day resolution
}
//this window should throw an error in the opencost.GetWindows method
resp := atlasCostSource.GetCustomCosts(&customCostRequest)
assert.True(t, len(resp[0].Errors) > 0)
}
func TestGetCostsReturnsErrorForPendingInvoices(t *testing.T) {
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
//return costs
return nil, fmt.Errorf("mock error: failed to execute request")
},
}
atlasCostSource := AtlasCostSource{
orgID: "myOrg",
atlasClient: mockClient,
}
// Define the start and end time for the window
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
customCostRequest := pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart), // Start in current month
End: timestamppb.New(currentMonthStart.Add(48 * time.Hour)), // End in current month
Resolution: durationpb.New(24 * time.Hour), // 1 day resolution
}
resp := atlasCostSource.GetCustomCosts(&customCostRequest)
assert.True(t, len(resp[0].Errors) > 0)
}

View File

@ -77,8 +77,8 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
return false
}
if len(respHourly) == 0 {
log.Errorf("no hourly response received from mongodb-atlas plugin")
if len(respHourly) != 0 {
log.Errorf("mongo plugin does not support hourly costs")
return false
}

View File

@ -219,7 +219,7 @@ func (d *OpenAICostSource) getOpenAIBilling(start time.Time, end time.Time) (*op
req, errReq = http.NewRequest("GET", openAIBillingURL, nil)
if errReq != nil {
log.Warnf("error creating billing export request: %v", errReq)
log.Warnf("retrying request after 30s")
log.Infof("retrying request after 30s")
time.Sleep(30 * time.Second)
continue
}
@ -246,7 +246,7 @@ func (d *OpenAICostSource) getOpenAIBilling(start time.Time, end time.Time) (*op
errReq = fmt.Errorf("received non-200 response for billing export request: %d", resp.StatusCode)
log.Warnf("got non-200 response for billing export request: %d, body is: %s", resp.StatusCode, bodyString)
log.Warnf("retrying request after 30s")
log.Infof("retrying request after 30s")
time.Sleep(30 * time.Second)
continue
} else {
@ -304,7 +304,7 @@ func (d *OpenAICostSource) getOpenAITokenUsages(targetTime time.Time) (*openaipl
resp, errReq = client.Do(req)
if errReq != nil {
log.Warnf("error doing token request: %v", errReq)
log.Warnf("retrying request after 30s")
log.Infof("retrying request after 30s")
time.Sleep(30 * time.Second)
continue
}
@ -320,7 +320,7 @@ func (d *OpenAICostSource) getOpenAITokenUsages(targetTime time.Time) (*openaipl
bodyString = string(bodyBytes)
}
log.Warnf("got non-200 response for token usage request: %d, body is: %s", resp.StatusCode, bodyString)
log.Warnf("retrying request after 30s")
log.Infof("retrying request after 30s")
time.Sleep(30 * time.Second)
continue
} else {

View File

@ -106,7 +106,7 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
if cost.GetBilledCost() == 0 {
log.Debugf("got zero cost for %v", cost)
}
if cost.GetBilledCost() > 1 {
if cost.GetBilledCost() > 2 {
log.Errorf("daily cost returned by plugin openai for %v is greater than 1", cost)
return false
}
@ -120,7 +120,6 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
expectedCosts := []string{
"GPT-4o mini",
"GPT-4o",
"Other models",
}
for _, cost := range expectedCosts {

View File

@ -41,6 +41,10 @@ func main() {
// for each plugin given via a flag
for _, plugin := range plugins {
log.Infof("Testing plugin: %s", plugin)
plugin = strings.TrimSpace(plugin)
if plugin == "" {
continue
}
// write the config in PLUGIN_NAME_CONFIG out to a file
envVarName := fmt.Sprintf("%s_CONFIG", strings.ReplaceAll(strings.ToUpper(plugin), "-", "_"))
@ -71,9 +75,9 @@ func main() {
pluginPath := cwd + "/pkg/plugins/" + plugin
respDaily := getResponse(pluginPath, file.Name(), windowStart, windowEnd, 24*time.Hour)
// request usage for 2 days ago in hourly increments
windowStart = time.Now().AddDate(0, 0, -2).Truncate(24 * time.Hour)
windowEnd = time.Now().AddDate(0, 0, -1).Truncate(24 * time.Hour)
// request usage for 3 days ago in hourly increments
windowStart = time.Now().AddDate(0, 0, -4).Truncate(24 * time.Hour)
windowEnd = time.Now().AddDate(0, 0, -3).Truncate(24 * time.Hour)
// invoke plugin via harness
respHourly := getResponse(pluginPath, file.Name(), windowStart, windowEnd, 1*time.Hour)