Alerting: Backend state filtering for history UI (#109647)

This commit is contained in:
Alexander Akhmetov
2025-09-03 17:47:03 +02:00
committed by GitHub
parent 9dfd250049
commit 8a7c1f595a
17 changed files with 425 additions and 204 deletions
@@ -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&current=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())
})
}
}
+2
View File
@@ -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)
}
})
}