queryservice.go 6.9 KB

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