diff --git a/pkg/query-service/app/explorer/db.go b/pkg/query-service/app/explorer/db.go index d7b1193508..7e520abfe8 100644 --- a/pkg/query-service/app/explorer/db.go +++ b/pkg/query-service/app/explorer/db.go @@ -1,26 +1,32 @@ package explorer import ( + "context" "encoding/json" "fmt" + "strings" "time" "github.com/google/uuid" "github.com/jmoiron/sqlx" + "go.signoz.io/signoz/pkg/query-service/auth" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" ) var db *sqlx.DB -type ExplorerQuery struct { +type SavedView struct { UUID string `json:"uuid" db:"uuid"` + Name string `json:"name" db:"name"` + Category string `json:"category" db:"category"` CreatedAt time.Time `json:"created_at" db:"created_at"` + CreatedBy string `json:"created_by" db:"created_by"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + UpdatedBy string `json:"updated_by" db:"updated_by"` SourcePage string `json:"source_page" db:"source_page"` - // 0 - false, 1 - true - IsView int8 `json:"is_view" db:"is_view"` - Data string `json:"data" db:"data"` - ExtraData string `json:"extra_data" db:"extra_data"` + Tags string `json:"tags" db:"tags"` + Data string `json:"data" db:"data"` + ExtraData string `json:"extra_data" db:"extra_data"` } // InitWithDSN sets up setting up the connection pool global variable. @@ -32,19 +38,23 @@ func InitWithDSN(dataSourceName string) (*sqlx.DB, error) { return nil, err } - tableSchema := `CREATE TABLE IF NOT EXISTS explorer_queries ( + tableSchema := `CREATE TABLE IF NOT EXISTS saved_views ( uuid TEXT PRIMARY KEY, + name TEXT NOT NULL, + category TEXT NOT NULL, created_at datetime NOT NULL, + created_by TEXT, updated_at datetime NOT NULL, + updated_by TEXT, source_page TEXT NOT NULL, - is_view INTEGER NOT NULL, + tags TEXT, data TEXT NOT NULL, extra_data TEXT );` _, err = db.Exec(tableSchema) if err != nil { - return nil, fmt.Errorf("Error in creating explorer queries table: %s", err.Error()) + return nil, fmt.Errorf("error in creating saved views table: %s", err.Error()) } return db, nil @@ -54,38 +64,79 @@ func InitWithDB(sqlDB *sqlx.DB) { db = sqlDB } -func GetQueries() ([]*v3.ExplorerQuery, error) { - var queries []ExplorerQuery - err := db.Select(&queries, "SELECT * FROM explorer_queries") +func GetViews() ([]*v3.SavedView, error) { + var views []SavedView + err := db.Select(&views, "SELECT * FROM saved_views") if err != nil { - return nil, fmt.Errorf("Error in getting explorer queries: %s", err.Error()) + return nil, fmt.Errorf("error in getting saved views: %s", err.Error()) } - var explorerQueries []*v3.ExplorerQuery - for _, query := range queries { + var savedViews []*v3.SavedView + for _, view := range views { var compositeQuery v3.CompositeQuery - err = json.Unmarshal([]byte(query.Data), &compositeQuery) + err = json.Unmarshal([]byte(view.Data), &compositeQuery) if err != nil { - return nil, fmt.Errorf("Error in unmarshalling explorer query data: %s", err.Error()) + return nil, fmt.Errorf("error in unmarshalling explorer query data: %s", err.Error()) } - explorerQueries = append(explorerQueries, &v3.ExplorerQuery{ - UUID: query.UUID, - SourcePage: query.SourcePage, + savedViews = append(savedViews, &v3.SavedView{ + UUID: view.UUID, + Name: view.Name, + Category: view.Category, + CreatedAt: view.CreatedAt, + CreatedBy: view.CreatedBy, + UpdatedAt: view.UpdatedAt, + UpdatedBy: view.UpdatedBy, + Tags: strings.Split(view.Tags, ","), + SourcePage: view.SourcePage, CompositeQuery: &compositeQuery, - IsView: query.IsView, - ExtraData: query.ExtraData, + ExtraData: view.ExtraData, }) } - return explorerQueries, nil + return savedViews, nil } -func CreateQuery(query v3.ExplorerQuery) (string, error) { - data, err := json.Marshal(query.CompositeQuery) +func GetViewsForFilters(sourcePage string, name string, category string) ([]*v3.SavedView, error) { + var views []SavedView + var err error + if len(category) == 0 { + err = db.Select(&views, "SELECT * FROM saved_views WHERE source_page = ? AND name LIKE ?", sourcePage, "%"+name+"%") + } else { + err = db.Select(&views, "SELECT * FROM saved_views WHERE source_page = ? AND category LIKE ? AND name LIKE ?", sourcePage, "%"+category+"%", "%"+name+"%") + } if err != nil { - return "", fmt.Errorf("Error in marshalling explorer query data: %s", err.Error()) + return nil, fmt.Errorf("error in getting saved views: %s", err.Error()) } - uuid_ := query.UUID + var savedViews []*v3.SavedView + for _, view := range views { + var compositeQuery v3.CompositeQuery + err = json.Unmarshal([]byte(view.Data), &compositeQuery) + if err != nil { + return nil, fmt.Errorf("error in unmarshalling explorer query data: %s", err.Error()) + } + savedViews = append(savedViews, &v3.SavedView{ + UUID: view.UUID, + Name: view.Name, + CreatedAt: view.CreatedAt, + CreatedBy: view.CreatedBy, + UpdatedAt: view.UpdatedAt, + UpdatedBy: view.UpdatedBy, + Tags: strings.Split(view.Tags, ","), + SourcePage: view.SourcePage, + CompositeQuery: &compositeQuery, + ExtraData: view.ExtraData, + }) + } + return savedViews, nil +} + +func CreateView(ctx context.Context, view v3.SavedView) (string, error) { + data, err := json.Marshal(view.CompositeQuery) + if err != nil { + return "", fmt.Errorf("error in marshalling explorer query data: %s", err.Error()) + } + + uuid_ := view.UUID if uuid_ == "" { uuid_ = uuid.New().String() @@ -93,63 +144,101 @@ func CreateQuery(query v3.ExplorerQuery) (string, error) { createdAt := time.Now() updatedAt := time.Now() + email, err := getEmailFromJwt(ctx) + if err != nil { + return "", err + } + + createBy := email + updatedBy := email + _, err = db.Exec( - "INSERT INTO explorer_queries (uuid, created_at, updated_at, source_page, is_view, data, extra_data) VALUES (?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO saved_views (uuid, name, category, created_at, created_by, updated_at, updated_by, source_page, tags, data, extra_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", uuid_, + view.Name, + view.Category, createdAt, + createBy, updatedAt, - query.SourcePage, - query.IsView, + updatedBy, + view.SourcePage, + strings.Join(view.Tags, ","), data, - query.ExtraData, + view.ExtraData, ) if err != nil { - return "", fmt.Errorf("Error in creating explorer query: %s", err.Error()) + return "", fmt.Errorf("error in creating saved view: %s", err.Error()) } return uuid_, nil } -func GetQuery(uuid_ string) (*v3.ExplorerQuery, error) { - var query ExplorerQuery - err := db.Get(&query, "SELECT * FROM explorer_queries WHERE uuid = ?", uuid_) +func GetView(uuid_ string) (*v3.SavedView, error) { + var view SavedView + err := db.Get(&view, "SELECT * FROM saved_views WHERE uuid = ?", uuid_) if err != nil { - return nil, fmt.Errorf("Error in getting explorer query: %s", err.Error()) + return nil, fmt.Errorf("error in getting saved view: %s", err.Error()) } var compositeQuery v3.CompositeQuery - err = json.Unmarshal([]byte(query.Data), &compositeQuery) + err = json.Unmarshal([]byte(view.Data), &compositeQuery) if err != nil { - return nil, fmt.Errorf("Error in unmarshalling explorer query data: %s", err.Error()) + return nil, fmt.Errorf("error in unmarshalling explorer query data: %s", err.Error()) } - return &v3.ExplorerQuery{ - UUID: query.UUID, - SourcePage: query.SourcePage, + return &v3.SavedView{ + UUID: view.UUID, + Name: view.Name, + Category: view.Category, + CreatedAt: view.CreatedAt, + CreatedBy: view.CreatedBy, + UpdatedAt: view.UpdatedAt, + UpdatedBy: view.UpdatedBy, + SourcePage: view.SourcePage, + Tags: strings.Split(view.Tags, ","), CompositeQuery: &compositeQuery, - IsView: query.IsView, - ExtraData: query.ExtraData, + ExtraData: view.ExtraData, }, nil } -func UpdateQuery(uuid_ string, query v3.ExplorerQuery) error { - data, err := json.Marshal(query.CompositeQuery) +func UpdateView(ctx context.Context, uuid_ string, view v3.SavedView) error { + data, err := json.Marshal(view.CompositeQuery) if err != nil { - return fmt.Errorf("Error in marshalling explorer query data: %s", err.Error()) + return fmt.Errorf("error in marshalling explorer query data: %s", err.Error()) + } + + email, err := getEmailFromJwt(ctx) + if err != nil { + return err } updatedAt := time.Now() + updatedBy := email - _, err = db.Exec("UPDATE explorer_queries SET updated_at = ?, source_page = ?, is_view = ?, data = ?, extra_data = ? WHERE uuid = ?", - updatedAt, query.SourcePage, query.IsView, data, query.ExtraData, uuid_) + _, err = db.Exec("UPDATE saved_views SET updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ? WHERE uuid = ?", + updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData, uuid_) if err != nil { - return fmt.Errorf("Error in updating explorer query: %s", err.Error()) + return fmt.Errorf("error in updating saved view: %s", err.Error()) } return nil } -func DeleteQuery(uuid_ string) error { - _, err := db.Exec("DELETE FROM explorer_queries WHERE uuid = ?", uuid_) +func DeleteView(uuid_ string) error { + _, err := db.Exec("DELETE FROM saved_views WHERE uuid = ?", uuid_) if err != nil { - return fmt.Errorf("Error in deleting explorer query: %s", err.Error()) + return fmt.Errorf("error in deleting explorer query: %s", err.Error()) } return nil } + +func getEmailFromJwt(ctx context.Context) (string, error) { + jwt, err := auth.ExtractJwtFromContext(ctx) + if err != nil { + return "", err + } + + claims, err := auth.ParseJWT(jwt) + if err != nil { + return "", err + } + + return claims["email"].(string), nil +} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index fa55154ac2..45ba2ea970 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -338,11 +338,11 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) { router.HandleFunc("/api/v1/variables/query", am.ViewAccess(aH.queryDashboardVars)).Methods(http.MethodGet) router.HandleFunc("/api/v2/variables/query", am.ViewAccess(aH.queryDashboardVarsV2)).Methods(http.MethodPost) - router.HandleFunc("/api/v1/explorer/queries", am.ViewAccess(aH.getExplorerQueries)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/explorer/queries", am.EditAccess(aH.createExplorerQueries)).Methods(http.MethodPost) - router.HandleFunc("/api/v1/explorer/queries/{queryId}", am.ViewAccess(aH.getExplorerQuery)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/explorer/queries/{queryId}", am.EditAccess(aH.updateExplorerQuery)).Methods(http.MethodPut) - router.HandleFunc("/api/v1/explorer/queries/{queryId}", am.EditAccess(aH.deleteExplorerQuery)).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.getSavedViews)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.createSavedViews)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/explorer/views/{viewId}", am.ViewAccess(aH.getSavedView)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/explorer/views/{viewId}", am.ViewAccess(aH.updateSavedView)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/explorer/views/{viewId}", am.ViewAccess(aH.deleteSavedView)).Methods(http.MethodDelete) router.HandleFunc("/api/v1/feedback", am.OpenAccess(aH.submitFeedback)).Methods(http.MethodPost) // router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet) @@ -2522,8 +2522,13 @@ func (ah *APIHandler) createLogsPipeline(w http.ResponseWriter, r *http.Request) ah.Respond(w, res) } -func (aH *APIHandler) getExplorerQueries(w http.ResponseWriter, r *http.Request) { - queries, err := explorer.GetQueries() +func (aH *APIHandler) getSavedViews(w http.ResponseWriter, r *http.Request) { + // get sourcePage, name, and category from the query params + sourcePage := r.URL.Query().Get("sourcePage") + name := r.URL.Query().Get("name") + category := r.URL.Query().Get("category") + + queries, err := explorer.GetViewsForFilters(sourcePage, name, category) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return @@ -2531,19 +2536,20 @@ func (aH *APIHandler) getExplorerQueries(w http.ResponseWriter, r *http.Request) aH.Respond(w, queries) } -func (aH *APIHandler) createExplorerQueries(w http.ResponseWriter, r *http.Request) { - var query v3.ExplorerQuery - err := json.NewDecoder(r.Body).Decode(&query) +func (aH *APIHandler) createSavedViews(w http.ResponseWriter, r *http.Request) { + var view v3.SavedView + err := json.NewDecoder(r.Body).Decode(&view) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } // validate the query - if err := query.Validate(); err != nil { + if err := view.Validate(); err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } - uuid, err := explorer.CreateQuery(query) + ctx := auth.AttachJwtToContext(context.Background(), r) + uuid, err := explorer.CreateView(ctx, view) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return @@ -2552,44 +2558,45 @@ func (aH *APIHandler) createExplorerQueries(w http.ResponseWriter, r *http.Reque aH.Respond(w, uuid) } -func (aH *APIHandler) getExplorerQuery(w http.ResponseWriter, r *http.Request) { - queryID := mux.Vars(r)["queryId"] - query, err := explorer.GetQuery(queryID) +func (aH *APIHandler) getSavedView(w http.ResponseWriter, r *http.Request) { + viewID := mux.Vars(r)["viewId"] + view, err := explorer.GetView(viewID) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return } - aH.Respond(w, query) + aH.Respond(w, view) } -func (aH *APIHandler) updateExplorerQuery(w http.ResponseWriter, r *http.Request) { - queryID := mux.Vars(r)["queryId"] - var query v3.ExplorerQuery - err := json.NewDecoder(r.Body).Decode(&query) +func (aH *APIHandler) updateSavedView(w http.ResponseWriter, r *http.Request) { + viewID := mux.Vars(r)["viewId"] + var view v3.SavedView + err := json.NewDecoder(r.Body).Decode(&view) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } // validate the query - if err := query.Validate(); err != nil { + if err := view.Validate(); err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } - err = explorer.UpdateQuery(queryID, query) + ctx := auth.AttachJwtToContext(context.Background(), r) + err = explorer.UpdateView(ctx, viewID, view) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return } - aH.Respond(w, query) + aH.Respond(w, view) } -func (aH *APIHandler) deleteExplorerQuery(w http.ResponseWriter, r *http.Request) { +func (aH *APIHandler) deleteSavedView(w http.ResponseWriter, r *http.Request) { - queryID := mux.Vars(r)["queryId"] - err := explorer.DeleteQuery(queryID) + viewID := mux.Vars(r)["viewId"] + err := explorer.DeleteView(viewID) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index 55242199b3..d46c644eb4 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -638,24 +638,26 @@ func (p *Point) UnmarshalJSON(data []byte) error { return err } -// ExploreQuery is a query for the explore page -// It is a composite query with a source page name +// SavedView is a saved query for the explore page +// It is a composite query with a source page name and user defined tags // The source page name is used to identify the page that initiated the query -// The source page could be "traces", "logs", "metrics" or "dashboards", "alerts" etc. -type ExplorerQuery struct { +// The source page could be "traces", "logs", "metrics". +type SavedView struct { UUID string `json:"uuid,omitempty"` + Name string `json:"name"` + Category string `json:"category"` + CreatedAt time.Time `json:"createdAt"` + CreatedBy string `json:"createdBy"` + UpdatedAt time.Time `json:"updatedAt"` + UpdatedBy string `json:"updatedBy"` SourcePage string `json:"sourcePage"` + Tags []string `json:"tags"` CompositeQuery *CompositeQuery `json:"compositeQuery"` // ExtraData is JSON encoded data used by frontend to store additional data ExtraData string `json:"extraData"` - // 0 - false, 1 - true; this is int8 because sqlite doesn't support bool - IsView int8 `json:"isView"` } -func (eq *ExplorerQuery) Validate() error { - if eq.IsView != 0 && eq.IsView != 1 { - return fmt.Errorf("isView must be 0 or 1") - } +func (eq *SavedView) Validate() error { if eq.CompositeQuery == nil { return fmt.Errorf("composite query is required")