mirror of
https://github.com/prymitive/karma
synced 2026-08-23 11:56:20 +00:00
fix(backend): use less memory
This commit is contained in:
committed by
Łukasz Mierzwa
parent
d985c9d8c5
commit
f207da0f79
+4
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user