signoz/pkg/query-service/app/http_handler.go
Ankit Nayan 30961da59f
Crud APIs for dashboards (#286)
* added signoz.db to gitignore

* model and crud methods for dashboard package

* added signoz.db to dockerignore

* feat: dashboards crud WIP

* chore: moving response format to correct file

* chore: adding dependencies for sqlite3

* feat: CRUD APIs ready for dashboards

* fix: sqlite needs cgo enabled and hence need to add some flags in building go code

* feat: provision dashboards using json

* chore: mounting dashboard folder to container
2021-09-02 13:18:47 +05:30

768 lines
19 KiB
Go

package app
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
jsoniter "github.com/json-iterator/go"
_ "github.com/mattn/go-sqlite3"
"github.com/posthog/posthog-go"
"github.com/prometheus/prometheus/promql"
"go.signoz.io/query-service/app/dashboards"
"go.signoz.io/query-service/model"
"go.uber.org/zap"
)
type status string
const (
statusSuccess status = "success"
statusError status = "error"
)
// NewRouter creates and configures a Gorilla Router.
func NewRouter() *mux.Router {
return mux.NewRouter().UseEncodedPath()
}
// APIHandler implements the query service public API by registering routes at httpPrefix
type APIHandler struct {
// queryService *querysvc.QueryService
// queryParser queryParser
basePath string
apiPrefix string
reader *Reader
pc *posthog.Client
distinctId string
db *sqlx.DB
ready func(http.HandlerFunc) http.HandlerFunc
}
// NewAPIHandler returns an APIHandler
func NewAPIHandler(reader *Reader, pc *posthog.Client, distinctId string) (*APIHandler, error) {
aH := &APIHandler{
reader: reader,
pc: pc,
distinctId: distinctId,
}
aH.ready = aH.testReady
err := dashboards.InitDB("signoz.db")
if err != nil {
return nil, err
}
errReadingDashboards := dashboards.LoadDashboardFiles()
if errReadingDashboards != nil {
return nil, errReadingDashboards
}
return aH, nil
}
type structuredResponse struct {
Data interface{} `json:"data"`
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
Errors []structuredError `json:"errors"`
}
type structuredError struct {
Code int `json:"code,omitempty"`
Msg string `json:"msg"`
// TraceID ui.TraceID `json:"traceID,omitempty"`
}
var corsHeaders = map[string]string{
"Access-Control-Allow-Headers": "Accept, Authorization, Content-Type, Origin",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers": "Date",
}
// Enables cross-site script calls.
func setCORS(w http.ResponseWriter) {
for h, v := range corsHeaders {
w.Header().Set(h, v)
}
}
type apiFunc func(r *http.Request) (interface{}, *model.ApiError, func())
// Checks if server is ready, calls f if it is, returns 503 if it is not.
func (aH *APIHandler) testReady(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
}
type response struct {
Status status `json:"status"`
Data interface{} `json:"data,omitempty"`
ErrorType model.ErrorType `json:"errorType,omitempty"`
Error string `json:"error,omitempty"`
}
func (aH *APIHandler) respondError(w http.ResponseWriter, apiErr *model.ApiError, data interface{}) {
json := jsoniter.ConfigCompatibleWithStandardLibrary
b, err := json.Marshal(&response{
Status: statusError,
ErrorType: apiErr.Typ,
Error: apiErr.Err.Error(),
Data: data,
})
if err != nil {
zap.S().Error("msg", "error marshalling json response", "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var code int
switch apiErr.Typ {
case model.ErrorBadData:
code = http.StatusBadRequest
case model.ErrorExec:
code = 422
case model.ErrorCanceled, model.ErrorTimeout:
code = http.StatusServiceUnavailable
case model.ErrorInternal:
code = http.StatusInternalServerError
case model.ErrorNotFound:
code = http.StatusNotFound
case model.ErrorNotImplemented:
code = http.StatusNotImplemented
default:
code = http.StatusInternalServerError
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if n, err := w.Write(b); err != nil {
zap.S().Error("msg", "error writing response", "bytesWritten", n, "err", err)
}
}
func (aH *APIHandler) respond(w http.ResponseWriter, data interface{}) {
json := jsoniter.ConfigCompatibleWithStandardLibrary
b, err := json.Marshal(&response{
Status: statusSuccess,
Data: data,
})
if err != nil {
zap.S().Error("msg", "error marshalling json response", "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if n, err := w.Write(b); err != nil {
zap.S().Error("msg", "error writing response", "bytesWritten", n, "err", err)
}
}
// RegisterRoutes registers routes for this handler on the given router
func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
router.HandleFunc("/api/v1/query_range", aH.queryRangeMetrics).Methods(http.MethodGet)
router.HandleFunc("/api/v1/query", aH.queryMetrics).Methods(http.MethodGet)
router.HandleFunc("/api/v1/dashboards", aH.getDashboards).Methods(http.MethodGet)
router.HandleFunc("/api/v1/dashboards", aH.createDashboards).Methods(http.MethodPost)
router.HandleFunc("/api/v1/dashboards/{uuid}", aH.getDashboard).Methods(http.MethodGet)
router.HandleFunc("/api/v1/dashboards/{uuid}", aH.updateDashboard).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}", aH.deleteDashboard).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/user", aH.user).Methods(http.MethodPost)
// router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet)
router.HandleFunc("/api/v1/services", aH.getServices).Methods(http.MethodGet)
router.HandleFunc("/api/v1/services/list", aH.getServicesList).Methods(http.MethodGet)
router.HandleFunc("/api/v1/service/overview", aH.getServiceOverview).Methods(http.MethodGet)
router.HandleFunc("/api/v1/service/dbOverview", aH.getServiceDBOverview).Methods(http.MethodGet)
router.HandleFunc("/api/v1/service/externalAvgDuration", aH.GetServiceExternalAvgDuration).Methods(http.MethodGet)
router.HandleFunc("/api/v1/service/externalErrors", aH.getServiceExternalErrors).Methods(http.MethodGet)
router.HandleFunc("/api/v1/service/external", aH.getServiceExternal).Methods(http.MethodGet)
router.HandleFunc("/api/v1/service/{service}/operations", aH.getOperations).Methods(http.MethodGet)
router.HandleFunc("/api/v1/service/top_endpoints", aH.getTopEndpoints).Methods(http.MethodGet)
router.HandleFunc("/api/v1/spans", aH.searchSpans).Methods(http.MethodGet)
router.HandleFunc("/api/v1/spans/aggregates", aH.searchSpansAggregates).Methods(http.MethodGet)
router.HandleFunc("/api/v1/tags", aH.searchTags).Methods(http.MethodGet)
router.HandleFunc("/api/v1/traces/{traceId}", aH.searchTraces).Methods(http.MethodGet)
router.HandleFunc("/api/v1/usage", aH.getUsage).Methods(http.MethodGet)
router.HandleFunc("/api/v1/serviceMapDependencies", aH.serviceMapDependencies).Methods(http.MethodGet)
}
func Intersection(a, b []int) (c []int) {
m := make(map[int]bool)
for _, item := range a {
m[item] = true
}
for _, item := range b {
if _, ok := m[item]; ok {
c = append(c, item)
}
}
return
}
func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) {
allDashboards, err := dashboards.GetDashboards()
if err != nil {
aH.respondError(w, err, nil)
return
}
tagsFromReq, ok := r.URL.Query()["tags"]
if !ok || len(tagsFromReq) == 0 || tagsFromReq[0] == "" {
aH.respond(w, &allDashboards)
return
}
tags2Dash := make(map[string][]int)
for i := 0; i < len(*allDashboards); i++ {
tags, ok := (*allDashboards)[i].Data["tags"].([]interface{})
if !ok {
continue
}
tagsArray := make([]string, len(tags))
for i, v := range tags {
tagsArray[i] = v.(string)
}
for _, tag := range tagsArray {
tags2Dash[tag] = append(tags2Dash[tag], i)
}
}
inter := make([]int, len(*allDashboards))
for i := range inter {
inter[i] = i
}
for _, tag := range tagsFromReq {
inter = Intersection(inter, tags2Dash[tag])
}
filteredDashboards := []dashboards.Dashboard{}
for _, val := range inter {
dash := (*allDashboards)[val]
filteredDashboards = append(filteredDashboards, dash)
}
aH.respond(w, &filteredDashboards)
}
func (aH *APIHandler) deleteDashboard(w http.ResponseWriter, r *http.Request) {
uuid := mux.Vars(r)["uuid"]
err := dashboards.DeleteDashboard(uuid)
if err != nil {
aH.respondError(w, err, nil)
return
}
aH.respond(w, nil)
}
func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) {
uuid := mux.Vars(r)["uuid"]
var postData map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body")
return
}
err = dashboards.IsPostDataSane(&postData)
if err != nil {
aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body")
return
}
if postData["uuid"] != uuid {
aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("uuid in request param and uuid in request body do not match")}, "Error reading request body")
return
}
dashboard, apiError := dashboards.UpdateDashboard(&postData)
if apiError != nil {
aH.respondError(w, apiError, nil)
return
}
aH.respond(w, dashboard)
}
func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) {
uuid := mux.Vars(r)["uuid"]
dashboard, apiError := dashboards.GetDashboard(uuid)
if apiError != nil {
aH.respondError(w, apiError, nil)
return
}
aH.respond(w, dashboard)
}
func (aH *APIHandler) createDashboards(w http.ResponseWriter, r *http.Request) {
var postData map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
aH.respondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body")
return
}
err = dashboards.IsPostDataSane(&postData)
if err != nil {
aH.respondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body")
return
}
dash, apiErr := dashboards.CreateDashboard(&postData)
if apiErr != nil {
aH.respondError(w, apiErr, nil)
return
}
aH.respond(w, dash)
}
func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request) {
query, apiErrorObj := parseQueryRangeRequest(r)
if apiErrorObj != nil {
aH.respondError(w, apiErrorObj, nil)
return
}
// zap.S().Info(query, apiError)
ctx := r.Context()
if to := r.FormValue("timeout"); to != "" {
var cancel context.CancelFunc
timeout, err := parseMetricsDuration(to)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
res, qs, apiError := (*aH.reader).GetQueryRangeResult(ctx, query)
if apiError != nil {
aH.respondError(w, apiError, nil)
return
}
if res.Err != nil {
zap.S().Error(res.Err)
}
if res.Err != nil {
switch res.Err.(type) {
case promql.ErrQueryCanceled:
aH.respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil)
case promql.ErrQueryTimeout:
aH.respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil)
}
aH.respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil)
}
response_data := &model.QueryData{
ResultType: res.Value.Type(),
Result: res.Value,
Stats: qs,
}
aH.respond(w, response_data)
}
func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) {
queryParams, apiErrorObj := parseInstantQueryMetricsRequest(r)
if apiErrorObj != nil {
aH.respondError(w, apiErrorObj, nil)
return
}
// zap.S().Info(query, apiError)
ctx := r.Context()
if to := r.FormValue("timeout"); to != "" {
var cancel context.CancelFunc
timeout, err := parseMetricsDuration(to)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
res, qs, apiError := (*aH.reader).GetInstantQueryMetricsResult(ctx, queryParams)
if apiError != nil {
aH.respondError(w, apiError, nil)
return
}
if res.Err != nil {
zap.S().Error(res.Err)
}
if res.Err != nil {
switch res.Err.(type) {
case promql.ErrQueryCanceled:
aH.respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil)
case promql.ErrQueryTimeout:
aH.respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil)
}
aH.respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil)
}
response_data := &model.QueryData{
ResultType: res.Value.Type(),
Result: res.Value,
Stats: qs,
}
aH.respond(w, response_data)
}
func (aH *APIHandler) user(w http.ResponseWriter, r *http.Request) {
email := r.URL.Query().Get("email")
var err error
if len(email) == 0 {
err = fmt.Errorf("Email param is missing")
}
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
(*aH.pc).Enqueue(posthog.Identify{
DistinctId: aH.distinctId,
Properties: posthog.NewProperties().
Set("email", email),
})
}
func (aH *APIHandler) getOperations(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["service"]
var err error
if len(serviceName) == 0 {
err = fmt.Errorf("service param not found")
}
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetOperations(context.Background(), serviceName)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) getServicesList(w http.ResponseWriter, r *http.Request) {
result, err := (*aH.reader).GetServicesList(context.Background())
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) searchTags(w http.ResponseWriter, r *http.Request) {
serviceName := r.URL.Query().Get("service")
result, err := (*aH.reader).GetTags(context.Background(), serviceName)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) getTopEndpoints(w http.ResponseWriter, r *http.Request) {
query, err := parseGetTopEndpointsRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetTopEndpoints(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) getUsage(w http.ResponseWriter, r *http.Request) {
query, err := parseGetUsageRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetUsage(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) getServiceDBOverview(w http.ResponseWriter, r *http.Request) {
query, err := parseGetServiceExternalRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetServiceDBOverview(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) getServiceExternal(w http.ResponseWriter, r *http.Request) {
query, err := parseGetServiceExternalRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetServiceExternal(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) GetServiceExternalAvgDuration(w http.ResponseWriter, r *http.Request) {
query, err := parseGetServiceExternalRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetServiceExternalAvgDuration(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) getServiceExternalErrors(w http.ResponseWriter, r *http.Request) {
query, err := parseGetServiceExternalRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetServiceExternalErrors(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) getServiceOverview(w http.ResponseWriter, r *http.Request) {
query, err := parseGetServiceOverviewRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetServiceOverview(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) getServices(w http.ResponseWriter, r *http.Request) {
query, err := parseGetServicesRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetServices(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
if len(*result) != 4 {
(*aH.pc).Enqueue(posthog.Capture{
DistinctId: distinctId,
Event: "Different Number of Services",
Properties: posthog.NewProperties().Set("number", len(*result)),
})
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) serviceMapDependencies(w http.ResponseWriter, r *http.Request) {
query, err := parseGetServicesRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).GetServiceMapDependencies(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
traceId := vars["traceId"]
result, err := (*aH.reader).SearchTraces(context.Background(), traceId)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) searchSpansAggregates(w http.ResponseWriter, r *http.Request) {
query, err := parseSearchSpanAggregatesRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
result, err := (*aH.reader).SearchSpansAggregate(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
func (aH *APIHandler) searchSpans(w http.ResponseWriter, r *http.Request) {
query, err := parseSpanSearchRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
// result, err := druidQuery.SearchSpans(aH.client, query)
result, err := (*aH.reader).SearchSpans(context.Background(), query)
if aH.handleError(w, err, http.StatusBadRequest) {
return
}
aH.writeJSON(w, r, result)
}
// func (aH *APIHandler) getApplicationPercentiles(w http.ResponseWriter, r *http.Request) {
// // vars := mux.Vars(r)
// query, err := parseApplicationPercentileRequest(r)
// if aH.handleError(w, err, http.StatusBadRequest) {
// return
// }
// result, err := (*aH.reader).GetApplicationPercentiles(context.Background(), query)
// if aH.handleError(w, err, http.StatusBadRequest) {
// return
// }
// aH.writeJSON(w, r, result)
// }
func (aH *APIHandler) handleError(w http.ResponseWriter, err error, statusCode int) bool {
if err == nil {
return false
}
if statusCode == http.StatusInternalServerError {
zap.S().Error("HTTP handler, Internal Server Error", zap.Error(err))
}
structuredResp := structuredResponse{
Errors: []structuredError{
{
Code: statusCode,
Msg: err.Error(),
},
},
}
resp, _ := json.Marshal(&structuredResp)
http.Error(w, string(resp), statusCode)
return true
}
func (aH *APIHandler) writeJSON(w http.ResponseWriter, r *http.Request, response interface{}) {
marshall := json.Marshal
if prettyPrint := r.FormValue("pretty"); prettyPrint != "" && prettyPrint != "false" {
marshall = func(v interface{}) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
}
}
resp, _ := marshall(response)
w.Header().Set("Content-Type", "application/json")
w.Write(resp)
}