fix(api): use slices for labels instead of maps

This commit is contained in:
Łukasz Mierzwa
2021-10-29 17:18:15 +01:00
committed by Łukasz Mierzwa
parent 7d24a7ece0
commit 8f7cca40cc
20 changed files with 746 additions and 506 deletions
+12 -12
View File
@@ -141,14 +141,14 @@ func resolveLabelValue(name, value string) string {
}
func getGroupLabel(group *models.APIAlertGroup, label string) string {
if v, found := group.Labels[label]; found {
return resolveLabelValue(label, v)
if v := group.Labels.Get(label); v != nil {
return resolveLabelValue(label, v.Value)
}
if v, found := group.Shared.Labels[label]; found {
return resolveLabelValue(label, v)
if v := group.Shared.Labels.Get(label); v != nil {
return resolveLabelValue(label, v.Value)
}
if v, found := group.Alerts[0].Labels[label]; found {
return resolveLabelValue(label, v)
if v := group.Alerts[0].Labels.Get(label); v != nil {
return resolveLabelValue(label, v.Value)
}
return ""
}
@@ -283,14 +283,14 @@ func autoGridLabel(dedupedAlerts []models.AlertGroup) string {
for _, ag := range dedupedAlerts {
alertsCount += ag.Alerts.Len()
for _, alert := range ag.Alerts {
for key, val := range alert.Labels {
if _, ok := labelToAlertCount[key]; !ok {
labelToAlertCount[key] = map[string]int{}
for _, l := range alert.Labels {
if _, ok := labelToAlertCount[l.Name]; !ok {
labelToAlertCount[l.Name] = map[string]int{}
}
if _, ok := labelToAlertCount[key][val]; !ok {
labelToAlertCount[key][val] = 0
if _, ok := labelToAlertCount[l.Name][l.Value]; !ok {
labelToAlertCount[l.Name][l.Value] = 0
}
labelToAlertCount[key][val]++
labelToAlertCount[l.Name][l.Value]++
}
}
}
+127 -125
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
@@ -18,7 +19,7 @@ import (
)
type groupTest struct {
labels map[string]string
labels models.Labels
receiver string
alerts []models.Alert
id string
@@ -28,8 +29,8 @@ type groupTest struct {
var groupTests = []groupTest{
{
receiver: "by-name",
labels: map[string]string{
"alertname": "Memory_Usage_Too_High",
labels: models.Labels{
{Name: "alertname", Value: "Memory_Usage_Too_High"},
},
alerts: []models.Alert{
{
@@ -38,10 +39,10 @@ var groupTests = []groupTest{
models.Annotation{Visible: true, Name: "alert", Value: "Memory usage exceeding threshold"},
models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true},
},
Labels: map[string]string{
"cluster": "prod",
"instance": "server2",
"job": "node_exporter",
Labels: models.Labels{
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server2"},
{Name: "job", Value: "node_exporter"},
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -64,9 +65,9 @@ var groupTests = []groupTest{
},
{
receiver: "by-cluster-service",
labels: map[string]string{
"alertname": "Memory_Usage_Too_High",
"cluster": "prod",
labels: models.Labels{
{Name: "alertname", Value: "Memory_Usage_Too_High"},
{Name: "cluster", Value: "prod"},
},
alerts: []models.Alert{
{
@@ -83,9 +84,9 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"instance": "server2",
"job": "node_exporter",
Labels: models.Labels{
{Name: "instance", Value: "server2"},
{Name: "job", Value: "node_exporter"},
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -100,9 +101,9 @@ var groupTests = []groupTest{
},
{
receiver: "by-cluster-service",
labels: map[string]string{
"alertname": "Host_Down",
"cluster": "staging",
labels: models.Labels{
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "staging"},
},
alerts: []models.Alert{
{
@@ -116,9 +117,9 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"instance": "server3",
"ip": "127.0.0.3",
Labels: models.Labels{
{Name: "instance", Value: "server3"},
{Name: "ip", Value: "127.0.0.3"},
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -133,9 +134,9 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"instance": "server4",
"ip": "127.0.0.4",
Labels: models.Labels{
{Name: "instance", Value: "server4"},
{Name: "ip", Value: "127.0.0.4"},
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -150,9 +151,9 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"instance": "server5",
"ip": "127.0.0.5",
Labels: models.Labels{
{Name: "instance", Value: "server5"},
{Name: "ip", Value: "127.0.0.5"},
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -167,9 +168,9 @@ var groupTests = []groupTest{
},
{
receiver: "by-cluster-service",
labels: map[string]string{
"alertname": "Host_Down",
"cluster": "dev",
labels: models.Labels{
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "dev"},
},
alerts: []models.Alert{
{
@@ -183,9 +184,9 @@ var groupTests = []groupTest{
SilencedBy: []string{"168f139d-77e4-41d6-afb5-8fe2cfd0cc9d"},
},
},
Labels: map[string]string{
"instance": "server6",
"ip": "127.0.0.6",
Labels: models.Labels{
{Name: "instance", Value: "server6"},
{Name: "ip", Value: "127.0.0.6"},
},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
@@ -201,9 +202,9 @@ var groupTests = []groupTest{
SilencedBy: []string{"168f139d-77e4-41d6-afb5-8fe2cfd0cc9d", "378eaa69-097d-41c4-a8c2-fe6568c3abfc"},
},
},
Labels: map[string]string{
"instance": "server7",
"ip": "127.0.0.7",
Labels: models.Labels{
{Name: "instance", Value: "server7"},
{Name: "ip", Value: "127.0.0.7"},
},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
@@ -219,9 +220,9 @@ var groupTests = []groupTest{
SilencedBy: []string{"168f139d-77e4-41d6-afb5-8fe2cfd0cc9d"},
},
},
Labels: map[string]string{
"instance": "server8",
"ip": "127.0.0.8",
Labels: models.Labels{
{Name: "instance", Value: "server8"},
{Name: "ip", Value: "127.0.0.8"},
},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
@@ -236,8 +237,8 @@ var groupTests = []groupTest{
},
{
receiver: "by-name",
labels: map[string]string{
"alertname": "Host_Down",
labels: models.Labels{
{Name: "alertname", Value: "Host_Down"},
},
alerts: []models.Alert{
{
@@ -245,10 +246,10 @@ var groupTests = []groupTest{
Annotations: models.Annotations{
models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true},
},
Labels: map[string]string{
"cluster": "prod",
"instance": "server1",
"ip": "127.0.0.1",
Labels: models.Labels{
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server1"},
{Name: "ip", Value: "127.0.0.1"},
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -264,10 +265,10 @@ var groupTests = []groupTest{
{
StartsAt: time.Date(2019, time.January, 1, 0, 1, 0, 0, time.UTC),
Annotations: models.Annotations{},
Labels: map[string]string{
"cluster": "prod",
"instance": "server2",
"ip": "127.0.0.2",
Labels: models.Labels{
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server2"},
{Name: "ip", Value: "127.0.0.2"},
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -283,10 +284,10 @@ var groupTests = []groupTest{
{
StartsAt: time.Date(2019, time.January, 1, 0, 1, 0, 1, time.UTC),
Annotations: models.Annotations{},
Labels: map[string]string{
"cluster": "staging",
"instance": "server3",
"ip": "127.0.0.3",
Labels: models.Labels{
{Name: "cluster", Value: "staging"},
{Name: "instance", Value: "server3"},
{Name: "ip", Value: "127.0.0.3"},
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -302,10 +303,10 @@ var groupTests = []groupTest{
{
StartsAt: time.Date(2019, time.January, 1, 0, 0, 59, 0, time.UTC),
Annotations: models.Annotations{},
Labels: map[string]string{
"cluster": "staging",
"instance": "server4",
"ip": "127.0.0.4",
Labels: models.Labels{
{Name: "cluster", Value: "staging"},
{Name: "instance", Value: "server4"},
{Name: "ip", Value: "127.0.0.4"},
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -321,10 +322,10 @@ var groupTests = []groupTest{
{
StartsAt: time.Date(2019, time.January, 10, 0, 0, 0, 0, time.UTC),
Annotations: models.Annotations{},
Labels: map[string]string{
"cluster": "staging",
"instance": "server5",
"ip": "127.0.0.5",
Labels: models.Labels{
{Name: "cluster", Value: "staging"},
{Name: "instance", Value: "server5"},
{Name: "ip", Value: "127.0.0.5"},
},
State: models.AlertStateActive,
Alertmanager: []models.AlertmanagerInstance{
@@ -340,10 +341,10 @@ var groupTests = []groupTest{
{
StartsAt: time.Date(2019, time.January, 10, 1, 0, 0, 0, time.UTC),
Annotations: models.Annotations{},
Labels: map[string]string{
"cluster": "dev",
"instance": "server6",
"ip": "127.0.0.6",
Labels: models.Labels{
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "server6"},
{Name: "ip", Value: "127.0.0.6"},
},
State: models.AlertStateSuppressed,
Alertmanager: []models.AlertmanagerInstance{
@@ -359,10 +360,10 @@ var groupTests = []groupTest{
{
StartsAt: time.Date(2019, time.January, 10, 0, 20, 0, 0, time.UTC),
Annotations: models.Annotations{},
Labels: map[string]string{
"cluster": "dev",
"instance": "server7",
"ip": "127.0.0.7",
Labels: models.Labels{
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "server7"},
{Name: "ip", Value: "127.0.0.7"},
},
State: models.AlertStateSuppressed,
Alertmanager: []models.AlertmanagerInstance{
@@ -378,10 +379,10 @@ var groupTests = []groupTest{
{
StartsAt: time.Date(2019, time.January, 10, 0, 21, 0, 0, time.UTC),
Annotations: models.Annotations{},
Labels: map[string]string{
"cluster": "dev",
"instance": "server8",
"ip": "127.0.0.8",
Labels: models.Labels{
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "server8"},
{Name: "ip", Value: "127.0.0.8"},
},
State: models.AlertStateSuppressed,
Alertmanager: []models.AlertmanagerInstance{
@@ -404,9 +405,9 @@ var groupTests = []groupTest{
},
{
receiver: "by-cluster-service",
labels: map[string]string{
"alertname": "Free_Disk_Space_Too_Low",
"cluster": "staging",
labels: models.Labels{
{Name: "alertname", Value: "Free_Disk_Space_Too_Low"},
{Name: "cluster", Value: "staging"},
},
alerts: []models.Alert{
{
@@ -423,10 +424,10 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"instance": "server5",
"job": "node_exporter",
"disk": "sda",
Labels: models.Labels{
{Name: "disk", Value: "sda"},
{Name: "instance", Value: "server5"},
{Name: "job", Value: "node_exporter"},
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -441,9 +442,9 @@ var groupTests = []groupTest{
},
{
receiver: "by-cluster-service",
labels: map[string]string{
"alertname": "Host_Down",
"cluster": "prod",
labels: models.Labels{
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "prod"},
},
alerts: []models.Alert{
{
@@ -459,9 +460,9 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"instance": "server1",
"ip": "127.0.0.1",
Labels: models.Labels{
{Name: "instance", Value: "server1"},
{Name: "ip", Value: "127.0.0.1"},
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -476,9 +477,9 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"instance": "server2",
"ip": "127.0.0.2",
Labels: models.Labels{
{Name: "instance", Value: "server2"},
{Name: "ip", Value: "127.0.0.2"},
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -493,8 +494,8 @@ var groupTests = []groupTest{
},
{
receiver: "by-name",
labels: map[string]string{
"alertname": "HTTP_Probe_Failed",
labels: models.Labels{
{Name: "alertname", Value: "HTTP_Probe_Failed"},
},
alerts: []models.Alert{
{
@@ -510,8 +511,8 @@ var groupTests = []groupTest{
Source: "http://localhost/prometheus",
},
},
Labels: map[string]string{
"instance": "web1",
Labels: models.Labels{
{Name: "instance", Value: "web1"},
},
State: models.AlertStateSuppressed,
Receiver: "by-name",
@@ -527,8 +528,8 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"instance": "web2",
Labels: models.Labels{
{Name: "instance", Value: "web2"},
},
State: models.AlertStateActive,
Receiver: "by-name",
@@ -543,8 +544,8 @@ var groupTests = []groupTest{
},
{
receiver: "by-name",
labels: map[string]string{
"alertname": "Free_Disk_Space_Too_Low",
labels: models.Labels{
{Name: "alertname", Value: "Free_Disk_Space_Too_Low"},
},
alerts: []models.Alert{
{
@@ -561,11 +562,11 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"cluster": "staging",
"instance": "server5",
"job": "node_exporter",
"disk": "sda",
Labels: models.Labels{
{Name: "cluster", Value: "staging"},
{Name: "disk", Value: "sda"},
{Name: "instance", Value: "server5"},
{Name: "job", Value: "node_exporter"},
},
State: models.AlertStateActive,
Receiver: "by-name",
@@ -580,9 +581,9 @@ var groupTests = []groupTest{
},
{
receiver: "by-cluster-service",
labels: map[string]string{
"alertname": "HTTP_Probe_Failed",
"cluster": "dev",
labels: models.Labels{
{Name: "alertname", Value: "HTTP_Probe_Failed"},
{Name: "cluster", Value: "dev"},
},
alerts: []models.Alert{
{
@@ -599,8 +600,8 @@ var groupTests = []groupTest{
SilencedBy: []string{"0804764c-6163-4c64-b0a9-08feebe2db4b"},
},
},
Labels: map[string]string{
"instance": "web1",
Labels: models.Labels{
{Name: "instance", Value: "web1"},
},
State: models.AlertStateSuppressed,
Receiver: "by-cluster-service",
@@ -616,8 +617,8 @@ var groupTests = []groupTest{
SilencedBy: []string{},
},
},
Labels: map[string]string{
"instance": "web2",
Labels: models.Labels{
{Name: "instance", Value: "web2"},
},
State: models.AlertStateActive,
Receiver: "by-cluster-service",
@@ -641,12 +642,12 @@ func compareAlertGroups(testCase groupTest, group models.APIAlertGroup) bool {
if len(testCase.labels) != len(group.Labels) {
return false
}
for key, val := range testCase.labels {
v, found := group.Labels[key]
if !found {
for _, l := range testCase.labels {
v := group.Labels.Get(l.Name)
if v == nil {
return false
}
if val != v {
if l.Value != v.Value {
return false
}
}
@@ -660,12 +661,12 @@ func compareAlerts(expectedAlert, gotAlert models.Alert) bool {
if len(gotAlert.Labels) != len(expectedAlert.Labels) {
return false
}
for key, val := range expectedAlert.Labels {
v, found := gotAlert.Labels[key]
if !found {
for _, l := range expectedAlert.Labels {
v := gotAlert.Labels.Get(l.Name)
if v == nil {
return false
}
if val != v {
if l.Value != v.Value {
return false
}
}
@@ -1000,22 +1001,23 @@ func TestSortOrder(t *testing.T) {
} else {
values := []string{}
for _, ag := range ur.Grids[0].AlertGroups {
v := ag.Labels[testCase.expectedLabel]
v := ag.Labels.GetValue(testCase.expectedLabel)
if v == "" {
v = ag.Shared.Labels[testCase.expectedLabel]
v = ag.Shared.Labels.GetValue(testCase.expectedLabel)
}
if v != "" {
values = append(values, v)
} else {
for _, alert := range ag.Alerts {
v = alert.Labels[testCase.expectedLabel]
v = alert.Labels.GetValue(testCase.expectedLabel)
values = append(values, v)
}
}
}
if diff := cmp.Diff(testCase.expectedValues, values); diff != "" {
t.Errorf("Incorrectly sorted values (-want +got):\n%s", diff)
t.Errorf("Incorrectly sorted values for filter=%q order=%s label=%q reverse=%v (-want +got):\n%s",
strings.Join(testCase.filter, " "), testCase.sortOrder, testCase.sortLabel, testCase.sortReverse, diff)
}
}
}
@@ -1023,22 +1025,22 @@ func TestSortOrder(t *testing.T) {
}
}
func verifyStrippedLabels(t *testing.T, labels map[string]string, keep, strip []string) {
func verifyStrippedLabels(t *testing.T, labels models.Labels, keep, strip []string) {
for _, l := range strip {
if val, ok := labels[l]; ok {
t.Errorf("Found stripped label %s=%s on %v", l, val, labels)
if val := labels.Get(l); val != nil {
t.Errorf("Found stripped label %s=%s on %v", val.Name, val.Value, labels)
}
}
if len(keep) > 0 && len(strip) == 0 {
for k, v := range labels {
for _, ll := range labels {
ok := false
for _, l := range keep {
if k == l {
if ll.Name == l {
ok = true
}
}
if !ok {
t.Errorf("Found label %s=%s that's not on the keep list: %v", k, v, keep)
t.Errorf("Found label %s=%s that's not on the keep list: %v", ll.Name, ll.Value, keep)
}
}
}
+32 -29
View File
@@ -224,7 +224,6 @@ func alerts(w http.ResponseWriter, r *http.Request) {
return
}
// use full URI (including query args) as cache key
cacheKey := fmt.Sprintf("%x", structhash.Sha1(request, 1))
data, found := apiCache.Get(cacheKey)
@@ -265,14 +264,14 @@ func alerts(w http.ResponseWriter, r *http.Request) {
for _, ag := range filtered {
perGridAlertGroup := map[string]*models.AlertGroup{}
for k := range ag.Labels {
labelMap[k] = struct{}{}
for _, l := range ag.Labels {
labelMap[l.Name] = struct{}{}
}
for _, alert := range ag.Alerts {
alert := alert // scopelint pin
for k := range alert.Labels {
labelMap[k] = struct{}{}
for _, l := range alert.Labels {
labelMap[l.Name] = struct{}{}
}
allReceivers[alert.Receiver] = true
@@ -292,7 +291,7 @@ func alerts(w http.ResponseWriter, r *http.Request) {
alertGridLabelValues[am.Cluster] = true
}
default:
alertGridLabelValues[alert.Labels[gridLabel]] = true
alertGridLabelValues[alert.Labels.GetValue(gridLabel)] = true
}
for alertGridLabelValue := range alertGridLabelValues {
@@ -390,13 +389,13 @@ func alerts(w http.ResponseWriter, r *http.Request) {
}
}
for key, value := range alert.Labels {
if keyMap, foundKey := dedupedColors[key]; foundKey {
if color, foundColor := keyMap[value]; foundColor {
if _, found := colors[key]; !found {
colors[key] = map[string]models.LabelColors{}
for _, l := range alert.Labels {
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[key][value] = color
colors[l.Name][l.Value] = color
}
}
}
@@ -684,7 +683,7 @@ func silences(w http.ResponseWriter, r *http.Request) {
}
type AlertList struct {
Alerts []map[string]string `json:"alerts"`
Alerts []models.Labels `json:"alerts"`
}
func alertList(w http.ResponseWriter, r *http.Request) {
@@ -711,12 +710,9 @@ func alertList(w http.ResponseWriter, r *http.Request) {
labelMap := map[string]map[string]string{}
for _, ag := range filtered {
for _, alert := range ag.Alerts {
labels := map[string]string{}
for k, v := range ag.Labels {
labels[k] = v
}
for k, v := range alert.Labels {
labels[k] = v
labels := ag.Labels.Map()
for _, l := range alert.Labels {
labels[l.Name] = l.Value
}
h := fmt.Sprintf("%x", structhash.Sha1(labels, 1))
labelMap[h] = labels
@@ -736,10 +732,15 @@ func alertList(w http.ResponseWriter, r *http.Request) {
sort.Strings(sortKeys)
al := AlertList{
Alerts: []map[string]string{},
Alerts: []models.Labels{},
}
for _, labels := range labelMap {
al.Alerts = append(al.Alerts, labels)
alert := models.Labels{}
for k, v := range labels {
alert = alert.Set(k, v)
}
sort.Sort(alert)
al.Alerts = append(al.Alerts, alert)
}
sortSliceOfLabels(al.Alerts, sortKeys, "alertname")
@@ -751,20 +752,22 @@ func alertList(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(data)
}
func sortSliceOfLabels(labels []map[string]string, sortKeys []string, fallback string) {
func sortSliceOfLabels(labels []models.Labels, sortKeys []string, fallback string) {
sort.SliceStable(labels, func(i, j int) bool {
for _, k := range sortKeys {
if labels[i][k] != "" && labels[j][k] == "" {
vi := labels[i].GetValue(k)
vj := labels[j].GetValue(k)
if vi != "" && vj == "" {
return true
}
if labels[i][k] == "" && labels[j][k] != "" {
if vi == "" && vj != "" {
return false
}
if labels[i][k] != labels[j][k] {
return sortorder.NaturalLess(labels[i][k], labels[j][k])
if vi != vj {
return sortorder.NaturalLess(vi, vj)
}
}
return sortorder.NaturalLess(labels[i][fallback], labels[j][fallback])
return sortorder.NaturalLess(labels[i].GetValue(fallback), labels[j].GetValue(fallback))
})
}
@@ -814,8 +817,8 @@ func counters(w http.ResponseWriter, r *http.Request) {
}
countLabel(counters, "@state", alert.State)
countLabel(counters, "@receiver", alert.Receiver)
for key, value := range alert.Labels {
countLabel(counters, key, value)
for _, l := range alert.Labels {
countLabel(counters, l.Name, l.Value)
}
}
}
+135 -135
View File
@@ -3474,87 +3474,87 @@ func TestAlertList(t *testing.T) {
{
args: "",
alerts: AlertList{
Alerts: []map[string]string{
Alerts: []models.Labels{
{
"alertname": "Free_Disk_Space_Too_Low",
"cluster": "staging",
"disk": "sda",
"instance": "server5",
"job": "node_exporter",
{Name: "alertname", Value: "Free_Disk_Space_Too_Low"},
{Name: "cluster", Value: "staging"},
{Name: "disk", Value: "sda"},
{Name: "instance", Value: "server5"},
{Name: "job", Value: "node_exporter"},
},
{
"alertname": "HTTP_Probe_Failed",
"cluster": "dev",
"instance": "web1",
"job": "node_exporter",
{Name: "alertname", Value: "HTTP_Probe_Failed"},
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "web1"},
{Name: "job", Value: "node_exporter"},
},
{
"alertname": "HTTP_Probe_Failed",
"cluster": "dev",
"instance": "web2",
"job": "node_exporter",
{Name: "alertname", Value: "HTTP_Probe_Failed"},
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "web2"},
{Name: "job", Value: "node_exporter"},
},
{
"alertname": "Host_Down",
"cluster": "dev",
"instance": "server6",
"ip": "127.0.0.6",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "server6"},
{Name: "ip", Value: "127.0.0.6"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Host_Down",
"cluster": "dev",
"instance": "server7",
"ip": "127.0.0.7",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "server7"},
{Name: "ip", Value: "127.0.0.7"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Host_Down",
"cluster": "dev",
"instance": "server8",
"ip": "127.0.0.8",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "server8"},
{Name: "ip", Value: "127.0.0.8"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Host_Down",
"cluster": "prod",
"instance": "server1",
"ip": "127.0.0.1",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server1"},
{Name: "ip", Value: "127.0.0.1"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Host_Down",
"cluster": "prod",
"instance": "server2",
"ip": "127.0.0.2",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server2"},
{Name: "ip", Value: "127.0.0.2"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Host_Down",
"cluster": "staging",
"instance": "server3",
"ip": "127.0.0.3",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "staging"},
{Name: "instance", Value: "server3"},
{Name: "ip", Value: "127.0.0.3"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Host_Down",
"cluster": "staging",
"instance": "server4",
"ip": "127.0.0.4",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "staging"},
{Name: "instance", Value: "server4"},
{Name: "ip", Value: "127.0.0.4"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Host_Down",
"cluster": "staging",
"instance": "server5",
"ip": "127.0.0.5",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "staging"},
{Name: "instance", Value: "server5"},
{Name: "ip", Value: "127.0.0.5"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Memory_Usage_Too_High",
"cluster": "prod",
"instance": "server2",
"job": "node_exporter",
{Name: "alertname", Value: "Memory_Usage_Too_High"},
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server2"},
{Name: "job", Value: "node_exporter"},
},
},
},
@@ -3562,13 +3562,13 @@ func TestAlertList(t *testing.T) {
{
args: "q=alertname=Free_Disk_Space_Too_Low",
alerts: AlertList{
Alerts: []map[string]string{
Alerts: []models.Labels{
{
"alertname": "Free_Disk_Space_Too_Low",
"cluster": "staging",
"disk": "sda",
"instance": "server5",
"job": "node_exporter",
{Name: "alertname", Value: "Free_Disk_Space_Too_Low"},
{Name: "cluster", Value: "staging"},
{Name: "disk", Value: "sda"},
{Name: "instance", Value: "server5"},
{Name: "job", Value: "node_exporter"},
},
},
},
@@ -3576,18 +3576,18 @@ func TestAlertList(t *testing.T) {
{
args: "q=alertname=HTTP_Probe_Failed",
alerts: AlertList{
Alerts: []map[string]string{
Alerts: []models.Labels{
{
"alertname": "HTTP_Probe_Failed",
"cluster": "dev",
"instance": "web1",
"job": "node_exporter",
{Name: "alertname", Value: "HTTP_Probe_Failed"},
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "web1"},
{Name: "job", Value: "node_exporter"},
},
{
"alertname": "HTTP_Probe_Failed",
"cluster": "dev",
"instance": "web2",
"job": "node_exporter",
{Name: "alertname", Value: "HTTP_Probe_Failed"},
{Name: "cluster", Value: "dev"},
{Name: "instance", Value: "web2"},
{Name: "job", Value: "node_exporter"},
},
},
},
@@ -3595,19 +3595,19 @@ func TestAlertList(t *testing.T) {
{
args: "q=instance=server2",
alerts: AlertList{
Alerts: []map[string]string{
Alerts: []models.Labels{
{
"alertname": "Host_Down",
"cluster": "prod",
"instance": "server2",
"ip": "127.0.0.2",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server2"},
{Name: "ip", Value: "127.0.0.2"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Memory_Usage_Too_High",
"cluster": "prod",
"instance": "server2",
"job": "node_exporter",
{Name: "alertname", Value: "Memory_Usage_Too_High"},
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server2"},
{Name: "job", Value: "node_exporter"},
},
},
},
@@ -3615,20 +3615,20 @@ func TestAlertList(t *testing.T) {
{
args: "q=alertname=Host_Down&q=cluster=prod",
alerts: AlertList{
Alerts: []map[string]string{
Alerts: []models.Labels{
{
"alertname": "Host_Down",
"cluster": "prod",
"instance": "server1",
"ip": "127.0.0.1",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server1"},
{Name: "ip", Value: "127.0.0.1"},
{Name: "job", Value: "node_ping"},
},
{
"alertname": "Host_Down",
"cluster": "prod",
"instance": "server2",
"ip": "127.0.0.2",
"job": "node_ping",
{Name: "alertname", Value: "Host_Down"},
{Name: "cluster", Value: "prod"},
{Name: "instance", Value: "server2"},
{Name: "ip", Value: "127.0.0.2"},
{Name: "job", Value: "node_ping"},
},
},
},
@@ -3636,7 +3636,7 @@ func TestAlertList(t *testing.T) {
{
args: "q=foo=bar",
alerts: AlertList{
Alerts: []map[string]string{},
Alerts: []models.Labels{},
},
},
}
@@ -3675,85 +3675,85 @@ func TestAlertList(t *testing.T) {
func TestSortSliceOfLabels(t *testing.T) {
type testCaseT struct {
labels []map[string]string
labels []models.Labels
sortKeys []string
fallback string
output []map[string]string
output []models.Labels
}
testCases := []testCaseT{
{
labels: []map[string]string{
{"alertname": "alert2"},
{"alertname": "alert1"},
labels: []models.Labels{
{{Name: "alertname", Value: "alert2"}},
{{Name: "alertname", Value: "alert1"}},
},
sortKeys: []string{},
fallback: "",
output: []map[string]string{
{"alertname": "alert2"},
{"alertname": "alert1"},
output: []models.Labels{
{{Name: "alertname", Value: "alert2"}},
{{Name: "alertname", Value: "alert1"}},
},
},
{
labels: []map[string]string{
{"alertname": "alert2"},
{"alertname": "alert1"},
labels: []models.Labels{
{{Name: "alertname", Value: "alert2"}},
{{Name: "alertname", Value: "alert1"}},
},
sortKeys: []string{"alertname"},
fallback: "alertname",
output: []map[string]string{
{"alertname": "alert1"},
{"alertname": "alert2"},
output: []models.Labels{
{{Name: "alertname", Value: "alert1"}},
{{Name: "alertname", Value: "alert2"}},
},
},
{
labels: []map[string]string{
{"alertname": "alert2"},
{"alertname": "alert1"},
labels: []models.Labels{
{{Name: "alertname", Value: "alert2"}},
{{Name: "alertname", Value: "alert1"}},
},
sortKeys: []string{},
fallback: "alertname",
output: []map[string]string{
{"alertname": "alert1"},
{"alertname": "alert2"},
output: []models.Labels{
{{Name: "alertname", Value: "alert1"}},
{{Name: "alertname", Value: "alert2"}},
},
},
{
labels: []map[string]string{
{"alertname": "alert2"},
{"alertname": "alert1"},
labels: []models.Labels{
{{Name: "alertname", Value: "alert2"}},
{{Name: "alertname", Value: "alert1"}},
},
sortKeys: []string{"foo"},
fallback: "alertname",
output: []map[string]string{
{"alertname": "alert1"},
{"alertname": "alert2"},
output: []models.Labels{
{{Name: "alertname", Value: "alert1"}},
{{Name: "alertname", Value: "alert2"}},
},
},
{
labels: []map[string]string{
{"alertname": "alert1"},
{"alertname": "alert1"},
labels: []models.Labels{
{{Name: "alertname", Value: "alert1"}},
{{Name: "alertname", Value: "alert1"}},
},
sortKeys: []string{"alertname"},
fallback: "alertname",
output: []map[string]string{
{"alertname": "alert1"},
{"alertname": "alert1"},
output: []models.Labels{
{{Name: "alertname", Value: "alert1"}},
{{Name: "alertname", Value: "alert1"}},
},
},
{
labels: []map[string]string{
{"alertname": "alert2", "job": "a"},
{"alertname": "alert1"},
{"alertname": "alert3", "job": "b"},
labels: []models.Labels{
{{Name: "alertname", Value: "alert2"}, {Name: "job", Value: "a"}},
{{Name: "alertname", Value: "alert1"}},
{{Name: "alertname", Value: "alert3"}, {Name: "job", Value: "b"}},
},
sortKeys: []string{"job"},
fallback: "alertname",
output: []map[string]string{
{"alertname": "alert2", "job": "a"},
{"alertname": "alert3", "job": "b"},
{"alertname": "alert1"},
output: []models.Labels{
{{Name: "alertname", Value: "alert2"}, {Name: "job", Value: "a"}},
{{Name: "alertname", Value: "alert3"}, {Name: "job", Value: "b"}},
{{Name: "alertname", Value: "alert1"}},
},
},
}
+3 -2
View File
@@ -236,8 +236,9 @@ func DedupKnownLabelValues(name string) []string {
for _, am := range upstreams {
for _, ag := range am.Alerts() {
for _, alert := range ag.Alerts {
if val, found := alert.Labels[name]; found {
dedupedValues[val] = true
if v := alert.Labels.Get(name); v != nil {
dedupedValues[v.Value] = true
}
}
}
+4 -4
View File
@@ -253,8 +253,8 @@ func (am *Alertmanager) pullAlerts(version string) error {
if _, found := uniqueAlerts[agID][alertCFP]; !found {
uniqueAlerts[agID][alertCFP] = alert
}
for key := range alert.Labels {
knownLabelsMap[key] = true
for _, l := range alert.Labels {
knownLabelsMap[l.Name] = true
}
if name, hc := am.IsHealthCheckAlert(&alert); hc != nil {
@@ -305,8 +305,8 @@ func (am *Alertmanager) pullAlerts(version string) error {
transform.ColorLabel(colors, "@alertmanager", am.Name)
transform.ColorLabel(colors, "@cluster", am.Cluster)
}
for k, v := range alert.Labels {
transform.ColorLabel(colors, k, v)
for _, l := range alert.Labels {
transform.ColorLabel(colors, l.Name, l.Value)
}
alert.UpdateFingerprints()
+6 -6
View File
@@ -31,9 +31,9 @@ var acTests = []acTest{
Alerts: []models.Alert{
{
State: models.AlertStateActive,
Labels: map[string]string{
"foo": "bar",
"number": "1",
Labels: models.Labels{
{Name: "foo", Value: "bar"},
{Name: "number", Value: "1"},
},
Receiver: "default",
Alertmanager: []models.AlertmanagerInstance{
@@ -43,9 +43,9 @@ var acTests = []acTest{
},
{
State: models.AlertStateSuppressed,
Labels: map[string]string{
"foo": "bar baz",
"number": "5",
Labels: models.Labels{
{Name: "foo", Value: "bar baz"},
{Name: "number", Value: "5"},
},
Receiver: "not default",
Alertmanager: []models.AlertmanagerInstance{
+2 -2
View File
@@ -33,8 +33,8 @@ func (filter *fuzzyFilter) Match(alert *models.Alert, matches int) bool {
}
}
for _, val := range alert.Labels {
if filter.Matcher.Compare(val, filter.Value) {
for _, l := range alert.Labels {
if filter.Matcher.Compare(l.Value, filter.Value) {
filter.Hits++
return true
}
+16 -16
View File
@@ -14,7 +14,7 @@ type labelFilter struct {
func (filter *labelFilter) Match(alert *models.Alert, matches int) bool {
if filter.IsValid {
isMatch := filter.Matcher.Compare(alert.Labels[filter.Matched], filter.Value)
isMatch := filter.Matcher.Compare(alert.Labels.GetValue(filter.Matched), filter.Value)
if isMatch {
filter.Hits++
}
@@ -32,44 +32,44 @@ func newLabelFilter() FilterT {
func labelAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
tokens := map[string]models.Autocomplete{}
for _, alert := range alerts {
for key, value := range alert.Labels {
for _, l := range alert.Labels {
for _, operator := range operators {
switch operator {
case equalOperator, notEqualOperator:
token := fmt.Sprintf("%s%s%s", key, operator, value)
token := fmt.Sprintf("%s%s%s", l.Name, operator, l.Value)
tokens[token] = makeAC(
token,
[]string{
key,
fmt.Sprintf("%s%s", key, operator),
value,
l.Name,
fmt.Sprintf("%s%s", l.Name, operator),
l.Value,
},
)
case regexpOperator, negativeRegexOperator:
substrings := strings.Split(value, " ")
substrings := strings.Split(l.Value, " ")
if len(substrings) > 1 {
for _, substring := range substrings {
token := fmt.Sprintf("%s%s%s", key, operator, substring)
token := fmt.Sprintf("%s%s%s", l.Name, operator, substring)
tokens[token] = makeAC(
token,
[]string{
key,
fmt.Sprintf("%s%s", key, operator),
value,
l.Name,
fmt.Sprintf("%s%s", l.Name, operator),
l.Value,
substring,
},
)
}
}
case moreThanOperator, lessThanOperator:
if _, err := strconv.Atoi(value); err == nil {
token := fmt.Sprintf("%s%s%s", key, operator, value)
if _, err := strconv.Atoi(l.Value); err == nil {
token := fmt.Sprintf("%s%s%s", l.Name, operator, l.Value)
tokens[token] = makeAC(
token,
[]string{
key,
fmt.Sprintf("%s%s", key, operator),
value,
l.Name,
fmt.Sprintf("%s%s", l.Name, operator),
l.Value,
},
)
}
+12 -12
View File
@@ -496,7 +496,7 @@ var tests = []filterTest{
{
Expression: "node=vps1",
IsValid: true,
Alert: models.Alert{Labels: map[string]string{"node": "vps1"}},
Alert: models.Alert{Labels: models.Labels{{Name: "node", Value: "vps1"}}},
IsMatch: true,
},
{
@@ -508,68 +508,68 @@ var tests = []filterTest{
{
Expression: "node!=vps1",
IsValid: true,
Alert: models.Alert{Labels: map[string]string{"node": "vps1"}},
Alert: models.Alert{Labels: models.Labels{{Name: "node", Value: "vps1"}}},
IsMatch: false,
},
{
Expression: "node!=vps1",
IsValid: true,
Alert: models.Alert{Labels: map[string]string{"node": "vps2"}},
Alert: models.Alert{Labels: models.Labels{{Name: "node", Value: "vps2"}}},
IsMatch: true,
},
{
Expression: "node=~vps",
IsValid: true,
Alert: models.Alert{Labels: map[string]string{"node": "vps1"}},
Alert: models.Alert{Labels: models.Labels{{Name: "node", Value: "vps1"}}},
IsMatch: true,
},
{
Expression: "node!~vps",
IsValid: true,
Alert: models.Alert{Labels: map[string]string{"node": "vps1"}},
Alert: models.Alert{Labels: models.Labels{{Name: "node", Value: "vps1"}}},
IsMatch: false,
},
{
Expression: "node!~abc",
IsValid: true,
Alert: models.Alert{Labels: map[string]string{"node": "vps1"}},
Alert: models.Alert{Labels: models.Labels{{Name: "node", Value: "vps1"}}},
IsMatch: true,
},
{
Expression: "node!~",
IsValid: false,
Alert: models.Alert{Labels: map[string]string{"node": "vps1"}},
Alert: models.Alert{Labels: models.Labels{{Name: "node", Value: "vps1"}}},
IsMatch: false,
},
{
Expression: "node=",
IsValid: false,
Alert: models.Alert{Labels: map[string]string{"node": "vps1"}},
Alert: models.Alert{Labels: models.Labels{{Name: "node", Value: "vps1"}}},
IsMatch: false,
},
{
Expression: "node===",
IsValid: false,
Alert: models.Alert{Labels: map[string]string{"node": "vps1"}},
Alert: models.Alert{Labels: models.Labels{{Name: "node", Value: "vps1"}}},
IsMatch: false,
},
{
Expression: "abc",
IsValid: true,
Alert: models.Alert{Labels: map[string]string{"key": "abc"}},
Alert: models.Alert{Labels: models.Labels{{Name: "key", Value: "abc"}}},
IsMatch: true,
},
{
Expression: "abc",
IsValid: true,
Alert: models.Alert{Labels: map[string]string{"key": "XXXabcx"}},
Alert: models.Alert{Labels: models.Labels{{Name: "key", Value: "XXXabcx"}}},
IsMatch: true,
},
{
Expression: "abc",
IsValid: true,
Alert: models.Alert{Labels: map[string]string{"abc": "xxxab"}},
Alert: models.Alert{Labels: models.Labels{{Name: "abc", Value: "xxxab"}}},
IsMatch: false,
},
{
+12 -2
View File
@@ -49,17 +49,27 @@ func groups(c *client.AlertmanagerAPI, timeout time.Duration) ([]models.AlertGro
ret := make([]models.AlertGroup, 0, len(groups.Payload))
for _, group := range groups.Payload {
ls := models.Labels{}
for k, v := range group.Labels {
ls = ls.Set(k, v)
}
sort.Sort(ls)
g := models.AlertGroup{
Receiver: *group.Receiver.Name,
Labels: group.Labels,
Labels: ls,
Alerts: make(models.AlertList, 0, len(group.Alerts)),
}
for _, alert := range group.Alerts {
ls := models.Labels{}
for k, v := range alert.Labels {
ls = ls.Set(k, v)
}
sort.Sort(ls)
a := models.Alert{
Fingerprint: *alert.Fingerprint,
Receiver: *group.Receiver.Name,
Annotations: models.AnnotationsFromMap(alert.Annotations),
Labels: alert.Labels,
Labels: ls,
StartsAt: time.Time(*alert.StartsAt),
GeneratorURL: alert.GeneratorURL.String(),
State: *alert.Status.State,
+41 -17
View File
@@ -1,9 +1,12 @@
/* snapshot: SharedMaps */
{
"receiver": "",
"labels": {
"alertname": "FakeAlert"
},
"labels": [
{
"name": "alertname",
"value": "FakeAlert"
}
],
"alerts": [
{
"annotations": [
@@ -15,10 +18,16 @@
"isAction": false
}
],
"labels": {
"instance": "1",
"job": "node_exporter"
},
"labels": [
{
"name": "job",
"value": "node_exporter"
},
{
"name": "instance",
"value": "1"
}
],
"startsAt": "0001-01-01T00:00:00Z",
"state": "suppressed",
"alertmanager": [
@@ -54,10 +63,16 @@
},
{
"annotations": [],
"labels": {
"instance": "2",
"job": "node_exporter"
},
"labels": [
{
"name": "job",
"value": "node_exporter"
},
{
"name": "instance",
"value": "2"
}
],
"startsAt": "0001-01-01T00:00:00Z",
"state": "active",
"alertmanager": [
@@ -93,11 +108,20 @@
},
{
"annotations": [],
"labels": {
"extra": "ignore",
"instance": "3",
"job": "blackbox"
},
"labels": [
{
"name": "job",
"value": "blackbox"
},
{
"name": "instance",
"value": "3"
},
{
"name": "extra",
"value": "ignore"
}
],
"startsAt": "0001-01-01T00:00:00Z",
"state": "suppressed",
"alertmanager": [
@@ -146,7 +170,7 @@
"isAction": false
}
],
"labels": {},
"labels": [],
"silences": {
"fakeCluster": [
"fakeSilence1",
+82 -5
View File
@@ -2,9 +2,11 @@ package models
import (
"fmt"
"strings"
"time"
"github.com/cnf/structhash"
"github.com/fvbommel/sortorder"
)
// AlertStateUnprocessed means that Alertmanager notify didn't yet process it
@@ -24,16 +26,91 @@ var AlertStateList = []string{
AlertStateSuppressed,
}
type Label struct {
Name string `json:"name"`
Value string `json:"value"`
}
func (l Label) String() string {
return fmt.Sprintf("%s=\"%s\"", l.Name, l.Value)
}
type Labels []Label
func (ls Labels) String() string {
var s []string
for _, l := range ls {
s = append(s, l.String())
}
return strings.Join(s, ",")
}
func (ls Labels) Map() map[string]string {
m := make(map[string]string, len(ls))
for _, l := range ls {
m[l.Name] = l.Value
}
return m
}
func (ls Labels) Len() int {
return len(ls)
}
func (ls Labels) Swap(i, j int) {
ls[i], ls[j] = ls[j], ls[i]
}
func (ls Labels) Less(i, j int) bool {
if ls[i].Name == ls[j].Name {
return sortorder.NaturalLess(ls[i].Value, ls[j].Value)
}
return sortorder.NaturalLess(ls[i].Name, ls[j].Name)
}
func (ls Labels) Get(name string) *Label {
for _, l := range ls {
l := l
if l.Name == name {
return &l
}
}
return nil
}
func (ls Labels) GetValue(name string) string {
for _, l := range ls {
if l.Name == name {
return l.Value
}
}
return ""
}
func (ls Labels) Add(l Label) Labels {
if ls.Get(l.Name) != nil {
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: name, Value: value})
}
// Alert is vanilla alert + some additional attributes
// karma extends an alert object with:
// * Links map, it's generated from annotations if annotation value is an url
// 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 {
Annotations Annotations `json:"annotations"`
Labels map[string]string `json:"labels"`
StartsAt time.Time `json:"startsAt"`
State string `json:"state"`
Annotations Annotations `json:"annotations"`
Labels Labels `json:"labels"`
StartsAt time.Time `json:"startsAt"`
State string `json:"state"`
// those are not exposed in JSON, Alertmanager specific value will be in kept
// in the Alertmanager slice
// skip those when generating alert fingerprint too
@@ -52,7 +129,7 @@ type Alert struct {
// UpdateFingerprints will generate a new set of fingerprints for this alert
// it should be called after modifying any field that isn't tagged with hash:"-"
func (a *Alert) UpdateFingerprints() {
a.LabelsFP = fmt.Sprintf("%x", structhash.Sha1(a.Labels, 1))
a.LabelsFP = fmt.Sprintf("%x", structhash.Sha1(a.Labels.Map(), 1))
a.contentFP = fmt.Sprintf("%x", structhash.Sha1(a, 1))
}
+111
View File
@@ -0,0 +1,111 @@
package models_test
import (
"sort"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/prymitive/karma/internal/models"
)
func TestLabelsSet(t *testing.T) {
l := models.Labels{}
if l.String() != "" {
t.Errorf("Invalid labels: %s", l)
}
l = l.Set("foo", "bar")
if l.String() != `foo="bar"` {
t.Errorf("Invalid labels: %s", l)
}
l = l.Set("foo", "bar")
if l.String() != `foo="bar"` {
t.Errorf("Invalid labels: %s", l)
}
l = l.Set("bar", "foo")
if l.String() != `foo="bar",bar="foo"` {
t.Errorf("Invalid labels: %s", l)
}
l = l.Set("bar", "foo")
if l.String() != `foo="bar",bar="foo"` {
t.Errorf("Invalid labels: %s", l)
}
l = l.Set("foo", "bar")
if l.String() != `foo="bar",bar="foo"` {
t.Errorf("Invalid labels: %s", l)
}
}
type sortLabelsTestCase struct {
in models.Labels
out models.Labels
}
func TestSortLabels(t *testing.T) {
testCases := []sortLabelsTestCase{
{
in: models.Labels{
{Name: "foo", Value: "bar"},
},
out: models.Labels{
{Name: "foo", Value: "bar"},
},
},
{
in: models.Labels{
{Name: "foo", Value: "bar"},
{Name: "bar", Value: "foo"},
},
out: models.Labels{
{Name: "bar", Value: "foo"},
{Name: "foo", Value: "bar"},
},
},
{
in: models.Labels{
{Name: "bar", Value: "foo"},
{Name: "foo", Value: "bar"},
},
out: models.Labels{
{Name: "bar", Value: "foo"},
{Name: "foo", Value: "bar"},
},
},
{
in: models.Labels{
{Name: "foo", Value: "foo"},
{Name: "bar", Value: "foo"},
{Name: "foo", Value: "bar"},
},
out: models.Labels{
{Name: "bar", Value: "foo"},
{Name: "foo", Value: "bar"},
{Name: "foo", Value: "foo"},
},
},
{
in: models.Labels{
{Name: "1", Value: "a12"},
{Name: "1", Value: "1"},
{Name: "1", Value: "a2"},
},
out: models.Labels{
{Name: "1", Value: "1"},
{Name: "1", Value: "a2"},
{Name: "1", Value: "a12"},
},
},
}
for _, testCase := range testCases {
sort.Sort(testCase.in)
if diff := cmp.Diff(testCase.in, testCase.out); diff != "" {
t.Errorf("Incorrectly sorted labels (-want +got):\n%s", diff)
break
}
}
}
+9 -9
View File
@@ -35,14 +35,14 @@ func (a AlertList) Less(i, j int) bool {
// 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 {
Receiver string `json:"receiver"`
Labels map[string]string `json:"labels"`
Alerts AlertList `json:"alerts"`
ID string `json:"id"`
Hash string `json:"-"`
AlertmanagerCount map[string]int `json:"alertmanagerCount"`
StateCount map[string]int `json:"stateCount"`
LatestStartsAt time.Time `json:"-"`
Receiver string `json:"receiver"`
Labels Labels `json:"labels"`
Alerts AlertList `json:"alerts"`
ID string `json:"id"`
Hash string `json:"-"`
AlertmanagerCount map[string]int `json:"alertmanagerCount"`
StateCount map[string]int `json:"stateCount"`
LatestStartsAt time.Time `json:"-"`
}
// LabelsFingerprint is a checksum of this AlertGroup labels and the receiver
@@ -50,7 +50,7 @@ type AlertGroup struct {
func (ag AlertGroup) LabelsFingerprint() string {
agIDHasher := sha1.New()
_, _ = io.WriteString(agIDHasher, ag.Receiver)
_, _ = io.WriteString(agIDHasher, fmt.Sprintf("%x", structhash.Sha1(ag.Labels, 1)))
_, _ = io.WriteString(agIDHasher, fmt.Sprintf("%x", structhash.Sha1(ag.Labels.Map(), 1)))
return fmt.Sprintf("%x", agIDHasher.Sum(nil))
}
+18 -8
View File
@@ -97,8 +97,10 @@ var agFPTests = []agFPTest{
{
name: "different Labels shouldn't change content fingerprint",
ag: models.AlertGroup{
Receiver: "default",
Labels: map[string]string{"foo": "bar"},
Receiver: "default",
Labels: models.Labels{
{Name: "foo", Value: "bar"},
},
StateCount: map[string]int{"default": 0},
},
fpChange: false,
@@ -107,10 +109,12 @@ var agFPTests = []agFPTest{
name: "different set of alerts should change content fingerprint",
ag: models.AlertGroup{
Receiver: "default",
Labels: map[string]string{"foo": "bar"},
Labels: models.Labels{{Name: "foo", Value: "bar"}},
Alerts: models.AlertList{
models.Alert{
Labels: map[string]string{"foo1": "bar"},
Labels: models.Labels{
{Name: "foo1", Value: "bar"},
},
},
},
StateCount: map[string]int{"default": 0},
@@ -121,10 +125,12 @@ var agFPTests = []agFPTest{
name: "another different set of alerts should change content fingerprint",
ag: models.AlertGroup{
Receiver: "default",
Labels: map[string]string{"bar": "foo"},
Labels: models.Labels{{Name: "bar", Value: "foo"}},
Alerts: models.AlertList{
models.Alert{
Labels: map[string]string{"bar": "foo"},
Labels: models.Labels{
{Name: "bar", Value: "foo"},
},
},
},
StateCount: map[string]int{"default": 0},
@@ -135,10 +141,14 @@ var agFPTests = []agFPTest{
name: "repeating last set of alerts shouldn't change content fingerprint",
ag: models.AlertGroup{
Receiver: "default",
Labels: map[string]string{"bar": "foo"},
Labels: models.Labels{
{Name: "bar", Value: "foo"},
},
Alerts: models.AlertList{
models.Alert{
Labels: map[string]string{"bar": "foo"},
Labels: models.Labels{
{Name: "bar", Value: "foo"},
},
},
},
StateCount: map[string]int{"default": 0},
+29 -29
View File
@@ -94,7 +94,7 @@ func (lnsl LabelNameStatsList) Less(i, j int) bool {
// APIAlertGroupSharedMaps defines shared part of APIAlertGroup
type APIAlertGroupSharedMaps struct {
Annotations Annotations `json:"annotations"`
Labels map[string]string `json:"labels"`
Labels Labels `json:"labels"`
Silences map[string][]string `json:"silences"`
Sources []string `json:"sources"`
Clusters []string `json:"clusters"`
@@ -117,8 +117,8 @@ func (ag *APIAlertGroup) dedupLabels() {
labelCounts := make(map[string]int, len(ag.Alerts))
for _, alert := range ag.Alerts {
for name, val := range alert.Labels {
key := fmt.Sprintf("%s\n%s", name, val)
for _, l := range alert.Labels {
key := fmt.Sprintf("%s\n%s", l.Name, l.Value)
_, found := labelCounts[key]
if found {
labelCounts[key]++
@@ -128,16 +128,16 @@ func (ag *APIAlertGroup) dedupLabels() {
}
}
sharedLabels := map[string]string{}
sharedLabels := Labels{}
for i, alert := range ag.Alerts {
newAlertLabels := map[string]string{}
for name, val := range alert.Labels {
key := fmt.Sprintf("%s\n%s", name, val)
newAlertLabels := Labels{}
for _, l := range alert.Labels {
key := fmt.Sprintf("%s\n%s", l.Name, l.Value)
if labelCounts[key] == totalAlerts {
sharedLabels[name] = val
sharedLabels = sharedLabels.Add(l)
} else {
newAlertLabels[name] = val
newAlertLabels = newAlertLabels.Add(l)
}
}
ag.Alerts[i].Labels = newAlertLabels
@@ -148,27 +148,27 @@ func (ag *APIAlertGroup) dedupLabels() {
}
func (ag *APIAlertGroup) removeGroupingLabels(dropNames []string) {
newGroupLabels := map[string]string{}
for name, val := range ag.Labels {
if slices.StringInSlice(dropNames, name) {
newGroupLabels := Labels{}
for _, l := range ag.Labels {
if slices.StringInSlice(dropNames, l.Name) {
continue
}
newGroupLabels[name] = val
newGroupLabels = newGroupLabels.Add(l)
}
ag.Labels = newGroupLabels
for i, alert := range ag.Alerts {
newAlertLabels := map[string]string{}
for name, val := range alert.Labels {
if slices.StringInSlice(dropNames, name) {
newAlertLabels := Labels{}
for _, l := range alert.Labels {
if slices.StringInSlice(dropNames, l.Name) {
// skip all labels from the drop list
continue
}
if _, found := ag.Labels[name]; found {
if v := ag.Labels.Get(l.Name); v != nil {
// skip all labels that are used for grouping
continue
}
newAlertLabels[name] = val
newAlertLabels = newAlertLabels.Add(l)
}
ag.Alerts[i].Labels = newAlertLabels
}
@@ -322,11 +322,11 @@ func (ag *APIAlertGroup) populateAllLabels() {
labels := map[string]int{}
for _, alert := range ag.Alerts {
for k := range alert.Labels {
if _, ok := labels[k]; !ok {
labels[k] = 0
for _, l := range alert.Labels {
if _, ok := labels[l.Name]; !ok {
labels[l.Name] = 0
}
labels[k]++
labels[l.Name]++
}
}
@@ -339,15 +339,15 @@ func (ag *APIAlertGroup) populateAllLabels() {
}
for _, alert := range ag.Alerts {
for k, v := range alert.Labels {
if _, ok := labelNames[k]; !ok {
for _, l := range alert.Labels {
if _, ok := labelNames[l.Name]; !ok {
continue
}
if _, ok := ag.AllLabels[alert.State][k]; !ok {
ag.AllLabels[alert.State][k] = []string{}
if _, ok := ag.AllLabels[alert.State][l.Name]; !ok {
ag.AllLabels[alert.State][l.Name] = []string{}
}
if !slices.StringInSlice(ag.AllLabels[alert.State][k], v) {
ag.AllLabels[alert.State][k] = append(ag.AllLabels[alert.State][k], v)
if !slices.StringInSlice(ag.AllLabels[alert.State][l.Name], l.Value) {
ag.AllLabels[alert.State][l.Name] = append(ag.AllLabels[alert.State][l.Name], l.Value)
}
}
}
@@ -372,7 +372,7 @@ func (ag *APIAlertGroup) DedupSharedMaps(ignoredLabels []string) {
ag.dedupClusters()
} else {
ag.Shared = APIAlertGroupSharedMaps{
Labels: map[string]string{},
Labels: Labels{},
Annotations: Annotations{},
Silences: map[string][]string{},
Clusters: []string{},
+17 -17
View File
@@ -14,8 +14,8 @@ import (
func TestDedupSharedMaps(t *testing.T) {
ag := models.APIAlertGroup{
AlertGroup: models.AlertGroup{
Labels: map[string]string{
"alertname": "FakeAlert",
Labels: models.Labels{
{"alertname", "FakeAlert"},
},
Alerts: models.AlertList{
models.Alert{
@@ -30,10 +30,10 @@ func TestDedupSharedMaps(t *testing.T) {
Value: "bar",
},
},
Labels: map[string]string{
"alertname": "FakeAlert",
"job": "node_exporter",
"instance": "1",
Labels: models.Labels{
{"alertname", "FakeAlert"},
{"job", "node_exporter"},
{"instance", "1"},
},
Alertmanager: []models.AlertmanagerInstance{
{
@@ -60,10 +60,10 @@ func TestDedupSharedMaps(t *testing.T) {
Value: "this is summary",
},
},
Labels: map[string]string{
"alertname": "FakeAlert",
"job": "node_exporter",
"instance": "2",
Labels: models.Labels{
{"alertname", "FakeAlert"},
{"job", "node_exporter"},
{"instance", "2"},
},
Alertmanager: []models.AlertmanagerInstance{
{
@@ -90,11 +90,11 @@ func TestDedupSharedMaps(t *testing.T) {
Value: "this is summary",
},
},
Labels: map[string]string{
"alertname": "FakeAlert",
"job": "blackbox",
"instance": "3",
"extra": "ignore",
Labels: models.Labels{
{"alertname", "FakeAlert"},
{"job", "blackbox"},
{"instance", "3"},
{"extra", "ignore"},
},
Alertmanager: []models.AlertmanagerInstance{
{
@@ -126,8 +126,8 @@ func TestDedupSharedMapsSingleGroup(t *testing.T) {
ag := models.APIAlertGroup{
AlertGroup: models.AlertGroup{
Alerts: models.AlertList{
models.Alert{State: models.AlertStateActive, Labels: map[string]string{"foo": "bar"}},
models.Alert{State: models.AlertStateUnprocessed, Labels: map[string]string{"foo": "bar"}},
models.Alert{State: models.AlertStateActive, Labels: models.Labels{{"foo", "bar"}}},
models.Alert{State: models.AlertStateUnprocessed, Labels: models.Labels{{"foo", "bar"}}},
},
},
}
+8 -6
View File
@@ -2,6 +2,7 @@ package transform
import (
"regexp"
"sort"
"strings"
"github.com/prymitive/karma/internal/models"
@@ -12,21 +13,22 @@ 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 StripLables(keptLabels, ignoredLabels []string, keptLabelsRegex, ignoredLabelsRegex []*regexp.Regexp,
sourceLabels map[string]string) map[string]string {
sourceLabels models.Labels) models.Labels {
// empty keep lists means keep everything by default
keepAll := len(keptLabels) == 0 && len(keptLabelsRegex) == 0
labels := map[string]string{}
for label, value := range sourceLabels {
labels := models.Labels{}
for _, label := range sourceLabels {
// is explicitly marked to be kept
inKeep := slices.StringInSlice(keptLabels, label) || matchesAnyRegex(label, keptLabelsRegex)
inKeep := slices.StringInSlice(keptLabels, label.Name) || matchesAnyRegex(label.Name, keptLabelsRegex)
// is explicitly marked to be stripped
inStrip := slices.StringInSlice(ignoredLabels, label) || matchesAnyRegex(label, ignoredLabelsRegex)
inStrip := slices.StringInSlice(ignoredLabels, label.Name) || matchesAnyRegex(label.Name, ignoredLabelsRegex)
if (keepAll || inKeep) && !inStrip {
// strip leading and trailing space in label value
// this is to normalize values in case space is added by Alertmanager rules
labels[label] = strings.TrimSpace(value)
labels = labels.Set(label.Name, strings.TrimSpace(label.Value))
}
}
sort.Sort(labels)
return labels
}
+70 -70
View File
@@ -1,12 +1,12 @@
package transform_test
import (
"github.com/prymitive/karma/internal/regex"
"reflect"
"regexp"
"testing"
"github.com/prymitive/karma/internal/models"
"github.com/prymitive/karma/internal/regex"
"github.com/prymitive/karma/internal/transform"
)
@@ -15,8 +15,8 @@ type stripLabelTest struct {
keep []string
stripRegex []string
keepRegex []string
before map[string]string
after map[string]string
before models.Labels
after models.Labels
}
var stripLabelTests = []stripLabelTest{
@@ -25,14 +25,14 @@ var stripLabelTests = []stripLabelTest{
keep: []string{},
stripRegex: []string{},
keepRegex: []string{},
before: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
before: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "env", Value: "production"},
{Name: "level", Value: "info"},
},
after: map[string]string{
"host": "localhost",
"level": "info",
after: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "level", Value: "info"},
},
},
{
@@ -40,15 +40,15 @@ var stripLabelTests = []stripLabelTest{
keep: []string{},
stripRegex: []string{},
keepRegex: []string{},
before: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
before: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "env", Value: "production"},
{Name: "level", Value: "info"},
},
after: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
after: models.Labels{
{Name: "env", Value: "production"},
{Name: "host", Value: "localhost"},
{Name: "level", Value: "info"},
},
},
{
@@ -56,15 +56,15 @@ var stripLabelTests = []stripLabelTest{
keep: []string{},
stripRegex: []string{},
keepRegex: []string{},
before: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
before: models.Labels{
{Name: "env", Value: "production"},
{Name: "host", Value: "localhost"},
{Name: "level", Value: "info"},
},
after: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
after: models.Labels{
{Name: "env", Value: "production"},
{Name: "host", Value: "localhost"},
{Name: "level", Value: "info"},
},
},
{
@@ -72,23 +72,23 @@ var stripLabelTests = []stripLabelTest{
keep: []string{},
stripRegex: []string{},
keepRegex: []string{},
before: map[string]string{
"host": "localhost",
before: models.Labels{
{Name: "host", Value: "localhost"},
},
after: map[string]string{},
after: models.Labels{},
},
{
strip: []string{},
keep: []string{"env"},
stripRegex: []string{},
keepRegex: []string{},
before: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
before: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "env", Value: "production"},
{Name: "level", Value: "info"},
},
after: map[string]string{
"env": "production",
after: models.Labels{
{Name: "env", Value: "production"},
},
},
{
@@ -96,13 +96,13 @@ var stripLabelTests = []stripLabelTest{
keep: []string{"host"},
stripRegex: []string{},
keepRegex: []string{},
before: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
before: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "env", Value: "production"},
{Name: "level", Value: "info"},
},
after: map[string]string{
"host": "localhost",
after: models.Labels{
{Name: "host", Value: "localhost"},
},
},
{
@@ -110,24 +110,24 @@ var stripLabelTests = []stripLabelTest{
keep: []string{"env"},
stripRegex: []string{},
keepRegex: []string{},
before: map[string]string{
"host": "localhost",
"level": "info",
before: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "level", Value: "info"},
},
after: map[string]string{},
after: models.Labels{},
},
{
strip: []string{},
keep: []string{},
stripRegex: []string{".*e.*"},
keepRegex: []string{},
before: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
before: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "env", Value: "production"},
{Name: "level", Value: "info"},
},
after: map[string]string{
"host": "localhost",
after: models.Labels{
{Name: "host", Value: "localhost"},
},
},
{
@@ -135,14 +135,14 @@ var stripLabelTests = []stripLabelTest{
keep: []string{},
stripRegex: []string{},
keepRegex: []string{".*e.*"},
before: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
before: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "env", Value: "production"},
{Name: "level", Value: "info"},
},
after: map[string]string{
"env": "production",
"level": "info",
after: models.Labels{
{Name: "env", Value: "production"},
{Name: "level", Value: "info"},
},
},
{
@@ -150,13 +150,13 @@ var stripLabelTests = []stripLabelTest{
keep: []string{"env", "level"},
stripRegex: []string{".*el"},
keepRegex: []string{},
before: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
before: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "env", Value: "production"},
{Name: "level", Value: "info"},
},
after: map[string]string{
"env": "production",
after: models.Labels{
{Name: "env", Value: "production"},
},
},
{
@@ -164,13 +164,13 @@ var stripLabelTests = []stripLabelTest{
keep: []string{},
stripRegex: []string{},
keepRegex: []string{".*e.*"},
before: map[string]string{
"host": "localhost",
"env": "production",
"level": "info",
before: models.Labels{
{Name: "host", Value: "localhost"},
{Name: "env", Value: "production"},
{Name: "level", Value: "info"},
},
after: map[string]string{
"env": "production",
after: models.Labels{
{Name: "env", Value: "production"},
},
},
}