fix(backend): use less memory

This commit is contained in:
Lukasz Mierzwa
2026-03-11 13:10:15 +00:00
committed by Łukasz Mierzwa
parent d985c9d8c5
commit f207da0f79
40 changed files with 1772 additions and 1856 deletions
+4 -4
View File
@@ -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) {
+51 -39
View File
@@ -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
+14 -46
View File
@@ -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},
},
}
+194 -303
View File
@@ -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)
}
}
}
+10 -11
View File
@@ -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,
)
}
}
+100 -82
View File
@@ -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)
}
+83 -184
View File
@@ -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)
}
}
+4 -2
View File
@@ -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
+12 -5
View File
@@ -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=
+16 -10
View File
@@ -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
}
+11 -9
View File
@@ -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()
+7 -17
View File
@@ -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")
}
+5 -8
View File
@@ -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
}
+13 -21
View File
@@ -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"},
+12 -10
View File
@@ -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
}
}
}
}
+12 -10
View File
@@ -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
}
}
}
}
+1 -1
View File
@@ -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) {
+12 -6
View File
@@ -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 {
+10 -8
View File
@@ -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
}
}
}
}
+41 -33
View File
@@ -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 {
+28 -24
View File
@@ -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
}
}
}
}
+10 -8
View File
@@ -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
}
}
}
}
+10 -8
View File
@@ -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
}
}
}
}
+10 -8
View File
@@ -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
}
}
}
}
+22 -15
View File
@@ -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
}
+23 -22
View File
@@ -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,
},
+5 -14
View File
@@ -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,
}
+157 -157
View File
@@ -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
}
+169 -90
View File
@@ -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)
}
+154 -147
View File
@@ -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"))
}
}
+18 -15
View File
@@ -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)
}
+38 -54
View File
@@ -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)
+8 -8
View File
@@ -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
+11 -11
View File
@@ -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),
+63 -68
View File
@@ -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)
}
}
+155 -105
View File
@@ -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 {
+221 -169
View File
@@ -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
+6 -6
View File
@@ -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
}
+15 -18
View File
@@ -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)
}
+37 -100
View File
@@ -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"},
},
},
}