types.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. package customcost
  2. import (
  3. "fmt"
  4. "strings"
  5. "time"
  6. "github.com/opencost/opencost/core/pkg/filter"
  7. "github.com/opencost/opencost/core/pkg/model/pb"
  8. "github.com/opencost/opencost/core/pkg/opencost"
  9. )
  10. type CostTotalRequest struct {
  11. Start time.Time
  12. End time.Time
  13. AggregateBy []string
  14. Accumulate opencost.AccumulateOption
  15. Filter filter.Filter
  16. }
  17. type CostTimeseriesRequest struct {
  18. Start time.Time
  19. End time.Time
  20. AggregateBy []string
  21. Accumulate opencost.AccumulateOption
  22. Filter filter.Filter
  23. }
  24. type CostResponse struct {
  25. Window opencost.Window `json:"window"`
  26. TotalBilledCost float32 `json:"totalBilledCost"`
  27. TotalListCost float32 `json:"totalListCost"`
  28. CustomCosts []*CustomCost `json:"customCosts"`
  29. }
  30. type CustomCost struct {
  31. Id string `json:"id"`
  32. Zone string `json:"zone"`
  33. AccountName string `json:"account_name"`
  34. ChargeCategory string `json:"charge_category"`
  35. Description string `json:"description"`
  36. ResourceName string `json:"resource_name"`
  37. ResourceType string `json:"resource_type"`
  38. ProviderId string `json:"provider_id"`
  39. BilledCost float32 `json:"billedCost"`
  40. ListCost float32 `json:"listCost"`
  41. ListUnitPrice float32 `json:"list_unit_price"`
  42. UsageQuantity float32 `json:"usage_quantity"`
  43. UsageUnit string `json:"usage_unit"`
  44. Domain string `json:"domain"`
  45. Aggregate string `json:"aggregate"`
  46. }
  47. type CostTimeseriesResponse struct {
  48. Window opencost.Window `json:"window"`
  49. Timeseries []*CostResponse `json:"timeseries"`
  50. }
  51. func NewCostResponse(ccs *CustomCostSet) *CostResponse {
  52. costResponse := &CostResponse{
  53. Window: ccs.Window,
  54. CustomCosts: []*CustomCost{},
  55. }
  56. for _, cc := range ccs.CustomCosts {
  57. costResponse.TotalBilledCost += cc.BilledCost
  58. costResponse.TotalListCost += cc.ListCost
  59. costResponse.CustomCosts = append(costResponse.CustomCosts, cc)
  60. }
  61. return costResponse
  62. }
  63. func ParseCustomCostResponse(ccResponse *pb.CustomCostResponse) []*CustomCost {
  64. costs := ccResponse.GetCosts()
  65. customCosts := make([]*CustomCost, len(costs))
  66. for i, cost := range costs {
  67. customCosts[i] = &CustomCost{
  68. Id: cost.GetId(),
  69. Zone: cost.GetZone(),
  70. AccountName: cost.GetAccountName(),
  71. ChargeCategory: cost.GetChargeCategory(),
  72. Description: cost.GetDescription(),
  73. ResourceName: cost.GetResourceName(),
  74. ResourceType: cost.GetResourceType(),
  75. ProviderId: cost.GetProviderId(),
  76. BilledCost: cost.GetBilledCost(),
  77. ListCost: cost.GetListCost(),
  78. ListUnitPrice: cost.GetListUnitPrice(),
  79. UsageQuantity: cost.GetUsageQuantity(),
  80. UsageUnit: cost.GetUsageUnit(),
  81. Domain: ccResponse.GetDomain(),
  82. }
  83. }
  84. return customCosts
  85. }
  86. func (cc *CustomCost) Add(other *CustomCost) {
  87. cc.BilledCost += other.BilledCost
  88. cc.ListCost += other.ListCost
  89. cc.ListUnitPrice += other.ListUnitPrice
  90. if cc.Id != other.Id {
  91. cc.Id = ""
  92. }
  93. if cc.Zone != other.Zone {
  94. cc.Zone = ""
  95. }
  96. if cc.AccountName != other.AccountName {
  97. cc.AccountName = ""
  98. }
  99. if cc.ChargeCategory != other.ChargeCategory {
  100. cc.ChargeCategory = ""
  101. }
  102. if cc.Description != other.Description {
  103. cc.Description = ""
  104. }
  105. if cc.ResourceName != other.ResourceName {
  106. cc.ResourceName = ""
  107. }
  108. if cc.ResourceType != other.ResourceType {
  109. cc.ResourceType = ""
  110. }
  111. if cc.ProviderId != other.ProviderId {
  112. cc.ProviderId = ""
  113. }
  114. if cc.UsageUnit != other.UsageUnit {
  115. cc.UsageUnit = ""
  116. } else {
  117. // when usage units are the same, then we can sum the usages
  118. cc.UsageQuantity += other.UsageQuantity
  119. }
  120. if cc.Domain != other.Domain {
  121. cc.Domain = ""
  122. }
  123. if cc.Aggregate != other.Aggregate {
  124. cc.Aggregate = ""
  125. }
  126. }
  127. type CustomCostSet struct {
  128. CustomCosts []*CustomCost
  129. Window opencost.Window
  130. }
  131. func NewCustomCostSet(window opencost.Window) *CustomCostSet {
  132. return &CustomCostSet{
  133. CustomCosts: []*CustomCost{},
  134. Window: window,
  135. }
  136. }
  137. func (ccs *CustomCostSet) Add(customCosts []*CustomCost) {
  138. ccs.CustomCosts = append(ccs.CustomCosts, customCosts...)
  139. }
  140. func (ccs *CustomCostSet) Aggregate(aggregateBy []string) error {
  141. // when no aggregation, return the original CustomCostSet
  142. if len(aggregateBy) == 0 {
  143. return nil
  144. }
  145. aggMap := make(map[string]*CustomCost)
  146. for _, cc := range ccs.CustomCosts {
  147. aggKey, err := generateAggKey(cc, aggregateBy)
  148. if err != nil {
  149. return fmt.Errorf("failed to aggregate CustomCostSet: %w", err)
  150. }
  151. cc.Aggregate = aggKey
  152. if existing, ok := aggMap[aggKey]; ok {
  153. existing.Add(cc)
  154. } else {
  155. aggMap[aggKey] = cc
  156. }
  157. }
  158. var newCustomCosts []*CustomCost
  159. for _, customCost := range aggMap {
  160. newCustomCosts = append(newCustomCosts, customCost)
  161. }
  162. ccs.CustomCosts = newCustomCosts
  163. return nil
  164. }
  165. func generateAggKey(cc *CustomCost, aggregateBy []string) (string, error) {
  166. var aggKeys []string
  167. for _, agg := range aggregateBy {
  168. // TODO only domain is supported currently
  169. if agg == string(CustomCostDomainProp) {
  170. aggKeys = append(aggKeys, cc.Domain)
  171. } else {
  172. return "", fmt.Errorf("unsupported aggregation type: %s", agg)
  173. }
  174. }
  175. aggKey := strings.Join(aggKeys, "/")
  176. return aggKey, nil
  177. }