queryservice_helper.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. package cloudcost
  2. import (
  3. "encoding/csv"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. filter21 "github.com/opencost/opencost/pkg/filter21"
  8. "github.com/opencost/opencost/pkg/filter21/cloudcost"
  9. "github.com/opencost/opencost/pkg/kubecost"
  10. "github.com/opencost/opencost/pkg/prom"
  11. "github.com/opencost/opencost/pkg/util/httputil"
  12. )
  13. func ParseCloudCostRequest(qp httputil.QueryParams) (*QueryRequest, error) {
  14. windowStr := qp.Get("window", "")
  15. if windowStr == "" {
  16. return nil, fmt.Errorf("missing require window param")
  17. }
  18. window, err := kubecost.ParseWindowUTC(windowStr)
  19. if err != nil {
  20. return nil, fmt.Errorf("invalid window parameter: %w", err)
  21. }
  22. if window.IsOpen() {
  23. return nil, fmt.Errorf("invalid window parameter: %s", window.String())
  24. }
  25. aggregateByRaw := qp.GetList("aggregate", ",")
  26. var aggregateBy []string
  27. for _, aggBy := range aggregateByRaw {
  28. prop, err := ParseCloudCostProperty(aggBy)
  29. if err != nil {
  30. return nil, fmt.Errorf("error parsing aggregate by %v", err)
  31. }
  32. aggregateBy = append(aggregateBy, prop)
  33. }
  34. // if we're aggregating by nothing (aka `item` on the frontend) then aggregate by all
  35. if len(aggregateBy) == 0 {
  36. aggregateBy = []string{kubecost.CloudCostInvoiceEntityIDProp, kubecost.CloudCostAccountIDProp, kubecost.CloudCostProviderProp, kubecost.CloudCostProviderIDProp, kubecost.CloudCostCategoryProp, kubecost.CloudCostServiceProp}
  37. }
  38. accumulate := kubecost.ParseAccumulate(qp.Get("accumulate", ""))
  39. var filter filter21.Filter
  40. filterString := qp.Get("filter", "")
  41. if filterString != "" {
  42. parser := cloudcost.NewCloudCostFilterParser()
  43. filter, err = parser.Parse(filterString)
  44. if err != nil {
  45. return nil, fmt.Errorf("Parsing 'filter' parameter: %s", err)
  46. }
  47. }
  48. opts := &QueryRequest{
  49. Start: *window.Start(),
  50. End: *window.End(),
  51. AggregateBy: aggregateBy,
  52. Accumulate: accumulate,
  53. Filter: filter,
  54. }
  55. return opts, nil
  56. }
  57. func ParseCloudCostProperty(text string) (string, error) {
  58. switch strings.TrimSpace(strings.ToLower(text)) {
  59. case strings.ToLower(kubecost.CloudCostInvoiceEntityIDProp):
  60. return kubecost.CloudCostInvoiceEntityIDProp, nil
  61. case strings.ToLower(kubecost.CloudCostAccountIDProp):
  62. return kubecost.CloudCostAccountIDProp, nil
  63. case strings.ToLower(kubecost.CloudCostProviderProp):
  64. return kubecost.CloudCostProviderProp, nil
  65. case strings.ToLower(kubecost.CloudCostProviderIDProp):
  66. return kubecost.CloudCostProviderIDProp, nil
  67. case strings.ToLower(kubecost.CloudCostCategoryProp):
  68. return kubecost.CloudCostCategoryProp, nil
  69. case strings.ToLower(kubecost.CloudCostServiceProp):
  70. return kubecost.CloudCostServiceProp, nil
  71. }
  72. if strings.HasPrefix(text, "label:") {
  73. label := prom.SanitizeLabelName(strings.TrimSpace(strings.TrimPrefix(text, "label:")))
  74. return fmt.Sprintf("label:%s", label), nil
  75. }
  76. return "", fmt.Errorf("invalid cloud cost property: %s", text)
  77. }
  78. func parseCloudCostViewRequest(qp httputil.QueryParams) (*ViewQueryRequest, error) {
  79. qr, err := ParseCloudCostRequest(qp)
  80. if err != nil {
  81. return nil, err
  82. }
  83. // parse cost metric
  84. costMetricName, err := kubecost.ParseCostMetricName(qp.Get("costMetric", string(kubecost.CostMetricAmortizedNetCost)))
  85. if err != nil {
  86. return nil, fmt.Errorf("error parsing 'costMetric': %w", err)
  87. }
  88. limit := qp.GetInt("limit", 0)
  89. if limit < 0 {
  90. return nil, fmt.Errorf("invalid value for limit %d", limit)
  91. }
  92. offset := qp.GetInt("offset", 0)
  93. if offset < 0 {
  94. return nil, fmt.Errorf("invalid value for offset %d", offset)
  95. }
  96. // parse order
  97. order, err := ParseSortDirection(qp.Get("sortByOrder", "desc"))
  98. if err != nil {
  99. return nil, fmt.Errorf("error parsing 'sortByOrder: %w", err)
  100. }
  101. sortColumn, err := ParseSortField(qp.Get("sortBy", "cost"))
  102. if err != nil {
  103. return nil, fmt.Errorf("error parsing 'sortBy': %w", err)
  104. }
  105. return &ViewQueryRequest{
  106. QueryRequest: *qr,
  107. CostMetricName: costMetricName,
  108. ChartItemsLength: DefaultChartItemsLength,
  109. Limit: limit,
  110. Offset: offset,
  111. SortDirection: order,
  112. SortColumn: sortColumn,
  113. }, nil
  114. }
  115. // CloudCostViewTableRowsToCSV takes the csv writer and writes the ViewTableRows into the writer.
  116. func CloudCostViewTableRowsToCSV(writer *csv.Writer, ctr ViewTableRows, window string) error {
  117. defer writer.Flush()
  118. // Write the column headers
  119. headers := []string{
  120. "Name",
  121. "K8s Utilization",
  122. "Total",
  123. "Window",
  124. }
  125. err := writer.Write(headers)
  126. if err != nil {
  127. return fmt.Errorf("CloudCostViewTableRowsToCSV: failed to convert ViewTableRows to csv with error: %w", err)
  128. }
  129. // Write one row per entry in the ViewTableRows
  130. for _, row := range ctr {
  131. err = writer.Write([]string{
  132. row.Name,
  133. fmt.Sprintf("%.3f", row.KubernetesPercent),
  134. fmt.Sprintf("%.3f", row.Cost),
  135. window,
  136. })
  137. if err != nil {
  138. return fmt.Errorf("CloudCostViewTableRowsToCSV: failed to convert ViewTableRows to csv with error: %w", err)
  139. }
  140. }
  141. return nil
  142. }
  143. func writeCloudCostViewTableRowsAsCSV(w http.ResponseWriter, ctr ViewTableRows, window string) {
  144. writer := csv.NewWriter(w)
  145. err := CloudCostViewTableRowsToCSV(writer, ctr, window)
  146. if err != nil {
  147. protocol.WriteError(w, protocol.InternalServerError(err.Error()))
  148. return
  149. }
  150. }