| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- package customcost
- import (
- "fmt"
- "sort"
- "strings"
- "time"
- "github.com/opencost/opencost/core/pkg/filter"
- "github.com/opencost/opencost/core/pkg/log"
- "github.com/opencost/opencost/core/pkg/model/pb"
- "github.com/opencost/opencost/core/pkg/opencost"
- )
- type CostType string
- type SortProperty string
- type SortDirection string
- const (
- CostTypeBlended CostType = "blended"
- CostTypeList CostType = "list"
- CostTypeBilled CostType = "billed"
- SortPropertyCost SortProperty = "cost"
- SortPropertyAggregate SortProperty = "aggregate"
- SortPropertyCostType SortProperty = "costType"
- SortDirectionAsc SortDirection = "asc"
- SortDirectionDesc SortDirection = "desc"
- )
- type CostTotalRequest struct {
- Start time.Time
- End time.Time
- AggregateBy []CustomCostProperty
- Accumulate opencost.AccumulateOption
- Filter filter.Filter
- CostType CostType
- SortBy SortProperty
- SortDirection SortDirection
- }
- type CostTimeseriesRequest struct {
- Start time.Time
- End time.Time
- AggregateBy []CustomCostProperty
- Accumulate opencost.AccumulateOption
- Filter filter.Filter
- CostType CostType
- SortBy SortProperty
- SortDirection SortDirection
- }
- type CostResponse struct {
- Window opencost.Window `json:"window"`
- TotalCost float32 `json:"totalCost"`
- TotalCostType CostType `json:"totalCostType"`
- CustomCosts []*CustomCost `json:"customCosts"`
- }
- type CustomCost struct {
- Id string `json:"id"`
- Zone string `json:"zone"`
- AccountName string `json:"account_name"`
- ChargeCategory string `json:"charge_category"`
- Description string `json:"description"`
- ResourceName string `json:"resource_name"`
- ResourceType string `json:"resource_type"`
- ProviderId string `json:"provider_id"`
- Cost float32 `json:"cost"`
- ListUnitPrice float32 `json:"list_unit_price"`
- UsageQuantity float32 `json:"usage_quantity"`
- UsageUnit string `json:"usage_unit"`
- Domain string `json:"domain"`
- CostSource string `json:"cost_source"`
- Aggregate string `json:"aggregate"`
- CostType CostType `json:"cost_type"`
- }
- type CostTimeseriesResponse struct {
- Window opencost.Window `json:"window"`
- Timeseries []*CostResponse `json:"timeseries"`
- }
- func NewCostResponse(ccs *CustomCostSet, costType CostType) *CostResponse {
- costResponse := &CostResponse{
- Window: ccs.Window,
- CustomCosts: []*CustomCost{},
- TotalCostType: costType,
- }
- for _, cc := range ccs.CustomCosts {
- costResponse.TotalCost += cc.Cost
- costResponse.CustomCosts = append(costResponse.CustomCosts, cc)
- }
- return costResponse
- }
- func ParseCostType(costTypeStr string) (CostType, error) {
- switch costTypeStr {
- case string(CostTypeBlended):
- return CostTypeBlended, nil
- case string(CostTypeList):
- return CostTypeList, nil
- case string(CostTypeBilled):
- return CostTypeBilled, nil
- default:
- return "", fmt.Errorf("unsupported cost type: %s", costTypeStr)
- }
- }
- func ParseCustomCostResponse(ccResponse *pb.CustomCostResponse, costType CostType) []*CustomCost {
- costs := ccResponse.GetCosts()
- customCosts := []*CustomCost{}
- for _, cost := range costs {
- selectedCost, selectedCostType := determineCost(cost, costType)
- if selectedCost == 0 {
- log.Debugf("cost %s had 0 cost for cost type %s, not including in response", cost.ProviderId, costType)
- continue
- }
- customCosts = append(customCosts, &CustomCost{
- Id: cost.GetId(),
- Zone: cost.GetZone(),
- AccountName: cost.GetAccountName(),
- ChargeCategory: cost.GetChargeCategory(),
- Description: cost.GetDescription(),
- ResourceName: cost.GetResourceName(),
- ResourceType: cost.GetResourceType(),
- ProviderId: cost.GetProviderId(),
- Cost: selectedCost,
- ListUnitPrice: cost.GetListUnitPrice(),
- UsageQuantity: cost.GetUsageQuantity(),
- UsageUnit: cost.GetUsageUnit(),
- Domain: ccResponse.GetDomain(),
- CostSource: ccResponse.GetCostSource(),
- CostType: selectedCostType,
- })
- }
- return customCosts
- }
- func determineCost(cc *pb.CustomCost, costType CostType) (float32, CostType) {
- switch costType {
- // if the cost type is blended, first check if the billed cost is non-zero
- // if it is, return the billed cost
- // if it is zero, return the list cost
- case CostTypeBlended:
- if cc.BilledCost > 0 {
- return cc.BilledCost, CostTypeBilled
- }
- return cc.ListCost, CostTypeList
- // if the cost type is list, return the list cost
- case CostTypeList:
- return cc.ListCost, CostTypeList
- // if the cost type is billed, return the billed cost
- case CostTypeBilled:
- return cc.BilledCost, CostTypeBilled
- default:
- return 0, ""
- }
- }
- func (cc *CustomCost) Add(other *CustomCost) {
- cc.Cost += other.Cost
- cc.ListUnitPrice += other.ListUnitPrice
- if cc.CostType != other.CostType {
- cc.CostType = CostTypeBlended
- }
- if cc.Id != other.Id {
- cc.Id = ""
- }
- if cc.Zone != other.Zone {
- cc.Zone = ""
- }
- if cc.AccountName != other.AccountName {
- cc.AccountName = ""
- }
- if cc.ChargeCategory != other.ChargeCategory {
- cc.ChargeCategory = ""
- }
- if cc.Description != other.Description {
- cc.Description = ""
- }
- if cc.ResourceName != other.ResourceName {
- cc.ResourceName = ""
- }
- if cc.ResourceType != other.ResourceType {
- cc.ResourceType = ""
- }
- if cc.ProviderId != other.ProviderId {
- cc.ProviderId = ""
- }
- if cc.UsageUnit != other.UsageUnit {
- cc.UsageUnit = ""
- } else {
- // when usage units are the same, then we can sum the usages
- cc.UsageQuantity += other.UsageQuantity
- }
- if cc.Domain != other.Domain {
- cc.Domain = ""
- }
- if cc.CostSource != other.CostSource {
- cc.CostSource = ""
- }
- if cc.Aggregate != other.Aggregate {
- cc.Aggregate = ""
- }
- }
- type CustomCostSet struct {
- CustomCosts []*CustomCost
- Window opencost.Window
- }
- func NewCustomCostSet(window opencost.Window) *CustomCostSet {
- return &CustomCostSet{
- CustomCosts: []*CustomCost{},
- Window: window,
- }
- }
- func (ccs *CustomCostSet) Add(customCost *CustomCost) {
- ccs.CustomCosts = append(ccs.CustomCosts, customCost)
- }
- func (ccs *CustomCostSet) Aggregate(aggregateBy []CustomCostProperty) error {
- // when no aggregation, return the original CustomCostSet
- if len(aggregateBy) == 0 {
- return nil
- }
- aggMap := make(map[string]*CustomCost)
- for _, cc := range ccs.CustomCosts {
- aggKey, err := generateAggKey(cc, aggregateBy)
- if err != nil {
- return fmt.Errorf("failed to aggregate CustomCostSet: %w", err)
- }
- cc.Aggregate = aggKey
- if existing, ok := aggMap[aggKey]; ok {
- existing.Add(cc)
- } else {
- aggMap[aggKey] = cc
- }
- }
- var newCustomCosts []*CustomCost
- for _, customCost := range aggMap {
- newCustomCosts = append(newCustomCosts, customCost)
- }
- ccs.CustomCosts = newCustomCosts
- return nil
- }
- func generateAggKey(cc *CustomCost, aggregateBy []CustomCostProperty) (string, error) {
- var aggKeys []string
- for _, agg := range aggregateBy {
- var aggKey string
- if agg == CustomCostZoneProp {
- aggKey = cc.Zone
- } else if agg == CustomCostAccountNameProp {
- aggKey = cc.AccountName
- } else if agg == CustomCostChargeCategoryProp {
- aggKey = cc.ChargeCategory
- } else if agg == CustomCostDescriptionProp {
- aggKey = cc.Description
- } else if agg == CustomCostResourceNameProp {
- aggKey = cc.ResourceName
- } else if agg == CustomCostResourceTypeProp {
- aggKey = cc.ResourceType
- } else if agg == CustomCostProviderIdProp {
- aggKey = cc.ProviderId
- } else if agg == CustomCostUsageUnitProp {
- aggKey = cc.UsageUnit
- } else if agg == CustomCostDomainProp {
- aggKey = cc.Domain
- } else if agg == CustomCostCostSourceProp {
- aggKey = cc.CostSource
- } else {
- return "", fmt.Errorf("unsupported aggregation type: %s", agg)
- }
- if len(aggKey) == 0 {
- aggKey = opencost.UnallocatedSuffix
- }
- aggKeys = append(aggKeys, aggKey)
- }
- aggKey := strings.Join(aggKeys, "/")
- return aggKey, nil
- }
- func (ccs *CustomCostSet) Sort(sortBy SortProperty, sortDirection SortDirection) {
- switch sortBy {
- case SortPropertyCost:
- if sortDirection == SortDirectionAsc {
- sort.Slice(ccs.CustomCosts, func(i, j int) bool {
- return ccs.CustomCosts[i].Cost < ccs.CustomCosts[j].Cost
- })
- } else {
- sort.Slice(ccs.CustomCosts, func(i, j int) bool {
- return ccs.CustomCosts[i].Cost > ccs.CustomCosts[j].Cost
- })
- }
- case SortPropertyAggregate:
- if sortDirection == SortDirectionAsc {
- sort.Slice(ccs.CustomCosts, func(i, j int) bool {
- return ccs.CustomCosts[i].Aggregate < ccs.CustomCosts[j].Aggregate
- })
- } else {
- sort.Slice(ccs.CustomCosts, func(i, j int) bool {
- return ccs.CustomCosts[i].Aggregate > ccs.CustomCosts[j].Aggregate
- })
- }
- case SortPropertyCostType:
- if sortDirection == SortDirectionAsc {
- sort.Slice(ccs.CustomCosts, func(i, j int) bool {
- return ccs.CustomCosts[i].CostType < ccs.CustomCosts[j].CostType
- })
- } else {
- sort.Slice(ccs.CustomCosts, func(i, j int) bool {
- return ccs.CustomCosts[i].CostType > ccs.CustomCosts[j].CostType
- })
- }
- }
- }
|