From f207da0f792bdd0333badb703f22cb7b80a9455e Mon Sep 17 00:00:00 2001 From: Lukasz Mierzwa Date: Tue, 10 Mar 2026 14:28:13 +0000 Subject: [PATCH] fix(backend): use less memory --- cmd/karma/acl.go | 8 +- cmd/karma/alerts.go | 90 ++-- cmd/karma/alerts_test.go | 60 +-- cmd/karma/api_test.go | 497 +++++++----------- cmd/karma/metrics.go | 21 +- cmd/karma/views.go | 182 ++++--- cmd/karma/views_test.go | 267 +++------- go.mod | 6 +- go.sum | 17 +- internal/alertmanager/dedup.go | 26 +- internal/alertmanager/models.go | 20 +- internal/alertmanager/models_test.go | 24 +- internal/filters/autocomplete.go | 13 +- internal/filters/autocomplete_test.go | 34 +- internal/filters/filter_alertmanager.go | 22 +- internal/filters/filter_cluster.go | 22 +- internal/filters/filter_fingerprint.go | 2 +- internal/filters/filter_fuzzy.go | 18 +- internal/filters/filter_inhibited_by.go | 18 +- internal/filters/filter_label.go | 74 +-- internal/filters/filter_receiver.go | 52 +- internal/filters/filter_silence_author.go | 18 +- internal/filters/filter_silence_ticket.go | 18 +- internal/filters/filter_silenced_by.go | 18 +- internal/filters/filter_state.go | 37 +- internal/filters/filter_test.go | 45 +- internal/mapper/v017/api.go | 19 +- internal/models/__snapshots__/models.snapshot | 314 +++++------ internal/models/alert.go | 259 +++++---- internal/models/alert_test.go | 301 +++++------ internal/models/alertgroup.go | 33 +- internal/models/alertgroup_test.go | 92 ++-- internal/models/alertmanager.go | 16 +- internal/models/annotation.go | 22 +- internal/models/annotation_test.go | 131 +++-- internal/models/api.go | 260 +++++---- internal/models/api_test.go | 390 ++++++++------ internal/models/silence.go | 12 +- internal/transform/strip.go | 33 +- internal/transform/strip_test.go | 137 ++--- 40 files changed, 1772 insertions(+), 1856 deletions(-) diff --git a/cmd/karma/acl.go b/cmd/karma/acl.go index 835342f51..127baa996 100644 --- a/cmd/karma/acl.go +++ b/cmd/karma/acl.go @@ -32,9 +32,9 @@ type silenceFilter struct { func (sf *silenceFilter) isMatch(silence *models.Silence) bool { for _, m := range silence.Matchers { var nameMatch bool - if sf.Name != "" && sf.Name == m.Name.Value() { + if sf.Name != "" && sf.Name == m.Name { nameMatch = true - } else if sf.NameRegex != nil && sf.NameRegex.MatchString(m.Name.Value()) { + } else if sf.NameRegex != nil && sf.NameRegex.MatchString(m.Name) { nameMatch = true } @@ -73,10 +73,10 @@ type silenceMatcher struct { } func (sm *silenceMatcher) isMatch(m models.SilenceMatcher) bool { - if sm.NameRegex != nil && !sm.NameRegex.MatchString(m.Name.Value()) { + if sm.NameRegex != nil && !sm.NameRegex.MatchString(m.Name) { return false } - if sm.Name != "" && sm.Name != m.Name.Value() { + if sm.Name != "" && sm.Name != m.Name { return false } if sm.ValueRegex != nil && !sm.ValueRegex.MatchString(m.Value) { diff --git a/cmd/karma/alerts.go b/cmd/karma/alerts.go index 0de49c666..2ff9618b7 100644 --- a/cmd/karma/alerts.go +++ b/cmd/karma/alerts.go @@ -8,6 +8,7 @@ import ( "sort" "github.com/fvbommel/sortorder" + promlabels "github.com/prometheus/prometheus/model/labels" "github.com/rs/zerolog/log" "github.com/prymitive/karma/internal/alertmanager" @@ -132,14 +133,16 @@ func resolveLabelValue(name, value string) string { } func getGroupLabel(group *models.APIAlertGroup, label string) string { - if v := group.Labels.Get(label); v != nil { - return resolveLabelValue(label, v.Value.Value()) + if v := group.Labels.Get(label); v != "" { + return resolveLabelValue(label, v) } - if v := group.Shared.Labels.Get(label); v != nil { - return resolveLabelValue(label, v.Value.Value()) + if v := group.Shared.Labels.Get(label); v != "" { + return resolveLabelValue(label, v) } - if v := group.Alerts[0].Labels.Get(label); v != nil { - return resolveLabelValue(label, v.Value.Value()) + if len(group.Alerts) > 0 { + if v := group.Alerts[0].Labels.Get(label); v != "" { + return resolveLabelValue(label, v) + } } return "" } @@ -270,26 +273,26 @@ func isPreferredLabel(label, other string) bool { func autoGridLabel(dedupedAlerts []models.AlertGroup) string { alertGroupsCount := len(dedupedAlerts) var alertsCount int - labelToAlertCount := map[models.UniqueString]map[string]int{} + labelToAlertCount := map[string]map[string]int{} for _, ag := range dedupedAlerts { alertsCount += len(ag.Alerts) for _, alert := range ag.Alerts { - for _, l := range alert.Labels { + alert.Labels.Range(func(l promlabels.Label) { if _, ok := labelToAlertCount[l.Name]; !ok { labelToAlertCount[l.Name] = map[string]int{} } - if _, ok := labelToAlertCount[l.Name][l.Value.Value()]; !ok { - labelToAlertCount[l.Name][l.Value.Value()] = 0 + if _, ok := labelToAlertCount[l.Name][l.Value]; !ok { + labelToAlertCount[l.Name][l.Value] = 0 } - labelToAlertCount[l.Name][l.Value.Value()]++ - } + labelToAlertCount[l.Name][l.Value]++ + }) } } log.Debug().Int("alerts", alertsCount).Int("groups", alertGroupsCount).Msg("Alerts count for automatic grid label") candidates := map[string]int{} for key, vals := range labelToAlertCount { - if slices.Contains(config.Config.Grid.Auto.Ignore, key.Value()) { + if slices.Contains(config.Config.Grid.Auto.Ignore, key) { continue } var total int @@ -298,11 +301,11 @@ func autoGridLabel(dedupedAlerts []models.AlertGroup) string { total += cnt uniqueValues[val] = struct{}{} } - log.Debug().Str("label", key.Value()).Int("alerts", total).Msg("Number of alerts per label") + log.Debug().Str("label", key).Int("alerts", total).Msg("Number of alerts per label") if total < alertsCount { continue } - candidates[key.Value()] = len(uniqueValues) + candidates[key] = len(uniqueValues) } var lastLabel string @@ -323,18 +326,26 @@ func autoGridLabel(dedupedAlerts []models.AlertGroup) string { func filterAlerts(dedupedAlerts []models.AlertGroup, fl []filters.FilterT) (filteredAlerts []models.AlertGroup) { var matches int + var hasAMFilters bool + for _, filter := range fl { + if filter.GetIsValid() && filter.GetIsAlertmanagerFilter() { + hasAMFilters = true + break + } + } + blockedAMs := map[string]struct{}{} for _, ag := range dedupedAlerts { agCopy := models.AlertGroup{ ID: ag.ID, Receiver: ag.Receiver, Labels: ag.Labels, LatestStartsAt: ag.LatestStartsAt, - Alerts: []models.Alert{}, - AlertmanagerCount: map[string]int{}, + Alerts: make([]models.Alert, 0, len(ag.Alerts)), + AlertmanagerCount: make(map[string]int, len(ag.AlertmanagerCount)), StateCount: map[string]int{}, } for _, s := range models.AlertStateList { - agCopy.StateCount[s.Value()] = 0 + agCopy.StateCount[s.String()] = 0 } for _, alert := range ag.Alerts { var hadMismatch bool @@ -349,23 +360,24 @@ func filterAlerts(dedupedAlerts []models.AlertGroup, fl []filters.FilterT) (filt continue } - blockedAMs := map[string]struct{}{} - for _, am := range alert.Alertmanager { - for _, filter := range fl { - if filter.GetIsValid() && filter.GetIsAlertmanagerFilter() && !filter.MatchAlertmanager(&am) { - blockedAMs[am.Name] = struct{}{} - } - } - } - if len(blockedAMs) > 0 { - ams := []models.AlertmanagerInstance{} + if hasAMFilters { + clear(blockedAMs) for _, am := range alert.Alertmanager { - _, found := blockedAMs[am.Name] - if !found { - ams = append(ams, am) + for _, filter := range fl { + if filter.GetIsValid() && filter.GetIsAlertmanagerFilter() && !filter.MatchAlertmanager(&am) { + blockedAMs[am.Name] = struct{}{} + } } } - alert.Alertmanager = ams + if len(blockedAMs) > 0 { + ams := make([]models.AlertmanagerInstance, 0, len(alert.Alertmanager)) + for _, am := range alert.Alertmanager { + if _, blocked := blockedAMs[am.Name]; !blocked { + ams = append(ams, am) + } + } + alert.Alertmanager = ams + } } if len(alert.Alertmanager) == 0 { continue @@ -373,7 +385,7 @@ func filterAlerts(dedupedAlerts []models.AlertGroup, fl []filters.FilterT) (filt matches++ agCopy.Alerts = append(agCopy.Alerts, alert) - agCopy.StateCount[alert.State.Value()]++ + agCopy.StateCount[alert.State.String()]++ } if len(agCopy.Alerts) > 0 { filteredAlerts = append(filteredAlerts, agCopy) @@ -383,19 +395,19 @@ func filterAlerts(dedupedAlerts []models.AlertGroup, fl []filters.FilterT) (filt return filteredAlerts } -func newStateCount() map[models.UniqueString]int { - stateCount := map[models.UniqueString]int{} +func newStateCount() map[string]int { + stateCount := map[string]int{} for _, s := range models.AlertStateList { - stateCount[s] = 0 + stateCount[s.String()] = 0 } return stateCount } -func stateFromStateCount(stateCount map[models.UniqueString]int) models.UniqueString { - if stateCount[models.AlertStateActive] > 0 { +func stateFromStateCount(stateCount map[string]int) models.AlertState { + if stateCount[models.AlertStateActive.String()] > 0 { return models.AlertStateActive } - if stateCount[models.AlertStateSuppressed] > 0 { + if stateCount[models.AlertStateSuppressed.String()] > 0 { return models.AlertStateSuppressed } return models.AlertStateUnprocessed diff --git a/cmd/karma/alerts_test.go b/cmd/karma/alerts_test.go index c168db052..c56011838 100644 --- a/cmd/karma/alerts_test.go +++ b/cmd/karma/alerts_test.go @@ -18,18 +18,18 @@ func TestSortByStartsAt(t *testing.T) { sortReverse bool } - g1 := models.AlertGroup{ - Receiver: models.NewUniqueString("default"), + g1 := models.APIAlertGroup{ + Receiver: "default", ID: "1", LatestStartsAt: time.Date(2020, time.January, 1, 0, 0, 0, 1, time.UTC), } - g2 := models.AlertGroup{ - Receiver: models.NewUniqueString("default"), + g2 := models.APIAlertGroup{ + Receiver: "default", ID: "2", LatestStartsAt: time.Date(2020, time.January, 1, 0, 0, 0, 2, time.UTC), } - g3 := models.AlertGroup{ - Receiver: models.NewUniqueString("default"), + g3 := models.APIAlertGroup{ + Receiver: "default", ID: "3", LatestStartsAt: time.Date(2020, time.January, 1, 0, 0, 0, 3, time.UTC), } @@ -45,54 +45,22 @@ func TestSortByStartsAt(t *testing.T) { sortReverse: true, }, { - groups: []models.APIAlertGroup{ - {AlertGroup: g1}, - {AlertGroup: g2}, - {AlertGroup: g3}, - }, - sorted: []models.APIAlertGroup{ - {AlertGroup: g1}, - {AlertGroup: g2}, - {AlertGroup: g3}, - }, + groups: []models.APIAlertGroup{g1, g2, g3}, + sorted: []models.APIAlertGroup{g1, g2, g3}, }, { - groups: []models.APIAlertGroup{ - {AlertGroup: g1}, - {AlertGroup: g2}, - {AlertGroup: g3}, - }, + groups: []models.APIAlertGroup{g1, g2, g3}, sortReverse: true, - sorted: []models.APIAlertGroup{ - {AlertGroup: g3}, - {AlertGroup: g2}, - {AlertGroup: g1}, - }, + sorted: []models.APIAlertGroup{g3, g2, g1}, }, { - groups: []models.APIAlertGroup{ - {AlertGroup: g2}, - {AlertGroup: g3}, - {AlertGroup: g1}, - }, - sorted: []models.APIAlertGroup{ - {AlertGroup: g1}, - {AlertGroup: g2}, - {AlertGroup: g3}, - }, + groups: []models.APIAlertGroup{g2, g3, g1}, + sorted: []models.APIAlertGroup{g1, g2, g3}, }, { - groups: []models.APIAlertGroup{ - {AlertGroup: g2}, - {AlertGroup: g3}, - {AlertGroup: g1}, - }, + groups: []models.APIAlertGroup{g2, g3, g1}, sortReverse: true, - sorted: []models.APIAlertGroup{ - {AlertGroup: g3}, - {AlertGroup: g2}, - {AlertGroup: g1}, - }, + sorted: []models.APIAlertGroup{g3, g2, g1}, }, } diff --git a/cmd/karma/api_test.go b/cmd/karma/api_test.go index c0751339b..aa8aa21b2 100644 --- a/cmd/karma/api_test.go +++ b/cmd/karma/api_test.go @@ -13,47 +13,43 @@ import ( "github.com/Masterminds/semver/v3" "github.com/google/go-cmp/cmp" + promlabels "github.com/prometheus/prometheus/model/labels" + "github.com/prymitive/karma/internal/config" "github.com/prymitive/karma/internal/mock" "github.com/prymitive/karma/internal/models" ) type groupTest struct { - stateCount map[models.UniqueString]int + stateCount map[string]int receiver string id string - labels models.Labels + labels promlabels.Labels alerts []models.Alert } var groupTests = []groupTest{ { receiver: "by-name", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Memory_Usage_Too_High")}, - }, + labels: promlabels.FromStrings("alertname", "Memory_Usage_Too_High"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 10, 0, 0, 0, 0, time.UTC), Annotations: models.Annotations{ models.Annotation{ Visible: true, - Name: models.NewUniqueString("alert"), - Value: models.NewUniqueString("Memory usage exceeding threshold"), + Name: "alert", + Value: "Memory usage exceeding threshold", }, models.Annotation{ Visible: true, - Name: models.NewUniqueString("dashboard"), - Value: models.NewUniqueString("http://localhost/dashboard.html"), + Name: "dashboard", + Value: "http://localhost/dashboard.html", IsLink: true, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server2")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, - State: models.AlertStateActive, + Labels: promlabels.FromStrings("cluster", "prod", "instance", "server2", "job", "node_exporter"), + State: models.AlertStateActive, Alertmanager: []models.AlertmanagerInstance{ { Name: "default", @@ -62,35 +58,32 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, }, - id: "990fb0cdc86aae89", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 1, - models.AlertStateSuppressed: 0, - models.AlertStateUnprocessed: 0, + id: "d9415cf0750fe30", + stateCount: map[string]int{ + models.AlertStateActive.String(): 1, + models.AlertStateSuppressed.String(): 0, + models.AlertStateUnprocessed.String(): 0, }, }, { receiver: "by-cluster-service", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Memory_Usage_Too_High")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - }, + labels: promlabels.FromStrings("alertname", "Memory_Usage_Too_High", "cluster", "prod"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 10, 0, 0, 0, 1, time.UTC), Annotations: models.Annotations{ models.Annotation{ Visible: true, - Name: models.NewUniqueString("alert"), - Value: models.NewUniqueString("Memory usage exceeding threshold"), + Name: "alert", + Value: "Memory usage exceeding threshold", }, models.Annotation{ Visible: true, - Name: models.NewUniqueString("dashboard"), - Value: models.NewUniqueString("http://localhost/dashboard.html"), + Name: "dashboard", + Value: "http://localhost/dashboard.html", IsLink: true, }, }, @@ -102,27 +95,21 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server2")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, + Labels: promlabels.FromStrings("instance", "server2", "job", "node_exporter"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, }, - id: "6b15d34b0ed69d02", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 1, - models.AlertStateSuppressed: 0, - models.AlertStateUnprocessed: 0, + id: "c3405fd66145d86e", + stateCount: map[string]int{ + models.AlertStateActive.String(): 1, + models.AlertStateSuppressed.String(): 0, + models.AlertStateUnprocessed.String(): 0, }, }, { receiver: "by-cluster-service", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - }, + labels: promlabels.FromStrings("alertname", "Host_Down", "cluster", "staging"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 10, 0, 0, 0, 0, time.UTC), @@ -135,12 +122,9 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server3")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.3")}, - }, + Labels: promlabels.FromStrings("instance", "server3", "ip", "127.0.0.3"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, { Annotations: models.Annotations{}, @@ -152,12 +136,9 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server4")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.4")}, - }, + Labels: promlabels.FromStrings("instance", "server4", "ip", "127.0.0.4"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, { Annotations: models.Annotations{}, @@ -169,27 +150,21 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server5")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.5")}, - }, + Labels: promlabels.FromStrings("instance", "server5", "ip", "127.0.0.5"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, }, - id: "f08998b6581752f4", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 3, - models.AlertStateSuppressed: 0, - models.AlertStateUnprocessed: 0, + id: "13043134f43c9070", + stateCount: map[string]int{ + models.AlertStateActive.String(): 3, + models.AlertStateSuppressed.String(): 0, + models.AlertStateUnprocessed.String(): 0, }, }, { receiver: "by-cluster-service", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - }, + labels: promlabels.FromStrings("alertname", "Host_Down", "cluster", "dev"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 10, 1, 0, 0, 0, time.UTC), @@ -202,12 +177,9 @@ var groupTests = []groupTest{ SilencedBy: []string{"168f139d-77e4-41d6-afb5-8fe2cfd0cc9d"}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server6")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.6")}, - }, + Labels: promlabels.FromStrings("instance", "server6", "ip", "127.0.0.6"), State: models.AlertStateSuppressed, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, { StartsAt: time.Date(2019, time.January, 10, 0, 59, 0, 0, time.UTC), @@ -220,12 +192,9 @@ var groupTests = []groupTest{ SilencedBy: []string{"168f139d-77e4-41d6-afb5-8fe2cfd0cc9d", "378eaa69-097d-41c4-a8c2-fe6568c3abfc"}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server7")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.7")}, - }, + Labels: promlabels.FromStrings("instance", "server7", "ip", "127.0.0.7"), State: models.AlertStateSuppressed, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, { StartsAt: time.Date(2019, time.January, 12, 0, 0, 0, 0, time.UTC), @@ -238,43 +207,34 @@ var groupTests = []groupTest{ SilencedBy: []string{"168f139d-77e4-41d6-afb5-8fe2cfd0cc9d"}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server8")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.8")}, - }, + Labels: promlabels.FromStrings("instance", "server8", "ip", "127.0.0.8"), State: models.AlertStateSuppressed, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, }, - id: "97dba9e211f41cf6", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 0, - models.AlertStateSuppressed: 3, - models.AlertStateUnprocessed: 0, + id: "1704fa283de6bc33", + stateCount: map[string]int{ + models.AlertStateActive.String(): 0, + models.AlertStateSuppressed.String(): 3, + models.AlertStateUnprocessed.String(): 0, }, }, { receiver: "by-name", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - }, + labels: promlabels.FromStrings("alertname", "Host_Down"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC), Annotations: models.Annotations{ models.Annotation{ Visible: true, - Name: models.NewUniqueString("url"), - Value: models.NewUniqueString("http://localhost/example.html"), + Name: "url", + Value: "http://localhost/example.html", IsLink: true, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server1")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.1")}, - }, - State: models.AlertStateActive, + Labels: promlabels.FromStrings("cluster", "prod", "instance", "server1", "ip", "127.0.0.1"), + State: models.AlertStateActive, Alertmanager: []models.AlertmanagerInstance{ { Name: "default", @@ -283,17 +243,13 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, { StartsAt: time.Date(2019, time.January, 1, 0, 1, 0, 0, time.UTC), Annotations: models.Annotations{}, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server2")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.2")}, - }, - State: models.AlertStateActive, + Labels: promlabels.FromStrings("cluster", "prod", "instance", "server2", "ip", "127.0.0.2"), + State: models.AlertStateActive, Alertmanager: []models.AlertmanagerInstance{ { Name: "default", @@ -302,17 +258,13 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, { StartsAt: time.Date(2019, time.January, 1, 0, 1, 0, 1, time.UTC), Annotations: models.Annotations{}, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server3")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.3")}, - }, - State: models.AlertStateActive, + Labels: promlabels.FromStrings("cluster", "staging", "instance", "server3", "ip", "127.0.0.3"), + State: models.AlertStateActive, Alertmanager: []models.AlertmanagerInstance{ { Name: "default", @@ -321,17 +273,13 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, { StartsAt: time.Date(2019, time.January, 1, 0, 0, 59, 0, time.UTC), Annotations: models.Annotations{}, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server4")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.4")}, - }, - State: models.AlertStateActive, + Labels: promlabels.FromStrings("cluster", "staging", "instance", "server4", "ip", "127.0.0.4"), + State: models.AlertStateActive, Alertmanager: []models.AlertmanagerInstance{ { Name: "default", @@ -340,17 +288,13 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, { StartsAt: time.Date(2019, time.January, 10, 0, 0, 0, 0, time.UTC), Annotations: models.Annotations{}, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server5")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.5")}, - }, - State: models.AlertStateActive, + Labels: promlabels.FromStrings("cluster", "staging", "instance", "server5", "ip", "127.0.0.5"), + State: models.AlertStateActive, Alertmanager: []models.AlertmanagerInstance{ { Name: "default", @@ -359,17 +303,13 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, { StartsAt: time.Date(2019, time.January, 10, 1, 0, 0, 0, time.UTC), Annotations: models.Annotations{}, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server6")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.6")}, - }, - State: models.AlertStateSuppressed, + Labels: promlabels.FromStrings("cluster", "dev", "instance", "server6", "ip", "127.0.0.6"), + State: models.AlertStateSuppressed, Alertmanager: []models.AlertmanagerInstance{ { Name: "default", @@ -378,17 +318,13 @@ var groupTests = []groupTest{ SilencedBy: []string{"168f139d-77e4-41d6-afb5-8fe2cfd0cc9d"}, }, }, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, { StartsAt: time.Date(2019, time.January, 10, 0, 20, 0, 0, time.UTC), Annotations: models.Annotations{}, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server7")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.7")}, - }, - State: models.AlertStateSuppressed, + Labels: promlabels.FromStrings("cluster", "dev", "instance", "server7", "ip", "127.0.0.7"), + State: models.AlertStateSuppressed, Alertmanager: []models.AlertmanagerInstance{ { Name: "default", @@ -397,17 +333,13 @@ var groupTests = []groupTest{ SilencedBy: []string{"168f139d-77e4-41d6-afb5-8fe2cfd0cc9d", "378eaa69-097d-41c4-a8c2-fe6568c3abfc"}, }, }, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, { StartsAt: time.Date(2019, time.January, 10, 0, 21, 0, 0, time.UTC), Annotations: models.Annotations{}, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server8")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.8")}, - }, - State: models.AlertStateSuppressed, + Labels: promlabels.FromStrings("cluster", "dev", "instance", "server8", "ip", "127.0.0.8"), + State: models.AlertStateSuppressed, Alertmanager: []models.AlertmanagerInstance{ { Name: "default", @@ -416,35 +348,32 @@ var groupTests = []groupTest{ SilencedBy: []string{"168f139d-77e4-41d6-afb5-8fe2cfd0cc9d"}, }, }, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, }, - id: "db6e3af075b36419", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 5, - models.AlertStateSuppressed: 3, - models.AlertStateUnprocessed: 0, + id: "c48b045a836d41d6", + stateCount: map[string]int{ + models.AlertStateActive.String(): 5, + models.AlertStateSuppressed.String(): 3, + models.AlertStateUnprocessed.String(): 0, }, }, { receiver: "by-cluster-service", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Free_Disk_Space_Too_Low")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - }, + labels: promlabels.FromStrings("alertname", "Free_Disk_Space_Too_Low", "cluster", "staging"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 10, 0, 19, 0, 0, time.UTC), Annotations: models.Annotations{ models.Annotation{ Visible: true, - Name: models.NewUniqueString("alert"), - Value: models.NewUniqueString("Less than 10% disk space is free"), + Name: "alert", + Value: "Less than 10% disk space is free", }, models.Annotation{ Visible: true, - Name: models.NewUniqueString("dashboard"), - Value: models.NewUniqueString("http://localhost/dashboard.html"), + Name: "dashboard", + Value: "http://localhost/dashboard.html", IsLink: true, }, }, @@ -456,36 +385,29 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("disk"), Value: models.NewUniqueString("sda")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server5")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, + Labels: promlabels.FromStrings("disk", "sda", "instance", "server5", "job", "node_exporter"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, }, - id: "c8a1ec76d51e9d96", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 1, - models.AlertStateSuppressed: 0, - models.AlertStateUnprocessed: 0, + id: "6dbbdb0f1e75b835", + stateCount: map[string]int{ + models.AlertStateActive.String(): 1, + models.AlertStateSuppressed.String(): 0, + models.AlertStateUnprocessed.String(): 0, }, }, { receiver: "by-cluster-service", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - }, + labels: promlabels.FromStrings("alertname", "Host_Down", "cluster", "prod"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 12, 0, 19, 0, 0, time.UTC), Annotations: models.Annotations{ models.Annotation{ Visible: true, - Name: models.NewUniqueString("url"), - Value: models.NewUniqueString("http://localhost/example.html"), + Name: "url", + Value: "http://localhost/example.html", IsLink: true, }, }, @@ -497,12 +419,9 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server1")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.1")}, - }, + Labels: promlabels.FromStrings("instance", "server1", "ip", "127.0.0.1"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, { Annotations: models.Annotations{}, @@ -514,39 +433,34 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server2")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.2")}, - }, + Labels: promlabels.FromStrings("instance", "server2", "ip", "127.0.0.2"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, }, - id: "922d04650baba3fe", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 2, - models.AlertStateSuppressed: 0, - models.AlertStateUnprocessed: 0, + id: "7cc564cafcfd4d9", + stateCount: map[string]int{ + models.AlertStateActive.String(): 2, + models.AlertStateSuppressed.String(): 0, + models.AlertStateUnprocessed.String(): 0, }, }, { receiver: "by-name", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("HTTP_Probe_Failed")}, - }, + labels: promlabels.FromStrings("alertname", "HTTP_Probe_Failed"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 14, 0, 0, 0, 0, time.UTC), Annotations: models.Annotations{ models.Annotation{ Visible: true, - Name: models.NewUniqueString("help"), - Value: models.NewUniqueString("Example help annotation"), + Name: "help", + Value: "Example help annotation", }, models.Annotation{ Visible: true, - Name: models.NewUniqueString("url"), - Value: models.NewUniqueString("http://localhost/example.html"), + Name: "url", + Value: "http://localhost/example.html", IsLink: true, }, }, @@ -557,11 +471,9 @@ var groupTests = []groupTest{ Source: "http://localhost/prometheus", }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("web1")}, - }, + Labels: promlabels.FromStrings("instance", "web1"), State: models.AlertStateSuppressed, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, { StartsAt: time.Date(2019, time.January, 14, 0, 0, 0, 0, time.UTC), @@ -574,38 +486,34 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("web2")}, - }, + Labels: promlabels.FromStrings("instance", "web2"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, }, - id: "69b99170489a6c64", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 1, - models.AlertStateSuppressed: 1, - models.AlertStateUnprocessed: 0, + id: "ced38b93167636b", + stateCount: map[string]int{ + models.AlertStateActive.String(): 1, + models.AlertStateSuppressed.String(): 1, + models.AlertStateUnprocessed.String(): 0, }, }, { receiver: "by-name", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Free_Disk_Space_Too_Low")}, - }, + labels: promlabels.FromStrings("alertname", "Free_Disk_Space_Too_Low"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 15, 0, 0, 0, 0, time.UTC), Annotations: models.Annotations{ models.Annotation{ Visible: true, - Name: models.NewUniqueString("alert"), - Value: models.NewUniqueString("Less than 10% disk space is free"), + Name: "alert", + Value: "Less than 10% disk space is free", }, models.Annotation{ Visible: true, - Name: models.NewUniqueString("dashboard"), - Value: models.NewUniqueString("http://localhost/dashboard.html"), + Name: "dashboard", + Value: "http://localhost/dashboard.html", IsLink: true, }, }, @@ -617,42 +525,34 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - {Name: models.NewUniqueString("disk"), Value: models.NewUniqueString("sda")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server5")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, + Labels: promlabels.FromStrings("cluster", "staging", "disk", "sda", "instance", "server5", "job", "node_exporter"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, }, - id: "37f9b50559e97fd0", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 1, - models.AlertStateSuppressed: 0, - models.AlertStateUnprocessed: 0, + id: "aa4f5d45db158b4e", + stateCount: map[string]int{ + models.AlertStateActive.String(): 1, + models.AlertStateSuppressed.String(): 0, + models.AlertStateUnprocessed.String(): 0, }, }, { receiver: "by-cluster-service", - labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("HTTP_Probe_Failed")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - }, + labels: promlabels.FromStrings("alertname", "HTTP_Probe_Failed", "cluster", "dev"), alerts: []models.Alert{ { StartsAt: time.Date(2019, time.January, 10, 20, 0, 0, 0, time.UTC), Annotations: models.Annotations{ models.Annotation{ Visible: true, - Name: models.NewUniqueString("help"), - Value: models.NewUniqueString("Example help annotation"), + Name: "help", + Value: "Example help annotation", }, models.Annotation{ Visible: true, - Name: models.NewUniqueString("url"), - Value: models.NewUniqueString("http://localhost/example.html"), + Name: "url", + Value: "http://localhost/example.html", IsLink: true, }, }, @@ -664,11 +564,9 @@ var groupTests = []groupTest{ SilencedBy: []string{"0804764c-6163-4c64-b0a9-08feebe2db4b"}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("web1")}, - }, + Labels: promlabels.FromStrings("instance", "web1"), State: models.AlertStateSuppressed, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, { StartsAt: time.Date(2019, time.January, 10, 19, 0, 0, 0, time.UTC), @@ -681,18 +579,16 @@ var groupTests = []groupTest{ SilencedBy: []string{}, }, }, - Labels: models.Labels{ - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("web2")}, - }, + Labels: promlabels.FromStrings("instance", "web2"), State: models.AlertStateActive, - Receiver: models.NewUniqueString("by-cluster-service"), + Receiver: "by-cluster-service", }, }, - id: "ca10a29d2e729cff", - stateCount: map[models.UniqueString]int{ - models.AlertStateActive: 1, - models.AlertStateSuppressed: 1, - models.AlertStateUnprocessed: 0, + id: "e764048aca571a6c", + stateCount: map[string]int{ + models.AlertStateActive.String(): 1, + models.AlertStateSuppressed.String(): 1, + models.AlertStateUnprocessed.String(): 0, }, }, } @@ -700,63 +596,58 @@ var groupTests = []groupTest{ var filtersExpected = []models.Filter{} func compareAlertGroups(testCase groupTest, group models.APIAlertGroup) bool { - if testCase.receiver != group.Receiver.Value() { + if testCase.receiver != group.Receiver { return false } - if len(testCase.labels) != len(group.Labels) { + if testCase.labels.Len() != len(group.Labels) { return false } - for _, l := range testCase.labels { - v := group.Labels.Get(l.Name.Value()) - if v == nil { - return false + match := true + testCase.labels.Range(func(l promlabels.Label) { + if group.Labels.Get(l.Name) != l.Value { + match = false } - if l.Value.Value() != v.Value.Value() { - return false - } - } - return true + }) + return match } -func compareAlerts(expectedAlert, gotAlert models.Alert) bool { +func compareAlerts(expectedAlert models.Alert, gotAlert models.APIAlert) bool { if gotAlert.Receiver != expectedAlert.Receiver { return false } - if len(gotAlert.Labels) != len(expectedAlert.Labels) { + if expectedAlert.Labels.Len() != len(gotAlert.Labels) { return false } - for _, l := range expectedAlert.Labels { - v := gotAlert.Labels.Get(l.Name.Value()) - if v == nil { - return false + match := true + expectedAlert.Labels.Range(func(l promlabels.Label) { + if gotAlert.Labels.Get(l.Name) != l.Value { + match = false } - if l.Value.Value() != v.Value.Value() { - return false - } - } - return true + }) + return match } -func testAlert(version string, t *testing.T, expectedAlert, gotAlert models.Alert) { +func testAlert(version string, t *testing.T, expectedAlert models.Alert, gotAlert models.APIAlert) { if gotAlert.Receiver != expectedAlert.Receiver { t.Errorf("[%s] Expected '%s' receiver but got '%s' on alert labels=%v", - version, expectedAlert.Receiver.Value(), gotAlert.Receiver.Value(), expectedAlert.Labels) + version, expectedAlert.Receiver, gotAlert.Receiver, expectedAlert.Labels) } - if gotAlert.State != expectedAlert.State { + if gotAlert.State != expectedAlert.State.String() { t.Errorf("[%s] Expected state '%s' but got '%s' on alert receiver='%s' labels=%v", - version, expectedAlert.State.Value(), gotAlert.State.Value(), gotAlert.Receiver.Value(), expectedAlert.Labels) + version, expectedAlert.State, gotAlert.State, gotAlert.Receiver, expectedAlert.Labels) } if !reflect.DeepEqual(gotAlert.Annotations, expectedAlert.Annotations) { t.Errorf("[%s] Annotation mismatch on alert receiver='%s' labels=%v, expected %v but got %v", - version, expectedAlert.Receiver.Value(), expectedAlert.Labels, expectedAlert.Annotations, gotAlert.Annotations) + version, expectedAlert.Receiver, expectedAlert.Labels, expectedAlert.Annotations, gotAlert.Annotations) } - if !reflect.DeepEqual(gotAlert.Labels, expectedAlert.Labels) { + expectedLabels := models.LabelsToOrderedLabels(expectedAlert.Labels) + if !reflect.DeepEqual(gotAlert.Labels, expectedLabels) { t.Errorf("[%s] Labels mismatch on alert receiver='%s', expected labels=%v but got %v", - version, expectedAlert.Receiver.Value(), expectedAlert.Labels, gotAlert.Labels) + version, expectedAlert.Receiver, expectedLabels, gotAlert.Labels) } if len(gotAlert.Alertmanager) != len(expectedAlert.Alertmanager) { t.Errorf("[%s] Expected %d alertmanager instances but got %d on alert receiver='%s' labels=%v", - version, len(expectedAlert.Alertmanager), len(gotAlert.Alertmanager), gotAlert.Receiver.Value(), expectedAlert.Labels) + version, len(expectedAlert.Alertmanager), len(gotAlert.Alertmanager), gotAlert.Receiver, expectedAlert.Labels) } for _, expectedAM := range expectedAlert.Alertmanager { found := false @@ -765,11 +656,11 @@ func testAlert(version string, t *testing.T, expectedAlert, gotAlert models.Aler found = true if gotAM.State != expectedAM.State { t.Errorf("[%s] Expected alertmanager '%s' to have state '%s' but got '%s' on alert receiver='%s' labels=%v", - version, expectedAM.Name, expectedAM.State.Value(), gotAM.State.Value(), gotAlert.Receiver.Value(), expectedAlert.Labels) + version, expectedAM.Name, expectedAM.State, gotAM.State, gotAlert.Receiver, expectedAlert.Labels) } if gotAM.Source != expectedAM.Source { t.Errorf("[%s] Expected alertmanager '%s' to have source '%s' but got '%s' on alert receiver='%s' labels=%v", - version, expectedAM.Name, expectedAM.Source, gotAM.Source, gotAlert.Receiver.Value(), expectedAlert.Labels) + version, expectedAM.Name, expectedAM.Source, gotAM.Source, gotAlert.Receiver, expectedAlert.Labels) } // multiple silences only work for >=0.6.1 versionRange, err := semver.NewConstraint(">=0.6.1") @@ -779,7 +670,7 @@ func testAlert(version string, t *testing.T, expectedAlert, gotAlert models.Aler if versionRange.Check(semver.MustParse(version)) { if len(gotAM.Silences) != len(expectedAM.Silences) { t.Errorf("[%s] Expected alertmanager '%s' to have %d silences but got %d on alert receiver='%s' labels=%v", - version, expectedAM.Name, len(expectedAM.Silences), len(gotAM.Silences), gotAlert.Receiver.Value(), expectedAlert.Labels) + version, expectedAM.Name, len(expectedAM.Silences), len(gotAM.Silences), gotAlert.Receiver, expectedAlert.Labels) } for _, es := range expectedAM.Silences { foundSilence := false @@ -793,7 +684,7 @@ func testAlert(version string, t *testing.T, expectedAlert, gotAlert models.Aler } if !foundSilence { t.Errorf("[%s] Silence %v not found on alertmanager '%s' on alert receiver='%s' labels=%v", - version, es, expectedAM.Name, expectedAlert.Receiver.Value(), expectedAlert.Labels) + version, es, expectedAM.Name, expectedAlert.Receiver, expectedAlert.Labels) } } } @@ -802,7 +693,7 @@ func testAlert(version string, t *testing.T, expectedAlert, gotAlert models.Aler } if !found { t.Errorf("[%s] Alertmanager instances '%s' not found on alert receiver='%s' labels=%v", - version, expectedAM.Name, gotAlert.Receiver.Value(), expectedAlert.Labels) + version, expectedAM.Name, gotAlert.Receiver, expectedAlert.Labels) } } } @@ -813,11 +704,11 @@ func testAlertGroup(version string, t *testing.T, testCase groupTest, group mode version, testCase.id, group.ID, group.Labels) } for key, val := range testCase.stateCount { - v, found := group.StateCount[key.Value()] + v, found := group.StateCount[key] if !found { - t.Errorf("[%s] Expected group.StateCount[%s]=%d not found", version, key.Value(), val) + t.Errorf("[%s] Expected group.StateCount[%s]=%d not found", version, key, val) } else if v != val { - t.Errorf("[%s] group.StateCount[%s] mismatch, expected %d but got %d", version, key.Value(), val, v) + t.Errorf("[%s] group.StateCount[%s] mismatch, expected %d but got %d", version, key, val, v) } } if len(testCase.alerts) != len(group.Alerts) { @@ -835,7 +726,7 @@ func testAlertGroup(version string, t *testing.T, testCase groupTest, group mode } if !alertFound { t.Errorf("[%s] Expected alert receiver='%s' labels=%v not found in group: %v", - version, expectedAlert.Receiver.Value(), expectedAlert.Labels, group.Alerts) + version, expectedAlert.Receiver, expectedAlert.Labels, group.Alerts) } } } @@ -972,7 +863,7 @@ var sortTests = []sortTest{ sortLabel: "", sortReverse: false, expectedLabel: "cluster", - expectedValues: []string{"staging", "dev", "staging", "dev", "prod", "prod"}, + expectedValues: []string{"dev", "prod", "prod", "staging", "dev", "staging"}, }, { filter: []string{"@receiver=by-cluster-service"}, @@ -980,7 +871,7 @@ var sortTests = []sortTest{ sortLabel: "", sortReverse: true, expectedLabel: "cluster", - expectedValues: []string{"prod", "prod", "dev", "staging", "dev", "staging"}, + expectedValues: []string{"staging", "dev", "staging", "prod", "prod", "dev"}, }, { filter: []string{"@receiver=by-cluster-service"}, @@ -1068,15 +959,15 @@ func TestSortOrder(t *testing.T) { } else { values := []string{} for _, ag := range ur.Grids[0].AlertGroups { - v := ag.Labels.GetValue(testCase.expectedLabel) + v := ag.Labels.Get(testCase.expectedLabel) if v == "" { - v = ag.Shared.Labels.GetValue(testCase.expectedLabel) + v = ag.Shared.Labels.Get(testCase.expectedLabel) } if v != "" { values = append(values, v) } else { for _, alert := range ag.Alerts { - v = alert.Labels.GetValue(testCase.expectedLabel) + v = alert.Labels.Get(testCase.expectedLabel) values = append(values, v) } } @@ -1092,22 +983,22 @@ func TestSortOrder(t *testing.T) { } } -func verifyStrippedLabels(t *testing.T, labels models.Labels, keep, strip []string) { +func verifyStrippedLabels(t *testing.T, ls models.OrderedLabels, keep, strip []string) { for _, l := range strip { - if val := labels.Get(l); val != nil { - t.Errorf("Found stripped label %s=%s on %v", val.Name.Value(), val.Value.Value(), labels) + if v := ls.Get(l); v != "" { + t.Errorf("Found stripped label %s=%s on %v", l, v, ls) } } if len(keep) > 0 && len(strip) == 0 { - for _, ll := range labels { + for _, ll := range ls { ok := false for _, l := range keep { - if ll.Name.Value() == l { + if ll.Name == l { ok = true } } if !ok { - t.Errorf("Found label %s=%s that's not on the keep list: %v", ll.Name.Value(), ll.Value.Value(), keep) + t.Errorf("Found label %s=%s that's not on the keep list: %v", ll.Name, ll.Value, keep) } } } diff --git a/cmd/karma/metrics.go b/cmd/karma/metrics.go index 64f12cf67..1e2638b85 100644 --- a/cmd/karma/metrics.go +++ b/cmd/karma/metrics.go @@ -6,7 +6,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prymitive/karma/internal/alertmanager" - "github.com/prymitive/karma/internal/models" ) type karmaCollector struct { @@ -100,9 +99,9 @@ func (c *karmaCollector) Collect(ch chan<- prometheus.Metric) { } // receiver name -> count - groupsByReceiver := map[models.UniqueString]float64{} + groupsByReceiver := map[string]float64{} // receiver name -> state -> count - alertsByReceiverByState := map[models.UniqueString]map[models.UniqueString]float64{} + alertsByReceiverByState := map[string]map[string]float64{} // iterate all alert groups this instance stores for _, group := range am.Alerts() { @@ -115,13 +114,13 @@ func (c *karmaCollector) Collect(ch chan<- prometheus.Metric) { // count all alerts per receiver & state for _, alert := range group.Alerts { if _, found := alertsByReceiverByState[alert.Receiver]; !found { - alertsByReceiverByState[alert.Receiver] = map[models.UniqueString]float64{ - models.NewUniqueString("unprocessed"): 0, - models.NewUniqueString("active"): 0, - models.NewUniqueString("suppressed"): 0, + alertsByReceiverByState[alert.Receiver] = map[string]float64{ + "unprocessed": 0, + "active": 0, + "suppressed": 0, } } - alertsByReceiverByState[alert.Receiver][alert.State]++ + alertsByReceiverByState[alert.Receiver][alert.State.String()]++ } } @@ -132,7 +131,7 @@ func (c *karmaCollector) Collect(ch chan<- prometheus.Metric) { prometheus.GaugeValue, count, am.Name, - reciver.Value(), + reciver, ) } for reciver, byState := range alertsByReceiverByState { @@ -142,8 +141,8 @@ func (c *karmaCollector) Collect(ch chan<- prometheus.Metric) { prometheus.GaugeValue, count, am.Name, - state.Value(), - reciver.Value(), + state, + reciver, ) } } diff --git a/cmd/karma/views.go b/cmd/karma/views.go index c907677bb..9cb65d5c6 100644 --- a/cmd/karma/views.go +++ b/cmd/karma/views.go @@ -18,6 +18,7 @@ import ( "github.com/cnf/structhash" "github.com/fvbommel/sortorder" "github.com/klauspost/compress/gzip" + promlabels "github.com/prometheus/prometheus/model/labels" "github.com/prymitive/karma/internal/alertmanager" "github.com/prymitive/karma/internal/config" @@ -36,6 +37,19 @@ var gzipWriterPool = sync.Pool{ }, } +var jsonBufPool = sync.Pool{ + New: func() any { + return bytes.NewBuffer(make([]byte, 0, 1<<16)) + }, +} + +func marshalJSON(v any) *bytes.Buffer { + buf := jsonBufPool.Get().(*bytes.Buffer) + buf.Reset() + _ = json.NewEncoder(buf).Encode(v) + return buf +} + func noCache(w http.ResponseWriter) { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") } @@ -248,17 +262,18 @@ func alerts(w http.ResponseWriter, r *http.Request) { newResp.Settings.Labels = labels newResp.Timestamp = string(ts) newResp.Authentication = resp.Authentication - newData, _ := json.Marshal(&newResp) + buf := marshalJSON(&newResp) mimeJSON(w) w.WriteHeader(http.StatusOK) - _, _ = w.Write(newData) + _, _ = w.Write(buf.Bytes()) + jsonBufPool.Put(buf) return } grids := map[string]models.APIGrid{} colors := models.LabelsColorMap{} silences := map[string]map[string]models.Silence{} - allReceivers := map[models.UniqueString]struct{}{} + allReceivers := map[string]struct{}{} dedupedAlerts := alertmanager.DedupAlerts() dedupedColors := alertmanager.DedupColors() @@ -272,17 +287,17 @@ func alerts(w http.ResponseWriter, r *http.Request) { } var matches int - labelMap := map[models.UniqueString]struct{}{} + labelMap := map[string]struct{}{} for _, ag := range filtered { perGridAlertGroup := map[string]*models.AlertGroup{} - for _, l := range ag.Labels { + ag.Labels.Range(func(l promlabels.Label) { labelMap[l.Name] = struct{}{} - } + }) for _, alert := range ag.Alerts { - for _, l := range alert.Labels { + alert.Labels.Range(func(l promlabels.Label) { labelMap[l.Name] = struct{}{} - } + }) allReceivers[alert.Receiver] = struct{}{} @@ -291,7 +306,7 @@ func alerts(w http.ResponseWriter, r *http.Request) { alertGridLabelValues := map[string]struct{}{} switch gridLabel { case "@receiver": - alertGridLabelValues[alert.Receiver.Value()] = struct{}{} + alertGridLabelValues[alert.Receiver] = struct{}{} case "@alertmanager": for _, am := range alert.Alertmanager { alertGridLabelValues[am.Name] = struct{}{} @@ -301,7 +316,7 @@ func alerts(w http.ResponseWriter, r *http.Request) { alertGridLabelValues[am.Cluster] = struct{}{} } default: - alertGridLabelValues[alert.Labels.GetValue(gridLabel)] = struct{}{} + alertGridLabelValues[alert.Labels.Get(gridLabel)] = struct{}{} } for alertGridLabelValue := range alertGridLabelValues { @@ -323,7 +338,7 @@ func alerts(w http.ResponseWriter, r *http.Request) { StateCount: map[string]int{}, } for _, s := range models.AlertStateList { - agCopy.StateCount[s.Value()] = 0 + agCopy.StateCount[s.String()] = 0 } perGridAlertGroup[alertGridLabelValue] = agCopy } @@ -335,7 +350,7 @@ func alerts(w http.ResponseWriter, r *http.Request) { for _, am := range alert.Alertmanager { if am.Name == alertGridLabelValue { ams = append(ams, am) - stateCount[am.State]++ + stateCount[am.State.String()]++ } } alert.Alertmanager = ams @@ -344,13 +359,13 @@ func alerts(w http.ResponseWriter, r *http.Request) { for _, am := range alert.Alertmanager { if am.Cluster == alertGridLabelValue { ams = append(ams, am) - stateCount[am.State]++ + stateCount[am.State.String()]++ } } alert.Alertmanager = ams default: for _, am := range alert.Alertmanager { - stateCount[am.State]++ + stateCount[am.State.String()]++ } } @@ -359,11 +374,11 @@ func alerts(w http.ResponseWriter, r *http.Request) { agCopy.Alerts = append(agCopy.Alerts, alert) if ck, foundKey := dedupedColors["@receiver"]; foundKey { - if cv, foundVal := ck[alert.Receiver.Value()]; foundVal { + if cv, foundVal := ck[alert.Receiver]; foundVal { if _, found := colors["@receiver"]; !found { colors["@receiver"] = map[string]models.LabelColors{} } - colors["@receiver"][alert.Receiver.Value()] = cv + colors["@receiver"][alert.Receiver] = cv } } @@ -389,7 +404,7 @@ func alerts(w http.ResponseWriter, r *http.Request) { } } - agCopy.StateCount[alert.State.Value()]++ + agCopy.StateCount[alert.State.String()]++ for _, am := range alert.Alertmanager { if _, found := agCopy.AlertmanagerCount[am.Name]; !found { @@ -399,16 +414,16 @@ func alerts(w http.ResponseWriter, r *http.Request) { } } - for _, l := range alert.Labels { - if keyMap, foundKey := dedupedColors[l.Name.Value()]; foundKey { - if color, foundColor := keyMap[l.Value.Value()]; foundColor { - if _, found := colors[l.Name.Value()]; !found { - colors[l.Name.Value()] = map[string]models.LabelColors{} + alert.Labels.Range(func(l promlabels.Label) { + if keyMap, foundKey := dedupedColors[l.Name]; foundKey { + if color, foundColor := keyMap[l.Value]; foundColor { + if _, found := colors[l.Name]; !found { + colors[l.Name] = map[string]models.LabelColors{} } - colors[l.Name.Value()][l.Value.Value()] = color + colors[l.Name][l.Value] = color } } - } + }) } } @@ -429,9 +444,9 @@ func alerts(w http.ResponseWriter, r *http.Request) { slices.SortFunc(ag.Alerts, models.CompareAlerts) ag.LatestStartsAt = ag.FindLatestStartsAt() ag.Hash = ag.ContentFingerprint() - apiAG := models.APIAlertGroup{AlertGroup: *ag, TotalAlerts: len(ag.Alerts)} - apiAG.DedupSharedMaps([]string{gridLabel}) - resp.TotalAlerts += len(ag.Alerts) + totalAlerts := len(ag.Alerts) + shared, allLabels := ag.DedupSharedMaps([]string{gridLabel}) + resp.TotalAlerts += totalAlerts alertLimit, found := request.GroupLimits[ag.ID] if !found { @@ -441,10 +456,11 @@ func alerts(w http.ResponseWriter, r *http.Request) { alertLimit = config.Config.UI.AlertsPerGroup } } - if alertLimit > apiAG.TotalAlerts { - alertLimit = apiAG.TotalAlerts + if alertLimit > totalAlerts { + alertLimit = totalAlerts } - apiAG.Alerts = apiAG.Alerts[0:alertLimit] + ag.Alerts = ag.Alerts[0:alertLimit] + apiAG := models.NewAPIAlertGroup(*ag, shared, allLabels, totalAlerts) grid, found := grids[gridLabelValue] if !found { @@ -455,7 +471,7 @@ func alerts(w http.ResponseWriter, r *http.Request) { StateCount: map[string]int{}, } for _, s := range models.AlertStateList { - grid.StateCount[s.Value()] = 0 + grid.StateCount[s.String()] = 0 } grids[gridLabelValue] = grid } @@ -493,13 +509,13 @@ func alerts(w http.ResponseWriter, r *http.Request) { receivers := []string{} for k := range allReceivers { - receivers = append(receivers, k.Value()) + receivers = append(receivers, k) } sort.Strings(receivers) resp.LabelNames = make([]string, 0, len(labelMap)) for label := range labelMap { - resp.LabelNames = append(resp.LabelNames, label.Value()) + resp.LabelNames = append(resp.LabelNames, label) } sort.Strings(resp.LabelNames) @@ -511,13 +527,14 @@ func alerts(w http.ResponseWriter, r *http.Request) { resp.Filters = populateAPIFilters(matchFilters) resp.Receivers = receivers - data, _ = json.Marshal(resp) - compressedData, _ := compressResponse(data, nil) + buf := marshalJSON(resp) + compressedData, _ := compressResponse(buf.Bytes(), nil) _ = apiCache.Add(cacheKey, compressedData) mimeJSON(w) w.WriteHeader(http.StatusOK) - _, _ = w.Write(data) + _, _ = w.Write(buf.Bytes()) + jsonBufPool.Put(buf) } func labelsSettings(grids []models.APIGrid, store models.LabelsSettings) { @@ -530,12 +547,12 @@ func labelsSettings(grids []models.APIGrid, store models.LabelsSettings) { for _, grid := range grids { labelSettings(grid.LabelName, store) for _, ag := range grid.AlertGroups { - for _, label := range ag.Labels { - labelSettings(label.Name.Value(), store) + for _, l := range ag.Labels { + labelSettings(l.Name, store) } for _, alert := range ag.Alerts { - for _, label := range alert.Labels { - labelSettings(label.Name.Value(), store) + for _, l := range alert.Labels { + labelSettings(l.Name, store) } } } @@ -586,25 +603,27 @@ func autocomplete(w http.ResponseWriter, r *http.Request) { lowerTerm := strings.ToLower(term) for _, hint := range dedupedAutocomplete { - if strings.HasPrefix(strings.ToLower(hint.Value.Value()), lowerTerm) { - acData = append(acData, hint.Value.Value()) + if strings.HasPrefix(strings.ToLower(hint.Value), lowerTerm) { + acData = append(acData, hint.Value) } else { for _, token := range hint.Tokens { - if strings.HasPrefix(strings.ToLower(token.Value()), lowerTerm) { - acData = append(acData, hint.Value.Value()) + if strings.HasPrefix(strings.ToLower(token), lowerTerm) { + acData = append(acData, hint.Value) } } } } sort.Sort(sort.Reverse(acData)) - data, _ = json.Marshal(acData) + buf := marshalJSON(acData) + data = append([]byte(nil), buf.Bytes()...) _ = apiCache.Add(cacheKey, data) mimeJSON(w) w.WriteHeader(http.StatusOK) - _, _ = w.Write(data) + _, _ = w.Write(buf.Bytes()) + jsonBufPool.Put(buf) } func silences(w http.ResponseWriter, r *http.Request) { @@ -669,9 +688,9 @@ func silences(w http.ResponseWriter, r *http.Request) { if match.IsRegex { eq = "=~" } - if searchTerm == fmt.Sprintf("%s%s\"%s\"", strings.ToLower(match.Name.Value()), eq, strings.ToLower(match.Value)) { + if searchTerm == fmt.Sprintf("%s%s\"%s\"", strings.ToLower(match.Name), eq, strings.ToLower(match.Value)) { isMatch = true - } else if strings.Contains(strings.ToLower(fmt.Sprintf("%s%s%s", match.Name.Value(), eq, match.Value)), searchTerm) { + } else if strings.Contains(strings.ToLower(fmt.Sprintf("%s%s%s", match.Name, eq, match.Value)), searchTerm) { isMatch = true } } @@ -724,17 +743,19 @@ func silences(w http.ResponseWriter, r *http.Request) { } } - data, _ = json.Marshal(dedupedSilences) + buf := marshalJSON(dedupedSilences) + data = append([]byte(nil), buf.Bytes()...) _ = apiCache.Add(cacheKey, data) mimeJSON(w) w.WriteHeader(http.StatusOK) - _, _ = w.Write(data) + _, _ = w.Write(buf.Bytes()) + jsonBufPool.Put(buf) } type AlertList struct { - Alerts []models.Labels `json:"alerts"` + Alerts []promlabels.Labels `json:"alerts"` } func alertList(w http.ResponseWriter, r *http.Request) { @@ -761,12 +782,12 @@ func alertList(w http.ResponseWriter, r *http.Request) { labelMap := map[string]map[string]string{} for _, ag := range filtered { for _, alert := range ag.Alerts { - labels := ag.Labels.Map() - for _, l := range alert.Labels { - labels[l.Name.Value()] = l.Value.Value() - } - h := hex.EncodeToString(structhash.Sha1(labels, 1)) - labelMap[h] = labels + lm := ag.Labels.Map() + alert.Labels.Range(func(l promlabels.Label) { + lm[l.Name] = l.Value + }) + h := hex.EncodeToString(structhash.Sha1(lm, 1)) + labelMap[h] = lm } } @@ -783,31 +804,27 @@ func alertList(w http.ResponseWriter, r *http.Request) { sort.Strings(sortKeys) al := AlertList{ - Alerts: []models.Labels{}, + Alerts: []promlabels.Labels{}, } - for _, labels := range labelMap { - alert := models.Labels{} - for k, v := range labels { - alert = alert.Set(k, v) - } - slices.SortFunc(alert, models.CompareLabels) - al.Alerts = append(al.Alerts, alert) + for _, lm := range labelMap { + al.Alerts = append(al.Alerts, models.LabelsFromMap(lm)) } sortSliceOfLabels(al.Alerts, sortKeys, "alertname") mimeJSON(w) w.WriteHeader(http.StatusOK) - data, _ := json.Marshal(al) - compressedData, _ := compressResponse(data, nil) + buf := marshalJSON(al) + compressedData, _ := compressResponse(buf.Bytes(), nil) _ = apiCache.Add(cacheKey, compressedData) - _, _ = w.Write(data) + _, _ = w.Write(buf.Bytes()) + jsonBufPool.Put(buf) } -func sortSliceOfLabels(labels []models.Labels, sortKeys []string, fallback string) { - sort.SliceStable(labels, func(i, j int) bool { +func sortSliceOfLabels(ls []promlabels.Labels, sortKeys []string, fallback string) { + sort.SliceStable(ls, func(i, j int) bool { for _, k := range sortKeys { - vi := labels[i].GetValue(k) - vj := labels[j].GetValue(k) + vi := ls[i].Get(k) + vj := ls[j].Get(k) if vi != "" && vj == "" { return true } @@ -818,7 +835,7 @@ func sortSliceOfLabels(labels []models.Labels, sortKeys []string, fallback strin return sortorder.NaturalLess(vi, vj) } } - return sortorder.NaturalLess(labels[i].GetValue(fallback), labels[j].GetValue(fallback)) + return sortorder.NaturalLess(ls[i].Get(fallback), ls[j].Get(fallback)) }) } @@ -858,7 +875,7 @@ func counters(w http.ResponseWriter, r *http.Request) { for _, alert := range ag.Alerts { stateCount := newStateCount() for _, am := range alert.Alertmanager { - stateCount[am.State]++ + stateCount[am.State.String()]++ } alert.State = stateFromStateCount(stateCount) @@ -871,11 +888,11 @@ func counters(w http.ResponseWriter, r *http.Request) { countLabel("@cluster", cluster) } } - countLabel("@state", alert.State.Value()) - countLabel("@receiver", alert.Receiver.Value()) - for _, l := range alert.Labels { - countLabel(l.Name.Value(), l.Value.Value()) - } + countLabel("@state", alert.State.String()) + countLabel("@receiver", alert.Receiver) + alert.Labels.Range(func(l promlabels.Label) { + countLabel(l.Name, l.Value) + }) } } @@ -886,8 +903,9 @@ func counters(w http.ResponseWriter, r *http.Request) { mimeJSON(w) w.WriteHeader(http.StatusOK) - data, _ := json.Marshal(resp) - compressedData, _ := compressResponse(data, nil) + buf := marshalJSON(resp) + compressedData, _ := compressResponse(buf.Bytes(), nil) _ = apiCache.Add(cacheKey, compressedData) - _, _ = w.Write(data) + _, _ = w.Write(buf.Bytes()) + jsonBufPool.Put(buf) } diff --git a/cmd/karma/views_test.go b/cmd/karma/views_test.go index 81acf075b..660dd75bf 100644 --- a/cmd/karma/views_test.go +++ b/cmd/karma/views_test.go @@ -11,7 +11,6 @@ import ( "net/http/httptest" "os" "regexp" - "slices" "sort" "strconv" "strings" @@ -19,6 +18,7 @@ import ( "github.com/beme/abide" lru "github.com/hashicorp/golang-lru/v2" + promlabels "github.com/prometheus/prometheus/model/labels" "github.com/prymitive/karma/internal/alertmanager" "github.com/prymitive/karma/internal/config" @@ -36,8 +36,8 @@ import ( var upstreamSetup = false -var cmpUnique = cmp.Comparer(func(x, y models.UniqueString) bool { - return cmp.Equal(x.Value(), y.Value()) +var cmpLabels = cmp.Comparer(func(x, y promlabels.Labels) bool { + return promlabels.Compare(x, y) == 0 }) type setenvFunc func(key, val string) @@ -637,8 +637,15 @@ func TestValidateAllAlerts(t *testing.T) { } for _, ag := range ur.Grids[0].AlertGroups { for _, a := range ag.Alerts { - if !slices.Contains(models.AlertStateList, a.State) { - t.Errorf("Invalid alert status '%s', not in %v", a.State.Value(), models.AlertStateList) + validState := false + for _, s := range models.AlertStateList { + if s.String() == a.State { + validState = true + break + } + } + if !validState { + t.Errorf("Invalid alert status '%s', not in %v", a.State, models.AlertStateList) } if len(a.Alertmanager) == 0 { t.Errorf("Alertmanager instance list is empty, %v", a) @@ -1060,7 +1067,7 @@ func TestSilences(t *testing.T) { results = append(results, silence.Silence.Comment) } sort.Strings(results) // can't rely on API order since it's sorted based on timestamps, resort - if diff := cmp.Diff(testCase.results, results, cmpUnique); diff != "" { + if diff := cmp.Diff(testCase.results, results); diff != "" { t.Errorf("Wrong silences returned for '%s' (-want +got):\n%s", uri, diff) } } @@ -1166,7 +1173,7 @@ func TestEmptySettings(t *testing.T) { Labels: models.LabelsSettings{}, } - if diff := cmp.Diff(expectedSettings, ur.Settings, cmpUnique); diff != "" { + if diff := cmp.Diff(expectedSettings, ur.Settings); diff != "" { t.Errorf("Wrong settings returned (-want +got):\n%s", diff) } } @@ -1505,7 +1512,7 @@ func TestAuthentication(t *testing.T) { if ur.Authentication.Username != testCase.responseUsername { t.Errorf("Got Authentication.Username=%s, expected %s", ur.Authentication.Username, testCase.responseUsername) } - if diff := cmp.Diff(ur.Authentication.Groups, testCase.responseGroups, cmpUnique); diff != "" { + if diff := cmp.Diff(ur.Authentication.Groups, testCase.responseGroups); diff != "" { t.Errorf("Incorrect groups list (-want +got):\n%s", diff) break } @@ -2253,169 +2260,61 @@ func TestAlertList(t *testing.T) { { args: "", alerts: AlertList{ - Alerts: []models.Labels{ - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Free_Disk_Space_Too_Low")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - {Name: models.NewUniqueString("disk"), Value: models.NewUniqueString("sda")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server5")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("HTTP_Probe_Failed")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("web1")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("HTTP_Probe_Failed")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("web2")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server6")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.6")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server7")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.7")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server8")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.8")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server1")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.1")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server2")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.2")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server3")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.3")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server4")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.4")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server5")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.5")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Memory_Usage_Too_High")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server2")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, + Alerts: []promlabels.Labels{ + promlabels.FromStrings("alertname", "Free_Disk_Space_Too_Low", "cluster", "staging", "disk", "sda", "instance", "server5", "job", "node_exporter"), + promlabels.FromStrings("alertname", "HTTP_Probe_Failed", "cluster", "dev", "instance", "web1", "job", "node_exporter"), + promlabels.FromStrings("alertname", "HTTP_Probe_Failed", "cluster", "dev", "instance", "web2", "job", "node_exporter"), + promlabels.FromStrings("alertname", "Host_Down", "cluster", "dev", "instance", "server6", "ip", "127.0.0.6", "job", "node_ping"), + promlabels.FromStrings("alertname", "Host_Down", "cluster", "dev", "instance", "server7", "ip", "127.0.0.7", "job", "node_ping"), + promlabels.FromStrings("alertname", "Host_Down", "cluster", "dev", "instance", "server8", "ip", "127.0.0.8", "job", "node_ping"), + promlabels.FromStrings("alertname", "Host_Down", "cluster", "prod", "instance", "server1", "ip", "127.0.0.1", "job", "node_ping"), + promlabels.FromStrings("alertname", "Host_Down", "cluster", "prod", "instance", "server2", "ip", "127.0.0.2", "job", "node_ping"), + promlabels.FromStrings("alertname", "Host_Down", "cluster", "staging", "instance", "server3", "ip", "127.0.0.3", "job", "node_ping"), + promlabels.FromStrings("alertname", "Host_Down", "cluster", "staging", "instance", "server4", "ip", "127.0.0.4", "job", "node_ping"), + promlabels.FromStrings("alertname", "Host_Down", "cluster", "staging", "instance", "server5", "ip", "127.0.0.5", "job", "node_ping"), + promlabels.FromStrings("alertname", "Memory_Usage_Too_High", "cluster", "prod", "instance", "server2", "job", "node_exporter"), }, }, }, { args: "q=alertname=Free_Disk_Space_Too_Low", alerts: AlertList{ - Alerts: []models.Labels{ - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Free_Disk_Space_Too_Low")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("staging")}, - {Name: models.NewUniqueString("disk"), Value: models.NewUniqueString("sda")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server5")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, + Alerts: []promlabels.Labels{ + promlabels.FromStrings("alertname", "Free_Disk_Space_Too_Low", "cluster", "staging", "disk", "sda", "instance", "server5", "job", "node_exporter"), }, }, }, { args: "q=alertname=HTTP_Probe_Failed", alerts: AlertList{ - Alerts: []models.Labels{ - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("HTTP_Probe_Failed")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("web1")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("HTTP_Probe_Failed")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("dev")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("web2")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, + Alerts: []promlabels.Labels{ + promlabels.FromStrings("alertname", "HTTP_Probe_Failed", "cluster", "dev", "instance", "web1", "job", "node_exporter"), + promlabels.FromStrings("alertname", "HTTP_Probe_Failed", "cluster", "dev", "instance", "web2", "job", "node_exporter"), }, }, }, { args: "q=instance=server2", alerts: AlertList{ - Alerts: []models.Labels{ - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server2")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.2")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Memory_Usage_Too_High")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server2")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - }, + Alerts: []promlabels.Labels{ + promlabels.FromStrings("alertname", "Host_Down", "cluster", "prod", "instance", "server2", "ip", "127.0.0.2", "job", "node_ping"), + promlabels.FromStrings("alertname", "Memory_Usage_Too_High", "cluster", "prod", "instance", "server2", "job", "node_exporter"), }, }, }, { args: "q=alertname=Host_Down&q=cluster=prod", alerts: AlertList{ - Alerts: []models.Labels{ - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server1")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.1")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, - { - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("Host_Down")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("server2")}, - {Name: models.NewUniqueString("ip"), Value: models.NewUniqueString("127.0.0.2")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_ping")}, - }, + Alerts: []promlabels.Labels{ + promlabels.FromStrings("alertname", "Host_Down", "cluster", "prod", "instance", "server1", "ip", "127.0.0.1", "job", "node_ping"), + promlabels.FromStrings("alertname", "Host_Down", "cluster", "prod", "instance", "server2", "ip", "127.0.0.2", "job", "node_ping"), }, }, }, { args: "q=foo=bar", alerts: AlertList{ - Alerts: []models.Labels{}, + Alerts: []promlabels.Labels{}, }, }, } @@ -2442,7 +2341,7 @@ func TestAlertList(t *testing.T) { if err != nil { t.Errorf("Failed to unmarshal response: %s", err) } - if diff := cmp.Diff(tc.alerts, ur, cmpUnique); diff != "" { + if diff := cmp.Diff(tc.alerts, ur, cmpLabels); diff != "" { t.Errorf("Wrong alert list returned (-want +got):\n%s", diff) } } @@ -2454,85 +2353,85 @@ func TestAlertList(t *testing.T) { func TestSortSliceOfLabels(t *testing.T) { type testCaseT struct { - labels []models.Labels + labels []promlabels.Labels sortKeys []string fallback string - output []models.Labels + output []promlabels.Labels } testCases := []testCaseT{ { - labels: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, + labels: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert2"), + promlabels.FromStrings("alertname", "alert1"), }, sortKeys: []string{}, fallback: "", - output: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, + output: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert2"), + promlabels.FromStrings("alertname", "alert1"), }, }, { - labels: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, + labels: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert2"), + promlabels.FromStrings("alertname", "alert1"), }, sortKeys: []string{"alertname"}, fallback: "alertname", - output: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}}, + output: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert1"), + promlabels.FromStrings("alertname", "alert2"), }, }, { - labels: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, + labels: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert2"), + promlabels.FromStrings("alertname", "alert1"), }, sortKeys: []string{}, fallback: "alertname", - output: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}}, + output: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert1"), + promlabels.FromStrings("alertname", "alert2"), }, }, { - labels: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, + labels: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert2"), + promlabels.FromStrings("alertname", "alert1"), }, sortKeys: []string{"foo"}, fallback: "alertname", - output: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}}, + output: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert1"), + promlabels.FromStrings("alertname", "alert2"), }, }, { - labels: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, + labels: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert1"), + promlabels.FromStrings("alertname", "alert1"), }, sortKeys: []string{"alertname"}, fallback: "alertname", - output: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, + output: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert1"), + promlabels.FromStrings("alertname", "alert1"), }, }, { - labels: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}, {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("a")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert3")}, {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("b")}}, + labels: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert2", "job", "a"), + promlabels.FromStrings("alertname", "alert1"), + promlabels.FromStrings("alertname", "alert3", "job", "b"), }, sortKeys: []string{"job"}, fallback: "alertname", - output: []models.Labels{ - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert2")}, {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("a")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert3")}, {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("b")}}, - {{Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("alert1")}}, + output: []promlabels.Labels{ + promlabels.FromStrings("alertname", "alert2", "job", "a"), + promlabels.FromStrings("alertname", "alert3", "job", "b"), + promlabels.FromStrings("alertname", "alert1"), }, }, } @@ -2540,7 +2439,7 @@ func TestSortSliceOfLabels(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d:%v", i, tc.sortKeys), func(t *testing.T) { sortSliceOfLabels(tc.labels, tc.sortKeys, tc.fallback) - if diff := cmp.Diff(tc.output, tc.labels, cmpUnique); diff != "" { + if diff := cmp.Diff(tc.output, tc.labels, cmpLabels); diff != "" { t.Errorf("Wrong labels order after sorting (-want +got):\n%s", diff) } }) @@ -2783,7 +2682,7 @@ func TestLabelSettings(t *testing.T) { t.Error("TotalAlerts=0") t.FailNow() } - if diff := cmp.Diff(tc.labels, ur.Settings.Labels, cmpUnique); diff != "" { + if diff := cmp.Diff(tc.labels, ur.Settings.Labels); diff != "" { t.Errorf("Wrong labels returned (-want +got):\n%s", diff) } } diff --git a/go.mod b/go.mod index e5557db2f..51441b2e8 100644 --- a/go.mod +++ b/go.mod @@ -26,9 +26,10 @@ require ( github.com/knadh/koanf/providers/file v1.2.1 github.com/knadh/koanf/providers/posflag v1.0.1 github.com/knadh/koanf/v2 v2.3.3 - github.com/pmezard/go-difflib v1.0.0 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/common v0.67.5 + github.com/prometheus/prometheus v0.310.0 github.com/prymitive/randomcolor v0.0.0-20210705210145-26c3401033a6 github.com/rogpeppe/go-internal v1.14.1 github.com/rs/zerolog v1.34.0 @@ -60,6 +61,7 @@ require ( github.com/go-openapi/swag/typeutils v0.25.5 // indirect github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -67,7 +69,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect diff --git a/go.sum b/go.sum index 341d6a887..f439b931a 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,9 @@ github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a h1:Ohw57yVY2dBTt+gs github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= @@ -76,13 +77,15 @@ github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocK github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= @@ -132,8 +135,9 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= @@ -142,8 +146,9 @@ github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= @@ -154,6 +159,8 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/prometheus/prometheus v0.310.0 h1:iS0Uul/dHjy8ifBnqo3YEOhRxlTOWantRoDWwmIowwA= +github.com/prometheus/prometheus v0.310.0/go.mod h1:rs6XoWKvgAStqxHxb2Twh1BR6rp7qw7fmUgW+gaXjbw= github.com/prymitive/randomcolor v0.0.0-20210705210145-26c3401033a6 h1:zL1c7Tzs1zTkak2si4V5Y0kop6Bue7UMN4ML68PGpIg= github.com/prymitive/randomcolor v0.0.0-20210705210145-26c3401033a6/go.mod h1:FnzxethsFNTR5+dT5nOGHEmV7DS7U6ml/kw2HFv5Q+8= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/internal/alertmanager/dedup.go b/internal/alertmanager/dedup.go index 9d474597d..eec5eb229 100644 --- a/internal/alertmanager/dedup.go +++ b/internal/alertmanager/dedup.go @@ -28,10 +28,15 @@ func DedupAlerts() []models.AlertGroup { } } - dedupedGroups := []models.AlertGroup{} - alertStates := map[string][]models.UniqueString{} + dedupedGroups := make([]models.AlertGroup, 0, len(uniqueGroups)) + alertStates := map[string][]models.AlertState{} for _, agList := range uniqueGroups { - alerts := map[string]models.Alert{} + totalAlerts := 0 + for _, ag := range agList { + totalAlerts += len(ag.Alerts) + } + alerts := make(map[string]models.Alert, totalAlerts) + clear(alertStates) for _, ag := range agList { for _, alert := range ag.Alerts { // remove all alerts for receiver(s) that the user doesn't @@ -41,7 +46,7 @@ func DedupAlerts() []models.AlertGroup { config.Config.Receivers.Strip, config.Config.Receivers.CompiledKeepRegex, config.Config.Receivers.CompiledStripRegex, - alert.Receiver.Value(), + alert.Receiver, ) { continue } @@ -79,7 +84,7 @@ func DedupAlerts() []models.AlertGroup { } else { alerts[alertLFP] = alert // seed alert state slice - alertStates[alertLFP] = []models.UniqueString{alert.State} + alertStates[alertLFP] = []models.AlertState{alert.State} } } } @@ -111,6 +116,7 @@ func DedupAlerts() []models.AlertGroup { default: alert.State = models.AlertStateUnprocessed } + // sort Alertmanager instances for every alert sort.Slice(alert.Alertmanager, func(i, j int) bool { return alert.Alertmanager[i].Name < alert.Alertmanager[j].Name @@ -187,7 +193,7 @@ func DedupColors() models.LabelsColorMap { // DedupAutocomplete returns a list of autocomplete hints merged from all // Alertmanager upstreams func DedupAutocomplete() []models.Autocomplete { - uniqueAutocomplete := map[models.UniqueString]*models.Autocomplete{} + uniqueAutocomplete := map[string]*models.Autocomplete{} upstreams := GetAlertmanagers() @@ -238,14 +244,14 @@ func DedupKnownLabels() []string { // DedupKnownLabelValues returns a list of all known values for label $name func DedupKnownLabelValues(name string) []string { - dedupedValues := map[models.UniqueString]struct{}{} + dedupedValues := map[string]struct{}{} upstreams := GetAlertmanagers() for _, am := range upstreams { for _, ag := range am.Alerts() { for _, alert := range ag.Alerts { - if v := alert.Labels.Get(name); v != nil { - dedupedValues[v.Value] = struct{}{} + if v := alert.Labels.Get(name); v != "" { + dedupedValues[v] = struct{}{} } } } @@ -253,7 +259,7 @@ func DedupKnownLabelValues(name string) []string { flatValues := make([]string, 0, len(dedupedValues)) for key := range dedupedValues { - flatValues = append(flatValues, key.Value()) + flatValues = append(flatValues, key) } return flatValues } diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 6d1c7f705..15f359417 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -10,6 +10,8 @@ import ( "sync" "time" + "github.com/prometheus/prometheus/model/labels" + "github.com/prymitive/karma/internal/config" "github.com/prymitive/karma/internal/filters" "github.com/prymitive/karma/internal/mapper" @@ -207,7 +209,7 @@ func (am *Alertmanager) pullAlerts(version string) error { Msg("Deduplicating alert groups") uniqueGroups := map[string]models.AlertGroup{} uniqueAlerts := map[string]map[string]models.Alert{} - knownLabelsMap := map[models.UniqueString]struct{}{} + knownLabelsMap := map[string]struct{}{} for _, ag := range groups { agID := ag.LabelsFingerprint() if _, found := uniqueGroups[agID]; !found { @@ -225,9 +227,9 @@ func (am *Alertmanager) pullAlerts(version string) error { if _, found := uniqueAlerts[agID][alertCFP]; !found { uniqueAlerts[agID][alertCFP] = alert } - for _, l := range alert.Labels { + alert.Labels.Range(func(l labels.Label) { knownLabelsMap[l.Name] = struct{}{} - } + }) if name, hc := am.IsHealthCheckAlert(&alert); hc != nil { healthchecks[name] = HealthCheck{ @@ -240,7 +242,7 @@ func (am *Alertmanager) pullAlerts(version string) error { dedupedGroups := make([]models.AlertGroup, 0, len(uniqueGroups)) colors := models.LabelsColorMap{} - autocompleteMap := map[models.UniqueString]*models.Autocomplete{} + autocompleteMap := map[string]*models.Autocomplete{} expiredSilences := am.ExpiredSilences() log.Info(). @@ -294,14 +296,14 @@ func (am *Alertmanager) pullAlerts(version string) error { }, } - transform.ColorLabel(colors, "@receiver", alert.Receiver.Value()) + transform.ColorLabel(colors, "@receiver", alert.Receiver) for _, am := range alert.Alertmanager { transform.ColorLabel(colors, "@alertmanager", am.Name) transform.ColorLabel(colors, "@cluster", am.Cluster) } - for _, l := range alert.Labels { - transform.ColorLabel(colors, l.Name.Value(), l.Value.Value()) - } + alert.Labels.Range(func(l labels.Label) { + transform.ColorLabel(colors, l.Name, l.Value) + }) alert.UpdateFingerprints() alerts = append(alerts, alert) @@ -331,7 +333,7 @@ func (am *Alertmanager) pullAlerts(version string) error { knownLabels := make([]string, 0, len(knownLabelsMap)) for key := range knownLabelsMap { - knownLabels = append(knownLabels, key.Value()) + knownLabels = append(knownLabels, key) } am.lock.Lock() diff --git a/internal/alertmanager/models_test.go b/internal/alertmanager/models_test.go index 922c2ae23..9bbfe69ea 100644 --- a/internal/alertmanager/models_test.go +++ b/internal/alertmanager/models_test.go @@ -7,12 +7,11 @@ import ( "time" "github.com/jarcoal/httpmock" + "github.com/prometheus/prometheus/model/labels" + "github.com/rs/zerolog" "github.com/prymitive/karma/internal/config" - "github.com/prymitive/karma/internal/mapper/v017/models" internalModels "github.com/prymitive/karma/internal/models" - - "github.com/rs/zerolog" ) type uriTest struct { @@ -295,9 +294,7 @@ func TestIsHealthCheckAlertAllMatch(t *testing.T) { } alert := &internalModels.Alert{ - Labels: internalModels.Labels{ - {Name: internalModels.NewUniqueString("alertname"), Value: internalModels.NewUniqueString("Watchdog")}, - }, + Labels: labels.FromStrings("alertname", "Watchdog"), } name, hc := am.IsHealthCheckAlert(alert) if name != "prom1" { @@ -320,10 +317,7 @@ func TestIsHealthCheckAlertPartialMatch(t *testing.T) { } alert := &internalModels.Alert{ - Labels: internalModels.Labels{ - {Name: internalModels.NewUniqueString("alertname"), Value: internalModels.NewUniqueString("Watchdog")}, - {Name: internalModels.NewUniqueString("severity"), Value: internalModels.NewUniqueString("warning")}, - }, + Labels: labels.FromStrings("alertname", "Watchdog", "severity", "warning"), } name, hc := am.IsHealthCheckAlert(alert) if name != "" { @@ -346,9 +340,7 @@ func TestIsHealthCheckAlertNoMatch(t *testing.T) { } alert := &internalModels.Alert{ - Labels: internalModels.Labels{ - {Name: internalModels.NewUniqueString("alertname"), Value: internalModels.NewUniqueString("DiskFull")}, - }, + Labels: labels.FromStrings("alertname", "DiskFull"), } name, hc := am.IsHealthCheckAlert(alert) if name != "" { @@ -367,9 +359,7 @@ func TestIsHealthCheckAlertNoHealthchecks(t *testing.T) { } alert := &internalModels.Alert{ - Labels: internalModels.Labels{ - {Name: internalModels.NewUniqueString("alertname"), Value: internalModels.NewUniqueString("Watchdog")}, - }, + Labels: labels.FromStrings("alertname", "Watchdog"), } name, hc := am.IsHealthCheckAlert(alert) if name != "" { @@ -502,7 +492,7 @@ func TestExpiredSilences(t *testing.T) { } for _, ag := range alertGroups { for _, alert := range ag.Alerts { - if alert.State.Value() == models.AlertStatusStateActive { + if alert.State == internalModels.AlertStateActive { if len(alert.SilencedBy) < 1 { t.Errorf("Alert should include expired silence") } diff --git a/internal/filters/autocomplete.go b/internal/filters/autocomplete.go index a2f434d2c..b19e044d7 100644 --- a/internal/filters/autocomplete.go +++ b/internal/filters/autocomplete.go @@ -7,16 +7,13 @@ import ( type autocompleteFactory func(name string, operators []string, alerts []models.Alert) []models.Autocomplete func makeAC(value string, tokens []string) models.Autocomplete { - uTokens := make([]models.UniqueString, 0, len(tokens)) - for _, token := range tokens { - uTokens = append(uTokens, models.NewUniqueString(token)) - } - + t := make([]string, len(tokens)+1) + copy(t, tokens) + t[len(tokens)] = value acHint := models.Autocomplete{ - Value: models.NewUniqueString(value), - Tokens: uTokens, + Value: value, + Tokens: t, } - acHint.Tokens = append(acHint.Tokens, models.NewUniqueString(value)) return acHint } diff --git a/internal/filters/autocomplete_test.go b/internal/filters/autocomplete_test.go index 5fd1f3295..78972b205 100644 --- a/internal/filters/autocomplete_test.go +++ b/internal/filters/autocomplete_test.go @@ -6,10 +6,11 @@ import ( "strconv" "testing" + "github.com/google/go-cmp/cmp" + "github.com/prometheus/prometheus/model/labels" + "github.com/prymitive/karma/internal/filters" "github.com/prymitive/karma/internal/models" - - "github.com/google/go-cmp/cmp" ) type acTest struct { @@ -34,24 +35,18 @@ var acTests = []acTest{ { Alerts: []models.Alert{ { - State: models.AlertStateActive, - Labels: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - {Name: models.NewUniqueString("number"), Value: models.NewUniqueString("1")}, - }, - Receiver: models.NewUniqueString("default"), + State: models.AlertStateActive, + Labels: labels.FromStrings("foo", "bar", "number", "1"), + Receiver: "default", Alertmanager: []models.AlertmanagerInstance{ {Cluster: "cluster", Name: "am1"}, {Cluster: "cluster", Name: "am2"}, }, }, { - State: models.AlertStateSuppressed, - Labels: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar baz")}, - {Name: models.NewUniqueString("number"), Value: models.NewUniqueString("5")}, - }, - Receiver: models.NewUniqueString("not default"), + State: models.AlertStateSuppressed, + Labels: labels.FromStrings("foo", "bar baz", "number", "5"), + Receiver: "not default", Alertmanager: []models.AlertmanagerInstance{ {Cluster: "cluster", Name: "am1"}, { @@ -134,7 +129,7 @@ func TestBuildAutocomplete(t *testing.T) { for _, acTest := range acTests { result := make([]string, 0, len(acTest.Alerts)) for _, hint := range filters.BuildAutocomplete(acTest.Alerts) { - result = append(result, hint.Value.Value()) + result = append(result, hint.Value) } sort.Strings(result) @@ -151,12 +146,9 @@ func BenchmarkAutocomplete(b *testing.B) { alerts := make([]models.Alert, 0, n) for i := range n { alerts = append(alerts, models.Alert{ - State: models.AlertStateActive, - Labels: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString(fmt.Sprintf("xxx%d", i))}, - {Name: models.NewUniqueString("number"), Value: models.NewUniqueString(strconv.Itoa(i))}, - }, - Receiver: models.NewUniqueString(fmt.Sprintf("receiver-%d", i%1000)), + State: models.AlertStateActive, + Labels: labels.FromStrings("foo", fmt.Sprintf("xxx%d", i), "number", strconv.Itoa(i)), + Receiver: fmt.Sprintf("receiver-%d", i%1000), Alertmanager: []models.AlertmanagerInstance{ {Cluster: "cluster", Name: "am1"}, {Cluster: "cluster", Name: "am2"}, diff --git a/internal/filters/filter_alertmanager.go b/internal/filters/filter_alertmanager.go index 01363cf79..c1cff5cd0 100644 --- a/internal/filters/filter_alertmanager.go +++ b/internal/filters/filter_alertmanager.go @@ -8,8 +8,8 @@ import ( ) type alertmanagerInstanceFilter struct { - alertFilter value string + alertFilter } func (filter *alertmanagerInstanceFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -61,15 +61,17 @@ func alertmanagerInstanceAutocomplete(name string, operators []string, alerts [] switch operator { case equalOperator, notEqualOperator: token := name + operator + am.Name - hint := makeAC( - token, - []string{ - name, - strings.TrimPrefix(name, "@"), - name + operator, - }, - ) - tokens[token] = &hint + if _, ok := tokens[token]; !ok { + hint := makeAC( + token, + []string{ + name, + strings.TrimPrefix(name, "@"), + name + operator, + }, + ) + tokens[token] = &hint + } } } } diff --git a/internal/filters/filter_cluster.go b/internal/filters/filter_cluster.go index ed9b8a094..a9bca47f7 100644 --- a/internal/filters/filter_cluster.go +++ b/internal/filters/filter_cluster.go @@ -8,8 +8,8 @@ import ( ) type alertmanagerClusterFilter struct { - alertFilter value string + alertFilter } func (filter *alertmanagerClusterFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -61,15 +61,17 @@ func alertmanagerClusterAutocomplete(name string, operators []string, alerts []m switch operator { case equalOperator, notEqualOperator: token := name + operator + am.Cluster - hint := makeAC( - token, - []string{ - name, - strings.TrimPrefix(name, "@"), - name + operator, - }, - ) - tokens[token] = &hint + if _, ok := tokens[token]; !ok { + hint := makeAC( + token, + []string{ + name, + strings.TrimPrefix(name, "@"), + name + operator, + }, + ) + tokens[token] = &hint + } } } } diff --git a/internal/filters/filter_fingerprint.go b/internal/filters/filter_fingerprint.go index 4477abf54..f44681101 100644 --- a/internal/filters/filter_fingerprint.go +++ b/internal/filters/filter_fingerprint.go @@ -7,8 +7,8 @@ import ( ) type fingerprintFilter struct { - alertFilter value string + alertFilter } func (filter *fingerprintFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { diff --git a/internal/filters/filter_fuzzy.go b/internal/filters/filter_fuzzy.go index 1d14f049b..4738ab2cf 100644 --- a/internal/filters/filter_fuzzy.go +++ b/internal/filters/filter_fuzzy.go @@ -4,12 +4,14 @@ import ( "fmt" "regexp" + "github.com/prometheus/prometheus/model/labels" + "github.com/prymitive/karma/internal/models" ) type fuzzyFilter struct { - alertFilter value *regexp.Regexp + alertFilter } func (filter *fuzzyFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -32,17 +34,21 @@ func (filter *fuzzyFilter) GetValue() string { func (filter *fuzzyFilter) Match(alert *models.Alert, _ int) bool { if filter.IsValid { for _, val := range alert.Annotations { - if filter.value.MatchString(val.Value.Value()) { + if filter.value.MatchString(val.Value) { filter.Hits++ return true } } - for _, l := range alert.Labels { - if filter.value.MatchString(l.Value.Value()) { - filter.Hits++ - return true + var labelMatch bool + alert.Labels.Range(func(l labels.Label) { + if filter.value.MatchString(l.Value) { + labelMatch = true } + }) + if labelMatch { + filter.Hits++ + return true } for _, silenceID := range alert.SilencedBy { diff --git a/internal/filters/filter_inhibited_by.go b/internal/filters/filter_inhibited_by.go index 6098a64f2..5c85fd0e5 100644 --- a/internal/filters/filter_inhibited_by.go +++ b/internal/filters/filter_inhibited_by.go @@ -8,8 +8,8 @@ import ( ) type inhibitedByFilter struct { - alertFilter value string + alertFilter } func (filter *inhibitedByFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -70,13 +70,15 @@ func inhibitedByAutocomplete(name string, operators []string, alerts []models.Al for _, silenceID := range am.InhibitedBy { for _, operator := range operators { token := name + operator + silenceID - hint := makeAC(token, []string{ - name, - strings.TrimPrefix(name, "@"), - name + operator, - silenceID, - }) - tokens[token] = &hint + if _, ok := tokens[token]; !ok { + hint := makeAC(token, []string{ + name, + strings.TrimPrefix(name, "@"), + name + operator, + silenceID, + }) + tokens[token] = &hint + } } } } diff --git a/internal/filters/filter_label.go b/internal/filters/filter_label.go index 7c5cdca3b..e08faa12b 100644 --- a/internal/filters/filter_label.go +++ b/internal/filters/filter_label.go @@ -5,12 +5,14 @@ import ( "strconv" "strings" + "github.com/prometheus/prometheus/model/labels" + "github.com/prymitive/karma/internal/models" ) type labelFilter struct { - alertFilter value string + alertFilter } func (filter *labelFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -29,7 +31,7 @@ func (filter *labelFilter) GetValue() string { func (filter *labelFilter) Match(alert *models.Alert, _ int) bool { if filter.IsValid { - isMatch := filter.Matcher.Compare(alert.Labels.GetValue(filter.Matched), filter.value) + isMatch := filter.Matcher.Compare(alert.Labels.Get(filter.Matched), filter.value) if isMatch { filter.Hits++ } @@ -47,53 +49,59 @@ func newLabelFilter() FilterT { func labelAutocomplete(_ string, operators []string, alerts []models.Alert) []models.Autocomplete { tokens := map[string]*models.Autocomplete{} for _, alert := range alerts { - for _, l := range alert.Labels { + alert.Labels.Range(func(l labels.Label) { for _, operator := range operators { switch operator { case equalOperator, notEqualOperator: - token := l.Name.Value() + operator + l.Value.Value() - hint := makeAC( - token, - []string{ - l.Name.Value(), - l.Name.Value() + operator, - l.Value.Value(), - }, - ) - tokens[token] = &hint + token := l.Name + operator + l.Value + if _, ok := tokens[token]; !ok { + hint := makeAC( + token, + []string{ + l.Name, + l.Name + operator, + l.Value, + }, + ) + tokens[token] = &hint + } case regexpOperator, negativeRegexOperator: - substrings := strings.Split(l.Value.Value(), " ") + substrings := strings.Split(l.Value, " ") if len(substrings) > 1 { for _, substring := range substrings { - token := l.Name.Value() + operator + substring + token := l.Name + operator + substring + if _, ok := tokens[token]; !ok { + hint := makeAC( + token, + []string{ + l.Name, + l.Name + operator, + l.Value, + substring, + }, + ) + tokens[token] = &hint + } + } + } + case moreThanOperator, lessThanOperator: + if _, err := strconv.Atoi(l.Value); err == nil { + token := l.Name + operator + l.Value + if _, ok := tokens[token]; !ok { hint := makeAC( token, []string{ - l.Name.Value(), - l.Name.Value() + operator, - l.Value.Value(), - substring, + l.Name, + l.Name + operator, + l.Value, }, ) tokens[token] = &hint } } - case moreThanOperator, lessThanOperator: - if _, err := strconv.Atoi(l.Value.Value()); err == nil { - token := l.Name.Value() + operator + l.Value.Value() - hint := makeAC( - token, - []string{ - l.Name.Value(), - l.Name.Value() + operator, - l.Value.Value(), - }, - ) - tokens[token] = &hint - } } } - } + }) } acData := make([]models.Autocomplete, 0, len(tokens)) for _, token := range tokens { diff --git a/internal/filters/filter_receiver.go b/internal/filters/filter_receiver.go index dbde1fc9a..21da138c9 100644 --- a/internal/filters/filter_receiver.go +++ b/internal/filters/filter_receiver.go @@ -8,8 +8,8 @@ import ( ) type receiverFilter struct { - alertFilter value string + alertFilter } func (filter *receiverFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -28,7 +28,7 @@ func (filter *receiverFilter) GetValue() string { func (filter *receiverFilter) Match(alert *models.Alert, _ int) bool { if filter.IsValid { - isMatch := filter.Matcher.Compare(alert.Receiver.Value(), filter.value) + isMatch := filter.Matcher.Compare(alert.Receiver, filter.value) if isMatch { filter.Hits++ } @@ -46,35 +46,39 @@ func newreceiverFilter() FilterT { func receiverAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete { tokens := map[string]*models.Autocomplete{} for _, alert := range alerts { - if alert.Receiver.Value() != "" { + if alert.Receiver != "" { for _, operator := range operators { switch operator { case equalOperator, notEqualOperator: - token := name + operator + alert.Receiver.Value() - hint := makeAC( - token, - []string{ - name, - strings.TrimPrefix(name, "@"), - name + operator, - }, - ) - tokens[token] = &hint + token := name + operator + alert.Receiver + if _, ok := tokens[token]; !ok { + hint := makeAC( + token, + []string{ + name, + strings.TrimPrefix(name, "@"), + name + operator, + }, + ) + tokens[token] = &hint + } case regexpOperator, negativeRegexOperator: - substrings := strings.Split(alert.Receiver.Value(), " ") + substrings := strings.Split(alert.Receiver, " ") if len(substrings) > 1 { for _, substring := range substrings { token := name + operator + substring - hint := makeAC( - token, - []string{ - name, - strings.TrimPrefix(name, "@"), - name + operator, - substring, - }, - ) - tokens[token] = &hint + if _, ok := tokens[token]; !ok { + hint := makeAC( + token, + []string{ + name, + strings.TrimPrefix(name, "@"), + name + operator, + substring, + }, + ) + tokens[token] = &hint + } } } } diff --git a/internal/filters/filter_silence_author.go b/internal/filters/filter_silence_author.go index 037b1e920..4774068c9 100644 --- a/internal/filters/filter_silence_author.go +++ b/internal/filters/filter_silence_author.go @@ -8,8 +8,8 @@ import ( ) type silenceAuthorFilter struct { - alertFilter value string + alertFilter } func (filter *silenceAuthorFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -79,13 +79,15 @@ func silenceAuthorAutocomplete(name string, operators []string, alerts []models. if found { for _, operator := range operators { token := name + operator + silence.CreatedBy - hint := makeAC(token, []string{ - name, - strings.TrimPrefix(name, "@"), - name + operator, - silence.CreatedBy, - }) - tokens[token] = &hint + if _, ok := tokens[token]; !ok { + hint := makeAC(token, []string{ + name, + strings.TrimPrefix(name, "@"), + name + operator, + silence.CreatedBy, + }) + tokens[token] = &hint + } } } } diff --git a/internal/filters/filter_silence_ticket.go b/internal/filters/filter_silence_ticket.go index 7e448af58..24964d5dc 100644 --- a/internal/filters/filter_silence_ticket.go +++ b/internal/filters/filter_silence_ticket.go @@ -8,8 +8,8 @@ import ( ) type silenceTicketFilter struct { - alertFilter value string + alertFilter } func (filter *silenceTicketFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -79,13 +79,15 @@ func silenceTicketIDAutocomplete(name string, operators []string, alerts []model if found && silence.TicketID != "" { for _, operator := range operators { token := name + operator + silence.TicketID - hint := makeAC(token, []string{ - name, - strings.TrimPrefix(name, "@"), - name + operator, - silence.TicketID, - }) - tokens[token] = &hint + if _, ok := tokens[token]; !ok { + hint := makeAC(token, []string{ + name, + strings.TrimPrefix(name, "@"), + name + operator, + silence.TicketID, + }) + tokens[token] = &hint + } } } } diff --git a/internal/filters/filter_silenced_by.go b/internal/filters/filter_silenced_by.go index 793fb93ba..7e5863d42 100644 --- a/internal/filters/filter_silenced_by.go +++ b/internal/filters/filter_silenced_by.go @@ -8,8 +8,8 @@ import ( ) type silenceIDFilter struct { - alertFilter value string + alertFilter } func (filter *silenceIDFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -70,13 +70,15 @@ func silenceIDAutocomplete(name string, operators []string, alerts []models.Aler for _, silenceID := range am.SilencedBy { for _, operator := range operators { token := name + operator + silenceID - hint := makeAC(token, []string{ - name, - strings.TrimPrefix(name, "@"), - name + operator, - silenceID, - }) - tokens[token] = &hint + if _, ok := tokens[token]; !ok { + hint := makeAC(token, []string{ + name, + strings.TrimPrefix(name, "@"), + name + operator, + silenceID, + }) + tokens[token] = &hint + } } } } diff --git a/internal/filters/filter_state.go b/internal/filters/filter_state.go index a994463d3..fd99fc767 100644 --- a/internal/filters/filter_state.go +++ b/internal/filters/filter_state.go @@ -2,15 +2,14 @@ package filters import ( "fmt" - "slices" "strings" "github.com/prymitive/karma/internal/models" ) type stateFilter struct { - alertFilter value string + alertFilter } func (filter *stateFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { @@ -21,7 +20,7 @@ func (filter *stateFilter) init(name string, matcher *matcherT, rawText string, filter.RawText = rawText filter.IsValid = isValid filter.value = value - if !slices.Contains(models.AlertStateList, models.NewUniqueString(value)) { + if _, ok := models.AlertStateFromString(value); !ok { filter.IsValid = false } } @@ -34,7 +33,7 @@ func (filter *stateFilter) Match(alert *models.Alert, _ int) bool { if filter.IsValid { var isMatch bool for _, am := range alert.Alertmanager { - if filter.Matcher.Compare(am.State.Value(), filter.value) { + if filter.Matcher.Compare(am.State.String(), filter.value) { isMatch = true } } @@ -48,7 +47,7 @@ func (filter *stateFilter) Match(alert *models.Alert, _ int) bool { } func (filter *stateFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - return filter.Matcher.Compare(am.State.Value(), filter.value) + return filter.Matcher.Compare(am.State.String(), filter.value) } func newStateFilter() FilterT { @@ -58,18 +57,26 @@ func newStateFilter() FilterT { } func stateAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete { - tokens := make([]models.Autocomplete, 0, len(operators)) + tokens := map[string]*models.Autocomplete{} for _, operator := range operators { for _, alert := range alerts { - tokens = append(tokens, makeAC( - name+operator+alert.State.Value(), - []string{ - name, - strings.TrimPrefix(name, "@"), - name + operator, - }, - )) + token := name + operator + alert.State.String() + if _, ok := tokens[token]; !ok { + hint := makeAC( + token, + []string{ + name, + strings.TrimPrefix(name, "@"), + name + operator, + }, + ) + tokens[token] = &hint + } } } - return tokens + acData := make([]models.Autocomplete, 0, len(tokens)) + for _, token := range tokens { + acData = append(acData, *token) + } + return acData } diff --git a/internal/filters/filter_test.go b/internal/filters/filter_test.go index b111a5f7c..2e6842522 100644 --- a/internal/filters/filter_test.go +++ b/internal/filters/filter_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" + "github.com/prometheus/prometheus/model/labels" + "github.com/rs/zerolog" + "github.com/prymitive/karma/internal/alertmanager" "github.com/prymitive/karma/internal/filters" "github.com/prymitive/karma/internal/models" - - "github.com/rs/zerolog" ) type filterTest struct { @@ -745,7 +746,7 @@ var tests = []filterTest{ { Expression: "node=vps1", IsValid: true, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("node"), Value: models.NewUniqueString("vps1")}}}, + Alert: models.Alert{Labels: labels.FromStrings("node", "vps1")}, IsMatch: true, }, { @@ -759,68 +760,68 @@ var tests = []filterTest{ { Expression: "node!=vps1", IsValid: true, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("node"), Value: models.NewUniqueString("vps1")}}}, + Alert: models.Alert{Labels: labels.FromStrings("node", "vps1")}, IsMatch: false, }, { Expression: "node!=vps1", IsValid: true, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("node"), Value: models.NewUniqueString("vps2")}}}, + Alert: models.Alert{Labels: labels.FromStrings("node", "vps2")}, IsMatch: true, }, { Expression: "node=~vps", IsValid: true, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("node"), Value: models.NewUniqueString("vps1")}}}, + Alert: models.Alert{Labels: labels.FromStrings("node", "vps1")}, IsMatch: true, }, { Expression: "node!~vps", IsValid: true, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("node"), Value: models.NewUniqueString("vps1")}}}, + Alert: models.Alert{Labels: labels.FromStrings("node", "vps1")}, IsMatch: false, }, { Expression: "node!~abc", IsValid: true, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("node"), Value: models.NewUniqueString("vps1")}}}, + Alert: models.Alert{Labels: labels.FromStrings("node", "vps1")}, IsMatch: true, }, { Expression: "node!~", IsValid: false, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("node"), Value: models.NewUniqueString("vps1")}}}, + Alert: models.Alert{Labels: labels.FromStrings("node", "vps1")}, IsMatch: false, }, { Expression: "node=", IsValid: false, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("node"), Value: models.NewUniqueString("vps1")}}}, + Alert: models.Alert{Labels: labels.FromStrings("node", "vps1")}, IsMatch: false, }, { Expression: "node===", IsValid: false, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("node"), Value: models.NewUniqueString("vps1")}}}, + Alert: models.Alert{Labels: labels.FromStrings("node", "vps1")}, IsMatch: false, }, { Expression: "abc", IsValid: true, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("key"), Value: models.NewUniqueString("abc")}}}, + Alert: models.Alert{Labels: labels.FromStrings("key", "abc")}, IsMatch: true, }, { Expression: "abc", IsValid: true, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("key"), Value: models.NewUniqueString("XXXabcx")}}}, + Alert: models.Alert{Labels: labels.FromStrings("key", "XXXabcx")}, IsMatch: true, }, { Expression: "abc", IsValid: true, - Alert: models.Alert{Labels: models.Labels{{Name: models.NewUniqueString("abc"), Value: models.NewUniqueString("xxxab")}}}, + Alert: models.Alert{Labels: labels.FromStrings("abc", "xxxab")}, IsMatch: false, }, { @@ -828,7 +829,7 @@ var tests = []filterTest{ IsValid: true, Alert: models.Alert{ Annotations: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("key"), Value: models.NewUniqueString("abc")}, + models.Annotation{Name: "key", Value: "abc"}, }, }, IsMatch: true, @@ -838,7 +839,7 @@ var tests = []filterTest{ IsValid: true, Alert: models.Alert{ Annotations: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("key"), Value: models.NewUniqueString("ccc abc")}, + models.Annotation{Name: "key", Value: "ccc abc"}, }, }, IsMatch: true, @@ -848,7 +849,7 @@ var tests = []filterTest{ IsValid: true, Alert: models.Alert{ Annotations: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("abc"), Value: models.NewUniqueString("zzz")}, + models.Annotation{Name: "abc", Value: "zzz"}, }, }, IsMatch: false, @@ -977,7 +978,7 @@ var tests = []filterTest{ Expression: "@receiver=by-name", IsValid: true, Alert: models.Alert{ - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, IsMatch: true, }, @@ -993,7 +994,7 @@ var tests = []filterTest{ Expression: "@cluster=HA", IsValid: true, Alert: models.Alert{ - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, IsMatch: true, IsAlertmanagerMatch: true, @@ -1002,7 +1003,7 @@ var tests = []filterTest{ Expression: "@cluster!=foo", IsValid: true, Alert: models.Alert{ - Receiver: models.NewUniqueString("by-name"), + Receiver: "by-name", }, IsMatch: true, IsAlertmanagerMatch: true, @@ -1015,7 +1016,7 @@ var tests = []filterTest{ Expression: "@receiver=by-name", IsValid: true, Alert: models.Alert{ - Receiver: models.NewUniqueString("by-not-name"), + Receiver: "by-not-name", }, IsMatch: false, }, @@ -1023,7 +1024,7 @@ var tests = []filterTest{ Expression: "@receiver=~name", IsValid: true, Alert: models.Alert{ - Receiver: models.NewUniqueString("by-not-name"), + Receiver: "by-not-name", }, IsMatch: true, }, diff --git a/internal/mapper/v017/api.go b/internal/mapper/v017/api.go index 677fbcdaa..74b281692 100644 --- a/internal/mapper/v017/api.go +++ b/internal/mapper/v017/api.go @@ -4,7 +4,6 @@ import ( "net/http" "net/url" "path" - "slices" "sort" "time" @@ -49,30 +48,22 @@ func groups(c *client.AlertmanagerAPI, timeout time.Duration) ([]models.AlertGro ret := make([]models.AlertGroup, 0, len(groups.Payload)) for _, group := range groups.Payload { - ls := make(models.Labels, 0, len(group.Labels)) - for k, v := range group.Labels { - ls = ls.Set(k, v) - } - slices.SortFunc(ls, models.CompareLabels) + ls := models.LabelsFromMap(group.Labels) g := models.AlertGroup{ - Receiver: models.NewUniqueString(*group.Receiver.Name), + Receiver: *group.Receiver.Name, Labels: ls, Alerts: make(models.AlertList, 0, len(group.Alerts)), } for _, alert := range group.Alerts { - ls := make(models.Labels, 0, len(alert.Labels)) - for k, v := range alert.Labels { - ls = ls.Set(k, v) - } - slices.SortFunc(ls, models.CompareLabels) + ls := models.LabelsFromMap(alert.Labels) a := models.Alert{ Fingerprint: *alert.Fingerprint, - Receiver: models.NewUniqueString(*group.Receiver.Name), + Receiver: *group.Receiver.Name, Annotations: models.AnnotationsFromMap(alert.Annotations), Labels: ls, StartsAt: time.Time(*alert.StartsAt), GeneratorURL: alert.GeneratorURL.String(), - State: models.NewUniqueString(*alert.Status.State), + State: models.ParseAlertState(*alert.Status.State), InhibitedBy: alert.Status.InhibitedBy, SilencedBy: alert.Status.SilencedBy, } diff --git a/internal/models/__snapshots__/models.snapshot b/internal/models/__snapshots__/models.snapshot index 35ec9f1d7..a3045a7bd 100644 --- a/internal/models/__snapshots__/models.snapshot +++ b/internal/models/__snapshots__/models.snapshot @@ -29,163 +29,8 @@ }, "alertmanagerCount": null, "stateCount": null, - "receiver": {}, + "receiver": "default", "id": "", - "labels": [ - { - "name": "alertname", - "value": "FakeAlert" - } - ], - "alerts": [ - { - "startsAt": "0001-01-01T00:00:00Z", - "state": "suppressed", - "receiver": "default", - "id": "", - "annotations": [ - { - "name": "foo", - "value": "bar", - "visible": false, - "isLink": false, - "isAction": false - } - ], - "labels": [ - { - "name": "job", - "value": "node_exporter" - }, - { - "name": "instance", - "value": "1" - } - ], - "alertmanager": [ - { - "fingerprint": "1", - "name": "am1", - "cluster": "fakeCluster", - "state": "suppressed", - "startsAt": "0001-01-01T00:00:00Z", - "source": "https://prom.example.com/graph?foo", - "silencedBy": [ - "fakeSilence1", - "fakeSilence2" - ], - "inhibitedBy": null - }, - { - "fingerprint": "2", - "name": "am2", - "cluster": "fakeCluster", - "state": "suppressed", - "startsAt": "0001-01-01T00:00:00Z", - "source": "https://prom.example.com/subdir/graph?bar", - "silencedBy": [ - "fakeSilence1", - "fakeSilence2" - ], - "inhibitedBy": null - } - ] - }, - { - "startsAt": "0001-01-01T00:00:00Z", - "state": "active", - "receiver": "default", - "id": "", - "annotations": [], - "labels": [ - { - "name": "job", - "value": "node_exporter" - }, - { - "name": "instance", - "value": "2" - } - ], - "alertmanager": [ - { - "fingerprint": "1", - "name": "am1", - "cluster": "fakeCluster", - "state": "active", - "startsAt": "0001-01-01T00:00:00Z", - "source": "https://am.example.com", - "silencedBy": [ - "fakeSilence1", - "fakeSilence2" - ], - "inhibitedBy": null - }, - { - "fingerprint": "1", - "name": "am2", - "cluster": "fakeCluster", - "state": "active", - "startsAt": "0001-01-01T00:00:00Z", - "source": "https://am.example.com", - "silencedBy": [ - "fakeSilence1", - "fakeSilence2" - ], - "inhibitedBy": null - } - ] - }, - { - "startsAt": "0001-01-01T00:00:00Z", - "state": "suppressed", - "receiver": "default", - "id": "", - "annotations": [], - "labels": [ - { - "name": "job", - "value": "blackbox" - }, - { - "name": "instance", - "value": "3" - }, - { - "name": "extra", - "value": "ignore" - } - ], - "alertmanager": [ - { - "fingerprint": "1", - "name": "am1", - "cluster": "fakeCluster", - "state": "suppressed", - "startsAt": "0001-01-01T00:00:00Z", - "source": "https://am.example.com/graph", - "silencedBy": [ - "fakeSilence1", - "fakeSilence2" - ], - "inhibitedBy": null - }, - { - "fingerprint": "1", - "name": "am2", - "cluster": "fakeCluster", - "state": "suppressed", - "startsAt": "0001-01-01T00:00:00Z", - "source": "https://am.example.com/graph", - "silencedBy": [ - "fakeSilence1", - "fakeSilence2" - ], - "inhibitedBy": null - } - ] - } - ], "shared": { "annotations": [ { @@ -213,5 +58,160 @@ "fakeCluster" ] }, - "totalAlerts": 0 + "labels": [ + { + "name": "alertname", + "value": "FakeAlert" + } + ], + "alerts": [ + { + "startsAt": "0001-01-01T00:00:00Z", + "state": "suppressed", + "receiver": "default", + "id": "", + "annotations": [ + { + "name": "foo", + "value": "bar", + "visible": false, + "isLink": false, + "isAction": false + } + ], + "labels": [ + { + "name": "instance", + "value": "1" + }, + { + "name": "job", + "value": "node_exporter" + } + ], + "alertmanager": [ + { + "startsAt": "0001-01-01T00:00:00Z", + "fingerprint": "1", + "name": "am1", + "cluster": "fakeCluster", + "source": "https://prom.example.com/graph?foo", + "silencedBy": [ + "fakeSilence1", + "fakeSilence2" + ], + "inhibitedBy": null, + "state": "suppressed" + }, + { + "startsAt": "0001-01-01T00:00:00Z", + "fingerprint": "2", + "name": "am2", + "cluster": "fakeCluster", + "source": "https://prom.example.com/subdir/graph?bar", + "silencedBy": [ + "fakeSilence1", + "fakeSilence2" + ], + "inhibitedBy": null, + "state": "suppressed" + } + ] + }, + { + "startsAt": "0001-01-01T00:00:00Z", + "state": "active", + "receiver": "default", + "id": "", + "annotations": [], + "labels": [ + { + "name": "instance", + "value": "2" + }, + { + "name": "job", + "value": "node_exporter" + } + ], + "alertmanager": [ + { + "startsAt": "0001-01-01T00:00:00Z", + "fingerprint": "1", + "name": "am1", + "cluster": "fakeCluster", + "source": "https://am.example.com", + "silencedBy": [ + "fakeSilence1", + "fakeSilence2" + ], + "inhibitedBy": null, + "state": "active" + }, + { + "startsAt": "0001-01-01T00:00:00Z", + "fingerprint": "1", + "name": "am2", + "cluster": "fakeCluster", + "source": "https://am.example.com", + "silencedBy": [ + "fakeSilence1", + "fakeSilence2" + ], + "inhibitedBy": null, + "state": "active" + } + ] + }, + { + "startsAt": "0001-01-01T00:00:00Z", + "state": "suppressed", + "receiver": "default", + "id": "", + "annotations": [], + "labels": [ + { + "name": "extra", + "value": "ignore" + }, + { + "name": "instance", + "value": "3" + }, + { + "name": "job", + "value": "blackbox" + } + ], + "alertmanager": [ + { + "startsAt": "0001-01-01T00:00:00Z", + "fingerprint": "1", + "name": "am1", + "cluster": "fakeCluster", + "source": "https://am.example.com/graph", + "silencedBy": [ + "fakeSilence1", + "fakeSilence2" + ], + "inhibitedBy": null, + "state": "suppressed" + }, + { + "startsAt": "0001-01-01T00:00:00Z", + "fingerprint": "1", + "name": "am2", + "cluster": "fakeCluster", + "source": "https://am.example.com/graph", + "silencedBy": [ + "fakeSilence1", + "fakeSilence2" + ], + "inhibitedBy": null, + "state": "suppressed" + } + ] + } + ], + "totalAlerts": 3 } \ No newline at end of file diff --git a/internal/models/alert.go b/internal/models/alert.go index 0f551c851..737cb2276 100644 --- a/internal/models/alert.go +++ b/internal/models/alert.go @@ -3,75 +3,141 @@ package models import ( "cmp" "encoding/json" + "slices" "strconv" "time" - "unique" "github.com/cespare/xxhash/v2" "github.com/fvbommel/sortorder" + "github.com/prometheus/prometheus/model/labels" "github.com/prymitive/karma/internal/config" ) -// AlertStateUnprocessed means that Alertmanager notify didn't yet process it -// and AM doesn't know if alert is active or suppressed -var AlertStateUnprocessed = NewUniqueString("unprocessed") +// AlertState encodes the state of an alert as a compact uint8. +type AlertState uint8 -// AlertStateActive is the state in which we know that the alert should fire -var AlertStateActive = NewUniqueString("active") - -// AlertStateSuppressed means that we know that alert is silenced or inhibited -var AlertStateSuppressed = NewUniqueString("suppressed") +const ( + // AlertStateUnprocessed means that Alertmanager notify didn't yet process it + // and AM doesn't know if alert is active or suppressed + AlertStateUnprocessed AlertState = iota + // AlertStateActive is the state in which we know that the alert should fire + AlertStateActive + // AlertStateSuppressed means that we know that alert is silenced or inhibited + AlertStateSuppressed +) // AlertStateList exports all alert states so other packages can get this list -var AlertStateList = []UniqueString{ +var AlertStateList = []AlertState{ AlertStateUnprocessed, AlertStateActive, AlertStateSuppressed, } -type UniqueString struct { - unique.Handle[string] +var alertStateToString = [3]string{"unprocessed", "active", "suppressed"} + +var alertStateFromString = map[string]AlertState{ + "unprocessed": AlertStateUnprocessed, + "active": AlertStateActive, + "suppressed": AlertStateSuppressed, } -func NewUniqueString(s string) UniqueString { - return UniqueString{Handle: unique.Make(s)} +func (s AlertState) String() string { + if int(s) < len(alertStateToString) { + return alertStateToString[s] + } + return "unprocessed" } -func (us *UniqueString) MarshalJSON() ([]byte, error) { - return json.Marshal(us.Value()) +func (s AlertState) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) } -func (us *UniqueString) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err != nil { +func (s *AlertState) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { return err } - us.Handle = unique.Make(s) + *s = ParseAlertState(str) return nil } -type Label struct { - Name UniqueString `json:"name"` - Value UniqueString `json:"value"` +// MarshalText implements encoding.TextMarshaler so AlertState can be used as +// a JSON map key. +func (s AlertState) MarshalText() ([]byte, error) { + return []byte(s.String()), nil } -type Labels []Label +// UnmarshalText implements encoding.TextUnmarshaler so AlertState can be used +// as a JSON map key. +func (s *AlertState) UnmarshalText(data []byte) error { + *s = ParseAlertState(string(data)) + return nil +} -func (ls Labels) Map() map[string]string { - m := make(map[string]string, len(ls)) - for _, l := range ls { - m[l.Name.Value()] = l.Value.Value() +// AlertStateFromString looks up an AlertState by its string representation. +// Returns the state and true if found, or (AlertStateUnprocessed, false) otherwise. +func AlertStateFromString(s string) (AlertState, bool) { + v, ok := alertStateFromString[s] + return v, ok +} + +// ParseAlertState converts a string to an AlertState. +func ParseAlertState(s string) AlertState { + if v, ok := alertStateFromString[s]; ok { + return v } - return m + return AlertStateUnprocessed } -func CompareLabels(a, b Label) int { +// OrderedLabel mirrors labels.Label for JSON serialization in the +// [{"name":"...","value":"..."}] format expected by the frontend. +type OrderedLabel struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// OrderedLabels is a slice of OrderedLabel used for JSON serialization. +// It preserves the display order configured via config.Config.Labels.Order. +type OrderedLabels []OrderedLabel + +func (ol OrderedLabels) Get(name string) string { + for _, l := range ol { + if l.Name == name { + return l.Value + } + } + return "" +} + +// LabelsToOrderedLabels converts prometheus Labels to OrderedLabels sorted +// by the configured display order. +func LabelsToOrderedLabels(ls labels.Labels) OrderedLabels { + dl := make(OrderedLabels, 0, ls.Len()) + ls.Range(func(l labels.Label) { + dl = append(dl, OrderedLabel{Name: l.Name, Value: l.Value}) + }) + slices.SortFunc(dl, CompareOrderedLabels) + return dl +} + +// orderedLabelsToLabels converts OrderedLabels back to prometheus Labels. +func orderedLabelsToLabels(ol OrderedLabels) labels.Labels { + s := make([]string, 0, len(ol)*2) + for _, l := range ol { + s = append(s, l.Name, l.Value) + } + return labels.FromStrings(s...) +} + +// CompareOrderedLabels sorts display labels by the configured label order, +// then by name, then by value using natural sort. +func CompareOrderedLabels(a, b OrderedLabel) int { ai, bi := -1, -1 for index, name := range config.Config.Labels.Order { - if a.Name.Value() == name { + if a.Name == name { ai = index - } else if b.Name.Value() == name { + } else if b.Name == name { bi = index } if ai >= 0 && bi >= 0 { @@ -81,54 +147,38 @@ func CompareLabels(a, b Label) int { if ai != bi { return cmp.Compare(bi, ai) } - if a.Name.Value() == b.Name.Value() { - if sortorder.NaturalLess(a.Value.Value(), b.Value.Value()) { + if a.Name == b.Name { + if sortorder.NaturalLess(a.Value, b.Value) { return -1 } - if sortorder.NaturalLess(b.Value.Value(), a.Value.Value()) { + if sortorder.NaturalLess(b.Value, a.Value) { return 1 } return 0 } - if sortorder.NaturalLess(a.Name.Value(), b.Name.Value()) { + if sortorder.NaturalLess(a.Name, b.Name) { return -1 } - if sortorder.NaturalLess(b.Name.Value(), a.Name.Value()) { + if sortorder.NaturalLess(b.Name, a.Name) { return 1 } return 0 } -func (ls Labels) Get(name string) *Label { - for i, l := range ls { - if l.Name.Value() == name { - return &ls[i] - } - } - return nil +// LabelsFromMap creates a Labels from a map, adding only keys not already present. +func LabelsFromMap(m map[string]string) labels.Labels { + return labels.FromMap(m) } -func (ls Labels) GetValue(name string) string { - for _, l := range ls { - if l.Name.Value() == name { - return l.Value.Value() - } - } - return "" -} - -func (ls Labels) Add(l Label) Labels { - if ls.Get(l.Name.Value()) != nil { +// LabelsSetIfMissing returns a new Labels with the given name/value added only if +// the name is not already present. +func LabelsSetIfMissing(ls labels.Labels, name, value string) labels.Labels { + if ls.Has(name) { return ls } - return append(ls, l) -} - -func (ls Labels) Set(name, value string) Labels { - if ls.Get(name) != nil { - return ls - } - return append(ls, Label{Name: NewUniqueString(name), Value: NewUniqueString(value)}) + b := labels.NewBuilder(ls) + b.Set(name, value) + return b.Labels() } // Alert is vanilla alert + some additional attributes @@ -137,38 +187,71 @@ func (ls Labels) Set(name, value string) Labels { // it's pulled out of annotation map and returned under links field, // karma UI used this to show links differently than other annotations type Alert struct { - StartsAt time.Time `json:"startsAt"` - State UniqueString `json:"state"` - Fingerprint string `json:"-"` - GeneratorURL string `json:"-"` - Receiver UniqueString `json:"receiver"` - LabelsFP string `json:"id"` + StartsAt time.Time `json:"startsAt"` + Fingerprint string `json:"-"` + GeneratorURL string `json:"-"` + Receiver string `json:"receiver"` + LabelsFP string `json:"id"` contentFP string + Labels labels.Labels `json:"-"` Annotations Annotations `json:"annotations"` - Labels Labels `json:"labels"` SilencedBy []string `json:"-"` InhibitedBy []string `json:"-"` Alertmanager []AlertmanagerInstance `json:"alertmanager"` + State AlertState `json:"state"` +} + +// APIAlert is the JSON-serializable representation of Alert. +// Labels are converted to OrderedLabels for the frontend. +type APIAlert struct { + StartsAt time.Time `json:"startsAt"` + State string `json:"state"` + Receiver string `json:"receiver"` + LabelsFP string `json:"id"` + Annotations Annotations `json:"annotations"` + Labels OrderedLabels `json:"labels"` + Alertmanager []AlertmanagerInstance `json:"alertmanager"` +} + +func (a Alert) MarshalJSON() ([]byte, error) { + return json.Marshal(APIAlert{ + StartsAt: a.StartsAt, + State: a.State.String(), + Receiver: a.Receiver, + LabelsFP: a.LabelsFP, + Annotations: a.Annotations, + Labels: LabelsToOrderedLabels(a.Labels), + Alertmanager: a.Alertmanager, + }) +} + +func (a *Alert) UnmarshalJSON(data []byte) error { + var j APIAlert + if err := json.Unmarshal(data, &j); err != nil { + return err + } + a.StartsAt = j.StartsAt + a.State = ParseAlertState(j.State) + a.Receiver = j.Receiver + a.LabelsFP = j.LabelsFP + a.Annotations = j.Annotations + a.Labels = orderedLabelsToLabels(j.Labels) + a.Alertmanager = j.Alertmanager + return nil } var seps = []byte{'\xff'} // UpdateFingerprints will generate a new set of fingerprints for this alert func (a *Alert) UpdateFingerprints() { - h := xxhash.New() - for _, l := range a.Labels { - _, _ = h.WriteString(l.Name.Value()) - _, _ = h.Write(seps) - _, _ = h.WriteString(l.Value.Value()) - _, _ = h.Write(seps) - } - a.LabelsFP = strconv.FormatUint(h.Sum64(), 16) + labelsHash := a.Labels.Hash() + a.LabelsFP = strconv.FormatUint(labelsHash, 16) - h.Reset() + h := xxhash.New() for _, a := range a.Annotations { - _, _ = h.WriteString(a.Name.Value()) + _, _ = h.WriteString(a.Name) _, _ = h.Write(seps) - _, _ = h.WriteString(a.Value.Value()) + _, _ = h.WriteString(a.Value) _, _ = h.Write(seps) _, _ = h.WriteString(strconv.FormatBool(a.IsAction)) _, _ = h.Write(seps) @@ -178,14 +261,10 @@ func (a *Alert) UpdateFingerprints() { _, _ = h.Write(seps) } - for _, l := range a.Labels { - _, _ = h.WriteString(l.Name.Value()) - _, _ = h.Write(seps) - _, _ = h.WriteString(l.Value.Value()) - _, _ = h.Write(seps) - } + _, _ = h.WriteString(strconv.FormatUint(labelsHash, 16)) + _, _ = h.Write(seps) _, _ = h.WriteString(a.StartsAt.Format(time.RFC3339)) - _, _ = h.WriteString(a.State.Value()) + _, _ = h.WriteString(a.State.String()) for _, am := range a.Alertmanager { _, _ = h.WriteString(am.Fingerprint) _, _ = h.Write(seps) @@ -193,7 +272,7 @@ func (a *Alert) UpdateFingerprints() { _, _ = h.Write(seps) _, _ = h.WriteString(am.Cluster) _, _ = h.Write(seps) - _, _ = h.WriteString(am.State.Value()) + _, _ = h.WriteString(am.State.String()) _, _ = h.Write(seps) _, _ = h.WriteString(am.StartsAt.Format(time.RFC3339)) _, _ = h.Write(seps) @@ -208,7 +287,7 @@ func (a *Alert) UpdateFingerprints() { _, _ = h.Write(seps) } } - _, _ = h.WriteString(a.Receiver.Value()) + _, _ = h.WriteString(a.Receiver) a.contentFP = strconv.FormatUint(h.Sum64(), 16) } diff --git a/internal/models/alert_test.go b/internal/models/alert_test.go index 6f2db332a..6c1cdbd9c 100644 --- a/internal/models/alert_test.go +++ b/internal/models/alert_test.go @@ -4,182 +4,168 @@ import ( "encoding/json" "fmt" "slices" - "strings" "testing" "time" "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" + "github.com/prometheus/prometheus/model/labels" "github.com/prymitive/karma/internal/config" "github.com/prymitive/karma/internal/models" ) -func labelsToString(ls models.Labels) string { - s := make([]string, 0, len(ls)) - for _, l := range ls { - s = append(s, fmt.Sprintf("%s=\"%s\"", l.Name.Value(), l.Value.Value())) - } - return strings.Join(s, ",") -} - -func TestLabelsSet(t *testing.T) { - l := models.Labels{} - if labelsToString(l) != "" { - t.Errorf("Invalid labels: %+v", l) +func TestLabelsSetIfMissing(t *testing.T) { + // verifies that LabelsSetIfMissing adds a label when missing + l := labels.EmptyLabels() + l = models.LabelsSetIfMissing(l, "foo", "bar") + if l.Get("foo") != "bar" { + t.Errorf("Expected foo=bar, got foo=%s", l.Get("foo")) } - l = l.Set("foo", "bar") - if labelsToString(l) != `foo="bar"` { - t.Errorf("Invalid labels: %+v", l) + // verifies that LabelsSetIfMissing does not overwrite an existing label + l = models.LabelsSetIfMissing(l, "foo", "baz") + if l.Get("foo") != "bar" { + t.Errorf("Expected foo=bar (unchanged), got foo=%s", l.Get("foo")) } - l = l.Set("foo", "bar") - if labelsToString(l) != `foo="bar"` { - t.Errorf("Invalid labels: %+v", l) + // verifies that LabelsSetIfMissing adds a second label + l = models.LabelsSetIfMissing(l, "bar", "foo") + if l.Get("bar") != "foo" { + t.Errorf("Expected bar=foo, got bar=%s", l.Get("bar")) } - - l = l.Set("bar", "foo") - if labelsToString(l) != `foo="bar",bar="foo"` { - t.Errorf("Invalid labels: %+v", l) - } - - l = l.Set("bar", "foo") - if labelsToString(l) != `foo="bar",bar="foo"` { - t.Errorf("Invalid labels: %+v", l) - } - - l = l.Set("foo", "bar") - if labelsToString(l) != `foo="bar",bar="foo"` { - t.Errorf("Invalid labels: %+v", l) + if l.Get("foo") != "bar" { + t.Errorf("Expected foo=bar (still), got foo=%s", l.Get("foo")) } } -type sortLabelsTestCase struct { +type sortOrderedLabelsTestCase struct { order []string - in models.Labels - out models.Labels + in models.OrderedLabels + out models.OrderedLabels } -func TestSortLabels(t *testing.T) { - testCases := []sortLabelsTestCase{ +func TestSortOrderedLabels(t *testing.T) { + testCases := []sortOrderedLabelsTestCase{ + // verifies that a single label stays in place { order: []string{}, - in: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - }, - out: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - }, + in: models.OrderedLabels{{Name: "foo", Value: "bar"}}, + out: models.OrderedLabels{{Name: "foo", Value: "bar"}}, }, + // verifies that two labels are sorted alphabetically by name { order: []string{}, - in: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}, + in: models.OrderedLabels{ + {Name: "foo", Value: "bar"}, + {Name: "bar", Value: "foo"}, }, - out: models.Labels{ - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + out: models.OrderedLabels{ + {Name: "bar", Value: "foo"}, + {Name: "foo", Value: "bar"}, }, }, + // verifies that already-sorted labels remain stable { order: []string{}, - in: models.Labels{ - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + in: models.OrderedLabels{ + {Name: "bar", Value: "foo"}, + {Name: "foo", Value: "bar"}, }, - out: models.Labels{ - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + out: models.OrderedLabels{ + {Name: "bar", Value: "foo"}, + {Name: "foo", Value: "bar"}, }, }, + // verifies that same-name labels sort by value naturally { order: []string{}, - in: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("foo")}, - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + in: models.OrderedLabels{ + {Name: "foo", Value: "foo"}, + {Name: "bar", Value: "foo"}, + {Name: "foo", Value: "bar"}, }, - out: models.Labels{ - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("foo")}, + out: models.OrderedLabels{ + {Name: "bar", Value: "foo"}, + {Name: "foo", Value: "bar"}, + {Name: "foo", Value: "foo"}, }, }, + // verifies natural sort on values with numbers { order: []string{}, - in: models.Labels{ - {Name: models.NewUniqueString("1"), Value: models.NewUniqueString("a12")}, - {Name: models.NewUniqueString("1"), Value: models.NewUniqueString("1")}, - {Name: models.NewUniqueString("1"), Value: models.NewUniqueString("a2")}, + in: models.OrderedLabels{ + {Name: "1", Value: "a12"}, + {Name: "1", Value: "1"}, + {Name: "1", Value: "a2"}, }, - out: models.Labels{ - {Name: models.NewUniqueString("1"), Value: models.NewUniqueString("1")}, - {Name: models.NewUniqueString("1"), Value: models.NewUniqueString("a2")}, - {Name: models.NewUniqueString("1"), Value: models.NewUniqueString("a12")}, + out: models.OrderedLabels{ + {Name: "1", Value: "1"}, + {Name: "1", Value: "a2"}, + {Name: "1", Value: "a12"}, }, }, + // verifies that configured order takes priority { order: []string{"bar"}, - in: models.Labels{ - {Name: models.NewUniqueString("baz"), Value: models.NewUniqueString("1")}, - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("1")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("1")}, + in: models.OrderedLabels{ + {Name: "baz", Value: "1"}, + {Name: "bar", Value: "1"}, + {Name: "foo", Value: "1"}, }, - out: models.Labels{ - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("1")}, - {Name: models.NewUniqueString("baz"), Value: models.NewUniqueString("1")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("1")}, + out: models.OrderedLabels{ + {Name: "bar", Value: "1"}, + {Name: "baz", Value: "1"}, + {Name: "foo", Value: "1"}, }, }, + // verifies that multiple order entries sort correctly with natural value sort { order: []string{"foo", "bar"}, - in: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("a10")}, - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("1")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("a3")}, + in: models.OrderedLabels{ + {Name: "foo", Value: "a10"}, + {Name: "bar", Value: "1"}, + {Name: "foo", Value: "a3"}, }, - out: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("a3")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("a10")}, - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("1")}, + out: models.OrderedLabels{ + {Name: "foo", Value: "a3"}, + {Name: "foo", Value: "a10"}, + {Name: "bar", Value: "1"}, }, }, // verifies that identical labels stay in their original positions { order: []string{}, - in: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + in: models.OrderedLabels{ + {Name: "foo", Value: "bar"}, + {Name: "foo", Value: "bar"}, }, - out: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + out: models.OrderedLabels{ + {Name: "foo", Value: "bar"}, + {Name: "foo", Value: "bar"}, }, }, - // verifies that same-name labels with different values sort by value descending naturally + // verifies that same-name labels with different values sort by value naturally { order: []string{}, - in: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("z")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("a")}, + in: models.OrderedLabels{ + {Name: "foo", Value: "z"}, + {Name: "foo", Value: "a"}, }, - out: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("a")}, - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("z")}, + out: models.OrderedLabels{ + {Name: "foo", Value: "a"}, + {Name: "foo", Value: "z"}, }, }, - // verifies that completely identical name labels (no order config) with equal names sort by name naturally + // verifies that labels with different names sort by name naturally { order: []string{}, - in: models.Labels{ - {Name: models.NewUniqueString("zzz"), Value: models.NewUniqueString("1")}, - {Name: models.NewUniqueString("aaa"), Value: models.NewUniqueString("1")}, + in: models.OrderedLabels{ + {Name: "zzz", Value: "1"}, + {Name: "aaa", Value: "1"}, }, - out: models.Labels{ - {Name: models.NewUniqueString("aaa"), Value: models.NewUniqueString("1")}, - {Name: models.NewUniqueString("zzz"), Value: models.NewUniqueString("1")}, + out: models.OrderedLabels{ + {Name: "aaa", Value: "1"}, + {Name: "zzz", Value: "1"}, }, }, } @@ -191,8 +177,8 @@ func TestSortLabels(t *testing.T) { for i, testCase := range testCases { t.Run(fmt.Sprintf("[%d] order=%v", i, testCase.order), func(t *testing.T) { config.Config.Labels.Order = testCase.order - slices.SortFunc(testCase.in, models.CompareLabels) - if diff := cmp.Diff(testCase.in, testCase.out, cmpopts.EquateComparable(models.Label{})); diff != "" { + slices.SortFunc(testCase.in, models.CompareOrderedLabels) + if diff := cmp.Diff(testCase.out, testCase.in); diff != "" { t.Errorf("Incorrectly sorted labels (-want +got):\n%s", diff) t.FailNow() } @@ -202,22 +188,19 @@ func TestSortLabels(t *testing.T) { func TestLabelsMap(t *testing.T) { type testCaseT struct { - labels models.Labels + labels labels.Labels expected map[string]string } testCases := []testCaseT{ // verifies that empty labels produce an empty map { - labels: models.Labels{}, + labels: labels.EmptyLabels(), expected: map[string]string{}, }, // verifies that labels are converted to a name->value map { - labels: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - {Name: models.NewUniqueString("baz"), Value: models.NewUniqueString("qux")}, - }, + labels: labels.FromStrings("baz", "qux", "foo", "bar"), expected: map[string]string{"foo": "bar", "baz": "qux"}, }, } @@ -230,11 +213,8 @@ func TestLabelsMap(t *testing.T) { } } -func TestLabelsGetValue(t *testing.T) { - labels := models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - {Name: models.NewUniqueString("baz"), Value: models.NewUniqueString("qux")}, - } +func TestLabelsGet(t *testing.T) { + ls := labels.FromStrings("baz", "qux", "foo", "bar") type testCaseT struct { name string @@ -251,38 +231,38 @@ func TestLabelsGetValue(t *testing.T) { } for _, tc := range testCases { - result := labels.GetValue(tc.name) + result := ls.Get(tc.name) if result != tc.expected { - t.Errorf("Labels.GetValue(%q) returned %q, expected %q", tc.name, result, tc.expected) + t.Errorf("Labels.Get(%q) returned %q, expected %q", tc.name, result, tc.expected) } } } -func TestUniqueStringJSONRoundTrip(t *testing.T) { - // verifies that UniqueString survives a JSON marshal/unmarshal round-trip - original := models.NewUniqueString("test_value") - data, err := json.Marshal(&original) +func TestAlertStateJSONRoundTrip(t *testing.T) { + // verifies that AlertState survives a JSON marshal/unmarshal round-trip + original := models.AlertStateActive + data, err := json.Marshal(original) if err != nil { t.Fatalf("json.Marshal failed: %s", err) } - if string(data) != `"test_value"` { - t.Errorf("json.Marshal produced %s, expected %q", string(data), `"test_value"`) + if string(data) != `"active"` { + t.Errorf("json.Marshal produced %s, expected %q", string(data), `"active"`) } - var decoded models.UniqueString + var decoded models.AlertState err = json.Unmarshal(data, &decoded) if err != nil { t.Fatalf("json.Unmarshal failed: %s", err) } - if decoded.Value() != "test_value" { - t.Errorf("json.Unmarshal produced %q, expected %q", decoded.Value(), "test_value") + if decoded != models.AlertStateActive { + t.Errorf("json.Unmarshal produced %v, expected %v", decoded, models.AlertStateActive) } } -func TestUniqueStringUnmarshalJSONError(t *testing.T) { +func TestAlertStateUnmarshalJSONError(t *testing.T) { // verifies that UnmarshalJSON returns an error for invalid JSON input - var us models.UniqueString - err := json.Unmarshal([]byte(`{invalid`), &us) + var s models.AlertState + err := json.Unmarshal([]byte(`{invalid`), &s) if err == nil { t.Error("json.Unmarshal should have returned an error for invalid JSON") } @@ -293,16 +273,14 @@ func TestUpdateFingerprints(t *testing.T) { // including the alertmanager instance, silenced-by, and inhibited-by branches alert := models.Alert{ StartsAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - State: models.NewUniqueString("active"), - Receiver: models.NewUniqueString("default"), + State: models.AlertStateActive, + Receiver: "default", Fingerprint: "abc123", - Labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("TestAlert")}, - }, + Labels: labels.FromStrings("alertname", "TestAlert"), Annotations: models.Annotations{ { - Name: models.NewUniqueString("summary"), - Value: models.NewUniqueString("test summary"), + Name: "summary", + Value: "test summary", Visible: true, IsLink: false, IsAction: false, @@ -313,7 +291,7 @@ func TestUpdateFingerprints(t *testing.T) { Fingerprint: "fp1", Name: "am1", Cluster: "cluster1", - State: models.NewUniqueString("active"), + State: models.AlertStateActive, StartsAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), Source: "http://source", SilencedBy: []string{"silence1", "silence2"}, @@ -344,11 +322,40 @@ func TestUpdateFingerprints(t *testing.T) { // verifies that changing a label produces a different fingerprint alert2 := alert - alert2.Labels = models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("DifferentAlert")}, - } + alert2.Labels = labels.FromStrings("alertname", "DifferentAlert") alert2.UpdateFingerprints() if alert2.LabelsFingerprint() == fp1 { t.Error("LabelsFingerprint() should differ when labels change") } } + +func TestLabelsToOrderedLabels(t *testing.T) { + // verifies that LabelsToOrderedLabels converts labels and applies display ordering + defer func() { + config.Config.Labels.Order = []string{} + }() + + config.Config.Labels.Order = []string{"alertname"} + ls := labels.FromStrings("alertname", "TestAlert", "job", "node") + dl := models.LabelsToOrderedLabels(ls) + + expected := models.OrderedLabels{ + {Name: "alertname", Value: "TestAlert"}, + {Name: "job", Value: "node"}, + } + if diff := cmp.Diff(expected, dl); diff != "" { + t.Errorf("LabelsToOrderedLabels mismatch (-want +got):\n%s", diff) + } +} + +func TestLabelsFromMap(t *testing.T) { + // verifies that LabelsFromMap creates labels from a map + m := map[string]string{"foo": "bar", "baz": "qux"} + ls := models.LabelsFromMap(m) + if ls.Get("foo") != "bar" { + t.Errorf("Expected foo=bar, got foo=%s", ls.Get("foo")) + } + if ls.Get("baz") != "qux" { + t.Errorf("Expected baz=qux, got baz=%s", ls.Get("baz")) + } +} diff --git a/internal/models/alertgroup.go b/internal/models/alertgroup.go index 77340a083..2edd2d5b7 100644 --- a/internal/models/alertgroup.go +++ b/internal/models/alertgroup.go @@ -7,6 +7,7 @@ import ( "time" "github.com/cespare/xxhash/v2" + "github.com/prometheus/prometheus/model/labels" ) // AlertList is flat list of karmaAlert objects @@ -27,28 +28,30 @@ func CompareAlerts(a, b Alert) int { // There is a hash computed from all alerts, it's used by UI to quickly tell // if there was any change in a group and it needs to refresh it type AlertGroup struct { - LatestStartsAt time.Time `json:"-"` - AlertmanagerCount map[string]int `json:"alertmanagerCount"` - StateCount map[string]int `json:"stateCount"` - Receiver UniqueString `json:"receiver"` - ID string `json:"id"` - Hash string `json:"-"` - Labels Labels `json:"labels"` - Alerts AlertList `json:"alerts"` + // LatestStartsAt is the most recent StartsAt timestamp across all alerts + // in this group, used for sorting groups by time in the API response. + LatestStartsAt time.Time + AlertmanagerCount map[string]int + StateCount map[string]int + Receiver string + ID string + // Hash is a content fingerprint of all alerts in this group, used by the + // UI to detect changes and avoid unnecessary re-renders. + Hash string + // Labels are the grouping labels for this alert group as returned by + // Alertmanager. They are converted to OrderedLabels in APIAlertGroup + // for JSON serialization. + Labels labels.Labels + Alerts AlertList } // LabelsFingerprint is a checksum of this AlertGroup labels and the receiver // it should be unique for each AlertGroup func (ag AlertGroup) LabelsFingerprint() string { h := xxhash.New() - _, _ = h.WriteString(ag.Receiver.Value()) + _, _ = h.WriteString(ag.Receiver) _, _ = h.Write(seps) - for _, l := range ag.Labels { - _, _ = h.WriteString(l.Name.Value()) - _, _ = h.Write(seps) - _, _ = h.WriteString(l.Value.Value()) - _, _ = h.Write(seps) - } + _, _ = h.WriteString(strconv.FormatUint(ag.Labels.Hash(), 16)) return strconv.FormatUint(h.Sum64(), 16) } diff --git a/internal/models/alertgroup_test.go b/internal/models/alertgroup_test.go index 3810ca249..025406b6b 100644 --- a/internal/models/alertgroup_test.go +++ b/internal/models/alertgroup_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/prometheus/prometheus/model/labels" + "github.com/prymitive/karma/internal/models" ) @@ -16,7 +18,7 @@ type alertListSortTest struct { var alertListSortTests = []alertListSortTest{ { alert: models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 5, time.UTC), }, @@ -24,7 +26,7 @@ var alertListSortTests = []alertListSortTest{ }, { alert: models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 1, time.UTC), }, @@ -32,7 +34,7 @@ var alertListSortTests = []alertListSortTest{ }, { alert: models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 0, time.UTC), }, @@ -40,7 +42,7 @@ var alertListSortTests = []alertListSortTest{ }, { alert: models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, StartsAt: time.Date(2015, time.March, 10, 0, 0, 0, 0, time.UTC), }, @@ -48,7 +50,7 @@ var alertListSortTests = []alertListSortTest{ }, { alert: models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, StartsAt: time.Date(2016, time.December, 10, 0, 0, 0, 0, time.UTC), }, @@ -56,7 +58,7 @@ var alertListSortTests = []alertListSortTest{ }, { alert: models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 0, time.UTC), }, @@ -64,7 +66,7 @@ var alertListSortTests = []alertListSortTest{ }, { alert: models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, StartsAt: time.Date(2015, time.March, 10, 0, 0, 0, 0, time.UTC), }, @@ -106,88 +108,78 @@ var agFPTests = []agFPTest{ { name: "empty group fingerprint", ag: models.AlertGroup{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", }, }, { name: "different Receiver shouldn't change content fingerprint", ag: models.AlertGroup{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", }, fpChange: false, }, { name: "different StateCount shouldn't change content fingerprint", ag: models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - StateCount: map[string]int{"default": 0}, + Receiver: "default", + StateCount: map[string]int{}, }, fpChange: false, }, { name: "different Labels shouldn't change content fingerprint", ag: models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - Labels: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - }, - StateCount: map[string]int{"default": 0}, + Receiver: "default", + Labels: labels.FromStrings("foo", "bar"), + StateCount: map[string]int{}, }, fpChange: false, }, { name: "different set of alerts should change content fingerprint", ag: models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - Labels: models.Labels{{Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}}, + Receiver: "default", + Labels: labels.FromStrings("foo", "bar"), Alerts: models.AlertList{ models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, - Labels: models.Labels{ - {Name: models.NewUniqueString("foo1"), Value: models.NewUniqueString("bar")}, - }, + Labels: labels.FromStrings("foo1", "bar"), }, }, - StateCount: map[string]int{"default": 0}, + StateCount: map[string]int{}, }, fpChange: true, }, { name: "another different set of alerts should change content fingerprint", ag: models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - Labels: models.Labels{{Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}}, + Receiver: "default", + Labels: labels.FromStrings("bar", "foo"), Alerts: models.AlertList{ models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, - Labels: models.Labels{ - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}, - }, + Labels: labels.FromStrings("bar", "foo"), }, }, - StateCount: map[string]int{"default": 0}, + StateCount: map[string]int{}, }, fpChange: true, }, { name: "repeating last set of alerts shouldn't change content fingerprint", ag: models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - Labels: models.Labels{ - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}, - }, + Receiver: "default", + Labels: labels.FromStrings("bar", "foo"), Alerts: models.AlertList{ models.Alert{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", State: models.AlertStateActive, - Labels: models.Labels{ - {Name: models.NewUniqueString("bar"), Value: models.NewUniqueString("foo")}, - }, + Labels: labels.FromStrings("bar", "foo"), }, }, - StateCount: map[string]int{"default": 0}, + StateCount: map[string]int{}, }, fpChange: false, }, @@ -225,7 +217,7 @@ func TestAlertGroupContentFingerprint(t *testing.T) { func TestFingerprint(t *testing.T) { ag := models.AlertGroup{ - Receiver: models.NewUniqueString("default"), + Receiver: "default", } if ag.LabelsFingerprint() == ag.ContentFingerprint() { t.Errorf("Expected LabelsFingerprint and ContentFingerprint to return different values") @@ -235,11 +227,8 @@ func TestFingerprint(t *testing.T) { func TestLabelsFingerprint(t *testing.T) { // verifies that LabelsFingerprint produces non-empty output when labels are present ag := models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - Labels: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - {Name: models.NewUniqueString("baz"), Value: models.NewUniqueString("qux")}, - }, + Receiver: "default", + Labels: labels.FromStrings("baz", "qux", "foo", "bar"), } fp := ag.LabelsFingerprint() if fp == "" { @@ -248,10 +237,8 @@ func TestLabelsFingerprint(t *testing.T) { // verifies that different labels produce a different fingerprint ag2 := models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - Labels: models.Labels{ - {Name: models.NewUniqueString("different"), Value: models.NewUniqueString("label")}, - }, + Receiver: "default", + Labels: labels.FromStrings("different", "label"), } fp2 := ag2.LabelsFingerprint() if fp == fp2 { @@ -260,11 +247,8 @@ func TestLabelsFingerprint(t *testing.T) { // verifies that same receiver and labels produce the same fingerprint ag3 := models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - Labels: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - {Name: models.NewUniqueString("baz"), Value: models.NewUniqueString("qux")}, - }, + Receiver: "default", + Labels: labels.FromStrings("baz", "qux", "foo", "bar"), } if ag3.LabelsFingerprint() != fp { t.Errorf("LabelsFingerprint() not stable: %q != %q", ag3.LabelsFingerprint(), fp) diff --git a/internal/models/alertmanager.go b/internal/models/alertmanager.go index 571660d44..43d98d2aa 100644 --- a/internal/models/alertmanager.go +++ b/internal/models/alertmanager.go @@ -5,22 +5,22 @@ import "time" // AlertmanagerInstance describes the Alertmanager instance alert was collected // from type AlertmanagerInstance struct { - Fingerprint string `json:"fingerprint"` - Name string `json:"name"` - Cluster string `json:"cluster"` - // per instance alert state - State UniqueString `json:"state"` // timestamp collected from this instance, those on the alert itself // will be calculated min/max values StartsAt time.Time `json:"startsAt"` - // Source links to alert source for given alertmanager instance - Source string `json:"source"` // all silences matching current alert in this upstream, we don't export this // in api responses, this is used internally - Silences map[string]*Silence `json:"-"` + Silences map[string]*Silence `json:"-"` + Fingerprint string `json:"fingerprint"` + Name string `json:"name"` + Cluster string `json:"cluster"` + // Source links to alert source for given alertmanager instance + Source string `json:"source"` // export list of silenced IDs in api response SilencedBy []string `json:"silencedBy"` InhibitedBy []string `json:"inhibitedBy"` + // per instance alert state + State AlertState `json:"state"` } // AlertmanagerAPIStatus describes the Alertmanager instance overall health diff --git a/internal/models/annotation.go b/internal/models/annotation.go index 9ad594a9f..e5990252c 100644 --- a/internal/models/annotation.go +++ b/internal/models/annotation.go @@ -13,11 +13,11 @@ import ( // Annotation extends Alertmanager scheme of key:value with additional data // to control how given annotation should be rendered type Annotation struct { - Name UniqueString `json:"name"` - Value UniqueString `json:"value"` - Visible bool `json:"visible"` - IsLink bool `json:"isLink"` - IsAction bool `json:"isAction"` + Name string `json:"name"` + Value string `json:"value"` + Visible bool `json:"visible"` + IsLink bool `json:"isLink"` + IsAction bool `json:"isAction"` } // Annotations is a slice of Annotation structs, needed to implement sorting @@ -28,9 +28,9 @@ func compareAnnotations(a, b Annotation) int { // the order they appear in that list; remaining annotations are sorted alphabetically. ai, bi := -1, -1 for index, name := range config.Config.Annotations.Order { - if a.Name.Value() == name { + if a.Name == name { ai = index - } else if b.Name.Value() == name { + } else if b.Name == name { bi = index } // If both annotations are in c.C.A.Order, sort them according to the @@ -50,10 +50,10 @@ func compareAnnotations(a, b Annotation) int { return 1 } // If neither annotation was in c.C.A.Order, sort alphabetically. - if sortorder.NaturalLess(a.Name.Value(), b.Name.Value()) { + if sortorder.NaturalLess(a.Name, b.Name) { return -1 } - if sortorder.NaturalLess(b.Name.Value(), a.Name.Value()) { + if sortorder.NaturalLess(b.Name, a.Name) { return 1 } return 0 @@ -65,8 +65,8 @@ func AnnotationsFromMap(m map[string]string) Annotations { annotations := make(Annotations, 0, len(m)) for name, value := range m { a := Annotation{ - Name: NewUniqueString(name), - Value: NewUniqueString(value), + Name: name, + Value: value, Visible: isVisible(name), IsLink: isLink(value), IsAction: isAction(name), diff --git a/internal/models/annotation_test.go b/internal/models/annotation_test.go index f14162a44..f3e7e7de3 100644 --- a/internal/models/annotation_test.go +++ b/internal/models/annotation_test.go @@ -2,7 +2,6 @@ package models_test import ( "testing" - "unique" "github.com/google/go-cmp/cmp" @@ -26,8 +25,8 @@ var annotationMapsTestCases = []annotationMapsTestCase{ }, annotations: models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("foo"), - Value: models.NewUniqueString("bar"), + Name: "foo", + Value: "bar", Visible: true, IsLink: false, IsAction: false, @@ -40,8 +39,8 @@ var annotationMapsTestCases = []annotationMapsTestCase{ }, annotations: models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("foo"), - Value: models.NewUniqueString("http://localhost"), + Name: "foo", + Value: "http://localhost", Visible: true, IsLink: true, IsAction: false, @@ -54,8 +53,8 @@ var annotationMapsTestCases = []annotationMapsTestCase{ }, annotations: models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("foo"), - Value: models.NewUniqueString("ftp://localhost"), + Name: "foo", + Value: "ftp://localhost", Visible: true, IsLink: true, IsAction: false, @@ -70,22 +69,22 @@ var annotationMapsTestCases = []annotationMapsTestCase{ }, annotations: models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("abc"), - Value: models.NewUniqueString("xyz"), + Name: "abc", + Value: "xyz", Visible: true, IsLink: false, IsAction: false, }, models.Annotation{ - Name: models.NewUniqueString("act"), - Value: models.NewUniqueString("https://localhost/act "), + Name: "act", + Value: "https://localhost/act ", Visible: true, IsLink: true, IsAction: true, }, models.Annotation{ - Name: models.NewUniqueString("foo"), - Value: models.NewUniqueString("https://localhost/xxx"), + Name: "foo", + Value: "https://localhost/xxx", Visible: true, IsLink: true, IsAction: false, @@ -99,8 +98,8 @@ var annotationMapsTestCases = []annotationMapsTestCase{ }, annotations: models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("notLink"), - Value: models.NewUniqueString("https://some-links.domain.com/healthcheck in dev (job: blackbox) is not successfully probing via the blackbox prober. this could be due to the endpoint being offline, returning an invalid status code, taking too long to respond, etc."), + Name: "notLink", + Value: "https://some-links.domain.com/healthcheck in dev (job: blackbox) is not successfully probing via the blackbox prober. this could be due to the endpoint being offline, returning an invalid status code, taking too long to respond, etc.", Visible: true, IsLink: false, IsAction: false, @@ -113,8 +112,8 @@ var annotationMapsTestCases = []annotationMapsTestCase{ }, annotations: models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("notLink"), - Value: models.NewUniqueString("mailto:me@example.com"), + Name: "notLink", + Value: "mailto:me@example.com", Visible: true, IsLink: false, IsAction: false, @@ -129,8 +128,8 @@ var annotationMapsTestCases = []annotationMapsTestCase{ }, annotations: models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("hidden"), - Value: models.NewUniqueString("value"), + Name: "hidden", + Value: "value", Visible: false, IsLink: false, IsAction: false, @@ -148,22 +147,22 @@ var annotationMapsTestCases = []annotationMapsTestCase{ }, annotations: models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("default"), - Value: models.NewUniqueString("value"), + Name: "default", + Value: "value", Visible: false, IsLink: false, IsAction: false, }, models.Annotation{ - Name: models.NewUniqueString("hidden"), - Value: models.NewUniqueString("value"), + Name: "hidden", + Value: "value", Visible: false, IsLink: false, IsAction: false, }, models.Annotation{ - Name: models.NewUniqueString("visible"), - Value: models.NewUniqueString("value"), + Name: "visible", + Value: "value", Visible: true, IsLink: false, IsAction: false, @@ -173,17 +172,13 @@ var annotationMapsTestCases = []annotationMapsTestCase{ } func TestAnnotationsFromMap(t *testing.T) { - cmpUnique := cmp.Comparer(func(x, y unique.Handle[string]) bool { - return cmp.Equal(x.Value(), y.Value()) - }) - for _, testCase := range annotationMapsTestCases { config.Config.Annotations.Default.Hidden = testCase.defaultHidden config.Config.Annotations.Hidden = testCase.hidden config.Config.Annotations.Visible = testCase.visible config.Config.Annotations.Actions = testCase.actions result := models.AnnotationsFromMap(testCase.annotationMap) - if diff := cmp.Diff(testCase.annotations, result, cmpUnique); diff != "" { + if diff := cmp.Diff(testCase.annotations, result); diff != "" { t.Errorf("AnnotationsFromMap result mismatch (-want +got):\n%s", diff) } } @@ -192,73 +187,73 @@ func TestAnnotationsFromMap(t *testing.T) { func TestAnnotationsSort(t *testing.T) { annotations := models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("bar"), - Value: models.NewUniqueString("abc"), + Name: "bar", + Value: "abc", Visible: true, IsLink: false, }, models.Annotation{ - Name: models.NewUniqueString("xyz"), - Value: models.NewUniqueString("xyz"), + Name: "xyz", + Value: "xyz", Visible: true, IsLink: true, }, models.Annotation{ - Name: models.NewUniqueString("abc"), - Value: models.NewUniqueString("bar"), + Name: "abc", + Value: "bar", Visible: true, IsLink: true, }, } models.SortAnnotations(annotations) - if annotations[0].Name.Value() != "abc" { - t.Errorf("Expected 'abc' to be first, got '%s'", annotations[0].Name.Value()) + if annotations[0].Name != "abc" { + t.Errorf("Expected 'abc' to be first, got '%s'", annotations[0].Name) } - if annotations[2].Name.Value() != "xyz" { - t.Errorf("Expected 'xyz' to be last, got '%s'", annotations[2].Name.Value()) + if annotations[2].Name != "xyz" { + t.Errorf("Expected 'xyz' to be last, got '%s'", annotations[2].Name) } } func TestAnnotationsCustomOrderSort(t *testing.T) { annotations := models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("bar"), - Value: models.NewUniqueString("abc"), + Name: "bar", + Value: "abc", Visible: true, IsLink: false, }, models.Annotation{ - Name: models.NewUniqueString("xyz"), - Value: models.NewUniqueString("xyz"), + Name: "xyz", + Value: "xyz", Visible: true, IsLink: true, }, models.Annotation{ - Name: models.NewUniqueString("yyz"), - Value: models.NewUniqueString("yyz"), + Name: "yyz", + Value: "yyz", Visible: true, IsLink: true, }, models.Annotation{ - Name: models.NewUniqueString("abc"), - Value: models.NewUniqueString("bar"), + Name: "abc", + Value: "bar", Visible: true, IsLink: true, }, } config.Config.Annotations.Order = []string{"xyz", "yyz"} models.SortAnnotations(annotations) - if annotations[0].Name.Value() != "xyz" { - t.Errorf("Expected 'xyz' to be first, got '%s'", annotations[0].Name.Value()) + if annotations[0].Name != "xyz" { + t.Errorf("Expected 'xyz' to be first, got '%s'", annotations[0].Name) } - if annotations[1].Name.Value() != "yyz" { - t.Errorf("Expected 'yyz' to be second, got '%s'", annotations[1].Name.Value()) + if annotations[1].Name != "yyz" { + t.Errorf("Expected 'yyz' to be second, got '%s'", annotations[1].Name) } - if annotations[2].Name.Value() != "abc" { - t.Errorf("Expected 'abc' to be third, got '%s'", annotations[2].Name.Value()) + if annotations[2].Name != "abc" { + t.Errorf("Expected 'abc' to be third, got '%s'", annotations[2].Name) } - if annotations[3].Name.Value() != "bar" { - t.Errorf("Expected 'bar' to be last, got '%s'", annotations[3].Name.Value()) + if annotations[3].Name != "bar" { + t.Errorf("Expected 'bar' to be last, got '%s'", annotations[3].Name) } } @@ -267,29 +262,29 @@ func TestAnnotationsSortIdenticalNames(t *testing.T) { config.Config.Annotations.Order = []string{} annotations := models.Annotations{ models.Annotation{ - Name: models.NewUniqueString("dup"), - Value: models.NewUniqueString("second"), + Name: "dup", + Value: "second", Visible: true, }, models.Annotation{ - Name: models.NewUniqueString("dup"), - Value: models.NewUniqueString("first"), + Name: "dup", + Value: "first", Visible: true, }, models.Annotation{ - Name: models.NewUniqueString("aaa"), - Value: models.NewUniqueString("value"), + Name: "aaa", + Value: "value", Visible: true, }, } models.SortAnnotations(annotations) - if annotations[0].Name.Value() != "aaa" { - t.Errorf("Expected 'aaa' to be first, got '%s'", annotations[0].Name.Value()) + if annotations[0].Name != "aaa" { + t.Errorf("Expected 'aaa' to be first, got '%s'", annotations[0].Name) } - if annotations[1].Name.Value() != "dup" { - t.Errorf("Expected 'dup' to be second, got '%s'", annotations[1].Name.Value()) + if annotations[1].Name != "dup" { + t.Errorf("Expected 'dup' to be second, got '%s'", annotations[1].Name) } - if annotations[2].Name.Value() != "dup" { - t.Errorf("Expected 'dup' to be third, got '%s'", annotations[2].Name.Value()) + if annotations[2].Name != "dup" { + t.Errorf("Expected 'dup' to be third, got '%s'", annotations[2].Name) } } diff --git a/internal/models/api.go b/internal/models/api.go index bd059bfd6..85de3756b 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -7,8 +7,10 @@ import ( "slices" "sort" "strings" + "time" "github.com/fvbommel/sortorder" + "github.com/prometheus/prometheus/model/labels" ) // Filter holds returned data on any filter passed by the user as part of the query @@ -85,91 +87,139 @@ func CompareLabelNameStats(a, b LabelNameStats) int { return cmp.Compare(a.Name, b.Name) } -// APIAlertGroupSharedMaps defines shared part of APIAlertGroup +// AlertGroupSharedMaps holds data shared across all alerts in a group. +// Populated by AlertGroup.DedupSharedMaps. +type AlertGroupSharedMaps struct { + Annotations Annotations + Labels labels.Labels + Silences map[string][]string + Sources []string + Clusters []string +} + +// APIAlertGroupSharedMaps is the JSON representation of AlertGroupSharedMaps. type APIAlertGroupSharedMaps struct { Annotations Annotations `json:"annotations"` - Labels Labels `json:"labels"` + Labels OrderedLabels `json:"labels"` Silences map[string][]string `json:"silences"` Sources []string `json:"sources"` Clusters []string `json:"clusters"` } -// APIAlertGroup is how AlertGroup is returned in the API response +// 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 +// annotations that are unique to that instance. type APIAlertGroup struct { - AllLabels map[string]map[string][]string `json:"allLabels"` - AlertGroup - Shared APIAlertGroupSharedMaps `json:"shared"` - TotalAlerts int `json:"totalAlerts"` + LatestStartsAt time.Time `json:"-"` + AllLabels map[string]map[string][]string `json:"allLabels"` + AlertmanagerCount map[string]int `json:"alertmanagerCount"` + StateCount map[string]int `json:"stateCount"` + Receiver string `json:"receiver"` + ID string `json:"id"` + Shared APIAlertGroupSharedMaps `json:"shared"` + Labels OrderedLabels `json:"labels"` + Alerts []APIAlert `json:"alerts"` + TotalAlerts int `json:"totalAlerts"` } -func (ag *APIAlertGroup) dedupLabels() { +// NewAPIAlertGroup converts an AlertGroup into its API representation. +// ag must have been processed by DedupSharedMaps before calling this. +func NewAPIAlertGroup(ag AlertGroup, shared AlertGroupSharedMaps, allLabels map[string]map[string][]string, totalAlerts int) APIAlertGroup { + alerts := make([]APIAlert, len(ag.Alerts)) + for i, a := range ag.Alerts { + alerts[i] = APIAlert{ + StartsAt: a.StartsAt, + State: a.State.String(), + Receiver: a.Receiver, + LabelsFP: a.LabelsFP, + Annotations: a.Annotations, + Labels: LabelsToOrderedLabels(a.Labels), + Alertmanager: a.Alertmanager, + } + } + return APIAlertGroup{ + AllLabels: allLabels, + Labels: LabelsToOrderedLabels(ag.Labels), + Alerts: alerts, + AlertmanagerCount: ag.AlertmanagerCount, + StateCount: ag.StateCount, + Receiver: ag.Receiver, + ID: ag.ID, + LatestStartsAt: ag.LatestStartsAt, + Shared: APIAlertGroupSharedMaps{ + Annotations: shared.Annotations, + Labels: LabelsToOrderedLabels(shared.Labels), + Silences: shared.Silences, + Sources: shared.Sources, + Clusters: shared.Clusters, + }, + TotalAlerts: totalAlerts, + } +} + +func (ag *AlertGroup) dedupLabels(shared *AlertGroupSharedMaps) { totalAlerts := len(ag.Alerts) labelCounts := make(map[string]int, len(ag.Alerts)) for _, alert := range ag.Alerts { - for _, l := range alert.Labels { - key := l.Name.Value() + "\n" + l.Value.Value() + alert.Labels.Range(func(l labels.Label) { + key := l.Name + "\n" + l.Value labelCounts[key]++ - } + }) } - sharedLabels := Labels{} + sharedPairs := make([]string, 0, len(labelCounts)*2) + alertPairs := make([]string, 0, len(labelCounts)*2) + sharedSeen := map[string]struct{}{} for i, alert := range ag.Alerts { - newAlertLabels := Labels{} - for _, l := range alert.Labels { - key := l.Name.Value() + "\n" + l.Value.Value() + alertPairs = alertPairs[:0] + alert.Labels.Range(func(l labels.Label) { + key := l.Name + "\n" + l.Value if labelCounts[key] == totalAlerts { - sharedLabels = sharedLabels.Add(l) + if _, ok := sharedSeen[l.Name]; !ok { + sharedSeen[l.Name] = struct{}{} + sharedPairs = append(sharedPairs, l.Name, l.Value) + } } else { - newAlertLabels = newAlertLabels.Add(l) + alertPairs = append(alertPairs, l.Name, l.Value) } - } - ag.Alerts[i].Labels = newAlertLabels + }) + ag.Alerts[i].Labels = labels.FromStrings(alertPairs...) } - ag.Shared.Labels = sharedLabels + shared.Labels = labels.FromStrings(sharedPairs...) } -func (ag *APIAlertGroup) removeGroupingLabels(dropNames []string) { - newGroupLabels := Labels{} - for _, l := range ag.Labels { - if slices.Contains(dropNames, l.Name.Value()) { - continue - } - newGroupLabels = newGroupLabels.Add(l) +func (ag *AlertGroup) removeGroupingLabels(dropNames []string) { + b := labels.NewBuilder(ag.Labels) + for _, name := range dropNames { + b.Del(name) } - ag.Labels = newGroupLabels + ag.Labels = b.Labels() for i, alert := range ag.Alerts { - newAlertLabels := Labels{} - for _, l := range alert.Labels { - if slices.Contains(dropNames, l.Name.Value()) { - // skip all labels from the drop list - continue - } - if v := ag.Labels.Get(l.Name.Value()); v != nil { - // skip all labels that are used for grouping - continue - } - newAlertLabels = newAlertLabels.Add(l) + b.Reset(alert.Labels) + for _, name := range dropNames { + b.Del(name) } - ag.Alerts[i].Labels = newAlertLabels + ag.Labels.Range(func(l labels.Label) { + b.Del(l.Name) + }) + ag.Alerts[i].Labels = b.Labels() } } -func (ag *APIAlertGroup) dedupAnnotations() { +func (ag *AlertGroup) dedupAnnotations(shared *AlertGroupSharedMaps) { totalAlerts := len(ag.Alerts) annotationCount := map[string]int{} for _, alert := range ag.Alerts { for _, annotation := range alert.Annotations { - key := annotation.Name.Value() + "\n" + annotation.Value.Value() + key := annotation.Name + "\n" + annotation.Value annotationCount[key]++ } } @@ -180,7 +230,7 @@ func (ag *APIAlertGroup) dedupAnnotations() { for i, alert := range ag.Alerts { newAlertAnnotations := Annotations{} for _, annotation := range alert.Annotations { - key := annotation.Name.Value() + "\n" + annotation.Value.Value() + key := annotation.Name + "\n" + annotation.Value if annotationCount[key] == totalAlerts { if _, ok := sharedKeys[key]; !ok { sharedAnnotations = append(sharedAnnotations, annotation) @@ -193,11 +243,11 @@ func (ag *APIAlertGroup) dedupAnnotations() { ag.Alerts[i].Annotations = newAlertAnnotations } - ag.Shared.Annotations = sharedAnnotations + shared.Annotations = sharedAnnotations } -func (ag *APIAlertGroup) dedupSilences() { - ag.Shared.Silences = map[string][]string{} +func (ag *AlertGroup) dedupSilences(shared *AlertGroupSharedMaps) { + shared.Silences = map[string][]string{} silencesByCluster := map[string]map[string]int{} @@ -222,20 +272,20 @@ func (ag *APIAlertGroup) dedupSilences() { for cluster, silenceCountMap := range silencesByCluster { for silenceID, affectedAlertsCount := range silenceCountMap { if affectedAlertsCount == totalAlerts { - _, ok := ag.Shared.Silences[cluster] + _, ok := shared.Silences[cluster] if !ok { - ag.Shared.Silences[cluster] = []string{} + shared.Silences[cluster] = []string{} } - ag.Shared.Silences[cluster] = append(ag.Shared.Silences[cluster], silenceID) + shared.Silences[cluster] = append(shared.Silences[cluster], silenceID) // sort to have stable order of silences - sort.Strings(ag.Shared.Silences[cluster]) + sort.Strings(shared.Silences[cluster]) } } } } -func (ag *APIAlertGroup) dedupSources() { - ag.Shared.Sources = []string{} +func (ag *AlertGroup) dedupSources(shared *AlertGroupSharedMaps) { + shared.Sources = []string{} urls := map[string]struct{}{} var err error @@ -260,12 +310,12 @@ func (ag *APIAlertGroup) dedupSources() { } for u := range urls { - ag.Shared.Sources = append(ag.Shared.Sources, u) + shared.Sources = append(shared.Sources, u) } - sort.Strings(ag.Shared.Sources) + sort.Strings(shared.Sources) } -func (ag *APIAlertGroup) dedupClusters() { +func (ag *AlertGroup) dedupClusters(shared *AlertGroupSharedMaps) { totalAlerts := len(ag.Alerts) alertsPerCluster := map[string]int{} @@ -282,81 +332,81 @@ func (ag *APIAlertGroup) dedupClusters() { } } - ag.Shared.Clusters = []string{} + shared.Clusters = []string{} for cluster, alerts := range alertsPerCluster { if alerts == totalAlerts { - ag.Shared.Clusters = append(ag.Shared.Clusters, cluster) + shared.Clusters = append(shared.Clusters, cluster) } } - sort.Strings(ag.Shared.Clusters) + sort.Strings(shared.Clusters) } -func (ag *APIAlertGroup) populateAllLabels() { - ag.AllLabels = map[string]map[string][]string{ - AlertStateActive.Value(): {}, - AlertStateSuppressed.Value(): {}, - AlertStateUnprocessed.Value(): {}, +func (ag *AlertGroup) populateAllLabels() map[string]map[string][]string { + allLabels := map[string]map[string][]string{ + AlertStateActive.String(): {}, + AlertStateSuppressed.String(): {}, + AlertStateUnprocessed.String(): {}, } - labels := map[UniqueString]int{} - for _, alert := range ag.Alerts { - for _, l := range alert.Labels { - if _, ok := labels[l.Name]; !ok { - labels[l.Name] = 0 - } - labels[l.Name]++ - } - } - - labelNames := map[UniqueString]struct{}{} totalAlerts := len(ag.Alerts) - for k, totalValues := range labels { - if totalValues == totalAlerts { - labelNames[k] = struct{}{} - } + + var estLabels int + if totalAlerts > 0 { + estLabels = ag.Alerts[0].Labels.Len() + } + + labelNameCounts := make(map[string]int, estLabels) + for _, alert := range ag.Alerts { + alert.Labels.Range(func(l labels.Label) { + labelNameCounts[l.Name]++ + }) } for _, alert := range ag.Alerts { - for _, l := range alert.Labels { - if _, ok := labelNames[l.Name]; !ok { - continue + stateLabels := allLabels[alert.State.String()] + alert.Labels.Range(func(l labels.Label) { + if labelNameCounts[l.Name] != totalAlerts { + return } - if _, ok := ag.AllLabels[alert.State.Value()][l.Name.Value()]; !ok { - ag.AllLabels[alert.State.Value()][l.Name.Value()] = []string{} - } - if !slices.Contains(ag.AllLabels[alert.State.Value()][l.Name.Value()], l.Value.Value()) { - ag.AllLabels[alert.State.Value()][l.Name.Value()] = append(ag.AllLabels[alert.State.Value()][l.Name.Value()], l.Value.Value()) + vals := stateLabels[l.Name] + if !slices.Contains(vals, l.Value) { + stateLabels[l.Name] = append(vals, l.Value) } + }) + } + for state := range allLabels { + for k := range allLabels[state] { + sort.Strings(allLabels[state][k]) } } - for state := range ag.AllLabels { - for k := range ag.AllLabels[state] { - sort.Strings(ag.AllLabels[state][k]) - } - } + return allLabels } -// DedupSharedMaps will find all labels and annotations shared by all alerts -// in this group and moved them to Shared namespace -func (ag *APIAlertGroup) DedupSharedMaps(ignoredLabels []string) { - ag.populateAllLabels() +// DedupSharedMaps finds all labels and annotations shared by all alerts +// in this group, moves them to shared maps, and removes grouping labels. +// It mutates ag.Alerts and ag.Labels in place. +func (ag *AlertGroup) DedupSharedMaps(ignoredLabels []string) (AlertGroupSharedMaps, map[string]map[string][]string) { + allLabels := ag.populateAllLabels() // remove all labels that are used for grouping ag.removeGroupingLabels(ignoredLabels) + + var shared AlertGroupSharedMaps // don't dedup if we only have a single alert in this group if len(ag.Alerts) > 1 { - ag.dedupLabels() - ag.dedupAnnotations() - ag.dedupSilences() - ag.dedupClusters() + ag.dedupLabels(&shared) + ag.dedupAnnotations(&shared) + ag.dedupSilences(&shared) + ag.dedupClusters(&shared) } else { - ag.Shared = APIAlertGroupSharedMaps{ - Labels: Labels{}, + shared = AlertGroupSharedMaps{ + Labels: labels.EmptyLabels(), Annotations: Annotations{}, Silences: map[string][]string{}, Clusters: []string{}, } } - ag.dedupSources() + ag.dedupSources(&shared) + return shared, allLabels } // GridSettings exposes all grid settings from the config file @@ -457,8 +507,8 @@ type AlertsResponse struct { // Autocomplete is the structure of autocomplete object for filter hints // this is internal representation, not what's returned to the user type Autocomplete struct { - Value UniqueString `json:"value"` - Tokens []UniqueString `json:"tokens"` + Value string `json:"value"` + Tokens []string `json:"tokens"` } type Counters struct { diff --git a/internal/models/api_test.go b/internal/models/api_test.go index 24bbc3c0a..9604d56d8 100644 --- a/internal/models/api_test.go +++ b/internal/models/api_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/beme/abide" + "github.com/prometheus/prometheus/model/labels" "github.com/prymitive/karma/internal/models" ) @@ -44,237 +45,288 @@ func TestColorString(t *testing.T) { } func TestDedupSharedMaps(t *testing.T) { - ag := models.APIAlertGroup{ - AlertGroup: models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - Labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("FakeAlert")}, + ag := models.AlertGroup{ + Receiver: "default", + Labels: labels.FromStrings("alertname", "FakeAlert"), + Alerts: models.AlertList{ + models.Alert{ + Receiver: "default", + State: models.AlertStateSuppressed, + Annotations: models.Annotations{ + models.Annotation{ + Name: "summary", + Value: "this is summary", + }, + models.Annotation{ + Name: "foo", + Value: "bar", + }, + }, + Labels: labels.FromStrings("alertname", "FakeAlert", "instance", "1", "job", "node_exporter"), + Alertmanager: []models.AlertmanagerInstance{ + { + State: models.AlertStateSuppressed, + Fingerprint: "1", + Name: "am1", + Cluster: "fakeCluster", + SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, + Source: "https://prom.example.com/graph?foo", + }, + { + State: models.AlertStateSuppressed, + Fingerprint: "2", + Name: "am2", + Cluster: "fakeCluster", + SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, + Source: "https://prom.example.com/subdir/graph?bar", + }, + }, }, - Alerts: models.AlertList{ - models.Alert{ - Receiver: models.NewUniqueString("default"), - State: models.AlertStateSuppressed, - Annotations: models.Annotations{ - models.Annotation{ - Name: models.NewUniqueString("summary"), - Value: models.NewUniqueString("this is summary"), - }, - models.Annotation{ - Name: models.NewUniqueString("foo"), - Value: models.NewUniqueString("bar"), - }, - }, - Labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("FakeAlert")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("1")}, - }, - Alertmanager: []models.AlertmanagerInstance{ - { - State: models.AlertStateSuppressed, - Fingerprint: "1", - Name: "am1", - Cluster: "fakeCluster", - SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, - Source: "https://prom.example.com/graph?foo", - }, - { - State: models.AlertStateSuppressed, - Fingerprint: "2", - Name: "am2", - Cluster: "fakeCluster", - SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, - Source: "https://prom.example.com/subdir/graph?bar", - }, + models.Alert{ + Receiver: "default", + State: models.AlertStateActive, + Annotations: models.Annotations{ + models.Annotation{ + Name: "summary", + Value: "this is summary", }, }, - models.Alert{ - Receiver: models.NewUniqueString("default"), - State: models.AlertStateActive, - Annotations: models.Annotations{ - models.Annotation{ - Name: models.NewUniqueString("summary"), - Value: models.NewUniqueString("this is summary"), - }, + Labels: labels.FromStrings("alertname", "FakeAlert", "instance", "2", "job", "node_exporter"), + Alertmanager: []models.AlertmanagerInstance{ + { + State: models.AlertStateActive, + Fingerprint: "1", + Name: "am1", + Cluster: "fakeCluster", + SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, + Source: "https://am.example.com", }, - Labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("FakeAlert")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("node_exporter")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("2")}, - }, - Alertmanager: []models.AlertmanagerInstance{ - { - State: models.AlertStateActive, - Fingerprint: "1", - Name: "am1", - Cluster: "fakeCluster", - SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, - Source: "https://am.example.com", - }, - { - State: models.AlertStateActive, - Fingerprint: "1", - Name: "am2", - Cluster: "fakeCluster", - SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, - Source: "https://am.example.com", - }, + { + State: models.AlertStateActive, + Fingerprint: "1", + Name: "am2", + Cluster: "fakeCluster", + SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, + Source: "https://am.example.com", }, }, - models.Alert{ - Receiver: models.NewUniqueString("default"), - State: models.AlertStateSuppressed, - Annotations: models.Annotations{ - models.Annotation{ - Name: models.NewUniqueString("summary"), - Value: models.NewUniqueString("this is summary"), - }, + }, + models.Alert{ + Receiver: "default", + State: models.AlertStateSuppressed, + Annotations: models.Annotations{ + models.Annotation{ + Name: "summary", + Value: "this is summary", }, - Labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("FakeAlert")}, - {Name: models.NewUniqueString("job"), Value: models.NewUniqueString("blackbox")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("3")}, - {Name: models.NewUniqueString("extra"), Value: models.NewUniqueString("ignore")}, + }, + Labels: labels.FromStrings("alertname", "FakeAlert", "extra", "ignore", "instance", "3", "job", "blackbox"), + Alertmanager: []models.AlertmanagerInstance{ + { + State: models.AlertStateSuppressed, + Fingerprint: "1", + Name: "am1", + Cluster: "fakeCluster", + SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, + Source: "https://am.example.com/graph", }, - Alertmanager: []models.AlertmanagerInstance{ - { - State: models.AlertStateSuppressed, - Fingerprint: "1", - Name: "am1", - Cluster: "fakeCluster", - SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, - Source: "https://am.example.com/graph", - }, - { - State: models.AlertStateSuppressed, - Fingerprint: "1", - Name: "am2", - Cluster: "fakeCluster", - SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, - Source: "https://am.example.com/graph", - }, + { + State: models.AlertStateSuppressed, + Fingerprint: "1", + Name: "am2", + Cluster: "fakeCluster", + SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, + Source: "https://am.example.com/graph", }, }, }, }, } - ag.DedupSharedMaps(nil) + shared, allLabels := ag.DedupSharedMaps(nil) + apiAG := models.NewAPIAlertGroup(ag, shared, allLabels, len(ag.Alerts)) - agJSON, _ := json.MarshalIndent(ag, "", " ") + agJSON, _ := json.MarshalIndent(apiAG, "", " ") abide.AssertReader(t, "SharedMaps", bytes.NewReader(agJSON)) } func TestDedupSharedMapsSingleGroup(t *testing.T) { - ag := models.APIAlertGroup{ - AlertGroup: models.AlertGroup{ - Alerts: models.AlertList{ - models.Alert{ - State: models.AlertStateActive, - Labels: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - }, - }, - models.Alert{ - State: models.AlertStateUnprocessed, - Labels: models.Labels{ - {Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - }, - }, + ag := models.AlertGroup{ + Alerts: models.AlertList{ + models.Alert{ + State: models.AlertStateActive, + Labels: labels.FromStrings("foo", "bar"), + }, + models.Alert{ + State: models.AlertStateUnprocessed, + Labels: labels.FromStrings("foo", "bar"), }, }, } - ag.DedupSharedMaps(nil) - if len(ag.Shared.Annotations) > 0 { - t.Errorf("Expected empty shared annotations, got %v", ag.Shared.Annotations) + shared, _ := ag.DedupSharedMaps(nil) + if len(shared.Annotations) > 0 { + t.Errorf("Expected empty shared annotations, got %v", shared.Annotations) } - if len(ag.Shared.Labels) == 0 { - t.Errorf("Expected non-empty shared labels, got %v", ag.Shared.Labels) + if shared.Labels.Len() == 0 { + t.Errorf("Expected non-empty shared labels, got %v", shared.Labels) } } func TestDedupSharedMapsWithSingleAlert(t *testing.T) { - ag := models.APIAlertGroup{ - AlertGroup: models.AlertGroup{ - Alerts: models.AlertList{ - models.Alert{}, - }, + ag := models.AlertGroup{ + Alerts: models.AlertList{ + models.Alert{}, }, } - ag.DedupSharedMaps(nil) - if len(ag.Shared.Annotations) > 0 { - t.Errorf("Expected empty shared annotations, got %v", ag.Shared.Annotations) + shared, _ := ag.DedupSharedMaps(nil) + if len(shared.Annotations) > 0 { + t.Errorf("Expected empty shared annotations, got %v", shared.Annotations) } - if len(ag.Shared.Labels) > 0 { - t.Errorf("Expected empty shared labels, got %v", ag.Shared.Labels) + if shared.Labels.Len() > 0 { + t.Errorf("Expected empty shared labels, got %v", shared.Labels) } } func TestDedupWithBadSource(t *testing.T) { - ag := models.APIAlertGroup{ - AlertGroup: models.AlertGroup{ - Alerts: models.AlertList{ - models.Alert{Alertmanager: []models.AlertmanagerInstance{{Source: "%gh&%ij"}}}, - models.Alert{Alertmanager: []models.AlertmanagerInstance{{Source: ""}}}, - }, + ag := models.AlertGroup{ + Alerts: models.AlertList{ + models.Alert{Alertmanager: []models.AlertmanagerInstance{{Source: "%gh&%ij"}}}, + models.Alert{Alertmanager: []models.AlertmanagerInstance{{Source: ""}}}, }, } - ag.DedupSharedMaps(nil) - if len(ag.Shared.Sources) > 0 { - t.Errorf("Expected empty sources list, got %v", ag.Shared.Sources) + shared, _ := ag.DedupSharedMaps(nil) + if len(shared.Sources) > 0 { + t.Errorf("Expected empty sources list, got %v", shared.Sources) } } func TestDedupSharedMapsWithDropNames(t *testing.T) { // verifies that passing dropNames to DedupSharedMaps removes those labels // from both group labels and alert labels - ag := models.APIAlertGroup{ - AlertGroup: models.AlertGroup{ - Receiver: models.NewUniqueString("default"), - Labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("TestAlert")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, + ag := models.AlertGroup{ + Receiver: "default", + Labels: labels.FromStrings("alertname", "TestAlert", "cluster", "prod"), + Alerts: models.AlertList{ + models.Alert{ + State: models.AlertStateActive, + Labels: labels.FromStrings("alertname", "TestAlert", "cluster", "prod", "instance", "1"), }, - Alerts: models.AlertList{ - models.Alert{ - State: models.AlertStateActive, - Labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("TestAlert")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("1")}, - }, - }, - models.Alert{ - State: models.AlertStateActive, - Labels: models.Labels{ - {Name: models.NewUniqueString("alertname"), Value: models.NewUniqueString("TestAlert")}, - {Name: models.NewUniqueString("cluster"), Value: models.NewUniqueString("prod")}, - {Name: models.NewUniqueString("instance"), Value: models.NewUniqueString("2")}, - }, - }, + models.Alert{ + State: models.AlertStateActive, + Labels: labels.FromStrings("alertname", "TestAlert", "cluster", "prod", "instance", "2"), }, }, } ag.DedupSharedMaps([]string{"cluster"}) // "cluster" should be removed from group labels - if ag.Labels.Get("cluster") != nil { + if ag.Labels.Get("cluster") != "" { t.Error("Expected 'cluster' label to be removed from group labels") } // "alertname" should remain in group labels - if ag.Labels.Get("alertname") == nil { + if ag.Labels.Get("alertname") == "" { t.Error("Expected 'alertname' label to remain in group labels") } // alert labels should not contain "cluster" (dropped) or "alertname" (shared with group) for i, alert := range ag.Alerts { - if alert.Labels.Get("cluster") != nil { + if alert.Labels.Get("cluster") != "" { t.Errorf("Alert[%d]: expected 'cluster' label to be removed", i) } - if alert.Labels.Get("alertname") != nil { + if alert.Labels.Get("alertname") != "" { t.Errorf("Alert[%d]: expected 'alertname' label to be removed (shared with group)", i) } } } +func TestAPIAlertGroupJSONFieldsNonNull(t *testing.T) { + type testCaseT struct { + desc string + ag models.AlertGroup + } + + testCases := []testCaseT{ + { + // verifies that a group with multiple alerts produces non-null alerts and labels + desc: "multiple alerts", + ag: models.AlertGroup{ + Receiver: "default", + Labels: labels.FromStrings("alertname", "Test"), + Alerts: models.AlertList{ + {State: models.AlertStateActive, Labels: labels.FromStrings("alertname", "Test", "instance", "1")}, + {State: models.AlertStateActive, Labels: labels.FromStrings("alertname", "Test", "instance", "2")}, + }, + }, + }, + { + // verifies that a group with a single alert produces non-null alerts and labels + desc: "single alert", + ag: models.AlertGroup{ + Receiver: "default", + Labels: labels.FromStrings("alertname", "Test"), + Alerts: models.AlertList{ + {State: models.AlertStateActive, Labels: labels.FromStrings("alertname", "Test")}, + }, + }, + }, + { + // verifies that a group with empty labels produces non-null labels array + desc: "empty labels", + ag: models.AlertGroup{ + Alerts: models.AlertList{ + {State: models.AlertStateActive, Labels: labels.EmptyLabels()}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + shared, allLabels := tc.ag.DedupSharedMaps(nil) + apiAG := models.NewAPIAlertGroup(tc.ag, shared, allLabels, len(tc.ag.Alerts)) + + b, err := json.Marshal(apiAG) + if err != nil { + t.Fatalf("json.Marshal failed: %v", err) + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(b, &raw); err != nil { + t.Fatalf("json.Unmarshal into raw map failed: %v", err) + } + + // alerts field must be a non-null JSON array + alertsRaw, ok := raw["alerts"] + if !ok { + t.Fatal("JSON output missing 'alerts' field") + } + if string(alertsRaw) == "null" { + t.Error("JSON 'alerts' field is null, expected a non-null array") + } + + // labels field must be a non-null JSON array + labelsRaw, ok := raw["labels"] + if !ok { + t.Fatal("JSON output missing 'labels' field") + } + if string(labelsRaw) == "null" { + t.Error("JSON 'labels' field is null, expected a non-null array") + } + + // shared.labels must also be non-null + var sharedRaw map[string]json.RawMessage + if err := json.Unmarshal(raw["shared"], &sharedRaw); err != nil { + t.Fatalf("json.Unmarshal shared failed: %v", err) + } + sharedLabelsRaw, ok := sharedRaw["labels"] + if !ok { + t.Fatal("JSON output missing 'shared.labels' field") + } + if string(sharedLabelsRaw) == "null" { + t.Error("JSON 'shared.labels' field is null, expected a non-null array") + } + }) + } +} + func TestCompareLabelValueStats(t *testing.T) { type testCaseT struct { a models.LabelValueStats diff --git a/internal/models/silence.go b/internal/models/silence.go index 2a68c169c..bf4789aaa 100644 --- a/internal/models/silence.go +++ b/internal/models/silence.go @@ -8,16 +8,16 @@ import ( ) type SilenceMatcher struct { - Name UniqueString `json:"name"` - Value string `json:"value"` - IsRegex bool `json:"isRegex"` - IsEqual bool `json:"isEqual"` re *regexp.Regexp + Name string `json:"name"` + Value string `json:"value"` + IsRegex bool `json:"isRegex"` + IsEqual bool `json:"isEqual"` } func NewSilenceMatcher(name, value string, isRegexp, isEqual bool) SilenceMatcher { sm := SilenceMatcher{ - Name: NewUniqueString(name), + Name: name, Value: value, IsRegex: isRegexp, IsEqual: isEqual, @@ -29,7 +29,7 @@ func NewSilenceMatcher(name, value string, isRegexp, isEqual bool) SilenceMatche } func (sm SilenceMatcher) IsMatch(labels map[string]string) bool { - v, ok := labels[sm.Name.Value()] + v, ok := labels[sm.Name] if !ok { return !sm.IsEqual } diff --git a/internal/transform/strip.go b/internal/transform/strip.go index 42fba1aa3..c7119e82b 100644 --- a/internal/transform/strip.go +++ b/internal/transform/strip.go @@ -5,6 +5,8 @@ import ( "slices" "strings" + "github.com/prometheus/prometheus/model/labels" + "github.com/prymitive/karma/internal/models" sliceutils "github.com/prymitive/karma/internal/slices" ) @@ -13,33 +15,28 @@ import ( // it takes the list of label keys to ignore and alert label map // it will return label map without labels found on the ignore list func StripLabels(keptLabels, ignoredLabels []string, keptLabelsRegex, ignoredLabelsRegex []*regexp.Regexp, - sourceLabels models.Labels, -) models.Labels { + sourceLabels labels.Labels, +) labels.Labels { // empty keep lists means keep everything by default keepAll := len(keptLabels) == 0 && len(keptLabelsRegex) == 0 // if we keep everything and there's nothing to strip then simply return source labels as-is if keepAll && len(ignoredLabels) == 0 && len(ignoredLabelsRegex) == 0 { return sourceLabels } - labels := make(models.Labels, 0, len(sourceLabels)) + b := labels.NewBuilder(labels.EmptyLabels()) var inKeep, inStrip bool - for _, label := range sourceLabels { + sourceLabels.Range(func(l labels.Label) { // is explicitly marked to be kept - inKeep = slices.Contains(keptLabels, label.Name.Value()) || sliceutils.MatchesAnyRegex(label.Name.Value(), keptLabelsRegex) + inKeep = slices.Contains(keptLabels, l.Name) || sliceutils.MatchesAnyRegex(l.Name, keptLabelsRegex) // is explicitly marked to be stripped - inStrip = slices.Contains(ignoredLabels, label.Name.Value()) || sliceutils.MatchesAnyRegex(label.Name.Value(), ignoredLabelsRegex) + inStrip = slices.Contains(ignoredLabels, l.Name) || sliceutils.MatchesAnyRegex(l.Name, ignoredLabelsRegex) if (keepAll || inKeep) && !inStrip { - l := models.Label{ - Name: label.Name, - // strip leading and trailing space in label value - // this is to normalize values in case space is added by Alertmanager rules - Value: models.NewUniqueString(strings.TrimSpace(label.Value.Value())), - } - labels = labels.Add(l) + // strip leading and trailing space in label value + // this is to normalize values in case space is added by Alertmanager rules + b.Set(l.Name, strings.TrimSpace(l.Value)) } - } - slices.SortFunc(labels, models.CompareLabels) - return labels + }) + return b.Labels() } // StripReceivers allows filtering all alerts for specified receiver(s) @@ -67,9 +64,9 @@ func StripAnnotations(keptAnnotations, ignoredAnnotations []string, sourceAnnota annotations := make(models.Annotations, 0, len(sourceAnnotations)) for _, annotation := range sourceAnnotations { // is explicitly marked to be kept - inKeep := slices.Contains(keptAnnotations, annotation.Name.Value()) + inKeep := slices.Contains(keptAnnotations, annotation.Name) // is explicitly marked to be stripped - inStrip := slices.Contains(ignoredAnnotations, annotation.Name.Value()) + inStrip := slices.Contains(ignoredAnnotations, annotation.Name) if (keepAll || inKeep) && !inStrip { annotations = append(annotations, annotation) } diff --git a/internal/transform/strip_test.go b/internal/transform/strip_test.go index feb2ff5ec..2c1ed8575 100644 --- a/internal/transform/strip_test.go +++ b/internal/transform/strip_test.go @@ -6,6 +6,8 @@ import ( "regexp" "testing" + "github.com/prometheus/prometheus/model/labels" + "github.com/prymitive/karma/internal/models" "github.com/prymitive/karma/internal/regex" "github.com/prymitive/karma/internal/transform" @@ -16,8 +18,8 @@ type stripLabelTest struct { keep []string stripRegex []string keepRegex []string - before models.Labels - after models.Labels + before labels.Labels + after labels.Labels } var stripLabelTests = []stripLabelTest{ @@ -26,153 +28,88 @@ var stripLabelTests = []stripLabelTest{ keep: []string{}, stripRegex: []string{}, keepRegex: []string{}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, + before: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), + after: labels.FromStrings("host", "localhost", "level", "info"), }, { strip: []string{"server"}, keep: []string{}, stripRegex: []string{}, keepRegex: []string{}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{ - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, + before: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), + after: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), }, { strip: []string{}, keep: []string{}, stripRegex: []string{}, keepRegex: []string{}, - before: models.Labels{ - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{ - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, + before: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), + after: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), }, { strip: []string{"host"}, keep: []string{}, stripRegex: []string{}, keepRegex: []string{}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - }, - after: models.Labels{}, + before: labels.FromStrings("host", "localhost"), + after: labels.EmptyLabels(), }, { strip: []string{}, keep: []string{"env"}, stripRegex: []string{}, keepRegex: []string{}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{ - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - }, + before: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), + after: labels.FromStrings("env", "production"), }, { strip: []string{"env"}, keep: []string{"host"}, stripRegex: []string{}, keepRegex: []string{}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - }, + before: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), + after: labels.FromStrings("host", "localhost"), }, { strip: []string{}, keep: []string{"env"}, stripRegex: []string{}, keepRegex: []string{}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{}, + before: labels.FromStrings("host", "localhost", "level", "info"), + after: labels.EmptyLabels(), }, { strip: []string{}, keep: []string{}, stripRegex: []string{".*e.*"}, keepRegex: []string{}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - }, + before: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), + after: labels.FromStrings("host", "localhost"), }, { strip: []string{}, keep: []string{}, stripRegex: []string{}, keepRegex: []string{".*e.*"}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{ - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, + before: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), + after: labels.FromStrings("env", "production", "level", "info"), }, { strip: []string{}, keep: []string{"env", "level"}, stripRegex: []string{".*el"}, keepRegex: []string{}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{ - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - }, + before: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), + after: labels.FromStrings("env", "production"), }, { strip: []string{"level"}, keep: []string{}, stripRegex: []string{}, keepRegex: []string{".*e.*"}, - before: models.Labels{ - {Name: models.NewUniqueString("host"), Value: models.NewUniqueString("localhost")}, - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - {Name: models.NewUniqueString("level"), Value: models.NewUniqueString("info")}, - }, - after: models.Labels{ - {Name: models.NewUniqueString("env"), Value: models.NewUniqueString("production")}, - }, + before: labels.FromStrings("env", "production", "host", "localhost", "level", "info"), + after: labels.FromStrings("env", "production"), }, } @@ -310,17 +247,17 @@ var stripAnnotationTests = []stripAnnotationTest{ strip: []string{}, keep: []string{}, before: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + models.Annotation{Name: "foo", Value: "bar"}, }, after: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + models.Annotation{Name: "foo", Value: "bar"}, }, }, { strip: []string{"foo"}, keep: []string{}, before: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + models.Annotation{Name: "foo", Value: "bar"}, }, after: models.Annotations{}, }, @@ -328,32 +265,32 @@ var stripAnnotationTests = []stripAnnotationTest{ strip: []string{"foo"}, keep: []string{}, before: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - models.Annotation{Name: models.NewUniqueString("boo"), Value: models.NewUniqueString("baz")}, + models.Annotation{Name: "foo", Value: "bar"}, + models.Annotation{Name: "boo", Value: "baz"}, }, after: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("boo"), Value: models.NewUniqueString("baz")}, + models.Annotation{Name: "boo", Value: "baz"}, }, }, { strip: []string{}, keep: []string{"foo"}, before: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + models.Annotation{Name: "foo", Value: "bar"}, }, after: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + models.Annotation{Name: "foo", Value: "bar"}, }, }, { strip: []string{}, keep: []string{"foo"}, before: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, - models.Annotation{Name: models.NewUniqueString("boo"), Value: models.NewUniqueString("baz")}, + models.Annotation{Name: "foo", Value: "bar"}, + models.Annotation{Name: "boo", Value: "baz"}, }, after: models.Annotations{ - models.Annotation{Name: models.NewUniqueString("foo"), Value: models.NewUniqueString("bar")}, + models.Annotation{Name: "foo", Value: "bar"}, }, }, }