From b92fe0e0f5ef8f959f7a5c7a996ed1a846fd5b74 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 29 Apr 2022 09:55:33 +0200 Subject: [PATCH] Query history: Add from and to parameters (#48212) * Query history: Add from and to parameters * Update docs/sources/http_api/query_history.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/query_history.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/query_history.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Implement Grafana relative time range Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> --- docs/sources/http_api/query_history.md | 3 +- pkg/services/queryhistory/api.go | 5 + pkg/services/queryhistory/database.go | 5 +- pkg/services/queryhistory/models.go | 2 + .../queryhistory/queryhistory_search_test.go | 162 +++++++++++++++++- pkg/services/queryhistory/writers.go | 14 +- 6 files changed, 175 insertions(+), 16 deletions(-) diff --git a/docs/sources/http_api/query_history.md b/docs/sources/http_api/query_history.md index b68c6ee9042..9dbca08abdb 100644 --- a/docs/sources/http_api/query_history.md +++ b/docs/sources/http_api/query_history.md @@ -85,12 +85,13 @@ Returns a list of queries in the query history that matches the search criteria. Query parameters: -- **datasourceUid** - Filter the query history for selected data sources. You must specify at least one data source UID. To perform an "AND" filtering with multiple data sources, specify the data source parameter using the following format: `datasourceUid=uid1&datasourceUid=uid2`. +- **datasourceUid** - Filter the query history for the selected data source. To perform an "AND" filtering with multiple data sources, specify the data source parameter using the following format: `datasourceUid=uid1&datasourceUid=uid2`. - **searchString** – Filter the query history based on the content. - **sort** - Specify the sorting order. Sorting can be `time-asc` or `time-desc`. The default is `time-desc`. - **onlyStarred** - Search for queries that are starred. Defaults to `false`. - **page** - Search supports pagination. Specify which page number to return. Use the limit parameter to specify the number of queries per page. - **limit** - Limits the number of returned query history items per page. The default is 100 queries per page. +- **from/to** - Specifies time range for the query history search. The time can be either epoch timestamps in milliseconds or relative using Grafana time units. For example, now-5m. **Example request for query history search**: diff --git a/pkg/services/queryhistory/api.go b/pkg/services/queryhistory/api.go index fd719cf7b9e..eac4ca1667e 100644 --- a/pkg/services/queryhistory/api.go +++ b/pkg/services/queryhistory/api.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -39,6 +40,8 @@ func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Respo } func (s *QueryHistoryService) searchHandler(c *models.ReqContext) response.Response { + timeRange := legacydata.NewDataTimeRange(c.Query("from"), c.Query("to")) + query := SearchInQueryHistoryQuery{ DatasourceUIDs: c.QueryStrings("datasourceUid"), SearchString: c.Query("searchString"), @@ -46,6 +49,8 @@ func (s *QueryHistoryService) searchHandler(c *models.ReqContext) response.Respo Sort: c.Query("sort"), Page: c.QueryInt("page"), Limit: c.QueryInt("limit"), + From: timeRange.GetFromAsSecondsEpoch(), + To: timeRange.GetToAsSecondsEpoch(), } result, err := s.SearchInQueryHistory(c.Req.Context(), c.SignedInUser, query) diff --git a/pkg/services/queryhistory/database.go b/pkg/services/queryhistory/database.go index 3f718a6b69a..dfa0295c683 100644 --- a/pkg/services/queryhistory/database.go +++ b/pkg/services/queryhistory/database.go @@ -2,7 +2,6 @@ package queryhistory import ( "context" - "errors" "fmt" "time" @@ -47,8 +46,8 @@ func (s QueryHistoryService) searchQueries(ctx context.Context, user *models.Sig var dtos []QueryHistoryDTO var allQueries []interface{} - if len(query.DatasourceUIDs) == 0 { - return QueryHistorySearchResult{}, errors.New("no selected data source for query history search") + if query.To <= 0 { + query.To = time.Now().Unix() } if query.Page <= 0 { diff --git a/pkg/services/queryhistory/models.go b/pkg/services/queryhistory/models.go index 19d7d77ad5c..e930604725e 100644 --- a/pkg/services/queryhistory/models.go +++ b/pkg/services/queryhistory/models.go @@ -41,6 +41,8 @@ type SearchInQueryHistoryQuery struct { Sort string `json:"sort"` Page int `json:"page"` Limit int `json:"limit"` + From int64 `json:"from"` + To int64 `json:"to"` } type PatchQueryCommentInQueryHistoryCommand struct { diff --git a/pkg/services/queryhistory/queryhistory_search_test.go b/pkg/services/queryhistory/queryhistory_search_test.go index 9d3db5ca328..dbbacabf315 100644 --- a/pkg/services/queryhistory/queryhistory_search_test.go +++ b/pkg/services/queryhistory/queryhistory_search_test.go @@ -5,18 +5,14 @@ package queryhistory import ( "encoding/json" + "strconv" "testing" + "time" "github.com/stretchr/testify/require" ) func TestGetQueriesFromQueryHistory(t *testing.T) { - testScenario(t, "When users tries to get query without datasourceUid, it should fail", - func(t *testing.T, sc scenarioContext) { - resp := sc.service.searchHandler(sc.reqContext) - require.Equal(t, 500, resp.Status()) - }) - testScenario(t, "When users tries to get query in empty query history, it should return empty result", func(t *testing.T, sc scenarioContext) { sc.reqContext.Req.Form.Add("datasourceUid", "test") @@ -39,6 +35,16 @@ func TestGetQueriesFromQueryHistory(t *testing.T) { require.Equal(t, 1, response.Result.TotalCount) }) + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries without datasourceUid, it should return correct queries", + func(t *testing.T, sc scenarioContext) { + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 3, response.Result.TotalCount) + }) + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries with datasourceUid, it should return correct queries", func(t *testing.T, sc scenarioContext) { sc.reqContext.Req.Form.Add("datasourceUid", sc.initialResult.Result.DatasourceUID) @@ -77,7 +83,7 @@ func TestGetQueriesFromQueryHistory(t *testing.T) { require.Equal(t, 0, response.Result.TotalCount) }) - testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries with multiple datasourceUid, it should return correct queries", + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries with multiple datasourceUid, it should return correct queries", func(t *testing.T, sc scenarioContext) { sc.reqContext.Req.Form.Add("datasourceUid", testDsUID1) sc.reqContext.Req.Form.Add("datasourceUid", testDsUID2) @@ -114,4 +120,146 @@ func TestGetQueriesFromQueryHistory(t *testing.T) { require.Equal(t, 2, response.Result.TotalCount) require.Equal(t, true, response.Result.QueryHistory[0].Starred) }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using from filter, it should return correct queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("from", strconv.FormatInt(time.Now().UnixMilli()-60*1000, 10)) + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 3, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using relative from filter, it should return correct queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("from", "now-1m") + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 3, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using from filter, it should return no queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("from", strconv.FormatInt(time.Now().UnixMilli()+60*1000, 10)) + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 0, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using from filter, it should return no queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("from", "now+1m") + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 0, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using to filter, it should return correct queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("to", strconv.FormatInt(time.Now().UnixMilli(), 10)) + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 3, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using to filter, it should return correct queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("to", "now") + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 3, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using to filter, it should return no queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("to", strconv.FormatInt(time.Now().UnixMilli()-60*1000, 10)) + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 0, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using to filter, it should return no queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("to", "now-1m") + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 0, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using from and to filter, it should return correct queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("to", strconv.FormatInt(time.Now().UnixMilli(), 10)) + sc.reqContext.Req.Form.Add("from", strconv.FormatInt(time.Now().UnixMilli()-60*1000, 10)) + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 3, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using from and to filter with other filters, it should return correct queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("to", strconv.FormatInt(time.Now().UnixMilli(), 10)) + sc.reqContext.Req.Form.Add("from", strconv.FormatInt(time.Now().UnixMilli()-60*1000, 10)) + sc.reqContext.Req.Form.Add("datasourceUid", testDsUID1) + sc.reqContext.Req.Form.Add("searchString", "2") + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 2, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using from and to filter with other filters, it should return correct queries", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("to", "now") + sc.reqContext.Req.Form.Add("from", "now-1m") + sc.reqContext.Req.Form.Add("datasourceUid", testDsUID1) + sc.reqContext.Req.Form.Add("searchString", "2") + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 2, response.Result.TotalCount) + }) + + testScenarioWithMultipleQueriesInQueryHistory(t, "When users tries to get queries using from and to filter with other filters, it should return no query", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.Req.Form.Add("to", strconv.FormatInt(time.Now().UnixMilli()-60, 10)) + sc.reqContext.Req.Form.Add("from", strconv.FormatInt(time.Now().UnixMilli()+60, 10)) + sc.reqContext.Req.Form.Add("datasourceUid", testDsUID1) + sc.reqContext.Req.Form.Add("searchString", "2") + resp := sc.service.searchHandler(sc.reqContext) + var response QueryHistorySearchResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, 0, response.Result.TotalCount) + }) } diff --git a/pkg/services/queryhistory/writers.go b/pkg/services/queryhistory/writers.go index 034a055ecfb..4b8e4cf7087 100644 --- a/pkg/services/queryhistory/writers.go +++ b/pkg/services/queryhistory/writers.go @@ -23,12 +23,16 @@ func writeStarredSQL(query SearchInQueryHistoryQuery, sqlStore *sqlstore.SQLStor } func writeFiltersSQL(query SearchInQueryHistoryQuery, user *models.SignedInUser, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) { - params := []interface{}{user.OrgId, user.UserId, "%" + query.SearchString + "%", "%" + query.SearchString + "%"} - for _, uid := range query.DatasourceUIDs { - params = append(params, uid) - } + params := []interface{}{user.OrgId, user.UserId, query.From, query.To, "%" + query.SearchString + "%", "%" + query.SearchString + "%"} var sql bytes.Buffer - sql.WriteString(" WHERE query_history.org_id = ? AND query_history.created_by = ? AND (query_history.queries " + sqlStore.Dialect.LikeStr() + " ? OR query_history.comment " + sqlStore.Dialect.LikeStr() + " ?) AND query_history.datasource_uid IN (? " + strings.Repeat(",?", len(query.DatasourceUIDs)-1) + ") ") + sql.WriteString(" WHERE query_history.org_id = ? AND query_history.created_by = ? AND query_history.created_at >= ? AND query_history.created_at <= ? AND (query_history.queries " + sqlStore.Dialect.LikeStr() + " ? OR query_history.comment " + sqlStore.Dialect.LikeStr() + " ?) ") + + if len(query.DatasourceUIDs) > 0 { + for _, uid := range query.DatasourceUIDs { + params = append(params, uid) + } + sql.WriteString(" AND query_history.datasource_uid IN (? " + strings.Repeat(",?", len(query.DatasourceUIDs)-1) + ") ") + } builder.Write(sql.String(), params...) }