Alerting: Backend state filtering for history UI (#109647)
This commit is contained in:
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
@@ -32,6 +34,22 @@ func (srv *HistorySrv) RouteQueryStateHistory(c *contextmodel.ReqContext) respon
|
||||
dashUID := c.Query("dashboardUID")
|
||||
panelID := c.QueryInt64("panelID")
|
||||
|
||||
previous := c.Query("previous")
|
||||
if previous != "" {
|
||||
_, err := eval.ParseStateString(previous)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusBadRequest, fmt.Errorf("invalid previous state filter: %w", err), "")
|
||||
}
|
||||
}
|
||||
|
||||
current := c.Query("current")
|
||||
if current != "" {
|
||||
_, err := eval.ParseStateString(current)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusBadRequest, fmt.Errorf("invalid current state filter: %w", err), "")
|
||||
}
|
||||
}
|
||||
|
||||
labels := make(map[string]string)
|
||||
for k, v := range c.Req.URL.Query() {
|
||||
if strings.HasPrefix(k, labelQueryPrefix) {
|
||||
@@ -44,6 +62,8 @@ func (srv *HistorySrv) RouteQueryStateHistory(c *contextmodel.ReqContext) respon
|
||||
OrgID: c.GetOrgID(),
|
||||
DashboardUID: dashUID,
|
||||
PanelID: panelID,
|
||||
Previous: previous,
|
||||
Current: current,
|
||||
SignedInUser: c.SignedInUser,
|
||||
From: time.Unix(from, 0),
|
||||
To: time.Unix(to, 0),
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
type mockHistorian struct{}
|
||||
|
||||
func (m *mockHistorian) Query(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) {
|
||||
return &data.Frame{Name: "history"}, nil
|
||||
}
|
||||
|
||||
func TestRouteQueryStateHistory(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
queryParams string
|
||||
expectedCode int
|
||||
}{
|
||||
{"valid states", "previous=Normal¤t=Alerting", http.StatusOK},
|
||||
{"invalid previous", "previous=InvalidState", http.StatusBadRequest},
|
||||
{"invalid current", "current=InvalidState", http.StatusBadRequest},
|
||||
}
|
||||
|
||||
srv := &HistorySrv{
|
||||
logger: log.NewNopLogger(),
|
||||
hist: &mockHistorian{},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/?"+tt.queryParams, nil)
|
||||
|
||||
c := &contextmodel.ReqContext{
|
||||
Context: &web.Context{Req: req},
|
||||
SignedInUser: &user.SignedInUser{OrgID: 1},
|
||||
}
|
||||
|
||||
resp := srv.RouteQueryStateHistory(c)
|
||||
|
||||
assert.Equal(t, tt.expectedCode, resp.Status())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ type HistoryQuery struct {
|
||||
DashboardUID string
|
||||
PanelID int64
|
||||
Labels map[string]string
|
||||
Previous string
|
||||
Current string
|
||||
From time.Time
|
||||
To time.Time
|
||||
Limit int
|
||||
|
||||
@@ -465,6 +465,20 @@ func buildQueryTail(query models.HistoryQuery) (string, error) {
|
||||
b.WriteString(" | panelID=")
|
||||
b.WriteString(strconv.FormatInt(query.PanelID, 10))
|
||||
}
|
||||
if query.Previous != "" {
|
||||
b.WriteString(" | previous=~")
|
||||
_, err := fmt.Fprintf(&b, "%q", "^"+regexp.QuoteMeta(query.Previous)+".*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if query.Current != "" {
|
||||
b.WriteString(" | current=~")
|
||||
_, err := fmt.Fprintf(&b, "%q", "^"+regexp.QuoteMeta(query.Current)+".*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
requiredSize := 0
|
||||
labelKeys := make([]string, 0, len(query.Labels))
|
||||
@@ -491,6 +505,8 @@ func queryHasLogFilters(query models.HistoryQuery) bool {
|
||||
return query.RuleUID != "" ||
|
||||
query.DashboardUID != "" ||
|
||||
query.PanelID != 0 ||
|
||||
query.Previous != "" ||
|
||||
query.Current != "" ||
|
||||
len(query.Labels) > 0
|
||||
}
|
||||
|
||||
|
||||
@@ -206,13 +206,13 @@ func TestRemoteLokiBackend(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildLogQuery(t *testing.T) {
|
||||
maxQuerySize := 110
|
||||
cases := []struct {
|
||||
name string
|
||||
query models.HistoryQuery
|
||||
folderUIDs []string
|
||||
exp []string
|
||||
expErr error
|
||||
name string
|
||||
query models.HistoryQuery
|
||||
folderUIDs []string
|
||||
maxQuerySize int
|
||||
exp []string
|
||||
expErr error
|
||||
}{
|
||||
{
|
||||
name: "default includes state history label and orgID label",
|
||||
@@ -328,11 +328,54 @@ func TestBuildLogQuery(t *testing.T) {
|
||||
folderUIDs: []string{"folder-1", "folder-2", "folder-" + strings.Repeat("!", 14)},
|
||||
expErr: ErrLokiQueryTooLong,
|
||||
},
|
||||
{
|
||||
name: "filters by previous state",
|
||||
query: models.HistoryQuery{
|
||||
OrgID: 123,
|
||||
Previous: "Normal",
|
||||
},
|
||||
exp: []string{`{orgID="123",from="state-history"} | json | previous=~"^Normal.*"`},
|
||||
},
|
||||
{
|
||||
name: "filters by current state",
|
||||
query: models.HistoryQuery{
|
||||
OrgID: 123,
|
||||
Current: "Alerting",
|
||||
},
|
||||
exp: []string{`{orgID="123",from="state-history"} | json | current=~"^Alerting.*"`},
|
||||
},
|
||||
{
|
||||
name: "filters by both previous and current state",
|
||||
query: models.HistoryQuery{
|
||||
OrgID: 123,
|
||||
Previous: "Normal",
|
||||
Current: "Alerting",
|
||||
},
|
||||
exp: []string{`{orgID="123",from="state-history"} | json | previous=~"^Normal.*" | current=~"^Alerting.*"`},
|
||||
},
|
||||
{
|
||||
name: "combines state filters with other filters",
|
||||
query: models.HistoryQuery{
|
||||
OrgID: 123,
|
||||
RuleUID: "rule-uid",
|
||||
Previous: "Pending",
|
||||
Current: "Alerting",
|
||||
Labels: map[string]string{
|
||||
"instance": "localhost:9090",
|
||||
},
|
||||
},
|
||||
maxQuerySize: 200,
|
||||
exp: []string{`{orgID="123",from="state-history"} | json | ruleUID="rule-uid" | previous=~"^Pending.*" | current=~"^Alerting.*" | labels_instance="localhost:9090"`},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := BuildLogQuery(tc.query, tc.folderUIDs, maxQuerySize)
|
||||
querySize := tc.maxQuerySize
|
||||
if querySize == 0 {
|
||||
querySize = 110 // default size
|
||||
}
|
||||
res, err := BuildLogQuery(tc.query, tc.folderUIDs, querySize)
|
||||
if tc.expErr != nil {
|
||||
require.ErrorIs(t, err, tc.expErr)
|
||||
return
|
||||
@@ -340,7 +383,7 @@ func TestBuildLogQuery(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, tc.exp, res)
|
||||
for i, q := range res {
|
||||
assert.LessOrEqualf(t, len(q), maxQuerySize, "query at index %d exceeded max query size. Query: %s", i, q)
|
||||
assert.LessOrEqualf(t, len(q), querySize, "query at index %d exceeded max query size. Query: %s", i, q)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user