diff --git a/.golangci.yml b/.golangci.yml
index d9021044f..672d18f69 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -8,7 +8,6 @@ linters:
- golint
- dupl
- goconst
- - gocyclo
linters-settings:
govet:
diff --git a/alerts.go b/alerts.go
index 267c7f189..483d02488 100644
--- a/alerts.go
+++ b/alerts.go
@@ -1,12 +1,12 @@
package main
import (
- "sort"
- "strings"
-
"github.com/prymitive/karma/internal/alertmanager"
"github.com/prymitive/karma/internal/filters"
"github.com/prymitive/karma/internal/models"
+ "github.com/prymitive/karma/internal/slices"
+
+ log "github.com/sirupsen/logrus"
)
func getFiltersFromQuery(filterStrings []string) ([]filters.FilterT, bool) {
@@ -40,8 +40,11 @@ func getUpstreams() models.AlertmanagerAPISummary {
upstreams := alertmanager.GetAlertmanagers()
for _, upstream := range upstreams {
members := upstream.ClusterMemberNames()
- sort.Strings(members)
- key := strings.Join(members[:], "\n")
+ key, err := slices.StringSliceToSHA1(members)
+ if err != nil {
+ log.Errorf("slices.StringSliceToSHA1 error: %s", err)
+ continue
+ }
if _, found := clusters[key]; !found {
clusters[key] = members
}
@@ -52,6 +55,7 @@ func getUpstreams() models.AlertmanagerAPISummary {
PublicURI: upstream.PublicURI(),
Error: upstream.Error(),
Version: upstream.Version(),
+ Cluster: upstream.ClusterID(),
ClusterMembers: members,
}
summary.Instances = append(summary.Instances, u)
@@ -63,10 +67,7 @@ func getUpstreams() models.AlertmanagerAPISummary {
summary.Counters.Failed++
}
}
-
- for _, cluster := range clusters {
- summary.Clusters = append(summary.Clusters, cluster)
- }
+ summary.Clusters = clusters
return summary
}
diff --git a/api_test.go b/api_test.go
index 3b883c19d..b3e4163a3 100644
--- a/api_test.go
+++ b/api_test.go
@@ -867,9 +867,9 @@ func TestVerifyAllGroups(t *testing.T) {
}
}
- am, foundAM := ur.Silences["default"]
+ am, foundAM := ur.Silences["843c4a11660fe38ea61e6960a29d4f4796da6488"]
if !foundAM {
- t.Errorf("[%s] Alertmanager 'default' missing from silences", version)
+ t.Errorf("[%s] Alertmanager cluster '843c4a11660fe38ea61e6960a29d4f4796da6488' (default) missing from silences", version)
} else if len(am) == 0 {
t.Errorf("[%s] Silences mismatch, expected >0 but got %d", version, len(am))
}
diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go
index 3cc9081c1..4b584dec8 100644
--- a/internal/alertmanager/models.go
+++ b/internal/alertmanager/models.go
@@ -284,6 +284,7 @@ func (am *Alertmanager) pullAlerts(version string) error {
alert.Alertmanager = []models.AlertmanagerInstance{
models.AlertmanagerInstance{
Name: am.Name,
+ Cluster: am.ClusterID(),
State: alert.State,
StartsAt: alert.StartsAt,
EndsAt: alert.EndsAt,
@@ -500,5 +501,18 @@ func (am *Alertmanager) ClusterMemberNames() []string {
}
}
+ sort.Strings(members)
return members
}
+
+// ClusterID returns the ID (sha1) of the cluster this Alertmanager instance
+// belongs to
+func (am *Alertmanager) ClusterID() string {
+ members := am.ClusterMemberNames()
+ id, err := slices.StringSliceToSHA1(members)
+ if err != nil {
+ log.Errorf("slices.StringSliceToSHA1 error: %s", err)
+ return am.Name
+ }
+ return id
+}
diff --git a/internal/models/alertmanager.go b/internal/models/alertmanager.go
index 718322822..913e26efb 100644
--- a/internal/models/alertmanager.go
+++ b/internal/models/alertmanager.go
@@ -5,7 +5,8 @@ import "time"
// AlertmanagerInstance describes the Alertmanager instance alert was collected
// from
type AlertmanagerInstance struct {
- Name string `json:"name"`
+ Name string `json:"name"`
+ Cluster string `json:"cluster"`
// per instance alert state
State string `json:"state"`
// timestamp collected from this instance, those on the alert itself
@@ -32,6 +33,7 @@ type AlertmanagerAPIStatus struct {
PublicURI string `json:"publicURI"`
Error string `json:"error"`
Version string `json:"version"`
+ Cluster string `json:"cluster"`
ClusterMembers []string `json:"clusterMembers"`
}
@@ -47,5 +49,5 @@ type AlertmanagerAPICounters struct {
type AlertmanagerAPISummary struct {
Counters AlertmanagerAPICounters `json:"counters"`
Instances []AlertmanagerAPIStatus `json:"instances"`
- Clusters [][]string `json:"clusters"`
+ Clusters map[string][]string `json:"clusters"`
}
diff --git a/internal/slices/slices.go b/internal/slices/slices.go
index a18d400e3..47cddf4fb 100644
--- a/internal/slices/slices.go
+++ b/internal/slices/slices.go
@@ -1,5 +1,10 @@
package slices
+import (
+ "crypto/sha1"
+ "fmt"
+)
+
// BoolInSlice returns true if given bool is found in a slice of bools
func BoolInSlice(boolArray []bool, value bool) bool {
for _, s := range boolArray {
@@ -19,3 +24,19 @@ func StringInSlice(stringArray []string, value string) bool {
}
return false
}
+
+// StringSliceToSHA1 returns a SHA1 hash computed from a slice of strings
+func StringSliceToSHA1(stringArray []string) (string, error) {
+ h := sha1.New()
+ for _, s := range stringArray {
+ _, err := h.Write([]byte(s))
+ if err != nil {
+ return "", err
+ }
+ _, err = h.Write([]byte("\n"))
+ if err != nil {
+ return "", err
+ }
+ }
+ return fmt.Sprintf("%x", h.Sum(nil)), nil
+}
diff --git a/internal/slices/slices_test.go b/internal/slices/slices_test.go
index dee989619..2e5fc01d1 100644
--- a/internal/slices/slices_test.go
+++ b/internal/slices/slices_test.go
@@ -111,3 +111,22 @@ func TestBoolInSlice(t *testing.T) {
}
}
}
+
+func TestStringSliceToSHA1(t *testing.T) {
+ s, err := slices.StringSliceToSHA1([]string{"a", "b", "c"})
+ if err != nil {
+ t.Errorf("StringSliceToSHA1() returned error: %s", err)
+ }
+ if s == "" {
+ t.Errorf("StringSliceToSHA1() returned empty string")
+ }
+}
+
+func BenchmarkStringSliceToSHA1(b *testing.B) {
+ for _, stringSliceTest := range stringSliceTests {
+ _, err := slices.StringSliceToSHA1(stringSliceTest.array)
+ if err != nil {
+ b.Errorf("StringSliceToSHA1() returned error: %s", err)
+ }
+ }
+}
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js
index 20317401b..650582ea7 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js
@@ -45,18 +45,35 @@ const Alert = observer(
BorderClassMap[alert.state] || "border-warning"
];
+ let silences = {};
+ for (let am of alert.alertmanager) {
+ if (!silences[am.cluster]) {
+ silences[am.cluster] = {
+ alertmanager: am,
+ silences: []
+ };
+ }
+ for (let silenceID of am.silencedBy) {
+ if (!silences[am.cluster].silences.includes(silenceID)) {
+ silences[am.cluster].silences.push(silenceID);
+ }
+ }
+ }
+
return (
- {alert.annotations.filter(a => a.isLink === false).map(a => (
-
- ))}
+ {alert.annotations
+ .filter(a => a.isLink === false)
+ .map(a => (
+
+ ))}
) : null}
- {alert.annotations.filter(a => a.isLink === true).map(a => (
-
- ))}
- {alert.alertmanager.map(am =>
- am.silencedBy.map(silenceID => (
+ {alert.annotations
+ .filter(a => a.isLink === true)
+ .map(a => (
+
+ ))}
+ {Object.values(silences).map(clusterSilences =>
+ clusterSilences.silences.map(silenceID => (
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js
index 0f40aff00..f5efc1a1b 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js
@@ -96,6 +96,35 @@ describe("", () => {
expect(silence.html()).toMatch(/silence123456789/);
});
+ it("renders only one silence for HA cluster", () => {
+ const alert = MockedAlert();
+ alert.alertmanager = [
+ {
+ name: "am1",
+ cluster: "ha",
+ state: "suppressed",
+ startsAt: "2018-08-14T17:36:40.017867056Z",
+ endsAt: "0001-01-01T00:00:00Z",
+ source: "localhost/am1",
+ silencedBy: ["silence123456789"]
+ },
+ {
+ name: "am2",
+ cluster: "ha",
+ state: "suppressed",
+ startsAt: "2018-08-14T17:36:40.017867056Z",
+ endsAt: "0001-01-01T00:00:00Z",
+ source: "localhost/am2",
+ silencedBy: ["silence123456789"]
+ }
+ ];
+ const group = MockAlertGroup({}, [alert], [], {});
+ const tree = MountedAlert(alert, group, false, false);
+ const silence = tree.find("Silence");
+ expect(silence).toHaveLength(1);
+ expect(silence.html()).toMatch(/silence123456789/);
+ });
+
it("uses BorderClassMap.active when @state=active", () => {
const alert = MockedAlert();
alert.state = "active";
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js
index 4e26a0e13..97f1c6262 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js
@@ -249,7 +249,7 @@ const Silence = inject("alertStore")(
// and we need to lookup the actual silence data in the store.
// Data might be missing from the store so first check if we have
// anything for this alertmanager instance
- const amSilences = alertStore.data.silences[alertmanagerState.name];
+ const amSilences = alertStore.data.silences[alertmanagerState.cluster];
if (!amSilences) return null;
// next check if alertmanager has our silence ID
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js
index 293aa7d9e..7071ed80b 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js
@@ -17,6 +17,7 @@ const mockAfterUpdate = jest.fn();
const alertmanager = {
name: "default",
+ cluster: "default",
state: "suppressed",
startsAt: "2000-01-01T10:00:00Z",
endsAt: "0001-01-01T00:00:00Z",
@@ -62,6 +63,7 @@ beforeEach(() => {
instances: [
{
name: "default",
+ cluster: "default",
uri: "file:///mock",
publicURI: "http://example.com",
error: "",
@@ -69,7 +71,7 @@ beforeEach(() => {
clusterMembers: ["default"]
}
],
- clusters: [["default"]]
+ clusters: { default: ["default"] }
};
alertStore.data.silences = {
default: {
@@ -187,6 +189,7 @@ describe("", () => {
const am = instance.getAlertmanager();
expect(am).toEqual({
name: "default",
+ cluster: "default",
uri: "file:///mock",
publicURI: "http://example.com",
error: "",
diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js
index 51b80c94e..abe63a535 100644
--- a/ui/src/Components/Grid/AlertGrid/index.js
+++ b/ui/src/Components/Grid/AlertGrid/index.js
@@ -102,7 +102,7 @@ const AlertGrid = observer(
key={id}
group={alertStore.data.groups[id]}
showAlertmanagers={
- alertStore.data.upstreams.clusters.length > 1
+ Object.keys(alertStore.data.upstreams.clusters).length > 1
}
afterUpdate={this.masonryRepack}
settingsStore={settingsStore}
diff --git a/ui/src/Components/Grid/AlertGrid/index.test.js b/ui/src/Components/Grid/AlertGrid/index.test.js
index 2ce7457d7..29dbf4985 100644
--- a/ui/src/Components/Grid/AlertGrid/index.test.js
+++ b/ui/src/Components/Grid/AlertGrid/index.test.js
@@ -59,7 +59,7 @@ const MockGroupList = count => {
alertStore.data.upstreams = {
counters: { total: 0, healthy: 1, failed: 0 },
instances: [{ name: "am", uri: "http://am", error: "" }],
- clusters: [["am"]]
+ clusters: { am: ["am"] }
};
alertStore.data.groups = groups;
};
diff --git a/ui/src/Components/Grid/index.test.js b/ui/src/Components/Grid/index.test.js
index 0ad096b26..b8ffb6109 100644
--- a/ui/src/Components/Grid/index.test.js
+++ b/ui/src/Components/Grid/index.test.js
@@ -37,7 +37,7 @@ describe("", () => {
alertStore.data.upstreams = {
counters: { total: 1, healthy: 0, failed: 1 },
instances: [{ name: "am1", uri: "http://am1", error: "error" }],
- clusters: [["am1"]]
+ clusters: { am1: ["am1"] }
};
const tree = ShallowGrid();
expect(tree.text()).toBe("");
@@ -47,7 +47,7 @@ describe("", () => {
alertStore.data.upstreams = {
counters: { total: 1, healthy: 0, failed: 1 },
instances: [{ name: "am1", uri: "http://am1", error: "" }],
- clusters: [["am1"]]
+ clusters: { am1: ["am1"] }
};
const tree = ShallowGrid();
expect(tree.text()).toBe("");
@@ -61,7 +61,7 @@ describe("", () => {
{ name: "am2", uri: "file:///mock", error: "" },
{ name: "am3", uri: "http://am1", error: "error 2" }
],
- clusters: [["am1"], ["am2"], ["am3"]]
+ clusters: { am1: ["am1"], am2: ["am2"], am3: ["am3"] }
};
const tree = ShallowGrid();
expect(tree.text()).toBe("");
@@ -72,7 +72,7 @@ describe("", () => {
alertStore.data.upstreams = {
counters: { total: 0, healthy: 0, failed: 1 },
instances: [{ name: "am", uri: "http://am1", error: "error" }],
- clusters: [["am"]]
+ clusters: { am1: ["am1"] }
};
const tree = ShallowGrid();
expect(tree.text()).toBe("");
diff --git a/ui/src/Components/SilenceModal/AlertManagerInput/__snapshots__/index.test.js.snap b/ui/src/Components/SilenceModal/AlertManagerInput/__snapshots__/index.test.js.snap
index 39a8300ea..b3a06f6c1 100644
--- a/ui/src/Components/SilenceModal/AlertManagerInput/__snapshots__/index.test.js.snap
+++ b/ui/src/Components/SilenceModal/AlertManagerInput/__snapshots__/index.test.js.snap
@@ -7,24 +7,7 @@ exports[` matches snapshot 1`] = `
-
-
- am2
+ am1 | am2