fix(api): use POST requests for /alerts.json endpoint

This commit is contained in:
Łukasz Mierzwa
2021-07-12 22:09:11 +01:00
committed by Łukasz Mierzwa
parent ccb4d7498f
commit 1f32666036
11 changed files with 484 additions and 246 deletions
+4
View File
@@ -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
+13 -21
View File
@@ -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 {
+62 -67
View File
@@ -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{}
-23
View File
@@ -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)
+36 -6
View File
@@ -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)
+1 -1
View File
@@ -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)
@@ -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
}
@@ -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
}
+12 -9
View File
@@ -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
}
+319 -114
View File
@@ -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)
}
}
}
+10
View File
@@ -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"`