queryservice.go 7.1 KB

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