From 1f32666036732f1eae8a3dd68cea60142d3b52d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 12 Jul 2021 21:05:55 +0100 Subject: [PATCH] fix(api): use POST requests for /alerts.json endpoint --- CHANGELOG.md | 4 + cmd/karma/alerts.go | 34 +- cmd/karma/api_test.go | 129 +++--- cmd/karma/autocomplete.go | 23 - cmd/karma/benchmark_test.go | 42 +- cmd/karma/main.go | 2 +- .../tests/testscript/035_log_requests.txt | 19 +- .../tests/testscript/076_basicAuth_uri.txt | 13 +- cmd/karma/views.go | 21 +- cmd/karma/views_test.go | 433 +++++++++++++----- internal/models/api.go | 10 + 11 files changed, 484 insertions(+), 246 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f9d006d8..95d822c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Added a dedicated API endpoint for silence previews. - Added a dedicated API endpoint for overview modal. +### Changed + +- Refactored internal APIs. + ## v0.87 ### Added diff --git a/cmd/karma/alerts.go b/cmd/karma/alerts.go index e43dded45..a578c24f2 100644 --- a/cmd/karma/alerts.go +++ b/cmd/karma/alerts.go @@ -3,7 +3,6 @@ package main import ( "fmt" "math" - "net/http" "sort" "github.com/fvbommel/sortorder" @@ -164,36 +163,29 @@ func sortByStartsAt(i, j int, groups []models.APIAlertGroup, sortReverse bool) b return groups[i].LatestStartsAt.Before(groups[j].LatestStartsAt) } -func getSortOptions(r *http.Request) (string, string, string) { - sortOrder, found := lookupQueryString(r, "sortOrder") - if !found || sortOrder == "" { +func getSortOptions(r models.AlertsRequest) (string, bool, string) { + sortOrder := r.SortOrder + if sortOrder == "" { sortOrder = config.Config.Grid.Sorting.Order } - sortReverse, found := lookupQueryString(r, "sortReverse") - if !found || (sortReverse != "0" && sortReverse != "1") { - if config.Config.Grid.Sorting.Reverse { - sortReverse = "1" - } else { - sortReverse = "0" - } - } + sortReverse := r.SortReverse - sortLabel, found := lookupQueryString(r, "sortLabel") - if !found || sortLabel == "" { + sortLabel := r.SortLabel + if sortLabel == "" { sortLabel = config.Config.Grid.Sorting.Label } return sortOrder, sortReverse, sortLabel } -func sortAlertGroups(r *http.Request, groups []models.APIAlertGroup) []models.APIAlertGroup { +func sortAlertGroups(r models.AlertsRequest, groups []models.APIAlertGroup) []models.APIAlertGroup { sortOrder, sortReverse, sortLabel := getSortOptions(r) switch sortOrder { case "startsAt": sort.Slice(groups, func(i, j int) bool { - return sortByStartsAt(i, j, groups, sortReverse == "1") + return sortByStartsAt(i, j, groups, sortReverse) }) case "label": sort.Slice(groups, func(i, j int) bool { @@ -206,18 +198,18 @@ func sortAlertGroups(r *http.Request, groups []models.APIAlertGroup) []models.AP if vi == "" { // first label is missing - return sortReverse != "0" + return sortReverse } if vj == "" { // second label is missing - return sortReverse == "0" + return !sortReverse } if vi == vj { // both labels are equal fallback to timestamp sort return sortByStartsAt(i, j, groups, true) } // finnally return groups sorted by label - if sortReverse == "1" { + if sortReverse { return !sortorder.NaturalLess(vi, vj) } return sortorder.NaturalLess(vi, vj) @@ -226,7 +218,7 @@ func sortAlertGroups(r *http.Request, groups []models.APIAlertGroup) []models.AP // sort alert groups so they are always returned in the same order // use group ID which is unique and immutable sort.Slice(groups, func(i, j int) bool { - if sortReverse == "1" { + if sortReverse { return groups[i].ID < groups[j].ID } return groups[i].ID > groups[j].ID @@ -236,7 +228,7 @@ func sortAlertGroups(r *http.Request, groups []models.APIAlertGroup) []models.AP return groups } -func sortGrids(r *http.Request, gridLabel string, gridsMap map[string]models.APIGrid, gridSortReverse bool) []models.APIGrid { +func sortGrids(r models.AlertsRequest, gridLabel string, gridsMap map[string]models.APIGrid, gridSortReverse bool) []models.APIGrid { grids := make([]models.APIGrid, 0, len(gridsMap)) for _, g := range gridsMap { diff --git a/cmd/karma/api_test.go b/cmd/karma/api_test.go index c2473e9aa..448f85458 100644 --- a/cmd/karma/api_test.go +++ b/cmd/karma/api_test.go @@ -1,8 +1,8 @@ package main import ( + "bytes" "encoding/json" - "fmt" "net/http" "net/http/httptest" "reflect" @@ -776,6 +776,15 @@ func testAlertGroup(version string, t *testing.T, testCase groupTest, group mode } func TestVerifyAllGroups(t *testing.T) { + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + mockConfig() for _, version := range mock.ListAllMocks() { t.Logf("Testing API using mock files from Alertmanager %s", version) @@ -783,11 +792,11 @@ func TestVerifyAllGroups(t *testing.T) { apiCache.Purge() r := testRouter() setupRouter(r, nil) - req := httptest.NewRequest("GET", "/alerts.json", nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) } ur := models.AlertsResponse{} @@ -839,130 +848,103 @@ func TestVerifyAllGroups(t *testing.T) { type sortTest struct { defaultSortReverse bool - filter string + filter []string sortOrder string sortLabel string - sortReverse string + sortReverse bool expectedLabel string expectedValues []string } var sortTests = []sortTest{ { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "label", sortLabel: "cluster", - sortReverse: "0", + sortReverse: false, expectedLabel: "cluster", expectedValues: []string{"dev", "dev", "prod", "prod", "staging", "staging"}, }, { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "label", sortLabel: "cluster", - sortReverse: "1", + sortReverse: true, expectedLabel: "cluster", expectedValues: []string{"staging", "staging", "prod", "prod", "dev", "dev"}, }, { - filter: "q=cluster=dev", + filter: []string{"cluster=dev"}, sortOrder: "label", sortLabel: "cluster", - sortReverse: "0", + sortReverse: false, expectedLabel: "cluster", expectedValues: []string{"dev", "dev", "dev", "dev"}, }, { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "label", sortLabel: "disk", - sortReverse: "0", + sortReverse: false, expectedLabel: "disk", expectedValues: []string{"sda", "", "", "", "", "", "", "", "", "", "", ""}, }, { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "label", sortLabel: "disk", - sortReverse: "1", + sortReverse: true, expectedLabel: "disk", expectedValues: []string{"", "", "", "", "", "", "", "", "", "", "", "sda"}, }, { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "disabled", sortLabel: "", - sortReverse: "0", + sortReverse: false, expectedLabel: "cluster", expectedValues: []string{"dev", "prod", "staging", "dev", "staging", "prod"}, }, { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "disabled", sortLabel: "", - sortReverse: "1", + sortReverse: true, expectedLabel: "cluster", expectedValues: []string{"prod", "staging", "dev", "staging", "prod", "dev"}, }, { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "", sortLabel: "", - sortReverse: "0", + sortReverse: false, expectedLabel: "cluster", expectedValues: []string{"dev", "dev", "prod", "prod", "staging", "staging"}, }, { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "", sortLabel: "", - sortReverse: "1", + sortReverse: true, expectedLabel: "cluster", expectedValues: []string{"staging", "staging", "prod", "prod", "dev", "dev"}, }, { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "label", sortLabel: "job", - sortReverse: "0", + sortReverse: false, expectedLabel: "job", expectedValues: []string{"node_exporter", "node_exporter", "node_exporter", "node_ping", "node_ping", "node_ping"}, }, { - filter: "q=@receiver=by-cluster-service", + filter: []string{"@receiver=by-cluster-service"}, sortOrder: "label", sortLabel: "job", - sortReverse: "1", + sortReverse: true, expectedLabel: "job", expectedValues: []string{"node_ping", "node_ping", "node_ping", "node_exporter", "node_exporter", "node_exporter"}, }, - { - defaultSortReverse: true, - filter: "q=@receiver=by-cluster-service", - sortOrder: "label", - sortLabel: "job", - sortReverse: "a", - expectedLabel: "job", - expectedValues: []string{"node_ping", "node_ping", "node_ping", "node_exporter", "node_exporter", "node_exporter"}, - }, - { - defaultSortReverse: false, - filter: "q=@receiver=by-cluster-service", - sortOrder: "label", - sortLabel: "job", - sortReverse: "2", - expectedLabel: "job", - expectedValues: []string{"node_exporter", "node_exporter", "node_exporter", "node_ping", "node_ping", "node_ping"}, - }, - { - defaultSortReverse: false, - filter: "q=@alertmanager!=default", - sortOrder: "label", - sortLabel: "job", - sortReverse: "2", - expectedLabel: "job", - expectedValues: []string{}, - }, } func TestSortOrder(t *testing.T) { @@ -984,23 +966,27 @@ func TestSortOrder(t *testing.T) { for _, testCase := range sortTests { apiCache.Purge() config.Config.Grid.Sorting.Reverse = testCase.defaultSortReverse - uri := fmt.Sprintf( - "/alerts.json?sortOrder=%s&sortLabel=%s&sortReverse=%s&%s", - testCase.sortOrder, - testCase.sortLabel, - testCase.sortReverse, - testCase.filter, - ) - t.Logf("Request URI: %s", uri) - req := httptest.NewRequest("GET", uri, nil) + + payload, err := json.Marshal(models.AlertsRequest{ + Filters: testCase.filter, + GridLimits: map[string]int{}, + SortLabel: testCase.sortLabel, + SortOrder: testCase.sortOrder, + SortReverse: testCase.sortReverse, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) } ur := models.AlertsResponse{} - err := json.Unmarshal(resp.Body.Bytes(), &ur) + err = json.Unmarshal(resp.Body.Bytes(), &ur) if err != nil { t.Errorf("Failed to unmarshal response: %s", err) } @@ -1068,6 +1054,15 @@ func TestStripLabels(t *testing.T) { {keep: []string{"alertname"}, strip: []string{}}, } + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + mockConfig() for _, version := range mock.ListAllMocks() { t.Logf("Testing API using mock files from Alertmanager %s", version) @@ -1079,11 +1074,11 @@ func TestStripLabels(t *testing.T) { config.Config.Labels.Keep = testCase.keep config.Config.Labels.Strip = testCase.strip apiCache.Purge() - req := httptest.NewRequest("GET", "/alerts.json", nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) } ur := models.AlertsResponse{} diff --git a/cmd/karma/autocomplete.go b/cmd/karma/autocomplete.go index 47df20ec1..0038d8d55 100644 --- a/cmd/karma/autocomplete.go +++ b/cmd/karma/autocomplete.go @@ -4,7 +4,6 @@ import ( "encoding/json" "net/http" "sort" - "strconv" "strings" "github.com/prymitive/karma/internal/alertmanager" @@ -24,28 +23,6 @@ func lookupQueryStringSlice(r *http.Request, key string) ([]string, bool) { return vals, found } -func lookupLoadLimits(r *http.Request, key string) map[string]int { - limits := map[string]int{} - vals, found := lookupQueryStringSlice(r, key) - if !found { - return limits - } - var limit int - var err error - for _, val := range vals { - valSlice := strings.SplitN(val, "=", 2) - if len(valSlice) != 2 { - continue - } - limit, err = strconv.Atoi(valSlice[1]) - if err != nil { - continue - } - limits[valSlice[0]] = limit - } - return limits -} - // knownLabelNames allows querying known label names func knownLabelNames(w http.ResponseWriter, r *http.Request) { noCache(w) diff --git a/cmd/karma/benchmark_test.go b/cmd/karma/benchmark_test.go index b49da334a..f27fe6c91 100644 --- a/cmd/karma/benchmark_test.go +++ b/cmd/karma/benchmark_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/json" "fmt" "net/http/httptest" "os" @@ -9,6 +10,7 @@ import ( "testing" "github.com/prymitive/karma/internal/mock" + "github.com/prymitive/karma/internal/models" "github.com/rs/zerolog" ) @@ -122,6 +124,15 @@ func BenchmarkPullAlerts(b *testing.B) { func BenchmarkAlertsAPIMisses(b *testing.B) { zerolog.SetGlobalLevel(zerolog.FatalLevel) + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }) + if err != nil { + b.Error(err) + b.FailNow() + } + mockConfig() for _, version := range mock.ListAllMocks() { mockAlerts(version) @@ -130,8 +141,8 @@ func BenchmarkAlertsAPIMisses(b *testing.B) { b.Run(version, func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - uri := fmt.Sprintf("/alerts.json?q=&_=%d", i) - req := httptest.NewRequest("GET", uri, nil) + uri := fmt.Sprintf("/alerts.json?_=%d", i) + req := httptest.NewRequest("POST", uri, bytes.NewReader(payload)) resp := httptest.NewRecorder() b.StartTimer() r.ServeHTTP(resp, req) @@ -157,8 +168,18 @@ func BenchmarkAlertsAPIMissesAutoGrid(b *testing.B) { b.Run(version, func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - uri := fmt.Sprintf("/alerts.json?gridLabel=@auto&q=&_=%d", i) - req := httptest.NewRequest("GET", uri, nil) + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{ + fmt.Sprintf("foo!=%d", i), + }, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }) + if err != nil { + b.Error(err) + b.FailNow() + } + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() b.StartTimer() r.ServeHTTP(resp, req) @@ -176,20 +197,29 @@ func BenchmarkAlertsAPIMissesAutoGrid(b *testing.B) { func BenchmarkAlertsAPIHits(b *testing.B) { zerolog.SetGlobalLevel(zerolog.FatalLevel) + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }) + if err != nil { + b.Error(err) + b.FailNow() + } + mockConfig() for _, version := range mock.ListAllMocks() { mockAlerts(version) r := testRouter() setupRouter(r, nil) - req := httptest.NewRequest("GET", "/alerts.json?q=", nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) b.Run(version, func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - req := httptest.NewRequest("GET", "/alerts.json?q=", nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() b.StartTimer() r.ServeHTTP(resp, req) diff --git a/cmd/karma/main.go b/cmd/karma/main.go index 79cbf98b1..02fe8b4e3 100644 --- a/cmd/karma/main.go +++ b/cmd/karma/main.go @@ -137,7 +137,7 @@ func setupRouter(router *chi.Mux, historyPoller *historyPoller) { h := promhttp.Handler() h.ServeHTTP(w, r) })) - router.Get(getViewURL("/alerts.json"), alerts) + router.Post(getViewURL("/alerts.json"), alerts) router.Get(getViewURL("/alertList.json"), alertList) router.Get(getViewURL("/autocomplete.json"), autocomplete) router.Get(getViewURL("/labelNames.json"), knownLabelNames) diff --git a/cmd/karma/tests/testscript/035_log_requests.txt b/cmd/karma/tests/testscript/035_log_requests.txt index 04d35fcdf..1f23847af 100644 --- a/cmd/karma/tests/testscript/035_log_requests.txt +++ b/cmd/karma/tests/testscript/035_log_requests.txt @@ -3,12 +3,23 @@ exec bash -x ./test.sh & karma.bin-should-work --pid-file=karma.pid --alertmanager.uri=http://127.0.0.1 --listen.address=127.0.0.1 --listen.port=8035 --log.requests ! stdout . -stderr 'msg="Request completed" address=127.0.0.1:[0-9]+ bytes=[0-9]+ code=200 duration=".+" method=GET path=/alerts.json' -stderr 'msg="Request completed" address=127.0.0.1:[0-9]+ bytes=[0-9]+ code=200 duration=".+" method=GET path=/alerts.json' +stderr 'msg="Request completed" address=127.0.0.1:[0-9]+ bytes=[0-9]+ code=200 duration=".+" method=POST path=/alerts.json' +stderr 'msg="Request completed" address=127.0.0.1:[0-9]+ bytes=[0-9]+ code=200 duration=".+" method=POST path=/alerts.json' -- test.sh -- while [ ! -f karma.pid ]; do sleep 1 ; done sleep 1 -curl -so /dev/null http://127.0.0.1:8035/alerts.json -curl -so /dev/null http://127.0.0.1:8035/alerts.json +curl -XPOST -d @request.json -so /dev/null http://127.0.0.1:8035/alerts.json +curl -XPOST -d @request.json -so /dev/null http://127.0.0.1:8035/alerts.json cat karma.pid | xargs kill + +-- request.json -- +{ + "filters": [], + "gridLabel": "@auto", + "gridSortReverse": false, + "gridLimits": {}, + "sortOrder": "", + "sortLabel": "", + "sortReverse": false +} diff --git a/cmd/karma/tests/testscript/076_basicAuth_uri.txt b/cmd/karma/tests/testscript/076_basicAuth_uri.txt index 6bf5abb44..2db696584 100644 --- a/cmd/karma/tests/testscript/076_basicAuth_uri.txt +++ b/cmd/karma/tests/testscript/076_basicAuth_uri.txt @@ -9,5 +9,16 @@ stdout '"publicURI":"http://foo:bar@127.0.0.1"' -- test.sh -- while [ ! -f karma.pid ]; do sleep 1 ; done sleep 1 -curl -s http://127.0.0.1:8076/alerts.json +curl -XPOST -d @request.json -s http://127.0.0.1:8076/alerts.json cat karma.pid | xargs kill + +-- request.json -- +{ + "filters": [], + "gridLabel": "@auto", + "gridSortReverse": false, + "gridLimits": {}, + "sortOrder": "", + "sortLabel": "", + "sortReverse": false +} diff --git a/cmd/karma/views.go b/cmd/karma/views.go index 2a387c0d6..dd5e01196 100644 --- a/cmd/karma/views.go +++ b/cmd/karma/views.go @@ -195,8 +195,15 @@ func alerts(w http.ResponseWriter, r *http.Request) { resp.Settings.Sorting.ValueMapping = config.Config.Grid.Sorting.CustomValues.Labels } + var request models.AlertsRequest + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // use full URI (including query args) as cache key - cacheKey := r.RequestURI + cacheKey := fmt.Sprintf("%x", structhash.Sha1(request, 1)) data, found := apiCache.Get(cacheKey) if found { @@ -215,10 +222,7 @@ func alerts(w http.ResponseWriter, r *http.Request) { return } - gridLabel, _ := lookupQueryString(r, "gridLabel") - q, _ := lookupQueryStringSlice(r, "q") - matchFilters := getFiltersFromQuery(q) - limits := lookupLoadLimits(r, "limit") + matchFilters := getFiltersFromQuery(request.Filters) grids := map[string]models.APIGrid{} colors := models.LabelsColorMap{} @@ -238,6 +242,7 @@ func alerts(w http.ResponseWriter, r *http.Request) { filtered := filterAlerts(dedupedAlerts, matchFilters) + gridLabel := request.GridLabel if gridLabel == "@auto" { gridLabel = autoGridLabel(filtered) log.Debug().Str("label", gridLabel).Msg("Selected automatic grid label") @@ -435,13 +440,11 @@ func alerts(w http.ResponseWriter, r *http.Request) { } } - v, _ := lookupQueryString(r, "gridSortReverse") - gridSortReverse := v == "1" - sortedGrids := sortGrids(r, gridLabel, grids, gridSortReverse) + sortedGrids := sortGrids(request, gridLabel, grids, request.GridSortReverse) for i := 0; i < len(sortedGrids); i++ { sortedGrids[i].TotalGroups = len(sortedGrids[i].AlertGroups) - limit, found := limits[sortedGrids[i].LabelValue] + limit, found := request.GridLimits[sortedGrids[i].LabelValue] if !found { limit = config.Config.Grid.GroupLimit } diff --git a/cmd/karma/views_test.go b/cmd/karma/views_test.go index a3c542b1a..cfb558e05 100644 --- a/cmd/karma/views_test.go +++ b/cmd/karma/views_test.go @@ -119,7 +119,7 @@ func TestIndex(t *testing.T) { }, { prefix: "", - request: "/alerts.json", + request: "/alertList.json", status: 200, }, { @@ -129,7 +129,7 @@ func TestIndex(t *testing.T) { }, { prefix: "/", - request: "/alerts.json", + request: "/alertList.json", status: 200, }, { @@ -139,7 +139,7 @@ func TestIndex(t *testing.T) { }, { prefix: "/prefix", - request: "/alerts.json", + request: "/alertList.json", status: 404, }, { @@ -149,7 +149,7 @@ func TestIndex(t *testing.T) { }, { prefix: "/prefix", - request: "/prefix/alerts.json", + request: "/prefix/alertList.json", status: 200, }, { @@ -171,7 +171,7 @@ func TestIndex(t *testing.T) { }, { prefix: "/prefix/", - request: "/prefix/alerts.json", + request: "/prefix/alertList.json", status: 200, }, } @@ -226,6 +226,19 @@ func mockAlerts(version string) { } func TestAlerts(t *testing.T) { + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{ + "@receiver=by-cluster-service", + "alertname=HTTP_Probe_Failed", + "instance=web1", + }, + GridLimits: map[string]int{}, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + mockConfig() for _, version := range mock.ListAllMocks() { t.Logf("Testing alerts using mock files from Alertmanager %s", version) @@ -234,11 +247,11 @@ func TestAlerts(t *testing.T) { setupRouter(r, nil) // re-run a few times to test the cache for i := 1; i <= 3; i++ { - req := httptest.NewRequest("GET", "/alerts.json?q=@receiver=by-cluster-service&q=alertname=HTTP_Probe_Failed&q=instance=web1", nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) } ur := models.AlertsResponse{} @@ -296,20 +309,40 @@ func TestAlerts(t *testing.T) { } } +func TestAlertsBadRequest(t *testing.T) { + mockConfig() + for _, version := range mock.ListAllMocks() { + t.Logf("Testing alerts using mock files from Alertmanager %s", version) + mockAlerts(version) + r := testRouter() + setupRouter(r, nil) + // re-run a few times to test the cache + for i := 1; i <= 3; i++ { + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader([]byte("foo bar{}"))) + resp := httptest.NewRecorder() + r.ServeHTTP(resp, req) + if resp.Code != http.StatusBadRequest { + t.Errorf("POST /alerts.json returned status %d", resp.Code) + } + } + } +} + func TestGrids(t *testing.T) { type testCaseGridT struct { labelValue string alertGroupCount int } type testCaseT struct { - gridLabel string - requestQuery string - grids []testCaseGridT + request models.AlertsRequest + grids []testCaseGridT } testCases := []testCaseT{ { - gridLabel: "cluster", - requestQuery: "", + request: models.AlertsRequest{ + GridLabel: "cluster", + GridLimits: map[string]int{}, + }, grids: []testCaseGridT{ {labelValue: "dev", alertGroupCount: 4}, {labelValue: "prod", alertGroupCount: 4}, @@ -317,8 +350,11 @@ func TestGrids(t *testing.T) { }, }, { - gridLabel: "cluster", - requestQuery: "&gridSortReverse=1", + request: models.AlertsRequest{ + GridLabel: "cluster", + GridLimits: map[string]int{}, + GridSortReverse: true, + }, grids: []testCaseGridT{ {labelValue: "staging", alertGroupCount: 4}, {labelValue: "prod", alertGroupCount: 4}, @@ -326,80 +362,107 @@ func TestGrids(t *testing.T) { }, }, { - gridLabel: "foo", - requestQuery: "", + request: models.AlertsRequest{ + GridLabel: "foo", + GridLimits: map[string]int{}, + }, grids: []testCaseGridT{ {labelValue: "", alertGroupCount: 10}, }, }, { - gridLabel: "", - requestQuery: "", + request: models.AlertsRequest{ + GridLabel: "", + GridLimits: map[string]int{}, + }, grids: []testCaseGridT{ {labelValue: "", alertGroupCount: 10}, }, }, { - gridLabel: "", - requestQuery: "&q=foo=bar", - grids: []testCaseGridT{}, + request: models.AlertsRequest{ + GridLabel: "", + GridLimits: map[string]int{}, + Filters: []string{"foo=bar"}, + }, + grids: []testCaseGridT{}, }, { - gridLabel: "disk", - requestQuery: "", + request: models.AlertsRequest{ + GridLabel: "disk", + GridLimits: map[string]int{}, + }, grids: []testCaseGridT{ {labelValue: "sda", alertGroupCount: 2}, {labelValue: "", alertGroupCount: 8}, }, }, { - gridLabel: "disk", - requestQuery: "&gridSortReverse=1", + request: models.AlertsRequest{ + GridLabel: "disk", + GridLimits: map[string]int{}, + GridSortReverse: true, + }, grids: []testCaseGridT{ {labelValue: "", alertGroupCount: 8}, {labelValue: "sda", alertGroupCount: 2}, }, }, { - gridLabel: "disk", - requestQuery: "&q=alertname=Free_Disk_Space_Too_Low", + request: models.AlertsRequest{ + GridLabel: "disk", + GridLimits: map[string]int{}, + Filters: []string{"alertname=Free_Disk_Space_Too_Low"}, + }, grids: []testCaseGridT{ {labelValue: "sda", alertGroupCount: 2}, }, }, { - gridLabel: "@alertmanager", - requestQuery: "", + request: models.AlertsRequest{ + GridLabel: "@alertmanager", + GridLimits: map[string]int{}, + }, grids: []testCaseGridT{ {labelValue: "default", alertGroupCount: 10}, }, }, { - gridLabel: "@cluster", - requestQuery: "", + request: models.AlertsRequest{ + GridLabel: "@cluster", + GridLimits: map[string]int{}, + }, grids: []testCaseGridT{ {labelValue: "default", alertGroupCount: 10}, }, }, { - gridLabel: "@receiver", - requestQuery: "", + request: models.AlertsRequest{ + GridLabel: "@receiver", + GridLimits: map[string]int{}, + }, grids: []testCaseGridT{ {labelValue: "by-cluster-service", alertGroupCount: 6}, {labelValue: "by-name", alertGroupCount: 4}, }, }, { - gridLabel: "@receiver", - requestQuery: "&gridSortReverse=1", + request: models.AlertsRequest{ + GridLabel: "@receiver", + GridLimits: map[string]int{}, + GridSortReverse: true, + }, grids: []testCaseGridT{ {labelValue: "by-name", alertGroupCount: 4}, {labelValue: "by-cluster-service", alertGroupCount: 6}, }, }, { - gridLabel: "@receiver", - requestQuery: "&q=@receiver=by-name", + request: models.AlertsRequest{ + GridLabel: "@receiver", + GridLimits: map[string]int{}, + Filters: []string{"@receiver=by-name"}, + }, grids: []testCaseGridT{ {labelValue: "by-name", alertGroupCount: 4}, }, @@ -411,18 +474,24 @@ func TestGrids(t *testing.T) { version := version for _, testCase := range testCases { testCase := testCase - t.Run(fmt.Sprintf("version=%q gridLabel=%q query=%q", version, testCase.gridLabel, testCase.requestQuery), func(t *testing.T) { + t.Run(fmt.Sprintf("version=%q request=%v", version, testCase.request), func(t *testing.T) { + payload, err := json.Marshal(testCase.request) + if err != nil { + t.Error(err) + t.FailNow() + } + mockAlerts(version) r := testRouter() setupRouter(r, nil) // re-run a few times to test the cache for i := 1; i <= 3; i++ { apiCache.Purge() - req := httptest.NewRequest("GET", "/alerts.json?gridLabel="+testCase.gridLabel+testCase.requestQuery, nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) } ur := models.AlertsResponse{} @@ -436,8 +505,8 @@ func TestGrids(t *testing.T) { } else { for index, expectedGrid := range testCase.grids { grid := ur.Grids[index] - if grid.LabelName != testCase.gridLabel { - t.Errorf("Got wrong labelName for grid %d: %q, expected %q", index, grid.LabelName, testCase.gridLabel) + if grid.LabelName != testCase.request.GridLabel { + t.Errorf("Got wrong labelName for grid %d: %q, expected %q", index, grid.LabelName, testCase.request.GridLabel) } if grid.LabelValue != expectedGrid.labelValue { t.Errorf("Got wrong labelValue for grid %d: %q, expected %q", index, grid.LabelValue, expectedGrid.labelValue) @@ -454,6 +523,18 @@ func TestGrids(t *testing.T) { } func TestValidateAllAlerts(t *testing.T) { + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{ + "alertname=HTTP_Probe_Failed", + "instance=web1", + }, + GridLimits: map[string]int{}, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + mockConfig() for _, version := range mock.ListAllMocks() { t.Logf("Validating alerts.json response using mock files from Alertmanager %s", version) @@ -462,11 +543,11 @@ func TestValidateAllAlerts(t *testing.T) { setupRouter(r, nil) // re-run a few times to test the cache for i := 1; i <= 3; i++ { - req := httptest.NewRequest("GET", "/alerts.json?q=alertname=HTTP_Probe_Failed&q=instance=web1", nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) } ur := models.AlertsResponse{} body := resp.Body.Bytes() @@ -686,7 +767,7 @@ func TestGzipMiddleware(t *testing.T) { mockConfig() r := testRouter() setupRouter(r, nil) - paths := []string{"/", "/alerts.json", "/autocomplete.json", "/metrics"} + paths := []string{"/", "/alertList.json", "/autocomplete.json", "/metrics"} for _, path := range paths { // re-run a few times to test the cache for i := 1; i <= 3; i++ { @@ -713,7 +794,7 @@ func TestGzipMiddlewareWithoutAcceptEncoding(t *testing.T) { mockConfig() r := testRouter() setupRouter(r, nil) - paths := []string{"/", "/alerts.json", "/autocomplete.json", "/metrics"} + paths := []string{"/", "/alertList.json", "/autocomplete.json", "/metrics"} for _, path := range paths { // re-run a few times to test the cache for i := 1; i <= 3; i++ { @@ -921,19 +1002,28 @@ func TestCORS(t *testing.T) { } func TestEmptySettings(t *testing.T) { + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + mockConfig() r := testRouter() setupRouter(r, nil) - req := httptest.NewRequest("GET", "/alerts.json", nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) } ur := models.AlertsResponse{} body := resp.Body.Bytes() - err := json.Unmarshal(body, &ur) + err = json.Unmarshal(body, &ur) if err != nil { t.Errorf("Failed to unmarshal response: %s", err) } @@ -1100,7 +1190,7 @@ func TestAuthentication(t *testing.T) { mockCache() for _, path := range []string{ "/", - "/alerts.json", + "/alertList.json", "/autocomplete.json?term=foo", "/labelNames.json", "/labelValues.json?name=foo", @@ -1160,7 +1250,7 @@ func TestInvalidBasicAuthHeader(t *testing.T) { mockCache() for _, path := range []string{ "/", - "/alerts.json", + "/alertList.json", "/autocomplete.json?term=foo", "/labelNames.json", "/labelValues.json?name=foo", @@ -2357,6 +2447,19 @@ func TestUpstreamStatus(t *testing.T) { }, } + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{ + "@receiver=by-cluster-service", + "alertname=HTTP_Probe_Failed", + "instance=web1", + }, + GridLimits: map[string]int{}, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { zerolog.SetGlobalLevel(zerolog.FatalLevel) @@ -2385,11 +2488,11 @@ func TestUpstreamStatus(t *testing.T) { } pullFromAlertmanager() - req := httptest.NewRequest("GET", "/alerts.json?q=@receiver=by-cluster-service&q=alertname=HTTP_Probe_Failed&q=instance=web1", nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) } ur := models.AlertsResponse{} @@ -2421,7 +2524,15 @@ func TestUpstreamStatus(t *testing.T) { } func TestGetUserFromContextMissing(t *testing.T) { - req := httptest.NewRequest("GET", "/alerts.json", nil) + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) user := getUserFromContext(req) if user != "" { t.Errorf("getUserFromContext() returned user=%q", user) @@ -2429,7 +2540,15 @@ func TestGetUserFromContextMissing(t *testing.T) { } func TestGetUserFromContextPresent(t *testing.T) { - req := httptest.NewRequest("GET", "/alerts.json", nil) + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) ctx := context.WithValue(req.Context(), authUserKey("user"), "bob") user := getUserFromContext(req.WithContext(ctx)) if user == "" { @@ -2629,15 +2748,24 @@ func TestAlertFilters(t *testing.T) { pullFromAlertmanager() + payload, err := json.Marshal(models.AlertsRequest{ + Filters: tc.filters, + GridLimits: map[string]int{}, + }) + if err != nil { + t.Error(err) + t.FailNow() + } + r := testRouter() setupRouter(r, nil) // re-run a few times to test the cache for i := 1; i <= 3; i++ { - req := httptest.NewRequest("GET", fmt.Sprintf("/alerts.json?%s", q), nil) + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) } ur := models.AlertsResponse{} body := resp.Body.Bytes() @@ -2732,7 +2860,7 @@ func TestDecompressResponseReadError(t *testing.T) { func TestAutoGrid(t *testing.T) { type testCaseT struct { - q string + request models.AlertsRequest gridLabel string ignore []string order []string @@ -2740,65 +2868,112 @@ func TestAutoGrid(t *testing.T) { testCases := []testCaseT{ { - q: "", + request: models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + GridLabel: "", + }, gridLabel: "", }, { - q: "gridLabel=@auto", + request: models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "job", }, { - q: "gridLabel=@auto&q=cluster!=prod", + request: models.AlertsRequest{ + Filters: []string{"cluster!=prod"}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "cluster", ignore: []string{"job"}, order: []string{"cluster"}, }, { - q: "gridLabel=@auto&q=cluster!=prod", + request: models.AlertsRequest{ + Filters: []string{"cluster!=prod"}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "cluster", ignore: []string{}, order: []string{"cluster"}, }, { - q: "gridLabel=@auto&q=cluster!=prod", - gridLabel: "job", - ignore: []string{}, - order: []string{"job", "cluster"}, + request: models.AlertsRequest{ + Filters: []string{"cluster!=prod"}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "job", + ignore: []string{}, + order: []string{"job", "cluster"}, }, { - q: "gridLabel=@auto&q=job=node_exporter", + request: models.AlertsRequest{ + Filters: []string{"job=node_exporter"}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "cluster", ignore: []string{}, order: []string{"job", "cluster"}, }, { - q: "gridLabel=@auto&q=cluster=dev", + request: models.AlertsRequest{ + Filters: []string{"cluster!=dev"}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "job", ignore: []string{}, order: []string{"job", "cluster"}, }, { - q: "gridLabel=@auto&q=cluster=dev", + request: models.AlertsRequest{ + Filters: []string{"cluster=dev"}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "alertname", ignore: []string{}, order: []string{}, }, { - q: "gridLabel=job", + request: models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + GridLabel: "job", + }, gridLabel: "job", }, { - q: "gridLabel=@auto&q=instance=server5", + request: models.AlertsRequest{ + Filters: []string{"instance=server5"}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "job", ignore: []string{"alertname"}, }, { - q: "gridLabel=@auto&q=job=node_exporter", + request: models.AlertsRequest{ + Filters: []string{"job=node_exporter"}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "cluster", ignore: []string{"alertname"}, }, { - q: "gridLabel=@auto&q=cluster=prod", + request: models.AlertsRequest{ + Filters: []string{"cluster=prod"}, + GridLimits: map[string]int{}, + GridLabel: "@auto", + }, gridLabel: "job", ignore: []string{"alertname", "instance"}, }, @@ -2813,31 +2988,39 @@ func TestAutoGrid(t *testing.T) { for _, tc := range testCases { config.Config.Grid.Auto.Ignore = tc.ignore config.Config.Grid.Auto.Order = tc.order - for _, version := range mock.ListAllMocks() { + payload, err := json.Marshal(tc.request) + if err != nil { + t.Error(err) + t.FailNow() + } + for i, version := range mock.ListAllMocks() { t.Logf("Testing alerts using mock files from Alertmanager %s", version) mockAlerts(version) r := testRouter() setupRouter(r, nil) // re-run a few times to test the cache - for i := 1; i <= 3; i++ { - req := httptest.NewRequest("GET", fmt.Sprintf("/alerts.json?%s", tc.q), nil) + for j := 1; j <= 3; j++ { + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Errorf("GET /alerts.json returned status %d", resp.Code) + t.Errorf("POST /alerts.json returned status %d", resp.Code) + t.FailNow() } ur := models.AlertsResponse{} err := json.Unmarshal(resp.Body.Bytes(), &ur) if err != nil { t.Errorf("Failed to unmarshal response: %s", err) + t.FailNow() } if len(ur.Grids) == 0 { - t.Errorf("[%s] Got empty grid list", tc.q) + t.Errorf("[%d] Got empty grid list", i) + t.FailNow() } for _, g := range ur.Grids { if g.LabelName != tc.gridLabel { - t.Errorf("[%s] Got grid using label %s=%s, expected %s", tc.q, g.LabelName, g.LabelValue, tc.gridLabel) + t.Errorf("[%d] Got grid using label %s=%s, expected %s", i, g.LabelName, g.LabelValue, tc.gridLabel) } } } @@ -2848,72 +3031,89 @@ func TestAutoGrid(t *testing.T) { func TestGridLimit(t *testing.T) { type testCaseT struct { groupLimit int - q string + request models.AlertsRequest groups map[string][]int } - testCases := []testCaseT{ { - q: "", + request: models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }, groups: map[string][]int{ "": {10, 10}, }, }, { groupLimit: 5, - q: "", + request: models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }, groups: map[string][]int{ "": {10, 5}, }, }, { groupLimit: 15, - q: "", + request: models.AlertsRequest{ + Filters: []string{}, + GridLimits: map[string]int{}, + }, groups: map[string][]int{ "": {10, 10}, }, }, { - q: "limit==1", - groups: map[string][]int{ - "": {10, 1}, + request: models.AlertsRequest{ + Filters: []string{}, + GridLabel: "job", + GridLimits: map[string]int{ + "node_exporter": 1, + }, }, - }, - { - q: "limit", - groups: map[string][]int{ - "": {10, 10}, - }, - }, - { - q: "limit=", - groups: map[string][]int{ - "": {10, 10}, - }, - }, - { - q: "gridLabel=job&limit=node_exporter=1", groups: map[string][]int{ "node_exporter": {6, 1}, "node_ping": {4, 4}, }, }, { - q: "gridLabel=job&limit=node_exporter=a&limit=node_ping=1", + request: models.AlertsRequest{ + Filters: []string{}, + GridLabel: "job", + GridLimits: map[string]int{ + "node_exporter": 10, + "node_ping": 1, + }, + }, groups: map[string][]int{ "node_exporter": {6, 6}, "node_ping": {4, 1}, }, }, { - q: "gridLabel=job&limit=node_exporter=0&limit=node_ping=2", + request: models.AlertsRequest{ + Filters: []string{}, + GridLabel: "job", + GridLimits: map[string]int{ + "node_exporter": 0, + "node_ping": 2, + }, + }, groups: map[string][]int{ "node_exporter": {6, 1}, "node_ping": {4, 2}, }, }, { - q: "gridLabel=job&limit=node_exporter=0&limit=node_ping=20", + request: models.AlertsRequest{ + Filters: []string{}, + GridLabel: "job", + GridLimits: map[string]int{ + "node_exporter": 0, + "node_ping": 20, + }, + }, groups: map[string][]int{ "node_exporter": {6, 1}, "node_ping": {4, 4}, @@ -2932,14 +3132,19 @@ func TestGridLimit(t *testing.T) { } else { config.Config.Grid.GroupLimit = 50 } - for _, version := range mock.ListAllMocks() { + payload, err := json.Marshal(tc.request) + if err != nil { + t.Error(err) + t.FailNow() + } + for i, version := range mock.ListAllMocks() { t.Logf("Testing grids using mock files from Alertmanager %s", version) mockAlerts(version) r := testRouter() setupRouter(r, nil) // re-run a few times to test the cache - for i := 1; i <= 3; i++ { - req := httptest.NewRequest("GET", fmt.Sprintf("/alerts.json?%s", tc.q), nil) + for j := 1; j <= 3; j++ { + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) resp := httptest.NewRecorder() r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { @@ -2952,11 +3157,11 @@ func TestGridLimit(t *testing.T) { t.Errorf("Failed to unmarshal response: %s", err) } if len(ur.Grids) == 0 { - t.Errorf("[%s] Got empty grid list", tc.q) + t.Errorf("[%d] Got empty grid list", i) } for _, grid := range ur.Grids { if grid.TotalGroups == 0 { - t.Errorf("[%s] got empty grid for %s=%s", tc.q, grid.LabelName, grid.LabelValue) + t.Errorf("[%d] got empty grid for %s=%s", i, grid.LabelName, grid.LabelValue) } found := false for labelValue := range tc.groups { @@ -2966,7 +3171,7 @@ func TestGridLimit(t *testing.T) { } } if !found { - t.Errorf("[%s] got extra grid %s=%s", tc.q, grid.LabelName, grid.LabelValue) + t.Errorf("[%d] got extra grid %s=%s", i, grid.LabelName, grid.LabelValue) } } for labelValue, totals := range tc.groups { @@ -2977,16 +3182,16 @@ func TestGridLimit(t *testing.T) { if grid.LabelValue == labelValue { found = true if grid.TotalGroups != totalGroups { - t.Errorf("[%s] grid for label %s=%s returned totalGroups=%d, expected %d", tc.q, grid.LabelName, grid.LabelValue, grid.TotalGroups, totalGroups) + t.Errorf("[%d] grid for label %s=%s returned totalGroups=%d, expected %d", i, grid.LabelName, grid.LabelValue, grid.TotalGroups, totalGroups) } if len(grid.AlertGroups) != presentGroups { - t.Errorf("[%s] grid for label %s=%s returned %d alert groups, expected %d", tc.q, grid.LabelName, grid.LabelValue, len(grid.AlertGroups), presentGroups) + t.Errorf("[%d] grid for label %s=%s returned %d alert groups, expected %d", i, grid.LabelName, grid.LabelValue, len(grid.AlertGroups), presentGroups) } break } } if !found { - t.Errorf("[%s] grid with label value %s missing", tc.q, labelValue) + t.Errorf("[%d] grid with label value %s missing", i, labelValue) } } } diff --git a/internal/models/api.go b/internal/models/api.go index 27ef2fbc5..322b514bd 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -369,6 +369,16 @@ type APIGrid struct { StateCount map[string]int `json:"stateCount"` } +type AlertsRequest struct { + Filters []string `json:"filters"` + GridLabel string `json:"gridLabel"` + GridLimits map[string]int `json:"gridLimits"` + GridSortReverse bool `json:"gridSortReverse"` + SortOrder string `json:"sortOrder"` + SortLabel string `json:"sortLabel"` + SortReverse bool `json:"sortReverse"` +} + // AlertsResponse is the structure of JSON response UI will use to get alert data type AlertsResponse struct { Status string `json:"status"`