queryservice.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. package cloudcost
  2. import (
  3. "fmt"
  4. "net/http"
  5. "strings"
  6. "github.com/julienschmidt/httprouter"
  7. "github.com/opencost/opencost/pkg/kubecost"
  8. "github.com/opencost/opencost/pkg/util/httputil"
  9. "go.opentelemetry.io/otel"
  10. )
  11. const tracerName = "github.com/opencost/ooencost/pkg/cloudcost"
  12. const (
  13. csvFormat = "csv"
  14. )
  15. // QueryService surfaces endpoints for accessing CloudCost data in raw form or for display in views
  16. type QueryService struct {
  17. Querier Querier
  18. ViewQuerier ViewQuerier
  19. }
  20. func NewQueryService(querier Querier, viewQuerier ViewQuerier) *QueryService {
  21. return &QueryService{
  22. Querier: querier,
  23. ViewQuerier: viewQuerier,
  24. }
  25. }
  26. func (s *QueryService) GetCloudCostHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  27. // Return valid handler func
  28. return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  29. tracer := otel.Tracer(tracerName)
  30. ctx, span := tracer.Start(r.Context(), "Service.GetCloudCostHandler")
  31. defer span.End()
  32. // If Query Service is nil, always return 501
  33. if s == nil {
  34. http.Error(w, "Query Service is nil", http.StatusNotImplemented)
  35. return
  36. }
  37. if s.Querier == nil {
  38. http.Error(w, "CloudCost Query Service is nil", http.StatusNotImplemented)
  39. return
  40. }
  41. qp := httputil.NewQueryParams(r.URL.Query())
  42. request, err := ParseCloudCostRequest(qp)
  43. if err != nil {
  44. http.Error(w, err.Error(), http.StatusBadRequest)
  45. return
  46. }
  47. resp, err := s.Querier.Query(*request, ctx)
  48. if err != nil {
  49. http.Error(w, fmt.Sprintf("Internal server error: %s", err), http.StatusInternalServerError)
  50. return
  51. }
  52. _, spanResp := tracer.Start(ctx, "write response")
  53. w.Header().Set("Content-Type", "application/json")
  54. protocol.WriteData(w, resp)
  55. spanResp.End()
  56. }
  57. }
  58. func (s *QueryService) GetCloudCostViewGraphHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  59. // Return valid handler func
  60. return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  61. tracer := otel.Tracer(tracerName)
  62. ctx, span := tracer.Start(r.Context(), "Service.GetCloudCostViewGraphHandler")
  63. defer span.End()
  64. // If Query Service is nil, always return 501
  65. if s == nil {
  66. http.Error(w, "Query Service is nil", http.StatusNotImplemented)
  67. return
  68. }
  69. if s.ViewQuerier == nil {
  70. http.Error(w, "CloudCost Query Service is nil", http.StatusNotImplemented)
  71. return
  72. }
  73. qp := httputil.NewQueryParams(r.URL.Query())
  74. request, err := parseCloudCostViewRequest(qp)
  75. if err != nil {
  76. http.Error(w, err.Error(), http.StatusBadRequest)
  77. return
  78. }
  79. resp, err := s.ViewQuerier.QueryViewGraph(*request, ctx)
  80. if err != nil {
  81. http.Error(w, fmt.Sprintf("Internal server error: %s", err), http.StatusInternalServerError)
  82. return
  83. }
  84. _, spanResp := tracer.Start(ctx, "write response")
  85. w.Header().Set("Content-Type", "application/json")
  86. protocol.WriteData(w, resp)
  87. spanResp.End()
  88. }
  89. }
  90. type CloudCostViewTotalsResponse struct {
  91. NumResults int `json:"numResults"`
  92. Combined *ViewTableRow `json:"combined"`
  93. }
  94. func (s *QueryService) GetCloudCostViewTotalsHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  95. // Return valid handler func
  96. return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  97. tracer := otel.Tracer(tracerName)
  98. ctx, span := tracer.Start(r.Context(), "Service.GetCloudCostViewTotalsHandler")
  99. defer span.End()
  100. // If Query Service is nil, always return 501
  101. if s == nil {
  102. http.Error(w, "Query Service is nil", http.StatusNotImplemented)
  103. return
  104. }
  105. if s.ViewQuerier == nil {
  106. http.Error(w, "CloudCost Query Service is nil", http.StatusNotImplemented)
  107. return
  108. }
  109. qp := httputil.NewQueryParams(r.URL.Query())
  110. request, err := parseCloudCostViewRequest(qp)
  111. if err != nil {
  112. http.Error(w, err.Error(), http.StatusBadRequest)
  113. return
  114. }
  115. totals, count, err := s.ViewQuerier.QueryViewTotals(*request, ctx)
  116. if err != nil {
  117. http.Error(w, fmt.Sprintf("Internal server error: %s", err), http.StatusInternalServerError)
  118. return
  119. }
  120. resp := CloudCostViewTotalsResponse{
  121. NumResults: count,
  122. Combined: totals,
  123. }
  124. _, spanResp := tracer.Start(ctx, "write response")
  125. w.Header().Set("Content-Type", "application/json")
  126. protocol.WriteData(w, resp)
  127. spanResp.End()
  128. }
  129. }
  130. func (s *QueryService) GetCloudCostViewTableHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  131. // Return valid handler func
  132. return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  133. tracer := otel.Tracer(tracerName)
  134. ctx, span := tracer.Start(r.Context(), "Service.GetCloudCostViewTableHandler")
  135. defer span.End()
  136. // If Query Service is nil, always return 501
  137. if s == nil {
  138. http.Error(w, "Query Service is nil", http.StatusNotImplemented)
  139. return
  140. }
  141. if s.ViewQuerier == nil {
  142. http.Error(w, "CloudCost Query Service is nil", http.StatusNotImplemented)
  143. return
  144. }
  145. qp := httputil.NewQueryParams(r.URL.Query())
  146. request, err := parseCloudCostViewRequest(qp)
  147. if err != nil {
  148. http.Error(w, err.Error(), http.StatusBadRequest)
  149. return
  150. }
  151. format := qp.Get("format", "json")
  152. if strings.HasPrefix(format, csvFormat) {
  153. w.Header().Set("Content-Type", "text/csv")
  154. w.Header().Set("Transfer-Encoding", "chunked")
  155. } else {
  156. // By default, send JSON
  157. w.Header().Set("Content-Type", "application/json")
  158. }
  159. resp, err := s.ViewQuerier.QueryViewTable(*request, ctx)
  160. if err != nil {
  161. http.Error(w, fmt.Sprintf("Internal server error: %s", err), http.StatusInternalServerError)
  162. return
  163. }
  164. _, spanResp := tracer.Start(ctx, "write response")
  165. defer spanResp.End()
  166. if format == csvFormat {
  167. window := kubecost.NewClosedWindow(request.Start, request.End)
  168. writeCloudCostViewTableRowsAsCSV(w, resp, window.String())
  169. return
  170. }
  171. w.Header().Set("Content-Type", "application/json")
  172. protocol.WriteData(w, resp)
  173. }
  174. }