mirror of https://github.com/grafana/grafana
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 valuepull/44613/head
parent
ca24b95b49
commit
4e37a53a1c
@ -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) |
||||||
@ -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") |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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"` |
||||||
|
} |
||||||
@ -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) |
||||||
|
} |
||||||
@ -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()) |
||||||
|
}) |
||||||
|
} |
||||||
@ -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)) |
||||||
|
} |
||||||
@ -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])) |
||||||
|
} |
||||||
Loading…
Reference in new issue