refactor(api): deduplicate lables & annotations in API responses

Each alert in a group holds only keys that are unique to that alert instance, everything shared by the entire group is moved to group.shared ns
This commit is contained in:
Łukasz Mierzwa
2018-07-04 08:45:11 +02:00
parent e1530e01cd
commit 79ff668019
3 changed files with 245 additions and 162 deletions

View File

@@ -18,6 +18,7 @@ type groupTest struct {
alerts []models.Alert
hash string
id string
shared models.APIAlertGroupSharedMaps
stateCount map[string]int
}
@@ -34,10 +35,9 @@ var groupTests = []groupTest{
models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true},
},
Labels: map[string]string{
"alertname": "Memory_Usage_Too_High",
"cluster": "prod",
"instance": "server2",
"job": "node_exporter",
"cluster": "prod",
"instance": "server2",
"job": "node_exporter",
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -80,10 +80,8 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Memory_Usage_Too_High",
"cluster": "prod",
"instance": "server2",
"job": "node_exporter",
"instance": "server2",
"job": "node_exporter",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -105,9 +103,7 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: "default",
@@ -117,18 +113,13 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "staging",
"instance": "server3",
"job": "node_ping",
"instance": "server3",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: "default",
@@ -138,18 +129,13 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "staging",
"instance": "server4",
"job": "node_ping",
"instance": "server4",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: "default",
@@ -159,10 +145,7 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "staging",
"instance": "server5",
"job": "node_ping",
"instance": "server5",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -170,6 +153,14 @@ var groupTests = []groupTest{
},
hash: "db53e38245a7afe18f923518146326b6fe53109a",
id: "2d3f39413b41c873cb72e0b8065aa7b8631e983e",
shared: models.APIAlertGroupSharedMaps{
Annotations: models.Annotations{
models.Annotation{},
},
Labels: map[string]string{
"job": "node_ping",
},
},
stateCount: map[string]int{
models.AlertStateActive: 3,
models.AlertStateSuppressed: 0,
@@ -184,9 +175,7 @@ var groupTests = []groupTest{
},
alerts: []models.Alert{
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: "default",
@@ -202,18 +191,13 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "dev",
"instance": "server6",
"job": "node_ping",
"instance": "server6",
},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: "default",
@@ -232,18 +216,13 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "dev",
"instance": "server7",
"job": "node_ping",
"instance": "server7",
},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: "default",
@@ -258,10 +237,7 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "dev",
"instance": "server8",
"job": "node_ping",
"instance": "server8",
},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
@@ -269,6 +245,14 @@ var groupTests = []groupTest{
},
hash: "bcb440cdee1d6f818599cf405c40f3382a4b1229",
id: "3c09c4156e6784dcf6d5b2e1629253798f82909b",
shared: models.APIAlertGroupSharedMaps{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"job": "node_ping",
},
},
stateCount: map[string]int{
models.AlertStateActive: 0,
models.AlertStateSuppressed: 3,
@@ -283,14 +267,11 @@ var groupTests = []groupTest{
alerts: []models.Alert{
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true},
},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "prod",
"instance": "server1",
"job": "node_ping",
"cluster": "prod",
"instance": "server1",
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -304,14 +285,10 @@ var groupTests = []groupTest{
Receiver: "by-name",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "prod",
"instance": "server2",
"job": "node_ping",
"cluster": "prod",
"instance": "server2",
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -325,14 +302,10 @@ var groupTests = []groupTest{
Receiver: "by-name",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "staging",
"instance": "server3",
"job": "node_ping",
"cluster": "staging",
"instance": "server3",
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -346,14 +319,10 @@ var groupTests = []groupTest{
Receiver: "by-name",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "staging",
"instance": "server4",
"job": "node_ping",
"cluster": "staging",
"instance": "server4",
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -367,14 +336,10 @@ var groupTests = []groupTest{
Receiver: "by-name",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "staging",
"instance": "server5",
"job": "node_ping",
"cluster": "staging",
"instance": "server5",
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -388,14 +353,10 @@ var groupTests = []groupTest{
Receiver: "by-name",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "dev",
"instance": "server6",
"job": "node_ping",
"cluster": "dev",
"instance": "server6",
},
State: models.AlertStateSuppressed,
Alertmanager: []models.AlertmanagerInstance{
@@ -414,14 +375,10 @@ var groupTests = []groupTest{
Receiver: "by-name",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "dev",
"instance": "server7",
"job": "node_ping",
"cluster": "dev",
"instance": "server7",
},
State: models.AlertStateSuppressed,
Alertmanager: []models.AlertmanagerInstance{
@@ -444,14 +401,10 @@ var groupTests = []groupTest{
Receiver: "by-name",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "dev",
"instance": "server8",
"job": "node_ping",
"cluster": "dev",
"instance": "server8",
},
State: models.AlertStateSuppressed,
Alertmanager: []models.AlertmanagerInstance{
@@ -472,6 +425,14 @@ var groupTests = []groupTest{
},
id: "58c6a3467cebc53abe68ecbe8643ce478c5a1573",
hash: "68d0ac6e27b890e0f854611963b03b51b37242cf",
shared: models.APIAlertGroupSharedMaps{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"job": "node_ping",
},
},
stateCount: map[string]int{
models.AlertStateActive: 5,
models.AlertStateSuppressed: 3,
@@ -499,10 +460,8 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Free_Disk_Space_Too_Low",
"cluster": "staging",
"instance": "server5",
"job": "node_exporter",
"instance": "server5",
"job": "node_exporter",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -525,7 +484,6 @@ var groupTests = []groupTest{
alerts: []models.Alert{
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true},
},
Alertmanager: []models.AlertmanagerInstance{
@@ -537,18 +495,13 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "prod",
"instance": "server1",
"job": "node_ping",
"instance": "server1",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: "default",
@@ -558,10 +511,7 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Host_Down",
"cluster": "prod",
"instance": "server2",
"job": "node_ping",
"instance": "server2",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -569,6 +519,14 @@ var groupTests = []groupTest{
},
hash: "eee0a9960be86ab7308f50a8ff438caed5cf8540",
id: "98c1a53d0f71af9c734c9180697383f3b8aff80f",
shared: models.APIAlertGroupSharedMaps{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"job": "node_ping",
},
},
stateCount: map[string]int{
models.AlertStateActive: 2,
models.AlertStateSuppressed: 0,
@@ -584,7 +542,6 @@ var groupTests = []groupTest{
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "help", Value: "Example help annotation"},
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true},
},
Alertmanager: []models.AlertmanagerInstance{
@@ -601,18 +558,13 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "HTTP_Probe_Failed",
"cluster": "dev",
"instance": "web1",
"job": "node_exporter",
"instance": "web1",
},
State: models.AlertStateSuppressed,
Receiver: "by-name",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: "default",
@@ -622,10 +574,7 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "HTTP_Probe_Failed",
"cluster": "dev",
"instance": "web2",
"job": "node_exporter",
"instance": "web2",
},
State: models.AlertStateActive,
Receiver: "by-name",
@@ -633,6 +582,15 @@ var groupTests = []groupTest{
},
hash: "cc1b20a6b0ded9265ab96699638d844a4c992614",
id: "bc4845fec77585cdfebe946234279d785ca93891",
shared: models.APIAlertGroupSharedMaps{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Labels: map[string]string{
"cluster": "dev",
"job": "node_exporter",
},
},
stateCount: map[string]int{
models.AlertStateActive: 1,
models.AlertStateSuppressed: 1,
@@ -659,10 +617,9 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "Free_Disk_Space_Too_Low",
"cluster": "staging",
"instance": "server5",
"job": "node_exporter",
"cluster": "staging",
"instance": "server5",
"job": "node_exporter",
},
State: models.AlertStateActive,
Receiver: "by-name",
@@ -686,7 +643,6 @@ var groupTests = []groupTest{
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "help", Value: "Example help annotation"},
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true},
},
Alertmanager: []models.AlertmanagerInstance{
@@ -703,18 +659,13 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "HTTP_Probe_Failed",
"cluster": "dev",
"instance": "web1",
"job": "node_exporter",
"instance": "web1",
},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
},
models.Alert{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "summary", Value: "Example summary"},
},
Annotations: models.Annotations{},
Alertmanager: []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: "default",
@@ -724,10 +675,7 @@ var groupTests = []groupTest{
},
},
Labels: map[string]string{
"alertname": "HTTP_Probe_Failed",
"cluster": "dev",
"instance": "web2",
"job": "node_exporter",
"instance": "web2",
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -735,6 +683,12 @@ var groupTests = []groupTest{
},
hash: "1dd655dc8ac8ed51aca51a702e70b1a2f442f434",
id: "ecefc3705b1ab4e4c3283c879540be348d2d9dce",
shared: models.APIAlertGroupSharedMaps{
Annotations: models.Annotations{},
Labels: map[string]string{
"job": "node_exporter",
},
},
stateCount: map[string]int{
models.AlertStateActive: 1,
models.AlertStateSuppressed: 1,
@@ -783,7 +737,7 @@ var countsMap = models.LabelsCountMap{
var filtersExpected = []models.Filter{}
func compareAlertGroups(testCase groupTest, group models.AlertGroup) bool {
func compareAlertGroups(testCase groupTest, group models.APIAlertGroup) bool {
if testCase.receiver != group.Receiver {
return false
}
@@ -888,7 +842,7 @@ func testAlert(version string, t *testing.T, expectedAlert, gotAlert models.Aler
}
}
func testAlertGroup(version string, t *testing.T, testCase groupTest, group models.AlertGroup) {
func testAlertGroup(version string, t *testing.T, testCase groupTest, group models.APIAlertGroup) {
//if testCase.hash != group.Hash {
// FIXME this is different per mock version due to startsAt / endsAt
// t.Errorf("[%s] Alert group.Hash mismatch, expected '%s' but got '%s' for group %v",

View File

@@ -1,5 +1,11 @@
package models
import (
"fmt"
"github.com/prymitive/unsee/internal/slices"
)
// Filter holds returned data on any filter passed by the user as part of the query
type Filter struct {
Text string `json:"text"`
@@ -31,18 +37,139 @@ 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
// APIAlertGroupSharedMaps defines shared part of APIAlertGroup
type APIAlertGroupSharedMaps struct {
Annotations Annotations `json:"annotations"`
Labels map[string]string `json:"labels"`
}
// APIAlertGroup is how AlertGroup is returned in the API response
// All labels and annotations that are shared between all alerts in given group
// are moved to Shared namespace, each alert instance only tracks labels and
// annotations that are unique to that instance
type APIAlertGroup struct {
AlertGroup
Shared APIAlertGroupSharedMaps `json:"shared"`
}
func (ag *APIAlertGroup) dedupLabels() {
totalAlerts := len(ag.Alerts)
labelCounts := map[string]int{}
for _, alert := range ag.Alerts {
for name, val := range alert.Labels {
key := fmt.Sprintf("%s\n%s", name, val)
_, found := labelCounts[key]
if found {
labelCounts[key]++
} else {
labelCounts[key] = 1
}
}
}
sharedLabels := map[string]string{}
for i, alert := range ag.Alerts {
newAlertLabels := map[string]string{}
for name, val := range alert.Labels {
key := fmt.Sprintf("%s\n%s", name, val)
if labelCounts[key] == totalAlerts {
sharedLabels[name] = val
} else {
newAlertLabels[name] = val
}
}
ag.Alerts[i].Labels = newAlertLabels
}
ag.Shared.Labels = sharedLabels
}
func (ag *APIAlertGroup) removeGroupingLabels() {
for i, alert := range ag.Alerts {
newAlertLabels := map[string]string{}
for name, val := range alert.Labels {
if _, found := ag.Labels[name]; found {
// skip all labels that are used for grouping
continue
}
newAlertLabels[name] = val
}
ag.Alerts[i].Labels = newAlertLabels
}
}
func (ag *APIAlertGroup) dedupAnnotations() {
totalAlerts := len(ag.Alerts)
annotationCount := map[string]int{}
for _, alert := range ag.Alerts {
for _, annotation := range alert.Annotations {
key := fmt.Sprintf("%s\n%s", annotation.Name, annotation.Value)
_, found := annotationCount[key]
if found {
annotationCount[key]++
} else {
annotationCount[key] = 1
}
}
}
sharedAnnotations := Annotations{}
sharedKeys := []string{}
for i, alert := range ag.Alerts {
newAlertAnnotations := Annotations{}
for _, annotation := range alert.Annotations {
key := fmt.Sprintf("%s\n%s", annotation.Name, annotation.Value)
if annotationCount[key] == totalAlerts {
if !slices.StringInSlice(sharedKeys, key) {
sharedAnnotations = append(sharedAnnotations, annotation)
sharedKeys = append(sharedKeys, key)
}
} else {
newAlertAnnotations = append(newAlertAnnotations, annotation)
}
}
ag.Alerts[i].Annotations = newAlertAnnotations
}
ag.Shared.Annotations = sharedAnnotations
}
// DedupSharedMaps will find all labels and annotations shared by all alerts
// in this group and moved them to Shared namespace
func (ag *APIAlertGroup) DedupSharedMaps() {
// remove all labels that are used for grouping
ag.removeGroupingLabels()
// don't dedup if we only have a single alert in this group
if len(ag.Alerts) > 1 {
ag.dedupLabels()
ag.dedupAnnotations()
} else {
ag.Shared = APIAlertGroupSharedMaps{
Labels: map[string]string{},
Annotations: Annotations{},
}
}
}
// AlertsResponse is the structure of JSON response UI will use to get alert data
type AlertsResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Version string `json:"version"`
Upstreams AlertmanagerAPISummary `json:"upstreams"`
AlertGroups map[string]AlertGroup `json:"groups"`
TotalAlerts int `json:"totalAlerts"`
Colors LabelsColorMap `json:"colors"`
Filters []Filter `json:"filters"`
Counters LabelsCountMap `json:"counters"`
StaticColorLabels []string `json:"staticColorLabels"`
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Version string `json:"version"`
Upstreams AlertmanagerAPISummary `json:"upstreams"`
AlertGroups map[string]APIAlertGroup `json:"groups"`
TotalAlerts int `json:"totalAlerts"`
Colors LabelsColorMap `json:"colors"`
Filters []Filter `json:"filters"`
Counters LabelsCountMap `json:"counters"`
StaticColorLabels []string `json:"staticColorLabels"`
}
// Autocomplete is the structure of autocomplete object for filter hints

View File

@@ -68,7 +68,7 @@ func alerts(c *gin.Context) {
matchFilters, validFilters := getFiltersFromQuery(c.QueryArray("q"))
// set pointers for data store objects, need a lock until end of view is reached
alerts := map[string]models.AlertGroup{}
alerts := map[string]models.APIAlertGroup{}
colors := models.LabelsColorMap{}
// used for top labels dropdown
counters := models.LabelsCountMap{}
@@ -138,7 +138,9 @@ func alerts(c *gin.Context) {
if len(agCopy.Alerts) > 0 {
agCopy.Hash = agCopy.ContentFingerprint()
alerts[agCopy.ID] = agCopy
apiAG := models.APIAlertGroup{AlertGroup: agCopy}
apiAG.DedupSharedMaps()
alerts[agCopy.ID] = apiAG
resp.TotalAlerts += len(agCopy.Alerts)
}