Query history: Create API to add query to query history (#44479)

* Create config to enable/disable query history

* Create add to query history functionality

* Add documentation

* Add test

* Refactor

* Add test

* Fix built errors and linting errors

* Refactor

* Remove old tests

* Refactor, adjust based on feedback, add new test

* Update default value
This commit is contained in:
Ivana Huckova
2022-01-28 17:55:09 +01:00
committed by GitHub
parent ca24b95b49
commit 4e37a53a1c
15 changed files with 339 additions and 2 deletions

View File

@ -214,6 +214,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"verifyEmailEnabled": setting.VerifyEmailEnabled,
"sigV4AuthEnabled": setting.SigV4AuthEnabled,
"exploreEnabled": setting.ExploreEnabled,
"queryHistoryEnabled": hs.Cfg.QueryHistoryEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId,
"rudderstackWriteKey": setting.RudderstackWriteKey,
"rudderstackDataPlaneUrl": setting.RudderstackDataPlaneUrl,

View File

@ -47,6 +47,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/schemaloader"
@ -100,6 +101,7 @@ type HTTPServer struct {
pluginErrorResolver plugins.ErrorResolver
SearchService *search.SearchService
ShortURLService shorturls.Service
QueryHistoryService queryhistory.Service
Live *live.GrafanaLive
LivePushGateway *pushhttp.Gateway
ThumbService thumbs.Service
@ -137,8 +139,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
pluginDashboardManager plugins.PluginDashboardManager, pluginStore plugins.Store, pluginClient plugins.Client,
pluginErrorResolver plugins.ErrorResolver, settingsProvider setting.Provider,
dataSourceCache datasources.CacheService, userTokenService models.UserTokenService,
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, thumbService thumbs.Service,
remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, queryHistoryService queryhistory.Service,
thumbService thumbs.Service, remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
loginService login.Service, accessControl accesscontrol.AccessControl,
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
@ -175,6 +177,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
AuthTokenService: userTokenService,
cleanUpService: cleanUpService,
ShortURLService: shortURLService,
QueryHistoryService: queryHistoryService,
Features: features,
ThumbService: thumbService,
RemoteCacheService: remoteCache,

View File

@ -53,6 +53,7 @@ import (
"github.com/grafana/grafana/pkg/services/plugindashboards"
"github.com/grafana/grafana/pkg/services/pluginsettings"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/schemaloader"
@ -133,6 +134,8 @@ var wireBasicSet = wire.NewSet(
cleanup.ProvideService,
shorturls.ProvideService,
wire.Bind(new(shorturls.Service), new(*shorturls.ShortURLService)),
queryhistory.ProvideService,
wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)),
quota.ProvideService,
remotecache.ProvideService,
loginservice.ProvideService,

View File

@ -0,0 +1,31 @@
package queryhistory
import (
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/web"
)
func (s *QueryHistoryService) registerAPIEndpoints() {
s.RouteRegister.Group("/api/query-history", func(entities routing.RouteRegister) {
entities.Post("/", middleware.ReqSignedIn, routing.Wrap(s.createHandler))
})
}
func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Response {
cmd := CreateQueryInQueryHistoryCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
err := s.CreateQueryInQueryHistory(c.Req.Context(), c.SignedInUser, cmd)
if err != nil {
return response.Error(500, "Failed to create query history", err)
}
return response.Success("Query successfully added to query history")
}

View File

@ -0,0 +1,32 @@
package queryhistory
import (
"context"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
func (s QueryHistoryService) createQuery(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) error {
queryHistory := QueryHistory{
OrgId: user.OrgId,
Uid: util.GenerateShortUID(),
Queries: cmd.Queries,
DatasourceUid: cmd.DatasourceUid,
CreatedBy: user.UserId,
CreatedAt: time.Now().Unix(),
Comment: "",
}
err := s.SQLStore.WithDbSession(ctx, func(session *sqlstore.DBSession) error {
_, err := session.Insert(&queryHistory)
return err
})
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,21 @@
package queryhistory
import (
"github.com/grafana/grafana/pkg/components/simplejson"
)
type QueryHistory struct {
Id int64 `json:"id"`
Uid string `json:"uid"`
DatasourceUid string `json:"datasourceUid"`
OrgId int64 `json:"orgId"`
CreatedBy int64 `json:"createdBy"`
CreatedAt int64 `json:"createdAt"`
Comment string `json:"comment"`
Queries *simplejson.Json `json:"queries"`
}
type CreateQueryInQueryHistoryCommand struct {
DatasourceUid string `json:"datasourceUid"`
Queries *simplejson.Json `json:"queries"`
}

View File

@ -0,0 +1,42 @@
package queryhistory
import (
"context"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, routeRegister routing.RouteRegister) *QueryHistoryService {
s := &QueryHistoryService{
SQLStore: sqlStore,
Cfg: cfg,
RouteRegister: routeRegister,
log: log.New("query-history"),
}
// Register routes only when query history is enabled
if s.Cfg.QueryHistoryEnabled {
s.registerAPIEndpoints()
}
return s
}
type Service interface {
CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) error
}
type QueryHistoryService struct {
SQLStore *sqlstore.SQLStore
Cfg *setting.Cfg
RouteRegister routing.RouteRegister
log log.Logger
}
func (s QueryHistoryService) CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) error {
return s.createQuery(ctx, user, cmd)
}

View File

@ -0,0 +1,23 @@
package queryhistory
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/stretchr/testify/require"
)
func TestCreateQueryInQueryHistory(t *testing.T) {
testScenario(t, "When users tries to create query in query history it should succeed",
func(t *testing.T, sc scenarioContext) {
command := CreateQueryInQueryHistoryCommand{
DatasourceUid: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test",
}),
}
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
})
}

