cloudcost.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. package kubecost
  2. import (
  3. "errors"
  4. "fmt"
  5. "time"
  6. "github.com/opencost/opencost/pkg/filter"
  7. "github.com/opencost/opencost/pkg/log"
  8. )
  9. // CloudCost represents a CUR line item, identifying a cloud resource and
  10. // its cost over some period of time.
  11. type CloudCost struct {
  12. Properties *CloudCostProperties `json:"properties"`
  13. Window Window `json:"window"`
  14. ListCost CostMetric `json:"listCost"`
  15. NetCost CostMetric `json:"netCost"`
  16. AmortizedNetCost CostMetric `json:"amortizedNetCost"`
  17. InvoicedCost CostMetric `json:"invoicedCost"`
  18. AmortizedCost CostMetric `json:"amortizedCost"`
  19. }
  20. // NewCloudCost instantiates a new CloudCost
  21. func NewCloudCost(start, end time.Time, ccProperties *CloudCostProperties, kubernetesPercent, listCost, netCost, amortizedNetCost, invoicedCost, amortizedCost float64) *CloudCost {
  22. return &CloudCost{
  23. Properties: ccProperties,
  24. Window: NewWindow(&start, &end),
  25. ListCost: CostMetric{
  26. Cost: listCost,
  27. KubernetesPercent: kubernetesPercent,
  28. },
  29. NetCost: CostMetric{
  30. Cost: netCost,
  31. KubernetesPercent: kubernetesPercent,
  32. },
  33. AmortizedNetCost: CostMetric{
  34. Cost: amortizedNetCost,
  35. KubernetesPercent: kubernetesPercent,
  36. },
  37. InvoicedCost: CostMetric{
  38. Cost: invoicedCost,
  39. KubernetesPercent: kubernetesPercent,
  40. },
  41. AmortizedCost: CostMetric{
  42. Cost: amortizedCost,
  43. KubernetesPercent: kubernetesPercent,
  44. },
  45. }
  46. }
  47. func (cc *CloudCost) Clone() *CloudCost {
  48. return &CloudCost{
  49. Properties: cc.Properties.Clone(),
  50. Window: cc.Window.Clone(),
  51. ListCost: cc.ListCost.Clone(),
  52. NetCost: cc.NetCost.Clone(),
  53. AmortizedNetCost: cc.AmortizedNetCost.Clone(),
  54. InvoicedCost: cc.InvoicedCost.Clone(),
  55. AmortizedCost: cc.AmortizedCost.Clone(),
  56. }
  57. }
  58. func (cc *CloudCost) Equal(that *CloudCost) bool {
  59. if that == nil {
  60. return false
  61. }
  62. return cc.Properties.Equal(that.Properties) &&
  63. cc.Window.Equal(that.Window) &&
  64. cc.ListCost.Equal(that.ListCost) &&
  65. cc.NetCost.Equal(that.NetCost) &&
  66. cc.AmortizedNetCost.Equal(that.AmortizedNetCost) &&
  67. cc.InvoicedCost.Equal(that.InvoicedCost) &&
  68. cc.AmortizedCost.Equal(that.AmortizedCost)
  69. }
  70. func (cc *CloudCost) add(that *CloudCost) {
  71. if cc == nil {
  72. log.Warnf("cannot add to nil CloudCost")
  73. return
  74. }
  75. // Preserve properties of cloud cost that are matching between the two CloudCost
  76. cc.Properties = cc.Properties.Intersection(that.Properties)
  77. cc.ListCost = cc.ListCost.add(that.ListCost)
  78. cc.NetCost = cc.NetCost.add(that.NetCost)
  79. cc.AmortizedNetCost = cc.AmortizedNetCost.add(that.AmortizedNetCost)
  80. cc.InvoicedCost = cc.InvoicedCost.add(that.InvoicedCost)
  81. cc.AmortizedCost = cc.AmortizedCost.add(that.AmortizedCost)
  82. cc.Window = cc.Window.Expand(that.Window)
  83. }
  84. func (cc *CloudCost) StringProperty(prop string) (string, error) {
  85. if cc == nil {
  86. return "", nil
  87. }
  88. switch prop {
  89. case CloudCostInvoiceEntityIDProp:
  90. return cc.Properties.InvoiceEntityID, nil
  91. case CloudCostAccountIDProp:
  92. return cc.Properties.AccountID, nil
  93. case CloudCostProviderProp:
  94. return cc.Properties.Provider, nil
  95. case CloudCostProviderIDProp:
  96. return cc.Properties.ProviderID, nil
  97. case CloudCostServiceProp:
  98. return cc.Properties.Service, nil
  99. case CloudCostCategoryProp:
  100. return cc.Properties.Category, nil
  101. default:
  102. return "", fmt.Errorf("invalid property name: %s", prop)
  103. }
  104. }
  105. func (cc *CloudCost) StringMapProperty(property string) (map[string]string, error) {
  106. switch property {
  107. case CloudCostLabelProp:
  108. if cc.Properties == nil {
  109. return nil, nil
  110. }
  111. return cc.Properties.Labels, nil
  112. default:
  113. return nil, fmt.Errorf("CloudCost: StringMapProperty: invalid property name: %s", property)
  114. }
  115. }
  116. func (cc *CloudCost) GetCostMetric(costMetricName string) (CostMetric, error) {
  117. switch costMetricName {
  118. case ListCostMetric:
  119. return cc.ListCost, nil
  120. case NetCostMetric:
  121. return cc.NetCost, nil
  122. case AmortizedNetCostMetric:
  123. return cc.AmortizedNetCost, nil
  124. case InvoicedCostMetric:
  125. return cc.InvoicedCost, nil
  126. case AmortizedCostMetric:
  127. return cc.AmortizedCost, nil
  128. }
  129. return CostMetric{}, fmt.Errorf("invalid Cost Metric: %s", costMetricName)
  130. }
  131. // WeightCostMetrics weights all the cost metrics with the given weightedAverage
  132. func (cc *CloudCost) WeightCostMetrics(weightedAverge float64) {
  133. cc.ListCost.Cost *= weightedAverge
  134. cc.NetCost.Cost *= weightedAverge
  135. cc.AmortizedNetCost.Cost *= weightedAverge
  136. cc.InvoicedCost.Cost *= weightedAverge
  137. cc.AmortizedCost.Cost *= weightedAverge
  138. }
  139. // CloudCostSet follows the established set pattern of windowed data types. It has addition metadata types that can be
  140. // used to preserve data consistency and be used for validation.
  141. // - Integration is the ID for the integration that a CloudCostSet was sourced from, this value is cleared if when a
  142. // set is joined with another with a different key
  143. // - AggregationProperties is set by the Aggregate function and ensures that any additional inserts are keyed correctly
  144. type CloudCostSet struct {
  145. CloudCosts map[string]*CloudCost `json:"cloudCosts"`
  146. Window Window `json:"window"`
  147. Integration string `json:"-"`
  148. AggregationProperties []string `json:"aggregationProperties"`
  149. }
  150. // NewCloudCostSet instantiates a new CloudCostSet and, optionally, inserts
  151. // the given list of CloudCosts
  152. func NewCloudCostSet(start, end time.Time, cloudCosts ...*CloudCost) *CloudCostSet {
  153. ccs := &CloudCostSet{
  154. CloudCosts: map[string]*CloudCost{},
  155. Window: NewWindow(&start, &end),
  156. }
  157. for _, cc := range cloudCosts {
  158. ccs.Insert(cc)
  159. }
  160. return ccs
  161. }
  162. func (ccs *CloudCostSet) Aggregate(props []string) (*CloudCostSet, error) {
  163. if ccs == nil {
  164. return nil, errors.New("cannot aggregate a nil CloudCostSet")
  165. }
  166. if ccs.Window.IsOpen() {
  167. return nil, fmt.Errorf("cannot aggregate a CloudCostSet with an open window: %s", ccs.Window)
  168. }
  169. // Create a new result set, with the given aggregation property
  170. result := ccs.cloneSet()
  171. result.AggregationProperties = props
  172. // Insert clones of each item in the set, keyed by the given property.
  173. // The underlying insert logic will add binned items together.
  174. for name, cc := range ccs.CloudCosts {
  175. ccClone := cc.Clone()
  176. err := result.Insert(ccClone)
  177. if err != nil {
  178. return nil, fmt.Errorf("error aggregating %s by %v: %s", name, props, err)
  179. }
  180. }
  181. return result, nil
  182. }
  183. func (ccs *CloudCostSet) Accumulate(that *CloudCostSet) (*CloudCostSet, error) {
  184. if ccs.IsEmpty() {
  185. return that.Clone(), nil
  186. }
  187. acc := ccs.Clone()
  188. err := acc.accumulateInto(that)
  189. if err == nil {
  190. return nil, err
  191. }
  192. return acc, nil
  193. }
  194. // accumulateInto accumulates a the arg CloudCostSet Into the receiver
  195. func (ccs *CloudCostSet) accumulateInto(that *CloudCostSet) error {
  196. if ccs == nil {
  197. return fmt.Errorf("CloudCost: cannot accumulate into nil set")
  198. }
  199. if that.IsEmpty() {
  200. return nil
  201. }
  202. if ccs.Integration != that.Integration {
  203. ccs.Integration = ""
  204. }
  205. ccs.Window.Expand(that.Window)
  206. for _, cc := range that.CloudCosts {
  207. err := ccs.Insert(cc)
  208. if err != nil {
  209. return err
  210. }
  211. }
  212. return nil
  213. }
  214. func (ccs *CloudCostSet) Equal(that *CloudCostSet) bool {
  215. if ccs.Integration != that.Integration {
  216. return false
  217. }
  218. if !ccs.Window.Equal(that.Window) {
  219. return false
  220. }
  221. // Check Aggregation Properties, slice order is grounds for inequality
  222. if len(ccs.AggregationProperties) != len(that.AggregationProperties) {
  223. return false
  224. }
  225. for i, prop := range ccs.AggregationProperties {
  226. if that.AggregationProperties[i] != prop {
  227. return false
  228. }
  229. }
  230. if len(ccs.CloudCosts) != len(that.CloudCosts) {
  231. return false
  232. }
  233. for k, cc := range ccs.CloudCosts {
  234. if tcc, ok := that.CloudCosts[k]; !ok || !cc.Equal(tcc) {
  235. return false
  236. }
  237. }
  238. return true
  239. }
  240. func (ccs *CloudCostSet) Filter(filters filter.Filter[*CloudCost]) *CloudCostSet {
  241. if ccs == nil {
  242. return nil
  243. }
  244. if filters == nil {
  245. return ccs.Clone()
  246. }
  247. result := ccs.cloneSet()
  248. for _, cc := range ccs.CloudCosts {
  249. if filters.Matches(cc) {
  250. result.Insert(cc.Clone())
  251. }
  252. }
  253. return result
  254. }
  255. // Insert adds a CloudCost to a CloudCostSet using its AggregationProperties and LabelConfig
  256. // to determine the key where it will be inserted
  257. func (ccs *CloudCostSet) Insert(cc *CloudCost) error {
  258. if ccs == nil {
  259. return fmt.Errorf("cannot insert into nil CloudCostSet")
  260. }
  261. if cc == nil {
  262. return fmt.Errorf("cannot insert nil CloudCost into CloudCostSet")
  263. }
  264. if ccs.CloudCosts == nil {
  265. ccs.CloudCosts = map[string]*CloudCost{}
  266. }
  267. ccKey := cc.Properties.GenerateKey(ccs.AggregationProperties)
  268. // Add the given CloudCost to the existing entry, if there is one;
  269. // otherwise just set directly into allocations
  270. if _, ok := ccs.CloudCosts[ccKey]; !ok {
  271. ccs.CloudCosts[ccKey] = cc.Clone()
  272. } else {
  273. ccs.CloudCosts[ccKey].add(cc)
  274. }
  275. return nil
  276. }
  277. func (ccs *CloudCostSet) Clone() *CloudCostSet {
  278. cloudCosts := make(map[string]*CloudCost, len(ccs.CloudCosts))
  279. for k, v := range ccs.CloudCosts {
  280. cloudCosts[k] = v.Clone()
  281. }
  282. cloneCCS := ccs.cloneSet()
  283. cloneCCS.CloudCosts = cloudCosts
  284. return cloneCCS
  285. }
  286. // cloneSet creates a copy of the receiver without any of its CloudCosts
  287. func (ccs *CloudCostSet) cloneSet() *CloudCostSet {
  288. aggProps := make([]string, len(ccs.AggregationProperties))
  289. for i, v := range ccs.AggregationProperties {
  290. aggProps[i] = v
  291. }
  292. return &CloudCostSet{
  293. CloudCosts: make(map[string]*CloudCost),
  294. Integration: ccs.Integration,
  295. AggregationProperties: aggProps,
  296. Window: ccs.Window.Clone(),
  297. }
  298. }
  299. func (ccs *CloudCostSet) IsEmpty() bool {
  300. if ccs == nil {
  301. return true
  302. }
  303. if len(ccs.CloudCosts) == 0 {
  304. return true
  305. }
  306. return false
  307. }
  308. func (ccs *CloudCostSet) Length() int {
  309. if ccs == nil {
  310. return 0
  311. }
  312. return len(ccs.CloudCosts)
  313. }
  314. func (ccs *CloudCostSet) GetWindow() Window {
  315. return ccs.Window
  316. }
  317. func (ccs *CloudCostSet) Merge(that *CloudCostSet) (*CloudCostSet, error) {
  318. if ccs == nil {
  319. return nil, fmt.Errorf("cannot merge nil CloudCostSets")
  320. }
  321. if that.IsEmpty() {
  322. return ccs.Clone(), nil
  323. }
  324. if !ccs.Window.Equal(that.Window) {
  325. return nil, fmt.Errorf("cannot merge CloudCostSets with different windows")
  326. }
  327. result := ccs.cloneSet()
  328. // clear integration if it is not equal
  329. if ccs.Integration != that.Integration {
  330. result.Integration = ""
  331. }
  332. for _, cc := range ccs.CloudCosts {
  333. result.Insert(cc)
  334. }
  335. for _, cc := range that.CloudCosts {
  336. result.Insert(cc)
  337. }
  338. return result, nil
  339. }
  340. type CloudCostSetRange struct {
  341. CloudCostSets []*CloudCostSet `json:"sets"`
  342. Window Window `json:"window"`
  343. }
  344. // NewCloudCostSetRange create a CloudCostSetRange containing CloudCostSets with windows of equal duration
  345. // the duration between start and end must be divisible by the window duration argument
  346. func NewCloudCostSetRange(start time.Time, end time.Time, window time.Duration, integration string) (*CloudCostSetRange, error) {
  347. windows, err := GetWindows(start, end, window)
  348. if err != nil {
  349. return nil, err
  350. }
  351. // Build slice of CloudCostSet to cover the range
  352. cloudCostItemSets := make([]*CloudCostSet, len(windows))
  353. for i, w := range windows {
  354. ccs := NewCloudCostSet(*w.Start(), *w.End())
  355. ccs.Integration = integration
  356. cloudCostItemSets[i] = ccs
  357. }
  358. return &CloudCostSetRange{
  359. Window: NewWindow(&start, &end),
  360. CloudCostSets: cloudCostItemSets,
  361. }, nil
  362. }
  363. func (ccsr *CloudCostSetRange) Clone() *CloudCostSetRange {
  364. ccsSlice := make([]*CloudCostSet, len(ccsr.CloudCostSets))
  365. for i, ccs := range ccsr.CloudCostSets {
  366. ccsSlice[i] = ccs.Clone()
  367. }
  368. return &CloudCostSetRange{
  369. Window: ccsr.Window.Clone(),
  370. CloudCostSets: ccsSlice,
  371. }
  372. }
  373. func (ccsr *CloudCostSetRange) IsEmpty() bool {
  374. for _, ccs := range ccsr.CloudCostSets {
  375. if !ccs.IsEmpty() {
  376. return false
  377. }
  378. }
  379. return true
  380. }
  381. // Accumulate sums each CloudCostSet in the given range, returning a single cumulative
  382. // CloudCostSet for the entire range.
  383. func (ccsr *CloudCostSetRange) Accumulate() (*CloudCostSet, error) {
  384. var cloudCostSet *CloudCostSet
  385. var err error
  386. for _, ccs := range ccsr.CloudCostSets {
  387. if cloudCostSet == nil {
  388. cloudCostSet = ccs.Clone()
  389. continue
  390. }
  391. err = cloudCostSet.accumulateInto(ccs)
  392. if err != nil {
  393. return nil, err
  394. }
  395. }
  396. return cloudCostSet, nil
  397. }
  398. // LoadCloudCost loads CloudCosts into existing CloudCostSets of the CloudCostSetRange.
  399. // This function service to aggregate and distribute costs over predefined windows
  400. // are accumulated here so that the resulting CloudCost with the 1d window has the correct price for the entire day.
  401. // If all or a portion of the window of the CloudCost is outside of the windows of the existing CloudCostSets,
  402. // that portion of the CloudCost's cost will not be inserted
  403. func (ccsr *CloudCostSetRange) LoadCloudCost(cloudCost *CloudCost) {
  404. window := cloudCost.Window
  405. if window.IsOpen() {
  406. log.Errorf("CloudCostSetRange: LoadCloudCost: invalid window %s", window.String())
  407. return
  408. }
  409. totalPct := 0.0
  410. // Distribute cost of the current item across one or more CloudCosts in
  411. // across each relevant CloudCostSet. Stop when the end of the current
  412. // block reaches the item's end time or the end of the range.
  413. for _, ccs := range ccsr.CloudCostSets {
  414. setWindow := ccs.Window
  415. // get percent of item window contained in set window
  416. pct := setWindow.GetPercentInWindow(window)
  417. if pct == 0 {
  418. continue
  419. }
  420. cc := cloudCost
  421. // If the current set Window only contains a portion of the CloudCost Window, insert costs relative to that portion
  422. if pct < 1.0 {
  423. cc = &CloudCost{
  424. Properties: cloudCost.Properties,
  425. Window: window.Contract(setWindow),
  426. ListCost: cloudCost.ListCost.percent(pct),
  427. NetCost: cloudCost.NetCost.percent(pct),
  428. AmortizedNetCost: cloudCost.AmortizedNetCost.percent(pct),
  429. InvoicedCost: cloudCost.InvoicedCost.percent(pct),
  430. AmortizedCost: cloudCost.AmortizedCost.percent(pct),
  431. }
  432. }
  433. err := ccs.Insert(cc)
  434. if err != nil {
  435. log.Errorf("CloudCostSetRange: LoadCloudCost: failed to load CloudCost with window %s: %s", setWindow.String(), err.Error())
  436. }
  437. // If all cost has been inserted, then there is no need to check later days in the range
  438. totalPct += pct
  439. if totalPct >= 1.0 {
  440. return
  441. }
  442. }
  443. }
  444. const (
  445. ListCostMetric string = "ListCost"
  446. NetCostMetric string = "NetCost"
  447. AmortizedNetCostMetric string = "AmortizedNetCost"
  448. InvoicedCostMetric string = "InvoicedCost"
  449. AmortizedCostMetric string = "AmortizedCost"
  450. )
  451. type CostMetric struct {
  452. Cost float64 `json:"cost"`
  453. KubernetesPercent float64 `json:"kubernetesPercent"`
  454. }
  455. func (cm CostMetric) Equal(that CostMetric) bool {
  456. return cm.Cost == that.Cost && cm.KubernetesPercent == that.KubernetesPercent
  457. }
  458. func (cm CostMetric) Clone() CostMetric {
  459. return CostMetric{
  460. Cost: cm.Cost,
  461. KubernetesPercent: cm.KubernetesPercent,
  462. }
  463. }
  464. func (cm CostMetric) add(that CostMetric) CostMetric {
  465. // Compute KubernetesPercent for sum
  466. k8sPct := 0.0
  467. sumCost := cm.Cost + that.Cost
  468. if sumCost > 0.0 {
  469. thisK8sCost := cm.Cost * cm.KubernetesPercent
  470. thatK8sCost := that.Cost * that.KubernetesPercent
  471. k8sPct = (thisK8sCost + thatK8sCost) / sumCost
  472. }
  473. return CostMetric{
  474. Cost: sumCost,
  475. KubernetesPercent: k8sPct,
  476. }
  477. }
  478. // percent returns the product of the given percent and the cost, KubernetesPercent remains the same
  479. func (cm CostMetric) percent(pct float64) CostMetric {
  480. return CostMetric{
  481. Cost: cm.Cost * pct,
  482. KubernetesPercent: cm.KubernetesPercent,
  483. }
  484. }