repositoryquerier.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. package cloudcost
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "github.com/opencost/opencost/core/pkg/log"
  7. "github.com/opencost/opencost/core/pkg/opencost"
  8. )
  9. // RepositoryQuerier is an implementation of Querier and ViewQuerier which pulls directly from a Repository
  10. type RepositoryQuerier struct {
  11. repo Repository
  12. }
  13. func NewRepositoryQuerier(repo Repository) *RepositoryQuerier {
  14. return &RepositoryQuerier{repo: repo}
  15. }
  16. func (rq *RepositoryQuerier) Query(request QueryRequest, ctx context.Context) (*opencost.CloudCostSetRange, error) {
  17. repoKeys, err := rq.repo.Keys()
  18. if err != nil {
  19. return nil, fmt.Errorf("RepositoryQuerier: Query: failed to get list of keys from repository: %w", err)
  20. }
  21. // create filter
  22. compiler := opencost.NewCloudCostMatchCompiler()
  23. matcher, err := compiler.Compile(request.Filter)
  24. if err != nil {
  25. return nil, fmt.Errorf("RepositoryQuerier: Query: failed to compile filters: %w", err)
  26. }
  27. // Create a Cloud Cost Set Range in the resolution of the repository
  28. ccsr, err := opencost.NewCloudCostSetRange(request.Start, request.End, opencost.AccumulateOptionDay, "")
  29. if err != nil {
  30. return nil, fmt.Errorf("RepositoryQuerier: Query: failed to create Cloud Cost Set Range: %w", err)
  31. }
  32. for _, cloudCostSet := range ccsr.CloudCostSets {
  33. // Setting this values creates
  34. cloudCostSet.AggregationProperties = request.AggregateBy
  35. for _, key := range repoKeys {
  36. ccs, err := rq.repo.Get(cloudCostSet.Window.Start().UTC(), key)
  37. if err != nil {
  38. log.Errorf("RepositoryQuerier: Query: %s", err.Error())
  39. continue
  40. }
  41. if ccs == nil {
  42. continue
  43. }
  44. for _, cc := range ccs.CloudCosts {
  45. if matcher.Matches(cc) {
  46. cloudCostSet.Insert(cc)
  47. }
  48. }
  49. }
  50. }
  51. if request.Accumulate != opencost.AccumulateOptionNone {
  52. ccsr, err = ccsr.Accumulate(request.Accumulate)
  53. if err != nil {
  54. return nil, fmt.Errorf("RepositoryQuerier: Query: error accumulating: %w", err)
  55. }
  56. }
  57. return ccsr, nil
  58. }
  59. func (rq *RepositoryQuerier) QueryViewGraph(request ViewQueryRequest, ctx context.Context) (ViewGraphData, error) {
  60. ccasr, err := rq.Query(request.QueryRequest, ctx)
  61. if err != nil {
  62. return nil, fmt.Errorf("QueryViewGraph: query failed: %w", err)
  63. }
  64. if ccasr.IsEmpty() {
  65. return make([]*ViewGraphDataSet, 0), nil
  66. }
  67. var sets ViewGraphData
  68. for _, ccas := range ccasr.CloudCostSets {
  69. items := make([]ViewGraphDataSetItem, 0)
  70. for key, cc := range ccas.CloudCosts {
  71. costMetric, err := cc.GetCostMetric(request.CostMetricName)
  72. if err != nil {
  73. return nil, fmt.Errorf("QueryViewGraph: failed to get cost metric: %w", err)
  74. }
  75. items = append(items, ViewGraphDataSetItem{
  76. Name: key,
  77. Value: costMetric.Cost,
  78. })
  79. }
  80. sort.SliceStable(items, func(i, j int) bool {
  81. return items[i].Value > items[j].Value
  82. })
  83. if len(items) > request.ChartItemsLength {
  84. otherItems := items[request.ChartItemsLength:]
  85. newItems := items[:request.ChartItemsLength]
  86. // Rename last item other and add all other values into it
  87. newItems[request.ChartItemsLength-1].Name = "Other"
  88. for _, item := range otherItems {
  89. newItems[request.ChartItemsLength-1].Value += item.Value
  90. }
  91. items = newItems
  92. }
  93. sets = append(sets, &ViewGraphDataSet{
  94. Start: *ccas.Window.Start(),
  95. End: *ccas.Window.End(),
  96. Items: items,
  97. })
  98. }
  99. return sets, nil
  100. }
  101. func (rq *RepositoryQuerier) QueryViewTotals(request ViewQueryRequest, ctx context.Context) (*ViewTotals, error) {
  102. ccasr, err := rq.Query(request.QueryRequest, ctx)
  103. if err != nil {
  104. return nil, fmt.Errorf("QueryViewTotals: query failed: %w", err)
  105. }
  106. acc, err := ccasr.AccumulateAll()
  107. if err != nil {
  108. return nil, fmt.Errorf("QueryViewTotals: accumulate failed: %w", err)
  109. }
  110. if acc.IsEmpty() {
  111. return nil, nil
  112. }
  113. count := len(acc.CloudCosts)
  114. total, err := acc.Aggregate([]string{})
  115. if err != nil {
  116. return nil, fmt.Errorf("QueryViewTotals: aggregate total failed: %w", err)
  117. }
  118. if total.IsEmpty() {
  119. return nil, fmt.Errorf("QueryViewTotals: missing total: %w", err)
  120. }
  121. if len(total.CloudCosts) != 1 {
  122. return nil, fmt.Errorf("QueryViewTotals: total did not aggregate: %w", err)
  123. }
  124. cm, err := total.CloudCosts[""].GetCostMetric(request.CostMetricName)
  125. if err != nil {
  126. return nil, fmt.Errorf("QueryViewTotals: failed to retrieve cost metric: %w", err)
  127. }
  128. return &ViewTotals{
  129. NumResults: count,
  130. Combined: &ViewTableRow{
  131. Name: "Totals",
  132. KubernetesPercent: cm.KubernetesPercent,
  133. Cost: cm.Cost,
  134. },
  135. }, nil
  136. }
  137. func (rq *RepositoryQuerier) QueryViewTable(request ViewQueryRequest, ctx context.Context) (ViewTableRows, error) {
  138. ccasr, err := rq.Query(request.QueryRequest, ctx)
  139. if err != nil {
  140. return nil, fmt.Errorf("QueryViewTable: query failed: %w", err)
  141. }
  142. acc, err := ccasr.AccumulateAll()
  143. if err != nil {
  144. return nil, fmt.Errorf("QueryViewTable: accumulate failed: %w", err)
  145. }
  146. var rows ViewTableRows
  147. for key, cloudCost := range acc.CloudCosts {
  148. costMetric, err2 := cloudCost.GetCostMetric(request.CostMetricName)
  149. if err2 != nil {
  150. return nil, fmt.Errorf("QueryViewTable: failed to retrieve cost metric: %w", err)
  151. }
  152. var labels map[string]string
  153. if cloudCost.Properties != nil {
  154. labels = cloudCost.Properties.Labels
  155. }
  156. vtr := &ViewTableRow{
  157. Name: key,
  158. Labels: labels,
  159. KubernetesPercent: costMetric.KubernetesPercent,
  160. Cost: costMetric.Cost,
  161. }
  162. rows = append(rows, vtr)
  163. }
  164. // Sort Results
  165. // Sort by Name to ensure consistent return
  166. sort.SliceStable(rows, func(i, j int) bool {
  167. return rows[i].Name > rows[j].Name
  168. })
  169. switch request.SortColumn {
  170. case SortFieldName:
  171. if request.SortDirection == SortDirectionAscending {
  172. sort.SliceStable(rows, func(i, j int) bool {
  173. return rows[i].Name < rows[j].Name
  174. })
  175. }
  176. case SortFieldCost:
  177. if request.SortDirection == SortDirectionAscending {
  178. sort.SliceStable(rows, func(i, j int) bool {
  179. return rows[i].Cost < rows[j].Cost
  180. })
  181. } else {
  182. sort.SliceStable(rows, func(i, j int) bool {
  183. return rows[i].Cost > rows[j].Cost
  184. })
  185. }
  186. case SortFieldKubernetesPercent:
  187. if request.SortDirection == SortDirectionAscending {
  188. sort.SliceStable(rows, func(i, j int) bool {
  189. return rows[i].KubernetesPercent < rows[j].KubernetesPercent
  190. })
  191. } else {
  192. sort.SliceStable(rows, func(i, j int) bool {
  193. return rows[i].KubernetesPercent > rows[j].KubernetesPercent
  194. })
  195. }
  196. default:
  197. return nil, fmt.Errorf("invalid sort field '%s'", string(request.SortColumn))
  198. }
  199. // paginate sorted results
  200. if request.Offset > len(rows) {
  201. return make([]*ViewTableRow, 0), nil
  202. }
  203. if request.Limit > 0 {
  204. limit := request.Offset + request.Limit
  205. if limit > len(rows) {
  206. return rows[request.Offset:], nil
  207. }
  208. return rows[request.Offset:limit], nil
  209. }
  210. if request.Offset > 0 {
  211. return rows[request.Offset:], nil
  212. }
  213. return rows, nil
  214. }