feat(api): use a dedicated API endpoint for overview modal

This commit is contained in:
Łukasz Mierzwa
2021-07-09 12:25:32 +01:00
committed by Łukasz Mierzwa
parent 8fb76ef4c9
commit 907ac420d3
20 changed files with 1395 additions and 434 deletions
+2 -1
View File
@@ -4,7 +4,8 @@
### Added
- Added dedicated API endpoint for silence previews.
- Added a dedicated API endpoint for silence previews.
- Added a dedicated API endpoint for overview modal.
## v0.87
+1 -1
View File
@@ -5,7 +5,7 @@ include make/docker.mk
include make/lint-versions.mk
.PHONY: lint
lint: lint-bootstrap-version
lint: lint-go lint-bootstrap-version
make -C ui lint-js
.PHONY: test
+801
View File
@@ -0,0 +1,801 @@
/* snapshot: TestCounters/#00 */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache, no-store, must-revalidate
Content-Type: application/json
Vary: Origin
{
"counters": [
{
"hits": 48,
"name": "@cluster",
"values": [
{
"hits": 24,
"offset": 0,
"percent": 50,
"raw": "@cluster=cluster",
"value": "cluster"
},
{
"hits": 24,
"offset": 50,
"percent": 50,
"raw": "@cluster=single",
"value": "single"
}
]
},
{
"hits": 24,
"name": "@receiver",
"values": [
{
"hits": 12,
"offset": 0,
"percent": 50,
"raw": "@receiver=by-cluster-service",
"value": "by-cluster-service"
},
{
"hits": 12,
"offset": 50,
"percent": 50,
"raw": "@receiver=by-name",
"value": "by-name"
}
]
},
{
"hits": 24,
"name": "@state",
"values": [
{
"hits": 16,
"offset": 0,
"percent": 67,
"raw": "@state=active",
"value": "active"
},
{
"hits": 8,
"offset": 67,
"percent": 33,
"raw": "@state=suppressed",
"value": "suppressed"
}
]
},
{
"hits": 24,
"name": "alertname",
"values": [
{
"hits": 16,
"offset": 0,
"percent": 67,
"raw": "alertname=Host_Down",
"value": "Host_Down"
},
{
"hits": 4,
"offset": 67,
"percent": 17,
"raw": "alertname=HTTP_Probe_Failed",
"value": "HTTP_Probe_Failed"
},
{
"hits": 2,
"offset": 84,
"percent": 8,
"raw": "alertname=Free_Disk_Space_Too_Low",
"value": "Free_Disk_Space_Too_Low"
},
{
"hits": 2,
"offset": 92,
"percent": 8,
"raw": "alertname=Memory_Usage_Too_High",
"value": "Memory_Usage_Too_High"
}
]
},
{
"hits": 24,
"name": "cluster",
"values": [
{
"hits": 10,
"offset": 0,
"percent": 42,
"raw": "cluster=dev",
"value": "dev"
},
{
"hits": 8,
"offset": 42,
"percent": 33,
"raw": "cluster=staging",
"value": "staging"
},
{
"hits": 6,
"offset": 75,
"percent": 25,
"raw": "cluster=prod",
"value": "prod"
}
]
},
{
"hits": 24,
"name": "instance",
"values": [
{
"hits": 4,
"offset": 0,
"percent": 17,
"raw": "instance=server2",
"value": "server2"
},
{
"hits": 4,
"offset": 17,
"percent": 17,
"raw": "instance=server5",
"value": "server5"
},
{
"hits": 2,
"offset": 34,
"percent": 9,
"raw": "instance=server1",
"value": "server1"
},
{
"hits": 2,
"offset": 43,
"percent": 9,
"raw": "instance=server3",
"value": "server3"
},
{
"hits": 2,
"offset": 52,
"percent": 8,
"raw": "instance=server4",
"value": "server4"
},
{
"hits": 2,
"offset": 60,
"percent": 8,
"raw": "instance=server6",
"value": "server6"
},
{
"hits": 2,
"offset": 68,
"percent": 8,
"raw": "instance=server7",
"value": "server7"
},
{
"hits": 2,
"offset": 76,
"percent": 8,
"raw": "instance=server8",
"value": "server8"
},
{
"hits": 2,
"offset": 84,
"percent": 8,
"raw": "instance=web1",
"value": "web1"
},
{
"hits": 2,
"offset": 92,
"percent": 8,
"raw": "instance=web2",
"value": "web2"
}
]
},
{
"hits": 24,
"name": "job",
"values": [
{
"hits": 16,
"offset": 0,
"percent": 67,
"raw": "job=node_ping",
"value": "node_ping"
},
{
"hits": 8,
"offset": 67,
"percent": 33,
"raw": "job=node_exporter",
"value": "node_exporter"
}
]
},
{
"hits": 16,
"name": "ip",
"values": [
{
"hits": 2,
"offset": 0,
"percent": 13,
"raw": "ip=127.0.0.1",
"value": "127.0.0.1"
},
{
"hits": 2,
"offset": 13,
"percent": 13,
"raw": "ip=127.0.0.2",
"value": "127.0.0.2"
},
{
"hits": 2,
"offset": 26,
"percent": 13,
"raw": "ip=127.0.0.3",
"value": "127.0.0.3"
},
{
"hits": 2,
"offset": 39,
"percent": 13,
"raw": "ip=127.0.0.4",
"value": "127.0.0.4"
},
{
"hits": 2,
"offset": 52,
"percent": 12,
"raw": "ip=127.0.0.5",
"value": "127.0.0.5"
},
{
"hits": 2,
"offset": 64,
"percent": 12,
"raw": "ip=127.0.0.6",
"value": "127.0.0.6"
},
{
"hits": 2,
"offset": 76,
"percent": 12,
"raw": "ip=127.0.0.7",
"value": "127.0.0.7"
},
{
"hits": 2,
"offset": 88,
"percent": 12,
"raw": "ip=127.0.0.8",
"value": "127.0.0.8"
}
]
},
{
"hits": 2,
"name": "disk",
"values": [
{
"hits": 2,
"offset": 0,
"percent": 100,
"raw": "disk=sda",
"value": "sda"
}
]
}
],
"total": 24
}
/* snapshot: TestCounters/q=@cluster=single */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache, no-store, must-revalidate
Content-Type: application/json
Vary: Origin
{
"counters": [
{
"hits": 24,
"name": "@cluster",
"values": [
{
"hits": 24,
"offset": 0,
"percent": 100,
"raw": "@cluster=single",
"value": "single"
}
]
},
{
"hits": 24,
"name": "@receiver",
"values": [
{
"hits": 12,
"offset": 0,
"percent": 50,
"raw": "@receiver=by-cluster-service",
"value": "by-cluster-service"
},
{
"hits": 12,
"offset": 50,
"percent": 50,
"raw": "@receiver=by-name",
"value": "by-name"
}
]
},
{
"hits": 24,
"name": "@state",
"values": [
{
"hits": 16,
"offset": 0,
"percent": 67,
"raw": "@state=active",
"value": "active"
},
{
"hits": 8,
"offset": 67,
"percent": 33,
"raw": "@state=suppressed",
"value": "suppressed"
}
]
},
{
"hits": 24,
"name": "alertname",
"values": [
{
"hits": 16,
"offset": 0,
"percent": 67,
"raw": "alertname=Host_Down",
"value": "Host_Down"
},
{
"hits": 4,
"offset": 67,
"percent": 17,
"raw": "alertname=HTTP_Probe_Failed",
"value": "HTTP_Probe_Failed"
},
{
"hits": 2,
"offset": 84,
"percent": 8,
"raw": "alertname=Free_Disk_Space_Too_Low",
"value": "Free_Disk_Space_Too_Low"
},
{
"hits": 2,
"offset": 92,
"percent": 8,
"raw": "alertname=Memory_Usage_Too_High",
"value": "Memory_Usage_Too_High"
}
]
},
{
"hits": 24,
"name": "cluster",
"values": [
{
"hits": 10,
"offset": 0,
"percent": 42,
"raw": "cluster=dev",
"value": "dev"
},
{
"hits": 8,
"offset": 42,
"percent": 33,
"raw": "cluster=staging",
"value": "staging"
},
{
"hits": 6,
"offset": 75,
"percent": 25,
"raw": "cluster=prod",
"value": "prod"
}
]
},
{
"hits": 24,
"name": "instance",
"values": [
{
"hits": 4,
"offset": 0,
"percent": 17,
"raw": "instance=server2",
"value": "server2"
},
{
"hits": 4,
"offset": 17,
"percent": 17,
"raw": "instance=server5",
"value": "server5"
},
{
"hits": 2,
"offset": 34,
"percent": 9,
"raw": "instance=server1",
"value": "server1"
},
{
"hits": 2,
"offset": 43,
"percent": 9,
"raw": "instance=server3",
"value": "server3"
},
{
"hits": 2,
"offset": 52,
"percent": 8,
"raw": "instance=server4",
"value": "server4"
},
{
"hits": 2,
"offset": 60,
"percent": 8,
"raw": "instance=server6",
"value": "server6"
},
{
"hits": 2,
"offset": 68,
"percent": 8,
"raw": "instance=server7",
"value": "server7"
},
{
"hits": 2,
"offset": 76,
"percent": 8,
"raw": "instance=server8",
"value": "server8"
},
{
"hits": 2,
"offset": 84,
"percent": 8,
"raw": "instance=web1",
"value": "web1"
},
{
"hits": 2,
"offset": 92,
"percent": 8,
"raw": "instance=web2",
"value": "web2"
}
]
},
{
"hits": 24,
"name": "job",
"values": [
{
"hits": 16,
"offset": 0,
"percent": 67,
"raw": "job=node_ping",
"value": "node_ping"
},
{
"hits": 8,
"offset": 67,
"percent": 33,
"raw": "job=node_exporter",
"value": "node_exporter"
}
]
},
{
"hits": 16,
"name": "ip",
"values": [
{
"hits": 2,
"offset": 0,
"percent": 13,
"raw": "ip=127.0.0.1",
"value": "127.0.0.1"
},
{
"hits": 2,
"offset": 13,
"percent": 13,
"raw": "ip=127.0.0.2",
"value": "127.0.0.2"
},
{
"hits": 2,
"offset": 26,
"percent": 13,
"raw": "ip=127.0.0.3",
"value": "127.0.0.3"
},
{
"hits": 2,
"offset": 39,
"percent": 13,
"raw": "ip=127.0.0.4",
"value": "127.0.0.4"
},
{
"hits": 2,
"offset": 52,
"percent": 12,
"raw": "ip=127.0.0.5",
"value": "127.0.0.5"
},
{
"hits": 2,
"offset": 64,
"percent": 12,
"raw": "ip=127.0.0.6",
"value": "127.0.0.6"
},
{
"hits": 2,
"offset": 76,
"percent": 12,
"raw": "ip=127.0.0.7",
"value": "127.0.0.7"
},
{
"hits": 2,
"offset": 88,
"percent": 12,
"raw": "ip=127.0.0.8",
"value": "127.0.0.8"
}
]
},
{
"hits": 2,
"name": "disk",
"values": [
{
"hits": 2,
"offset": 0,
"percent": 100,
"raw": "disk=sda",
"value": "sda"
}
]
}
],
"total": 24
}
/* snapshot: TestCounters/q=@receiver=by-cluster-service&q=alertname=Host_Down&q=@state=active */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache, no-store, must-revalidate
Content-Type: application/json
Vary: Origin
{
"counters": [
{
"hits": 10,
"name": "@cluster",
"values": [
{
"hits": 5,
"offset": 0,
"percent": 50,
"raw": "@cluster=cluster",
"value": "cluster"
},
{
"hits": 5,
"offset": 50,
"percent": 50,
"raw": "@cluster=single",
"value": "single"
}
]
},
{
"hits": 5,
"name": "@receiver",
"values": [
{
"hits": 5,
"offset": 0,
"percent": 100,
"raw": "@receiver=by-cluster-service",
"value": "by-cluster-service"
}
]
},
{
"hits": 5,
"name": "@state",
"values": [
{
"hits": 5,
"offset": 0,
"percent": 100,
"raw": "@state=active",
"value": "active"
}
]
},
{
"hits": 5,
"name": "alertname",
"values": [
{
"hits": 5,
"offset": 0,
"percent": 100,
"raw": "alertname=Host_Down",
"value": "Host_Down"
}
]
},
{
"hits": 5,
"name": "cluster",
"values": [
{
"hits": 3,
"offset": 0,
"percent": 60,
"raw": "cluster=staging",
"value": "staging"
},
{
"hits": 2,
"offset": 60,
"percent": 40,
"raw": "cluster=prod",
"value": "prod"
}
]
},
{
"hits": 5,
"name": "instance",
"values": [
{
"hits": 1,
"offset": 0,
"percent": 20,
"raw": "instance=server1",
"value": "server1"
},
{
"hits": 1,
"offset": 20,
"percent": 20,
"raw": "instance=server2",
"value": "server2"
},
{
"hits": 1,
"offset": 40,
"percent": 20,
"raw": "instance=server3",
"value": "server3"
},
{
"hits": 1,
"offset": 60,
"percent": 20,
"raw": "instance=server4",
"value": "server4"
},
{
"hits": 1,
"offset": 80,
"percent": 20,
"raw": "instance=server5",
"value": "server5"
}
]
},
{
"hits": 5,
"name": "ip",
"values": [
{
"hits": 1,
"offset": 0,
"percent": 20,
"raw": "ip=127.0.0.1",
"value": "127.0.0.1"
},
{
"hits": 1,
"offset": 20,
"percent": 20,
"raw": "ip=127.0.0.2",
"value": "127.0.0.2"
},
{
"hits": 1,
"offset": 40,
"percent": 20,
"raw": "ip=127.0.0.3",
"value": "127.0.0.3"
},
{
"hits": 1,
"offset": 60,
"percent": 20,
"raw": "ip=127.0.0.4",
"value": "127.0.0.4"
},
{
"hits": 1,
"offset": 80,
"percent": 20,
"raw": "ip=127.0.0.5",
"value": "127.0.0.5"
}
]
},
{
"hits": 5,
"name": "job",
"values": [
{
"hits": 5,
"offset": 0,
"percent": 100,
"raw": "job=node_ping",
"value": "node_ping"
}
]
}
],
"total": 5
}
/* snapshot: TestCounters/q=foo=bar */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache, no-store, must-revalidate
Content-Type: application/json
Vary: Origin
{
"counters": [],
"total": 0
}
-259
View File
@@ -632,217 +632,6 @@ var groupTests = []groupTest{
},
}
var countsMap = models.LabelNameStatsList{
{
Name: "@receiver",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "by-cluster-service",
Hits: 12,
Percent: 50,
},
models.LabelValueStats{
Value: "by-name",
Hits: 12,
Percent: 50,
},
},
},
{
Name: "@state",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "active",
Hits: 16,
Percent: 67,
},
models.LabelValueStats{
Value: "suppressed",
Hits: 8,
Percent: 33,
},
},
},
{
Name: "alertname",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "Free_Disk_Space_Too_Low",
Hits: 2,
Percent: 8,
},
models.LabelValueStats{
Value: "HTTP_Probe_Failed",
Hits: 4,
Percent: 17,
},
models.LabelValueStats{
Value: "Host_Down",
Hits: 16,
Percent: 67,
},
models.LabelValueStats{
Value: "Memory_Usage_Too_High",
Hits: 2,
Percent: 8,
},
},
},
{
Name: "cluster",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "dev",
Hits: 10,
Percent: 42,
},
models.LabelValueStats{
Value: "prod",
Hits: 6,
Percent: 25,
},
models.LabelValueStats{
Value: "staging",
Hits: 8,
Percent: 33,
},
},
},
{
Name: "disk",
Hits: 2,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "sda",
Hits: 2,
Percent: 100,
},
},
},
{
Name: "instance",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "server1",
Hits: 2,
Percent: 9,
},
models.LabelValueStats{
Value: "server2",
Hits: 4,
Percent: 17,
},
models.LabelValueStats{
Value: "server3",
Hits: 2,
Percent: 9,
},
models.LabelValueStats{
Value: "server4",
Hits: 2,
Percent: 8,
},
models.LabelValueStats{
Value: "server5",
Hits: 4,
Percent: 17,
},
models.LabelValueStats{
Value: "server6",
Hits: 2,
Percent: 8,
},
models.LabelValueStats{
Value: "server7",
Hits: 2,
Percent: 8,
},
models.LabelValueStats{
Value: "server8",
Hits: 2,
Percent: 8,
},
models.LabelValueStats{
Value: "web1",
Hits: 2,
Percent: 8,
},
models.LabelValueStats{
Value: "web2",
Hits: 2,
Percent: 8,
},
},
},
{
Name: "ip",
Hits: 16,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "127.0.0.1",
Hits: 2,
Percent: 13,
},
models.LabelValueStats{
Value: "127.0.0.2",
Hits: 2,
Percent: 13,
},
models.LabelValueStats{
Value: "127.0.0.3",
Hits: 2,
Percent: 13,
},
models.LabelValueStats{
Value: "127.0.0.4",
Hits: 2,
Percent: 13,
},
models.LabelValueStats{
Value: "127.0.0.5",
Hits: 2,
Percent: 12,
},
models.LabelValueStats{
Value: "127.0.0.6",
Hits: 2,
Percent: 12,
},
models.LabelValueStats{
Value: "127.0.0.7",
Hits: 2,
Percent: 12,
},
models.LabelValueStats{
Value: "127.0.0.8",
Hits: 2,
Percent: 12,
},
},
},
{
Name: "job",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "node_exporter",
Hits: 8,
Percent: 33,
},
models.LabelValueStats{
Value: "node_ping",
Hits: 16,
Percent: 67,
},
},
},
}
var filtersExpected = []models.Filter{}
func compareAlertGroups(testCase groupTest, group models.APIAlertGroup) bool {
@@ -1032,54 +821,6 @@ func TestVerifyAllGroups(t *testing.T) {
t.Errorf("[%s] Silences mismatch, expected >0 but got %d", version, len(am))
}
for _, nameStats := range ur.Counters {
var totalPercent int
for _, valueStats := range nameStats.Values {
totalPercent += valueStats.Percent
}
if totalPercent != 100 {
t.Errorf("[%s] Counters %s sum is != 100: %d", version, nameStats.Name, totalPercent)
}
}
for _, expectedNameStats := range countsMap {
var foundName bool
for _, nameStats := range ur.Counters {
if nameStats.Name == expectedNameStats.Name {
if nameStats.Hits != expectedNameStats.Hits {
t.Errorf("[%s] Counters mismatch for '%s', expected %v hits but got %v",
version, nameStats.Name, expectedNameStats.Hits, nameStats.Hits)
}
for _, expectedValueStats := range expectedNameStats.Values {
var foundValue bool
for _, valueStats := range nameStats.Values {
if valueStats.Value == expectedValueStats.Value {
if valueStats.Hits != expectedValueStats.Hits {
t.Errorf("[%s] Counters mismatch for '%s: %s', expected %v hits but got %v",
version, nameStats.Name, valueStats.Value, expectedValueStats.Hits, valueStats.Hits)
}
if valueStats.Percent != expectedValueStats.Percent {
t.Errorf("[%s] Percent mismatch for '%s: %s', expected %v%% but got %v%%",
version, nameStats.Name, valueStats.Value, expectedValueStats.Percent, valueStats.Percent)
}
foundValue = true
break
}
}
if !foundValue {
if !foundName {
t.Errorf("[%s] Counters missing for label '%s: %s'", version, expectedNameStats.Name, expectedValueStats.Value)
}
}
}
foundName = true
break
}
}
if !foundName {
t.Errorf("[%s] Counters missing for label '%s'", version, expectedNameStats.Name)
}
}
if !reflect.DeepEqual(ur.Filters, filtersExpected) {
t.Errorf("[%s] Filters mismatch, expected %v but got %v", version, filtersExpected, ur.Filters)
}
+1
View File
@@ -143,6 +143,7 @@ func setupRouter(router *chi.Mux, historyPoller *historyPoller) {
router.Get(getViewURL("/labelNames.json"), knownLabelNames)
router.Get(getViewURL("/labelValues.json"), knownLabelValues)
router.Get(getViewURL("/silences.json"), silences)
router.Get(getViewURL("/counters.json"), counters)
router.Post(getViewURL("/history.json"), func(w http.ResponseWriter, r *http.Request) {
alertHistory(historyPoller, w, r)
})
+11 -2
View File
@@ -1,9 +1,11 @@
package main
import (
"fmt"
"os"
"testing"
"github.com/beme/abide"
"github.com/rogpeppe/go-internal/testscript"
"github.com/rs/zerolog/log"
"github.com/spf13/pflag"
@@ -31,10 +33,17 @@ func mainShouldWork() int {
}
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
ecode := testscript.RunMain(m, map[string]func() int{
"karma.bin-should-fail": mainShoulFail,
"karma.bin-should-work": mainShouldWork,
}))
})
err := abide.Cleanup()
if err != nil {
fmt.Printf("abide.Cleanup() error: %v\n", err)
ecode = 1
}
os.Exit(ecode)
}
func TestScripts(t *testing.T) {
+57 -16
View File
@@ -222,7 +222,6 @@ func alerts(w http.ResponseWriter, r *http.Request) {
grids := map[string]models.APIGrid{}
colors := models.LabelsColorMap{}
counters := map[string]map[string]int{}
dedupedAlerts := alertmanager.DedupAlerts()
dedupedColors := alertmanager.DedupColors()
@@ -342,19 +341,6 @@ func alerts(w http.ResponseWriter, r *http.Request) {
agCopy.Alerts = append(agCopy.Alerts, alert)
if len(upstreams.Clusters) > 1 {
clusters := map[string]bool{}
for _, am := range alert.Alertmanager {
clusters[am.Cluster] = true
}
for cluster := range clusters {
countLabel(counters, "@cluster", cluster)
}
}
countLabel(counters, "@state", alert.State)
countLabel(counters, "@receiver", alert.Receiver)
if ck, foundKey := dedupedColors["@receiver"]; foundKey {
if cv, foundVal := ck[alert.Receiver]; foundVal {
if _, found := colors["@receiver"]; !found {
@@ -405,7 +391,6 @@ func alerts(w http.ResponseWriter, r *http.Request) {
colors[key][value] = color
}
}
countLabel(counters, key, value)
}
}
}
@@ -493,7 +478,6 @@ func alerts(w http.ResponseWriter, r *http.Request) {
resp.Grids = sortedGrids
resp.Silences = silences
resp.Colors = colors
resp.Counters = countersToLabelStats(counters)
resp.Filters = populateAPIFilters(matchFilters)
resp.Receivers = receivers
@@ -761,3 +745,60 @@ func sortSliceOfLabels(labels []map[string]string, sortKeys []string, fallback s
return sortorder.NaturalLess(labels[i][fallback], labels[j][fallback])
})
}
func counters(w http.ResponseWriter, r *http.Request) {
noCache(w)
// use full URI (including query args) as cache key
cacheKey := r.RequestURI
d, found := apiCache.Get(cacheKey)
if found {
r := bytes.NewReader(d.([]byte))
rawData, _ := decompressCachedResponse(r)
mimeJSON(w)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(rawData)
return
}
q, _ := lookupQueryStringSlice(r, "q")
matchFilters := getFiltersFromQuery(q)
dedupedAlerts := alertmanager.DedupAlerts()
filtered := filterAlerts(dedupedAlerts, matchFilters)
upstreams := getUpstreams()
counters := map[string]map[string]int{}
var total int
for _, ag := range filtered {
total += len(ag.Alerts)
for _, alert := range ag.Alerts {
if len(upstreams.Clusters) > 1 {
clusters := map[string]struct{}{}
for _, am := range alert.Alertmanager {
clusters[am.Cluster] = struct{}{}
}
for cluster := range clusters {
countLabel(counters, "@cluster", cluster)
}
}
countLabel(counters, "@state", alert.State)
countLabel(counters, "@receiver", alert.Receiver)
for key, value := range alert.Labels {
countLabel(counters, key, value)
}
}
}
resp := models.Counters{
Total: total,
Counters: countersToLabelStats(counters),
}
mimeJSON(w)
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(resp)
compressedData, _ := compressResponse(data, nil)
_ = apiCache.Add(cacheKey, compressedData)
_, _ = w.Write(data)
}
+99 -3
View File
@@ -15,6 +15,7 @@ import (
"testing"
"time"
"github.com/beme/abide"
lru "github.com/hashicorp/golang-lru"
"github.com/prymitive/karma/internal/alertmanager"
"github.com/prymitive/karma/internal/config"
@@ -275,9 +276,6 @@ func TestAlerts(t *testing.T) {
if ur.Status != "success" {
t.Errorf("[%s] Invalid status in response: %s", version, ur.Status)
}
if len(ur.Counters) != 6 {
t.Errorf("[%s] Invalid number of counters in response (%d): %v", version, len(ur.Counters), ur.Counters)
}
for _, ag := range ur.Grids[0].AlertGroups {
for _, a := range ag.Alerts {
linkCount := 0
@@ -3299,3 +3297,101 @@ func TestSortSliceOfLabels(t *testing.T) {
})
}
}
func TestCounters(t *testing.T) {
type testCaseT struct {
args string
}
testCases := []testCaseT{
{
args: "q=foo=bar",
},
{
args: "",
},
{
args: "q=@receiver=by-cluster-service&q=alertname=Host_Down&q=@state=active",
},
{
args: "q=@cluster=single",
},
}
httpmock.Activate()
defer httpmock.DeactivateAndReset()
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
mockCache()
version := mock.ListAllMocks()[0]
am1, err := alertmanager.NewAlertmanager(
"cluster",
"am1",
"http://localhost/1",
)
if err != nil {
t.Error(err)
}
mock.RegisterURL("http://localhost/1/metrics", version, "metrics")
mock.RegisterURL("http://localhost/1/api/v2/status", version, "api/v2/status")
mock.RegisterURL("http://localhost/1/api/v2/silences", version, "api/v2/silences")
mock.RegisterURL("http://localhost/1/api/v2/alerts/groups", version, "api/v2/alerts/groups")
err = alertmanager.RegisterAlertmanager(am1)
if err != nil {
t.Error(err)
}
am2, err := alertmanager.NewAlertmanager(
"cluster",
"am2",
"http://localhost/2",
)
if err != nil {
t.Error(err)
}
mock.RegisterURL("http://localhost/2/metrics", version, "metrics")
mock.RegisterURL("http://localhost/2/api/v2/status", version, "api/v2/status")
mock.RegisterURL("http://localhost/2/api/v2/silences", version, "api/v2/silences")
mock.RegisterURL("http://localhost/2/api/v2/alerts/groups", version, "api/v2/alerts/groups")
err = alertmanager.RegisterAlertmanager(am2)
if err != nil {
t.Error(err)
}
am3, err := alertmanager.NewAlertmanager(
"single",
"single",
"http://localhost/3",
)
if err != nil {
t.Error(err)
}
mock.RegisterURL("http://localhost/3/metrics", version, "metrics")
mock.RegisterURL("http://localhost/3/api/v2/status", version, "api/v2/status")
mock.RegisterURL("http://localhost/3/api/v2/silences", version, "api/v2/silences")
mock.RegisterURL("http://localhost/3/api/v2/alerts/groups", version, "api/v2/alerts/groups")
err = alertmanager.RegisterAlertmanager(am3)
if err != nil {
t.Error(err)
}
pullFromAlertmanager()
for _, tc := range testCases {
t.Run(tc.args, func(t *testing.T) {
r := testRouter()
setupRouter(r, nil)
// re-run a few times to test the cache
for i := 1; i <= 3; i++ {
req := httptest.NewRequest("GET", "/counters.json?"+tc.args, nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Errorf("GET /counters.json returned status %d", resp.Code)
}
abide.AssertHTTPResponse(t, t.Name(), resp.Result())
}
})
}
}
+1
View File
@@ -5,6 +5,7 @@ go 1.16
require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08
github.com/fvbommel/sortorder v1.0.2
github.com/getsentry/sentry-go v0.11.0
+3
View File
@@ -65,6 +65,8 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/beme/abide v0.0.0-20190723115211-635a09831760 h1:FvTM5NSN5HYvfKpgL+8x73U5v063vHsd7AX05eV1DnM=
github.com/beme/abide v0.0.0-20190723115211-635a09831760/go.mod h1:6+8gCKsZnxzhGTmKRh4BSkLos9CbWRJNcrp55We4SqQ=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -525,6 +527,7 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+5 -1
View File
@@ -381,7 +381,6 @@ type AlertsResponse struct {
LabelNames []string `json:"labelNames"`
Colors LabelsColorMap `json:"colors"`
Filters []Filter `json:"filters"`
Counters LabelNameStatsList `json:"counters"`
Settings Settings `json:"settings"`
Authentication AuthenticationInfo `json:"authentication"`
Receivers []string `json:"receivers"`
@@ -393,3 +392,8 @@ type Autocomplete struct {
Value string `json:"value"`
Tokens []string `json:"tokens"`
}
type Counters struct {
Total int `json:"total"`
Counters LabelNameStatsList `json:"counters"`
}
@@ -2,6 +2,7 @@ import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { useFetchGetMock } from "__fixtures__/useFetchGet";
import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore";
import { OverviewModalContent } from "./OverviewModalContent";
@@ -29,44 +30,73 @@ const MountedOverviewModalContent = () =>
describe("<OverviewModalContent />", () => {
it("matches snapshot with labels to show", () => {
alertStore.filters.setFilterValues([
NewUnappliedFilter("abc=xyz"),
NewUnappliedFilter("foo=bar"),
]);
alertStore.data.setCounters([
{
name: "foo",
hits: 16,
values: [
{ value: "bar1", raw: "foo=bar1", hits: 8, percent: 50, offset: 0 },
{ value: "bar2", raw: "foo=bar2", hits: 4, percent: 25, offset: 50 },
{ value: "bar3", raw: "foo=bar3", hits: 4, percent: 25, offset: 75 },
],
},
{
name: "bar",
hits: 20,
values: Array.from(Array(20).keys()).map((i) => ({
value: `baz${i + 1}`,
raw: `bar=baz${i + 1}`,
hits: 1,
percent: 5,
offset: i * 5,
})),
},
{
name: "alertname",
hits: 5,
values: [
useFetchGetMock.fetch.setMockedData({
response: {
total: 1,
counters: [
{
value: "Host_Down",
raw: "alertname=Host_Down",
name: "foo",
hits: 16,
values: [
{
value: "bar1",
raw: "foo=bar1",
hits: 8,
percent: 50,
offset: 0,
},
{
value: "bar2",
raw: "foo=bar2",
hits: 4,
percent: 25,
offset: 50,
},
{
value: "bar3",
raw: "foo=bar3",
hits: 4,
percent: 25,
offset: 75,
},
],
},
{
name: "bar",
hits: 20,
values: Array.from(Array(20).keys()).map((i) => ({
value: `baz${i + 1}`,
raw: `bar=baz${i + 1}`,
hits: 1,
percent: 5,
offset: i * 5,
})),
},
{
name: "alertname",
hits: 5,
percent: 100,
offset: 0,
values: [
{
value: "Host_Down",
raw: "alertname=Host_Down",
hits: 5,
percent: 100,
offset: 0,
},
],
},
],
},
error: null,
isLoading: false,
isRetrying: false,
retryCount: 0,
get: jest.fn(),
cancelGet: jest.fn(),
});
alertStore.filters.setFilterValues([
NewUnappliedFilter("abc=xyz"),
NewUnappliedFilter("foo=bar"),
]);
const tree = MountedOverviewModalContent();
@@ -74,35 +104,62 @@ describe("<OverviewModalContent />", () => {
});
it("matches snapshot with no labels to show", () => {
alertStore.data.setCounters([]);
useFetchGetMock.fetch.setMockedData({
response: {
total: 20,
counters: [],
},
error: null,
isLoading: false,
isRetrying: false,
retryCount: 0,
get: jest.fn(),
cancelGet: jest.fn(),
});
const tree = MountedOverviewModalContent();
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("renders all labels after expand button click", () => {
alertStore.info.setTotalAlerts(5);
alertStore.data.setCounters([
{
name: "foo",
hits: 5,
values: [
{ value: "bar", raw: "foo=bar", hits: 5, percent: 100, offset: 0 },
],
},
{
name: "bar",
hits: 3,
values: [
useFetchGetMock.fetch.setMockedData({
response: {
total: 5,
counters: [
{
value: "foo",
raw: "bar=foo",
name: "foo",
hits: 5,
values: [
{
value: "bar",
raw: "foo=bar",
hits: 5,
percent: 100,
offset: 0,
},
],
},
{
name: "bar",
hits: 3,
percent: 100,
offset: 0,
values: [
{
value: "foo",
raw: "bar=foo",
hits: 3,
percent: 100,
offset: 0,
},
],
},
],
},
]);
error: null,
isLoading: false,
isRetrying: false,
retryCount: 0,
get: jest.fn(),
cancelGet: jest.fn(),
});
const tree = MountedOverviewModalContent();
expect(tree.find("span.components-label")).toHaveLength(2 + 1); // +1 for toggle icon
@@ -2,8 +2,16 @@ import { FC, useState } from "react";
import { observer } from "mobx-react-lite";
import type { APILabelCounterT } from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
import type { CountersResponseT, APILabelCounterT } from "Models/APITypes";
import {
AlertStore,
FormatBackendURI,
FormatAPIFilterQuery,
} from "Stores/AlertStore";
import { useFetchGet } from "Hooks/useFetchGet";
import { TooltipWrapper } from "Components/TooltipWrapper";
import LabelWithPercent from "Components/Labels/LabelWithPercent";
import { ToggleIcon } from "Components/ToggleIcon";
@@ -53,9 +61,10 @@ const TableRows: FC<{
const LabelsTable: FC<{
alertStore: AlertStore;
counters: CountersResponseT;
showAllLabels: boolean;
toggleAllLabels: () => void;
}> = observer(({ alertStore, showAllLabels, toggleAllLabels }) => (
}> = observer(({ alertStore, counters, showAllLabels, toggleAllLabels }) => (
<>
<table
className="table table-borderless top-labels"
@@ -64,12 +73,12 @@ const LabelsTable: FC<{
<tbody className="mw-100">
<TableRows
alertStore={alertStore}
nameStats={alertStore.data.counters.filter(
(nameStats) => nameStats.hits >= alertStore.info.totalAlerts
nameStats={counters.counters.filter(
(nameStats) => nameStats.hits >= counters.total
)}
></TableRows>
{alertStore.data.counters.filter(
(nameStats) => nameStats.hits < alertStore.info.totalAlerts
{counters.counters.filter(
(nameStats) => nameStats.hits < counters.total
).length > 0 ? (
<tr>
<td colSpan={2} className="px-1 py-0">
@@ -87,8 +96,8 @@ const LabelsTable: FC<{
{showAllLabels ? (
<TableRows
alertStore={alertStore}
nameStats={alertStore.data.counters.filter(
(nameStats) => nameStats.hits < alertStore.info.totalAlerts
nameStats={counters.counters.filter(
(nameStats) => nameStats.hits < counters.total
)}
></TableRows>
) : null}
@@ -105,11 +114,34 @@ const NothingToShow: FC = () => (
</div>
);
const LoadingMessage: FC = () => (
<div className="px-2 py-5 bg-transparent">
<h1 className="display-5 text-placeholder text-center">
<FontAwesomeIcon icon={faSpinner} size="lg" spin />
</h1>
</div>
);
const ErrorMessage: FC<{ error: string }> = ({ error }) => (
<div className="px-2 py-5 bg-transparent">
<h1 className="display-5 text-danger text-center">{error}</h1>
</div>
);
const OverviewModalContent: FC<{
alertStore: AlertStore;
onHide: () => void;
}> = observer(({ alertStore, onHide }) => {
const [showAllLabels, setShowAllLabels] = useState<boolean>(false);
const { response, error } = useFetchGet<CountersResponseT>(
FormatBackendURI(
`counters.json?${FormatAPIFilterQuery(
alertStore.filters.values.map((f) => f.raw)
)}`
),
{ deps: [alertStore.info.timestamp] }
);
return (
<>
<div className="modal-header">
@@ -117,11 +149,16 @@ const OverviewModalContent: FC<{
<button type="button" className="btn-close" onClick={onHide}></button>
</div>
<div className="modal-body">
{alertStore.data.counters.length === 0 ? (
{error !== null ? (
<ErrorMessage error={error} />
) : response === null ? (
<LoadingMessage />
) : response.counters.length === 0 ? (
<NothingToShow />
) : (
<LabelsTable
alertStore={alertStore}
counters={response}
showAllLabels={showAllLabels}
toggleAllLabels={() => setShowAllLabels(!showAllLabels)}
/>
@@ -2,6 +2,8 @@ import type { FC } from "react";
import { storiesOf } from "@storybook/react";
import fetchMock from "fetch-mock";
import { MockGrid } from "../../__fixtures__/Stories";
import { AlertStore } from "../../Stores/AlertStore";
import { OverviewModalContent } from "./OverviewModalContent";
@@ -17,10 +19,99 @@ storiesOf("OverviewModal", module).add("OverviewModal", () => {
</div>
);
const alertStore = new AlertStore([]);
fetchMock.mock(
"begin:/counters.json?q=full",
{
total: 0,
counters: [
{
name: "@receiver",
hits: 2,
values: [
{
value: "by-cluster-service",
raw: "@receiver=by-cluster-service",
hits: 2,
percent: 100,
offset: 0,
},
],
},
{
name: "alertname",
hits: 90,
values: [
{
value: "Fake Alert",
raw: "alertname=Fake Alert",
hits: 45,
percent: 50,
offset: 0,
},
{
value: "Second Fake Alert",
raw: "alertname=Second Fake Alert",
hits: 45,
percent: 50,
offset: 50,
},
],
},
{
name: "group",
hits: 100,
values: [
{
value: "group1",
raw: "group=group1",
hits: 25,
percent: 25,
offset: 0,
},
{
value: "group2",
raw: "group=group2",
hits: 70,
percent: 70,
offset: 25,
},
{
value: "group3",
raw: "group=group3",
hits: 4,
percent: 4,
offset: 95,
},
{
value: "group4",
raw: "group=group4",
hits: 1,
percent: 1,
offset: 99,
},
],
},
],
},
{
overwriteRoutes: true,
}
);
fetchMock.mock(
"begin:/counters.json?q=empty",
{
total: 0,
counters: [],
},
{
overwriteRoutes: true,
}
);
const alertStore = new AlertStore(["full"]);
MockGrid(alertStore);
const emptyAlertStore = new AlertStore([]);
const emptyAlertStore = new AlertStore(["empty"]);
return (
<div>
@@ -2,6 +2,7 @@ import { act } from "react-dom/test-utils";
import { mount } from "enzyme";
import { useFetchGetMock } from "__fixtures__/useFetchGet";
import { AlertStore } from "Stores/AlertStore";
import { OverviewModal } from ".";
@@ -35,7 +36,53 @@ describe("<OverviewModal />", () => {
expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(1);
});
it("renders a spinner placeholder while fetch is in progress", () => {
useFetchGetMock.fetch.setMockedData({
response: null,
error: null,
isLoading: true,
isRetrying: false,
retryCount: 0,
get: jest.fn(),
cancelGet: jest.fn(),
});
const tree = MountedOverviewModal();
const toggle = tree.find("div.navbar-brand");
toggle.simulate("click");
expect(tree.find("LoadingMessage")).toHaveLength(1);
expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(1);
});
it("renders an error message on fetch error", () => {
useFetchGetMock.fetch.setMockedData({
response: null,
error: "mock error",
isLoading: false,
isRetrying: false,
retryCount: 0,
get: jest.fn(),
cancelGet: jest.fn(),
});
const tree = MountedOverviewModal();
const toggle = tree.find("div.navbar-brand");
toggle.simulate("click");
expect(tree.find("ErrorMessage")).toHaveLength(1);
expect(tree.find("h1.text-danger").text()).toBe("mock error");
});
it("renders modal content if fallback is not used", () => {
useFetchGetMock.fetch.setMockedData({
response: {
total: 20,
counters: [],
},
error: null,
isLoading: false,
isRetrying: false,
retryCount: 0,
get: jest.fn(),
cancelGet: jest.fn(),
});
const tree = MountedOverviewModal();
const toggle = tree.find("div.navbar-brand");
toggle.simulate("click");
@@ -43,6 +90,31 @@ describe("<OverviewModal />", () => {
expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(0);
});
it("re-fetches counters after timestamp change", () => {
alertStore.info.setTimestamp("old");
useFetchGetMock.fetch.setMockedData({
response: {
total: 20,
counters: [],
},
error: null,
isLoading: false,
isRetrying: false,
retryCount: 0,
get: jest.fn(),
cancelGet: jest.fn(),
});
const tree = MountedOverviewModal();
const toggle = tree.find("div.navbar-brand");
toggle.simulate("click");
expect(useFetchGetMock.fetch.calls).toHaveLength(1);
act(() => {
alertStore.info.setTimestamp("new");
});
expect(useFetchGetMock.fetch.calls).toHaveLength(2);
});
it("hides the modal when toggle() is called twice", () => {
const tree = MountedOverviewModal();
const toggle = tree.find("div.navbar-brand");
+5 -1
View File
@@ -206,7 +206,6 @@ export interface APIAlertsResponseT {
totalAlerts: number;
colors: APIAlertsResponseColorsT;
filters: APIFilterT[];
counters: APILabelCounterT[];
settings: APISettingsT;
authentication: APIAlertsResponseAuthenticationT;
receivers: string[];
@@ -225,3 +224,8 @@ export interface HistoryResponseT {
export interface AlertListResponseT {
alerts: LabelsT[];
}
export interface CountersResponseT {
total: number;
counters: APILabelCounterT[];
}
+8 -9
View File
@@ -12,7 +12,6 @@ import type {
APILabelColorT,
APIAlertsResponseT,
APIAlertsResponseColorsT,
APILabelCounterT,
APIGridT,
APIAlertsResponseSilenceMapT,
APIAlertsResponseUpstreamsT,
@@ -137,7 +136,6 @@ interface AlertStoreFiltersT {
interface AlertStoreDataT {
colors: APIAlertsResponseColorsT;
counters: APILabelCounterT[];
grids: APIGridT[];
labelNames: string[];
setLabelNames: (v: string[]) => void;
@@ -157,7 +155,6 @@ interface AlertStoreDataT {
setUpstreams: (u: APIAlertsResponseUpstreamsT) => void;
setClusters: (c: APIAlertsResponseUpstreamsClusterMapT) => void;
setSilences: (s: APIAlertsResponseSilenceMapT) => void;
setCounters: (c: APILabelCounterT[]) => void;
setReceivers: (r: string[]) => void;
setColors: (c: APIAlertsResponseColorsT) => void;
readonly upstreamsWithErrors: APIAlertmanagerUpstreamT[];
@@ -169,6 +166,7 @@ interface AlertStoreInfoT {
username: string;
};
totalAlerts: number;
timestamp: string;
version: string;
upgradeReady: boolean;
upgradeNeeded: boolean;
@@ -182,6 +180,7 @@ interface AlertStoreInfoT {
setTotalAlerts: (n: number) => void;
setAuthentication: (enabled: boolean, username: string) => void;
setVersion: (v: string) => void;
setTimestamp: (v: string) => void;
}
interface AlertStoreSettingsT {
@@ -291,7 +290,6 @@ class AlertStore {
this.data = observable(
{
colors: {} as APIAlertsResponseColorsT,
counters: [] as APILabelCounterT[],
grids: [] as APIGridT[],
labelNames: [] as string[],
setLabelNames(v: string[]) {
@@ -373,9 +371,6 @@ class AlertStore {
setSilences(s: APIAlertsResponseSilenceMapT) {
this.silences = s;
},
setCounters(c: APILabelCounterT[]) {
this.counters = c;
},
setReceivers(r: string[]) {
this.receivers = r;
},
@@ -397,7 +392,6 @@ class AlertStore {
setUpstreams: action.bound,
setClusters: action.bound,
setSilences: action.bound,
setCounters: action.bound,
setReceivers: action.bound,
setColors: action.bound,
setLabelNames: action.bound,
@@ -413,6 +407,7 @@ class AlertStore {
},
totalAlerts: 0,
version: "unknown",
timestamp: "",
upgradeReady: false as boolean,
upgradeNeeded: false as boolean,
isRetrying: false as boolean,
@@ -442,6 +437,9 @@ class AlertStore {
setVersion(v: string) {
this.version = v;
},
setTimestamp(v: string) {
this.timestamp = v;
},
},
{
setIsRetrying: action.bound,
@@ -451,6 +449,7 @@ class AlertStore {
setTotalAlerts: action.bound,
setAuthentication: action.bound,
setVersion: action.bound,
setTimestamp: action.bound,
},
{ name: "API response info" }
);
@@ -668,7 +667,6 @@ class AlertStore {
const updates: Partial<APIAlertsResponseT> = {};
updates.colors = result.colors;
updates.counters = result.counters;
updates.grids = result.grids;
updates.labelNames = result.labelNames;
updates.silences = result.silences;
@@ -687,6 +685,7 @@ class AlertStore {
// update extra root level keys that are stored under 'info'
this.info.totalAlerts = result.totalAlerts;
this.info.version = result.version;
this.info.timestamp = result.timestamp;
this.info.authentication = result.authentication;
// settings exported via API
+1 -6
View File
@@ -1,9 +1,5 @@
import { MockAlert, MockAlertGroup, MockSilence } from "__fixtures__/Alerts";
import type {
APIAlertsResponseT,
APILabelCounterT,
APIManagedSilenceT,
} from "Models/APITypes";
import type { APIAlertsResponseT, APIManagedSilenceT } from "Models/APITypes";
const EmptyAPIResponse = (): APIAlertsResponseT => ({
status: "success",
@@ -44,7 +40,6 @@ const EmptyAPIResponse = (): APIAlertsResponseT => ({
isValid: true,
},
],
counters: [] as APILabelCounterT[],
settings: {
sorting: {
grid: {
-70
View File
@@ -132,76 +132,6 @@ const MockGrid = (alertStore: AlertStore): void => {
},
});
alertStore.data.setCounters([
{
name: "@receiver",
hits: 2,
values: [
{
value: "by-cluster-service",
raw: "@receiver=by-cluster-service",
hits: 2,
percent: 100,
offset: 0,
},
],
},
{
name: "alertname",
hits: 90,
values: [
{
value: "Fake Alert",
raw: "alertname=Fake Alert",
hits: 45,
percent: 50,
offset: 0,
},
{
value: "Second Fake Alert",
raw: "alertname=Second Fake Alert",
hits: 45,
percent: 50,
offset: 50,
},
],
},
{
name: "group",
hits: 100,
values: [
{
value: "group1",
raw: "group=group1",
hits: 25,
percent: 25,
offset: 0,
},
{
value: "group2",
raw: "group=group2",
hits: 70,
percent: 70,
offset: 25,
},
{
value: "group3",
raw: "group=group3",
hits: 4,
percent: 4,
offset: 95,
},
{
value: "group4",
raw: "group=group4",
hits: 1,
percent: 1,
offset: 99,
},
],
},
]);
const groups = [];
for (let i = 1; i <= 10; i++) {
const active = Math.max(1, Math.ceil(i / 3));
+79 -1
View File
@@ -6,6 +6,7 @@ import type {
APIAlertsResponseT,
APIManagedSilenceT,
AlertListResponseT,
CountersResponseT,
} from "Models/APITypes";
type responseT =
@@ -13,7 +14,8 @@ type responseT =
| string[]
| APIAlertsResponseT
| APIManagedSilenceT[]
| AlertListResponseT;
| AlertListResponseT
| CountersResponseT;
interface mockedDataT {
response: undefined | responseT;
@@ -126,6 +128,82 @@ const useFetchGetMock = (
re: /^.\/autocomplete\.json\?term=/,
response: ["foo=bar", "foo=~bar"],
},
// counters
{
uri: "./counters.json",
response: {
total: 90,
counters: [
{
name: "@receiver",
hits: 2,
values: [
{
value: "by-cluster-service",
raw: "@receiver=by-cluster-service",
hits: 2,
percent: 100,
offset: 0,
},
],
},
{
name: "alertname",
hits: 90,
values: [
{
value: "Fake Alert",
raw: "alertname=Fake Alert",
hits: 45,
percent: 50,
offset: 0,
},
{
value: "Second Fake Alert",
raw: "alertname=Second Fake Alert",
hits: 45,
percent: 50,
offset: 50,
},
],
},
{
name: "group",
hits: 100,
values: [
{
value: "group1",
raw: "group=group1",
hits: 25,
percent: 25,
offset: 0,
},
{
value: "group2",
raw: "group=group2",
hits: 70,
percent: 70,
offset: 25,
},
{
value: "group3",
raw: "group=group3",
hits: 4,
percent: 4,
offset: 95,
},
{
value: "group4",
raw: "group=group4",
hits: 1,
percent: 1,
offset: 99,
},
],
},
],
},
},
];
for (const m of mockResponses) {