feat(api): add labels stats to the api response

These will be used by an overview modal, showing top labels.
This commit is contained in:
Łukasz Mierzwa
2019-07-09 23:03:01 +01:00
parent 02f77adb35
commit f0fee594d9
8 changed files with 471 additions and 1 deletions
+42
View File
@@ -1,6 +1,9 @@
package main
import (
"math"
"sort"
"github.com/prymitive/karma/internal/alertmanager"
"github.com/prymitive/karma/internal/filters"
"github.com/prymitive/karma/internal/models"
@@ -22,6 +25,45 @@ func getFiltersFromQuery(filterStrings []string) ([]filters.FilterT, bool) {
return matchFilters, validFilters
}
func countLabel(countStore map[string]map[string]int, key string, val string) {
if _, found := countStore[key]; !found {
countStore[key] = make(map[string]int)
}
if _, found := countStore[key][val]; found {
countStore[key][val]++
} else {
countStore[key][val] = 1
}
}
func countersToLabelStats(counters map[string]map[string]int) models.LabelNameStatsList {
data := models.LabelNameStatsList{}
for name, valueMap := range counters {
nameStats := models.LabelNameStats{
Name: name,
Values: models.LabelValueStatsList{},
}
for value, hits := range valueMap {
nameStats.Hits += hits
valueStats := models.LabelValueStats{
Value: value,
Hits: hits,
}
nameStats.Values = append(nameStats.Values, valueStats)
}
for i, value := range nameStats.Values {
nameStats.Values[i].Percent = int(math.Round((float64(value.Hits) / float64(nameStats.Hits)) * 100.0))
}
sort.Sort(nameStats.Values)
data = append(data, nameStats)
}
sort.Sort(data)
return data
}
func getUpstreams() models.AlertmanagerAPISummary {
summary := models.AlertmanagerAPISummary{}
+192
View File
@@ -647,6 +647,160 @@ 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: "instance",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "server1",
Hits: 2,
Percent: 8,
},
models.LabelValueStats{
Value: "server2",
Hits: 4,
Percent: 17,
},
models.LabelValueStats{
Value: "server3",
Hits: 2,
Percent: 8,
},
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: "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 {
@@ -836,6 +990,44 @@ func TestVerifyAllGroups(t *testing.T) {
t.Errorf("[%s] Silences mismatch, expected >0 but got %d", version, len(am))
}
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)
}
+47
View File
@@ -35,6 +35,52 @@ type LabelColors struct {
// LabelsColorMap is a map of "Label Key" -> "Label Value" -> karmaLabelColors
type LabelsColorMap map[string]map[string]LabelColors
// LabelsCountMap is a map of "Label Key" -> "Label Value" -> number of occurence
type LabelsCountMap map[string]map[string]int
type LabelValueStats struct {
Value string `json:"value"`
Hits int `json:"hits"`
Percent int `json:"percent"`
}
type LabelValueStatsList []LabelValueStats
func (lvsl LabelValueStatsList) Len() int {
return len(lvsl)
}
func (lvsl LabelValueStatsList) Swap(i, j int) {
lvsl[i], lvsl[j] = lvsl[j], lvsl[i]
}
func (lvsl LabelValueStatsList) Less(i, j int) bool {
if lvsl[i].Hits == lvsl[j].Hits {
return lvsl[i].Value > lvsl[j].Value
}
return lvsl[i].Hits > lvsl[j].Hits
}
// LabelStats is used in the overview modal, it shows top labels across alerts
type LabelNameStats struct {
Name string `json:"name"`
Values LabelValueStatsList `json:"values"`
Hits int `json:"hits"`
}
type LabelNameStatsList []LabelNameStats
func (lnsl LabelNameStatsList) Len() int {
return len(lnsl)
}
func (lnsl LabelNameStatsList) Swap(i, j int) {
lnsl[i], lnsl[j] = lnsl[j], lnsl[i]
}
func (lnsl LabelNameStatsList) Less(i, j int) bool {
if lnsl[i].Hits == lnsl[j].Hits {
return lnsl[i].Name > lnsl[j].Name
}
return lnsl[i].Hits > lnsl[j].Hits
}
// APIAlertGroupSharedMaps defines shared part of APIAlertGroup
type APIAlertGroupSharedMaps struct {
Annotations Annotations `json:"annotations"`
@@ -248,6 +294,7 @@ type AlertsResponse struct {
TotalAlerts int `json:"totalAlerts"`
Colors LabelsColorMap `json:"colors"`
Filters []Filter `json:"filters"`
Counters LabelNameStatsList `json:"counters"`
Settings Settings `json:"settings"`
}
+178
View File
@@ -2,6 +2,7 @@ package models_test
import (
"encoding/json"
"sort"
"testing"
"github.com/pmezard/go-difflib/difflib"
@@ -255,3 +256,180 @@ func TestDedupSharedMapsWithSingleAlert(t *testing.T) {
}
ag.DedupSharedMaps()
}
func TestNameStatsSort(t *testing.T) {
var nameStats = models.LabelNameStatsList{
{
Name: "@state",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "suppressed",
Hits: 8,
Percent: 33,
},
models.LabelValueStats{
Value: "active",
Hits: 16,
Percent: 67,
},
},
},
{
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: "alertname",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "HTTP_Probe_Failed",
Hits: 4,
Percent: 17,
},
models.LabelValueStats{
Value: "Host_Down",
Hits: 16,
Percent: 67,
},
models.LabelValueStats{
Value: "Free_Disk_Space_Too_Low",
Hits: 2,
Percent: 8,
},
models.LabelValueStats{
Value: "Memory_Usage_Too_High",
Hits: 2,
Percent: 8,
},
},
},
{
Name: "instance",
Hits: 24,
Values: models.LabelValueStatsList{
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: "server1",
Hits: 2,
Percent: 8,
},
models.LabelValueStats{
Value: "server2",
Hits: 4,
Percent: 17,
},
models.LabelValueStats{
Value: "server3",
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: "@receiver",
Hits: 24,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "by-name",
Hits: 12,
Percent: 50,
},
models.LabelValueStats{
Value: "by-cluster-service",
Hits: 12,
Percent: 50,
},
},
},
{
Name: "job",
Hits: 16,
Values: models.LabelValueStatsList{
models.LabelValueStats{
Value: "node_exporter",
Hits: 8,
Percent: 50,
},
models.LabelValueStats{
Value: "node_ping",
Hits: 8,
Percent: 50,
},
},
},
}
b, err := json.Marshal(nameStats)
if err != nil {
t.Error(err)
}
before := string(b)
for _, n := range nameStats {
sort.Sort(n.Values)
}
sort.Sort(nameStats)
a, err := json.Marshal(nameStats)
if err != nil {
t.Error(err)
}
after := string(a)
if after == before {
t.Errorf("Sorting LabelNameStatsList produces the same output as unsorted instance")
}
}
+2 -1
View File
@@ -137,6 +137,7 @@ class AlertStore {
data = observable(
{
colors: {},
counters: {},
groups: {},
silences: {},
upstreams: { instances: [], clusters: {} },
@@ -302,7 +303,7 @@ class AlertStore {
let updates = {};
// update data dicts if they changed
for (const key of ["colors", "silences", "upstreams"]) {
for (const key of ["colors", "counters", "silences", "upstreams"]) {
if (!equal(this.data[key], result[key])) {
updates[key] = result[key];
}
+1
View File
@@ -22,6 +22,7 @@ const EmptyAPIResponse = () => ({
isValid: true
}
],
counters: {},
settings: {
sorting: {
grid: {
+6
View File
@@ -122,6 +122,7 @@ func alerts(c *gin.Context) {
// set pointers for data store objects, need a lock until end of view is reached
alerts := map[string]models.APIAlertGroup{}
colors := models.LabelsColorMap{}
counters := map[string]map[string]int{}
dedupedAlerts := alertmanager.DedupAlerts()
dedupedColors := alertmanager.DedupColors()
@@ -170,6 +171,9 @@ func alerts(c *gin.Context) {
alert.UpdateFingerprints()
agCopy.Alerts = append(agCopy.Alerts, alert)
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 {
@@ -209,6 +213,7 @@ func alerts(c *gin.Context) {
colors[key][value] = color
}
}
countLabel(counters, key, value)
}
}
}
@@ -248,6 +253,7 @@ func alerts(c *gin.Context) {
resp.AlertGroups = alerts
resp.Silences = silences
resp.Colors = colors
resp.Counters = countersToLabelStats(counters)
resp.Filters = populateAPIFilters(matchFilters)
data, err := json.Marshal(resp)
+3
View File
@@ -136,6 +136,9 @@ 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.AlertGroups {
for _, a := range ag.Alerts {
linkCount := 0