types.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. package customcost
  2. import (
  3. "fmt"
  4. "sort"
  5. "strings"
  6. "time"
  7. "github.com/opencost/opencost/core/pkg/filter"
  8. "github.com/opencost/opencost/core/pkg/log"
  9. "github.com/opencost/opencost/core/pkg/model/pb"
  10. "github.com/opencost/opencost/core/pkg/opencost"
  11. )
  12. type CostType string
  13. type SortProperty string
  14. type SortDirection string
  15. const (
  16. CostTypeBlended CostType = "blended"
  17. CostTypeList CostType = "list"
  18. CostTypeBilled CostType = "billed"
  19. SortPropertyCost SortProperty = "cost"
  20. SortPropertyAggregate SortProperty = "aggregate"
  21. SortPropertyCostType SortProperty = "costType"
  22. SortDirectionAsc SortDirection = "asc"
  23. SortDirectionDesc SortDirection = "desc"
  24. )
  25. type CostTotalRequest struct {
  26. Start time.Time
  27. End time.Time
  28. AggregateBy []CustomCostProperty
  29. Accumulate opencost.AccumulateOption
  30. Filter filter.Filter
  31. CostType CostType
  32. SortBy SortProperty
  33. SortDirection SortDirection
  34. }
  35. type CostTimeseriesRequest struct {
  36. Start time.Time
  37. End time.Time
  38. AggregateBy []CustomCostProperty
  39. Accumulate opencost.AccumulateOption
  40. Filter filter.Filter
  41. CostType CostType
  42. SortBy SortProperty
  43. SortDirection SortDirection
  44. }
  45. type CostResponse struct {
  46. Window opencost.Window `json:"window"`
  47. TotalCost float32 `json:"totalCost"`
  48. TotalCostType CostType `json:"totalCostType"`
  49. CustomCosts []*CustomCost `json:"customCosts"`
  50. }
  51. type CustomCost struct {
  52. Id string `json:"id"`
  53. Zone string `json:"zone"`
  54. AccountName string `json:"account_name"`
  55. ChargeCategory string `json:"charge_category"`
  56. Description string `json:"description"`
  57. ResourceName string `json:"resource_name"`
  58. ResourceType string `json:"resource_type"`
  59. ProviderId string `json:"provider_id"`
  60. Cost float32 `json:"cost"`
  61. ListUnitPrice float32 `json:"list_unit_price"`
  62. UsageQuantity float32 `json:"usage_quantity"`
  63. UsageUnit string `json:"usage_unit"`
  64. Domain string `json:"domain"`
  65. CostSource string `json:"cost_source"`
  66. Aggregate string `json:"aggregate"`
  67. CostType CostType `json:"cost_type"`
  68. }
  69. type CostTimeseriesResponse struct {
  70. Window opencost.Window `json:"window"`
  71. Timeseries []*CostResponse `json:"timeseries"`
  72. }
  73. func NewCostResponse(ccs *CustomCostSet, costType CostType) *CostResponse {
  74. costResponse := &CostResponse{
  75. Window: ccs.Window,
  76. CustomCosts: []*CustomCost{},
  77. TotalCostType: costType,
  78. }
  79. for _, cc := range ccs.CustomCosts {
  80. costResponse.TotalCost += cc.Cost
  81. costResponse.CustomCosts = append(costResponse.CustomCosts, cc)
  82. }
  83. return costResponse
  84. }
  85. func ParseCostType(costTypeStr string) (CostType, error) {
  86. switch costTypeStr {
  87. case string(CostTypeBlended):
  88. return CostTypeBlended, nil
  89. case string(CostTypeList):
  90. return CostTypeList, nil
  91. case string(CostTypeBilled):
  92. return CostTypeBilled, nil
  93. default:
  94. return "", fmt.Errorf("unsupported cost type: %s", costTypeStr)
  95. }
  96. }
  97. func ParseCustomCostResponse(ccResponse *pb.CustomCostResponse, costType CostType) []*CustomCost {
  98. costs := ccResponse.GetCosts()
  99. customCosts := []*CustomCost{}
  100. for _, cost := range costs {
  101. selectedCost, selectedCostType := determineCost(cost, costType)
  102. if selectedCost == 0 {
  103. log.Debugf("cost %s had 0 cost for cost type %s, not including in response", cost.ProviderId, costType)
  104. continue
  105. }
  106. customCosts = append(customCosts, &CustomCost{
  107. Id: cost.GetId(),
  108. Zone: cost.GetZone(),
  109. AccountName: cost.GetAccountName(),
  110. ChargeCategory: cost.GetChargeCategory(),
  111. Description: cost.GetDescription(),
  112. ResourceName: cost.GetResourceName(),
  113. ResourceType: cost.GetResourceType(),
  114. ProviderId: cost.GetProviderId(),
  115. Cost: selectedCost,
  116. ListUnitPrice: cost.GetListUnitPrice(),
  117. UsageQuantity: cost.GetUsageQuantity(),
  118. UsageUnit: cost.GetUsageUnit(),
  119. Domain: ccResponse.GetDomain(),
  120. CostSource: ccResponse.GetCostSource(),
  121. CostType: selectedCostType,
  122. })
  123. }
  124. return customCosts
  125. }
  126. func determineCost(cc *pb.CustomCost, costType CostType) (float32, CostType) {
  127. switch costType {
  128. // if the cost type is blended, first check if the billed cost is non-zero
  129. // if it is, return the billed cost
  130. // if it is zero, return the list cost
  131. case CostTypeBlended:
  132. if cc.BilledCost > 0 {
  133. return cc.BilledCost, CostTypeBilled
  134. }
  135. return cc.ListCost, CostTypeList
  136. // if the cost type is list, return the list cost
  137. case CostTypeList:
  138. return cc.ListCost, CostTypeList
  139. // if the cost type is billed, return the billed cost
  140. case CostTypeBilled:
  141. return cc.BilledCost, CostTypeBilled
  142. default:
  143. return 0, ""
  144. }
  145. }
  146. func (cc *CustomCost) Add(other *CustomCost) {
  147. cc.Cost += other.Cost
  148. cc.ListUnitPrice += other.ListUnitPrice
  149. if cc.CostType != other.CostType {
  150. cc.CostType = CostTypeBlended
  151. }
  152. if cc.Id != other.Id {
  153. cc.Id = ""
  154. }
  155. if cc.Zone != other.Zone {
  156. cc.Zone = ""
  157. }
  158. if cc.AccountName != other.AccountName {
  159. cc.AccountName = ""
  160. }
  161. if cc.ChargeCategory != other.ChargeCategory {
  162. cc.ChargeCategory = ""
  163. }
  164. if cc.Description != other.Description {
  165. cc.Description = ""
  166. }
  167. if cc.ResourceName != other.ResourceName {
  168. cc.ResourceName = ""
  169. }
  170. if cc.ResourceType != other.ResourceType {
  171. cc.ResourceType = ""
  172. }
  173. if cc.ProviderId != other.ProviderId {
  174. cc.ProviderId = ""
  175. }
  176. if cc.UsageUnit != other.UsageUnit {
  177. cc.UsageUnit = ""
  178. } else {
  179. // when usage units are the same, then we can sum the usages
  180. cc.UsageQuantity += other.UsageQuantity
  181. }
  182. if cc.Domain != other.Domain {
  183. cc.Domain = ""
  184. }
  185. if cc.CostSource != other.CostSource {
  186. cc.CostSource = ""
  187. }
  188. if cc.Aggregate != other.Aggregate {
  189. cc.Aggregate = ""
  190. }
  191. }
  192. type CustomCostSet struct {
  193. CustomCosts []*CustomCost
  194. Window opencost.Window
  195. }
  196. func NewCustomCostSet(window opencost.Window) *CustomCostSet {
  197. return &CustomCostSet{
  198. CustomCosts: []*CustomCost{},
  199. Window: window,
  200. }
  201. }
  202. func (ccs *CustomCostSet) Add(customCost *CustomCost) {
  203. ccs.CustomCosts = append(ccs.CustomCosts, customCost)
  204. }
  205. func (ccs *CustomCostSet) Aggregate(aggregateBy []CustomCostProperty) error {
  206. // when no aggregation, return the original CustomCostSet
  207. if len(aggregateBy) == 0 {
  208. return nil
  209. }
  210. aggMap := make(map[string]*CustomCost)
  211. for _, cc := range ccs.CustomCosts {
  212. aggKey, err := generateAggKey(cc, aggregateBy)
  213. if err != nil {
  214. return fmt.Errorf("failed to aggregate CustomCostSet: %w", err)
  215. }
  216. cc.Aggregate = aggKey
  217. if existing, ok := aggMap[aggKey]; ok {
  218. existing.Add(cc)
  219. } else {
  220. aggMap[aggKey] = cc
  221. }
  222. }
  223. var newCustomCosts []*CustomCost
  224. for _, customCost := range aggMap {
  225. newCustomCosts = append(newCustomCosts, customCost)
  226. }
  227. ccs.CustomCosts = newCustomCosts
  228. return nil
  229. }
  230. func generateAggKey(cc *CustomCost, aggregateBy []CustomCostProperty) (string, error) {
  231. var aggKeys []string
  232. for _, agg := range aggregateBy {
  233. var aggKey string
  234. if agg == CustomCostZoneProp {
  235. aggKey = cc.Zone
  236. } else if agg == CustomCostAccountNameProp {
  237. aggKey = cc.AccountName
  238. } else if agg == CustomCostChargeCategoryProp {
  239. aggKey = cc.ChargeCategory
  240. } else if agg == CustomCostDescriptionProp {
  241. aggKey = cc.Description
  242. } else if agg == CustomCostResourceNameProp {
  243. aggKey = cc.ResourceName
  244. } else if agg == CustomCostResourceTypeProp {
  245. aggKey = cc.ResourceType
  246. } else if agg == CustomCostProviderIdProp {
  247. aggKey = cc.ProviderId
  248. } else if agg == CustomCostUsageUnitProp {
  249. aggKey = cc.UsageUnit
  250. } else if agg == CustomCostDomainProp {
  251. aggKey = cc.Domain
  252. } else if agg == CustomCostCostSourceProp {
  253. aggKey = cc.CostSource
  254. } else {
  255. return "", fmt.Errorf("unsupported aggregation type: %s", agg)
  256. }
  257. if len(aggKey) == 0 {
  258. aggKey = opencost.UnallocatedSuffix
  259. }
  260. aggKeys = append(aggKeys, aggKey)
  261. }
  262. aggKey := strings.Join(aggKeys, "/")
  263. return aggKey, nil
  264. }
  265. func (ccs *CustomCostSet) Sort(sortBy SortProperty, sortDirection SortDirection) {
  266. switch sortBy {
  267. case SortPropertyCost:
  268. if sortDirection == SortDirectionAsc {
  269. sort.Slice(ccs.CustomCosts, func(i, j int) bool {
  270. return ccs.CustomCosts[i].Cost < ccs.CustomCosts[j].Cost
  271. })
  272. } else {
  273. sort.Slice(ccs.CustomCosts, func(i, j int) bool {
  274. return ccs.CustomCosts[i].Cost > ccs.CustomCosts[j].Cost
  275. })
  276. }
  277. case SortPropertyAggregate:
  278. if sortDirection == SortDirectionAsc {
  279. sort.Slice(ccs.CustomCosts, func(i, j int) bool {
  280. return ccs.CustomCosts[i].Aggregate < ccs.CustomCosts[j].Aggregate
  281. })
  282. } else {
  283. sort.Slice(ccs.CustomCosts, func(i, j int) bool {
  284. return ccs.CustomCosts[i].Aggregate > ccs.CustomCosts[j].Aggregate
  285. })
  286. }
  287. case SortPropertyCostType:
  288. if sortDirection == SortDirectionAsc {
  289. sort.Slice(ccs.CustomCosts, func(i, j int) bool {
  290. return ccs.CustomCosts[i].CostType < ccs.CustomCosts[j].CostType
  291. })
  292. } else {
  293. sort.Slice(ccs.CustomCosts, func(i, j int) bool {
  294. return ccs.CustomCosts[i].CostType > ccs.CustomCosts[j].CostType
  295. })
  296. }
  297. }
  298. }