View File

@ -0,0 +1,77 @@
package queryhistory
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require"
)
var (
testOrgID = int64(1)
testUserID = int64(1)
)
type scenarioContext struct {
ctx *web.Context
service *QueryHistoryService
reqContext *models.ReqContext
sqlStore *sqlstore.SQLStore
}
func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
t.Helper()
t.Run(desc, func(t *testing.T) {
ctx := web.Context{Req: &http.Request{}}
sqlStore := sqlstore.InitTestDB(t)
service := QueryHistoryService{
Cfg: setting.NewCfg(),
SQLStore: sqlStore,
}
service.Cfg.QueryHistoryEnabled = true
user := models.SignedInUser{
UserId: testUserID,
Name: "Signed In User",
Login: "signed_in_user",
Email: "signed.in.user@test.com",
OrgId: testOrgID,
OrgRole: models.ROLE_VIEWER,
LastSeenAt: time.Now(),
}
_, err := sqlStore.CreateUser(context.Background(), models.CreateUserCommand{
Email: "signed.in.user@test.com",
Name: "Signed In User",
Login: "signed_in_user",
})
require.NoError(t, err)
sc := scenarioContext{
ctx: &ctx,
service: &service,
sqlStore: sqlStore,
reqContext: &models.ReqContext{
Context: &ctx,
SignedInUser: &user,
},
}
fn(t, sc)
})
}
func mockRequestBody(v interface{}) io.ReadCloser {
b, _ := json.Marshal(v)
return io.NopCloser(bytes.NewReader(b))
}

View File

@ -68,6 +68,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
addKVStoreMigrations(mg)
ualert.AddDashboardUIDPanelIDMigration(mg)
accesscontrol.AddMigration(mg)
addQueryHistoryMigrations(mg)
}
func addMigrationLogMigrations(mg *Migrator) {

View File

@ -0,0 +1,28 @@
package migrations
import (
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
func addQueryHistoryMigrations(mg *Migrator) {
queryHistoryV1 := Table{
Name: "query_history",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, Nullable: false, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "datasource_uid", Type: DB_NVarchar, Length: 40, Nullable: false},
{Name: "created_by", Type: DB_Int, Nullable: false},
{Name: "created_at", Type: DB_Int, Nullable: false},
{Name: "comment", Type: DB_Text, Nullable: false},
{Name: "queries", Type: DB_Text, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"org_id", "created_by", "datasource_uid"}},
},
}
mg.AddMigration("create query_history table v1", NewAddTableMigration(queryHistoryV1))
mg.AddMigration("add index query_history.org_id-created_by-datasource_uid", NewAddIndexMigration(queryHistoryV1, queryHistoryV1.Indices[0]))
}

View File

@ -429,6 +429,9 @@ type Cfg struct {
// Unified Alerting
UnifiedAlerting UnifiedAlertingSettings
// Query history
QueryHistoryEnabled bool
}
type CommandLineArgs struct {
@ -940,6 +943,9 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
explore := iniFile.Section("explore")
ExploreEnabled = explore.Key("enabled").MustBool(true)
queryHistory := iniFile.Section("query_history")
cfg.QueryHistoryEnabled = queryHistory.Key("enabled").MustBool(false)
panelsSection := iniFile.Section("panels")
cfg.DisableSanitizeHtml = panelsSection.Key("disable_sanitize_html").MustBool(false)