mirror of
https://github.com/grafana/grafana.git
synced 2024-11-21 16:38:03 -06:00
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:
parent
ca24b95b49
commit
4e37a53a1c
@ -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]
|
||||
|
@ -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]
|
||||
|
59
docs/sources/http_api/query_history.md
Normal file
59
docs/sources/http_api/query_history.md
Normal 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)
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
31
pkg/services/queryhistory/api.go
Normal file
31
pkg/services/queryhistory/api.go
Normal 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")
|
||||
}
|
32
pkg/services/queryhistory/database.go
Normal file
32
pkg/services/queryhistory/database.go
Normal 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
|
||||
}
|
21
pkg/services/queryhistory/models.go
Normal file
21
pkg/services/queryhistory/models.go
Normal 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"`
|
||||
}
|
42
pkg/services/queryhistory/queryhistory.go
Normal file
42
pkg/services/queryhistory/queryhistory.go
Normal 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)
|
||||
}
|
23
pkg/services/queryhistory/queryhistory_create_test.go
Normal file
23
pkg/services/queryhistory/queryhistory_create_test.go
Normal 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())
|
||||
})
|
||||
}
|
77
pkg/services/queryhistory/queryhistory_test.go
Normal file
77
pkg/services/queryhistory/queryhistory_test.go
Normal 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))
|
||||
}
|
@ -68,6 +68,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
|
||||
addKVStoreMigrations(mg)
|
||||
ualert.AddDashboardUIDPanelIDMigration(mg)
|
||||
accesscontrol.AddMigration(mg)
|
||||
addQueryHistoryMigrations(mg)
|
||||
}
|
||||
|
||||
func addMigrationLogMigrations(mg *Migrator) {
|
||||
|
28
pkg/services/sqlstore/migrations/query_history_mig.go
Normal file
28
pkg/services/sqlstore/migrations/query_history_mig.go
Normal 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]))
|
||||
}
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user