From 79ff6680196b1206dc7de7f67c8ed47096101f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Wed, 4 Jul 2018 08:45:11 +0200 Subject: [PATCH] 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 --- api_test.go | 254 +++++++++++++++++------------------------ internal/models/api.go | 147 ++++++++++++++++++++++-- views.go | 6 +- 3 files changed, 245 insertions(+), 162 deletions(-) diff --git a/api_test.go b/api_test.go index fb635104b..1c4653bac 100644 --- a/api_test.go +++ b/api_test.go @@ -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", diff --git a/internal/models/api.go b/internal/models/api.go index 0d59c5c4a..25b5f0af8 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -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 diff --git a/views.go b/views.go index 588faab39..68482bbee 100644 --- a/views.go +++ b/views.go @@ -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) }