types.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  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 []CustomCostProperty
  14. Accumulate opencost.AccumulateOption
  15. Filter filter.Filter
  16. }
  17. type CostTimeseriesRequest struct {
  18. Start time.Time
  19. End time.Time
  20. AggregateBy []CustomCostProperty
  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. CostSource string `json:"cost_source"`
  46. Aggregate string `json:"aggregate"`
  47. }
  48. type CostTimeseriesResponse struct {
  49. Window opencost.Window `json:"window"`
  50. Timeseries []*CostResponse `json:"timeseries"`
  51. }
  52. func NewCostResponse(ccs *CustomCostSet) *CostResponse {
  53. costResponse := &CostResponse{
  54. Window: ccs.Window,
  55. CustomCosts: []*CustomCost{},
  56. }
  57. for _, cc := range ccs.CustomCosts {
  58. costResponse.TotalBilledCost += cc.BilledCost
  59. costResponse.TotalListCost += cc.ListCost
  60. costResponse.CustomCosts = append(costResponse.CustomCosts, cc)
  61. }
  62. return costResponse
  63. }
  64. func ParseCustomCostResponse(ccResponse *pb.CustomCostResponse) []*CustomCost {
  65. costs := ccResponse.GetCosts()
  66. customCosts := make([]*CustomCost, len(costs))
  67. for i, cost := range costs {
  68. customCosts[i] = &CustomCost{
  69. Id: cost.GetId(),
  70. Zone: cost.GetZone(),
  71. AccountName: cost.GetAccountName(),
  72. ChargeCategory: cost.GetChargeCategory(),
  73. Description: cost.GetDescription(),
  74. ResourceName: cost.GetResourceName(),
  75. ResourceType: cost.GetResourceType(),
  76. ProviderId: cost.GetProviderId(),
  77. BilledCost: cost.GetBilledCost(),
  78. ListCost: cost.GetListCost(),
  79. ListUnitPrice: cost.GetListUnitPrice(),
  80. UsageQuantity: cost.GetUsageQuantity(),
  81. UsageUnit: cost.GetUsageUnit(),
  82. Domain: ccResponse.GetDomain(),
  83. CostSource: ccResponse.GetCostSource(),
  84. }
  85. }
  86. return customCosts
  87. }
  88. func (cc *CustomCost) Add(other *CustomCost) {
  89. cc.BilledCost += other.BilledCost
  90. cc.ListCost += other.ListCost
  91. cc.ListUnitPrice += other.ListUnitPrice
  92. if cc.Id != other.Id {
  93. cc.Id = ""
  94. }
  95. if cc.Zone != other.Zone {
  96. cc.Zone = ""
  97. }
  98. if cc.AccountName != other.AccountName {
  99. cc.AccountName = ""
  100. }
  101. if cc.ChargeCategory != other.ChargeCategory {
  102. cc.ChargeCategory = ""
  103. }
  104. if cc.Description != other.Description {
  105. cc.Description = ""
  106. }
  107. if cc.ResourceName != other.ResourceName {
  108. cc.ResourceName = ""
  109. }
  110. if cc.ResourceType != other.ResourceType {
  111. cc.ResourceType = ""
  112. }
  113. if cc.ProviderId != other.ProviderId {
  114. cc.ProviderId = ""
  115. }
  116. if cc.UsageUnit != other.UsageUnit {
  117. cc.UsageUnit = ""
  118. } else {
  119. // when usage units are the same, then we can sum the usages
  120. cc.UsageQuantity += other.UsageQuantity
  121. }
  122. if cc.Domain != other.Domain {
  123. cc.Domain = ""
  124. }
  125. if cc.CostSource != other.CostSource {
  126. cc.CostSource = ""
  127. }
  128. if cc.Aggregate != other.Aggregate {
  129. cc.Aggregate = ""
  130. }
  131. }
  132. type CustomCostSet struct {
  133. CustomCosts []*CustomCost
  134. Window opencost.Window
  135. }
  136. func NewCustomCostSet(window opencost.Window) *CustomCostSet {
  137. return &CustomCostSet{
  138. CustomCosts: []*CustomCost{},
  139. Window: window,
  140. }
  141. }
  142. func (ccs *CustomCostSet) Add(customCost *CustomCost) {
  143. ccs.CustomCosts = append(ccs.CustomCosts, customCost)
  144. }
  145. func (ccs *CustomCostSet) Aggregate(aggregateBy []CustomCostProperty) error {
  146. // when no aggregation, return the original CustomCostSet
  147. if len(aggregateBy) == 0 {
  148. return nil
  149. }
  150. aggMap := make(map[string]*CustomCost)
  151. for _, cc := range ccs.CustomCosts {
  152. aggKey, err := generateAggKey(cc, aggregateBy)
  153. if err != nil {
  154. return fmt.Errorf("failed to aggregate CustomCostSet: %w", err)
  155. }
  156. cc.Aggregate = aggKey
  157. if existing, ok := aggMap[aggKey]; ok {
  158. existing.Add(cc)
  159. } else {
  160. aggMap[aggKey] = cc
  161. }
  162. }
  163. var newCustomCosts []*CustomCost
  164. for _, customCost := range aggMap {
  165. newCustomCosts = append(newCustomCosts, customCost)
  166. }
  167. ccs.CustomCosts = newCustomCosts
  168. return nil
  169. }
  170. func generateAggKey(cc *CustomCost, aggregateBy []CustomCostProperty) (string, error) {
  171. var aggKeys []string
  172. for _, agg := range aggregateBy {
  173. var aggKey string
  174. if agg == CustomCostZoneProp {
  175. aggKey = cc.Zone
  176. } else if agg == CustomCostAccountNameProp {
  177. aggKey = cc.AccountName
  178. } else if agg == CustomCostChargeCategoryProp {
  179. aggKey = cc.ChargeCategory
  180. } else if agg == CustomCostDescriptionProp {
  181. aggKey = cc.Description
  182. } else if agg == CustomCostResourceNameProp {
  183. aggKey = cc.ResourceName
  184. } else if agg == CustomCostResourceTypeProp {
  185. aggKey = cc.ResourceType
  186. } else if agg == CustomCostProviderIdProp {
  187. aggKey = cc.ProviderId
  188. } else if agg == CustomCostUsageUnitProp {
  189. aggKey = cc.UsageUnit
  190. } else if agg == CustomCostDomainProp {
  191. aggKey = cc.Domain
  192. } else if agg == CustomCostCostSourceProp {
  193. aggKey = cc.CostSource
  194. } else {
  195. return "", fmt.Errorf("unsupported aggregation type: %s", agg)
  196. }
  197. if len(aggKey) == 0 {
  198. aggKey = opencost.UnallocatedSuffix
  199. }
  200. aggKeys = append(aggKeys, aggKey)
  201. }
  202. aggKey := strings.Join(aggKeys, "/")
  203. return aggKey, nil
  204. }