queryservice.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. package cloudcost
  2. import (
  3. "fmt"
  4. "net/http"
  5. "strings"
  6. "github.com/julienschmidt/httprouter"
  7. "github.com/opencost/opencost/core/pkg/opencost"
  8. "github.com/opencost/opencost/core/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(ctx, *request)
  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) GetCloudCostAutocompleteHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  59. return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  60. tracer := otel.Tracer(tracerName)
  61. ctx, span := tracer.Start(r.Context(), "Service.GetCloudCostAutocompleteHandler")
  62. defer span.End()
  63. if s == nil {
  64. http.Error(w, "Query Service is nil", http.StatusNotImplemented)
  65. return
  66. }
  67. if s.Querier == nil {
  68. http.Error(w, "CloudCost Query Service is nil", http.StatusNotImplemented)
  69. return
  70. }
  71. qp := httputil.NewQueryParams(r.URL.Query())
  72. request, err := ParseCloudCostAutocompleteRequest(qp)
  73. if err != nil {
  74. http.Error(w, err.Error(), http.StatusBadRequest)
  75. return
  76. }
  77. resp, err := s.Querier.QueryCloudCostAutocomplete(ctx, *request)
  78. if err != nil {
  79. http.Error(w, fmt.Sprintf("Internal server error: %s", err), http.StatusInternalServerError)
  80. return
  81. }
  82. w.Header().Set("Content-Type", "application/json")
  83. protocol.WriteData(w, resp)
  84. }
  85. }
  86. func (s *QueryService) GetCloudCostViewGraphHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  87. // Return valid handler func
  88. return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  89. tracer := otel.Tracer(tracerName)
  90. ctx, span := tracer.Start(r.Context(), "Service.GetCloudCostViewGraphHandler")
  91. defer span.End()
  92. // If Query Service is nil, always return 501
  93. if s == nil {
  94. http.Error(w, "Query Service is nil", http.StatusNotImplemented)
  95. return
  96. }
  97. if s.ViewQuerier == nil {
  98. http.Error(w, "CloudCost Query Service is nil", http.StatusNotImplemented)
  99. return
  100. }
  101. qp := httputil.NewQueryParams(r.URL.Query())
  102. request, err := ParseCloudCostViewRequest(qp)
  103. if err != nil {
  104. http.Error(w, err.Error(), http.StatusBadRequest)
  105. return
  106. }
  107. resp, err := s.ViewQuerier.QueryViewGraph(ctx, *request)
  108. if err != nil {
  109. http.Error(w, fmt.Sprintf("Internal server error: %s", err), http.StatusInternalServerError)
  110. return
  111. }
  112. _, spanResp := tracer.Start(ctx, "write response")
  113. w.Header().Set("Content-Type", "application/json")
  114. protocol.WriteData(w, resp)
  115. spanResp.End()
  116. }
  117. }
  118. func (s *QueryService) GetCloudCostViewTotalsHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  119. // Return valid handler func
  120. return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  121. tracer := otel.Tracer(tracerName)
  122. ctx, span := tracer.Start(r.Context(), "Service.GetCloudCostViewTotalsHandler")
  123. defer span.End()
  124. // If Query Service is nil, always return 501
  125. if s == nil {
  126. http.Error(w, "Query Service is nil", http.StatusNotImplemented)
  127. return
  128. }
  129. if s.ViewQuerier == nil {
  130. http.Error(w, "CloudCost Query Service is nil", http.StatusNotImplemented)
  131. return
  132. }
  133. qp := httputil.NewQueryParams(r.URL.Query())
  134. request, err := ParseCloudCostViewRequest(qp)
  135. if err != nil {
  136. http.Error(w, err.Error(), http.StatusBadRequest)
  137. return
  138. }
  139. resp, err := s.ViewQuerier.QueryViewTotals(ctx, *request)
  140. if err != nil {
  141. http.Error(w, fmt.Sprintf("Internal server error: %s", err), http.StatusInternalServerError)
  142. return
  143. }
  144. _, spanResp := tracer.Start(ctx, "write response")
  145. w.Header().Set("Content-Type", "application/json")
  146. protocol.WriteData(w, resp)
  147. spanResp.End()
  148. }
  149. }
  150. func (s *QueryService) GetCloudCostViewTableHandler(tokenHook func(ViewTableRows) string) func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  151. // Return valid handler func
  152. return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  153. tracer := otel.Tracer(tracerName)
  154. ctx, span := tracer.Start(r.Context(), "Service.GetCloudCostViewTableHandler")
  155. defer span.End()
  156. // If Query Service is nil, always return 501
  157. if s == nil {
  158. http.Error(w, "Query Service is nil", http.StatusNotImplemented)
  159. return
  160. }
  161. if s.ViewQuerier == nil {
  162. http.Error(w, "CloudCost Query Service is nil", http.StatusNotImplemented)
  163. return
  164. }
  165. qp := httputil.NewQueryParams(r.URL.Query())
  166. request, err := ParseCloudCostViewRequest(qp)
  167. if err != nil {
  168. http.Error(w, err.Error(), http.StatusBadRequest)
  169. return
  170. }
  171. format := qp.Get("format", "json")
  172. if strings.HasPrefix(format, csvFormat) {
  173. w.Header().Set("Content-Type", "text/csv")
  174. w.Header().Set("Transfer-Encoding", "chunked")
  175. } else {
  176. // By default, send JSON
  177. w.Header().Set("Content-Type", "application/json")
  178. }
  179. rows, err := s.ViewQuerier.QueryViewTable(ctx, *request)
  180. if err != nil {
  181. http.Error(w, fmt.Sprintf("Internal server error: %s", err), http.StatusInternalServerError)
  182. return
  183. }
  184. resp := protocol.NewResponse().WithData(rows)
  185. if tokenHook != nil {
  186. resp = resp.WithMeta(map[string]any{
  187. "token": tokenHook(rows),
  188. })
  189. }
  190. _, spanResp := tracer.Start(ctx, "write response")
  191. defer spanResp.End()
  192. if format == csvFormat {
  193. window := opencost.NewClosedWindow(request.Start, request.End)
  194. writeCloudCostViewTableRowsAsCSV(w, rows, window.String())
  195. return
  196. }
  197. w.Header().Set("Content-Type", "application/json")
  198. protocol.WriteResponse(w, resp)
  199. }
  200. }