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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 339 additions and 2 deletions

View File

@ -865,6 +865,11 @@ max_annotations_to_keep =
# Enable the Explore section
enabled = true
#################################### Query History #############################
[query_history]
# Enable the Query history
enabled = false
#################################### Internal Grafana Metrics ############
# Metrics available at HTTP API Url /metrics
[metrics]

View File

@ -848,6 +848,11 @@
# Enable the Explore section
;enabled = true
#################################### Query History #############################
[query_history]
# Enable the Query history
;enabled = false
#################################### Internal Grafana Metrics ##########################
# Metrics available at HTTP API Url /metrics
[metrics]

View File

@ -0,0 +1,59 @@
+++
title = "Query History HTTP API "
description = "Grafana Query History HTTP API"
keywords = ["grafana", "http", "documentation", "api", "queryHistory"]
aliases = ["/docs/grafana/latest/http_api/query_history/"]
+++
# Query history API
This API can be used to add queries to Query history. It requires that the user is logged in and that Query history feature is enabled in config file.
## Add query to Query history
`POST /api/query-history`
Adds query to query history.
**Example request:**
```http
POST /api/query-history HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"dataSourceUid": "PE1C5CBDA0504A6A3",
"queries": [
{
"refId": "A",
"key": "Q-87fed8e3-62ba-4eb2-8d2a-4129979bb4de-0",
"scenarioId": "csv_content",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
}
}
]
}
```
JSON body schema:
- **datasourceUid** Data source uid.
- **queries** JSON of query or queries.
**Example response:**
```http
HTTP/1.1 200
Content-Type: application/json
{
"message": "Query successfully added to query history",
}
```
Status codes:
- **200** OK
- **500** Errors (invalid JSON, missing or invalid fields)

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)