diff --git a/internal/models/api.go b/internal/models/api.go index fad016380..ff2d40ba3 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -38,6 +38,7 @@ type LabelsColorMap map[string]map[string]LabelColors type APIAlertGroupSharedMaps struct { Annotations Annotations `json:"annotations"` Labels map[string]string `json:"labels"` + Silences map[string]string `json:"silences"` } // APIAlertGroup is how AlertGroup is returned in the API response @@ -138,6 +139,46 @@ func (ag *APIAlertGroup) dedupAnnotations() { ag.Shared.Annotations = sharedAnnotations } +func (ag *APIAlertGroup) dedupSilences() { + ag.Shared.Silences = map[string]string{} + + silencesByCluster := map[string][]string{} + + for _, alert := range ag.Alerts { + if alert.State != AlertStateSuppressed { + // if we find any alert that's not silenced then we can break early + return + } + for _, am := range alert.Alertmanager { + for _, silenceID := range am.SilencedBy { + _, ok := silencesByCluster[am.Cluster] + if !ok { + silencesByCluster[am.Cluster] = []string{} + } + if !slices.StringInSlice(silencesByCluster[am.Cluster], silenceID) { + silencesByCluster[am.Cluster] = append(silencesByCluster[am.Cluster], silenceID) + } + } + } + } + + // only deduplicate if all alerts are silenced with the same silence from a + // single cluster + if len(silencesByCluster) != 1 { + return + } + + // now check that all alerts are silenced with the same silenceID + for cluster, silences := range silencesByCluster { + for _, silenceID := range silences { + if silenceID != silences[0] { + return + } + } + ag.Shared.Silences[cluster] = silences[0] + } +} + // DedupSharedMaps will find all labels and annotations shared by all alerts // in this group and moved them to Shared namespace func (ag *APIAlertGroup) DedupSharedMaps() { @@ -147,10 +188,12 @@ func (ag *APIAlertGroup) DedupSharedMaps() { if len(ag.Alerts) > 1 { ag.dedupLabels() ag.dedupAnnotations() + ag.dedupSilences() } else { ag.Shared = APIAlertGroupSharedMaps{ Labels: map[string]string{}, Annotations: Annotations{}, + Silences: map[string]string{}, } } } diff --git a/internal/models/api_test.go b/internal/models/api_test.go index 0b1b54d05..ef42a2d71 100644 --- a/internal/models/api_test.go +++ b/internal/models/api_test.go @@ -4,10 +4,16 @@ import ( "encoding/json" "testing" + "github.com/pmezard/go-difflib/difflib" + "github.com/prymitive/karma/internal/models" ) func TestDedupSharedMaps(t *testing.T) { + am := models.AlertmanagerInstance{ + Cluster: "fakeCluster", + SilencedBy: []string{"fakeSilenceID"}, + } ag := models.APIAlertGroup{ AlertGroup: models.AlertGroup{ Labels: map[string]string{ @@ -15,6 +21,7 @@ func TestDedupSharedMaps(t *testing.T) { }, Alerts: models.AlertList{ models.Alert{ + State: models.AlertStateSuppressed, Annotations: models.Annotations{ models.Annotation{ Name: "summary", @@ -30,8 +37,10 @@ func TestDedupSharedMaps(t *testing.T) { "job": "node_exporter", "instance": "1", }, + Alertmanager: []models.AlertmanagerInstance{am}, }, models.Alert{ + State: models.AlertStateSuppressed, Annotations: models.Annotations{ models.Annotation{ Name: "summary", @@ -43,8 +52,10 @@ func TestDedupSharedMaps(t *testing.T) { "job": "node_exporter", "instance": "2", }, + Alertmanager: []models.AlertmanagerInstance{am}, }, models.Alert{ + State: models.AlertStateSuppressed, Annotations: models.Annotations{ models.Annotation{ Name: "summary", @@ -56,6 +67,7 @@ func TestDedupSharedMaps(t *testing.T) { "job": "blackbox", "instance": "3", }, + Alertmanager: []models.AlertmanagerInstance{am}, }, }, }, @@ -83,8 +95,21 @@ func TestDedupSharedMaps(t *testing.T) { }, "startsAt": "0001-01-01T00:00:00Z", "endsAt": "0001-01-01T00:00:00Z", - "state": "", - "alertmanager": null, + "state": "suppressed", + "alertmanager": [ + { + "name": "", + "cluster": "fakeCluster", + "state": "", + "startsAt": "0001-01-01T00:00:00Z", + "endsAt": "0001-01-01T00:00:00Z", + "source": "", + "silencedBy": [ + "fakeSilenceID" + ], + "inhibitedBy": null + } + ], "receiver": "" }, { @@ -95,8 +120,21 @@ func TestDedupSharedMaps(t *testing.T) { }, "startsAt": "0001-01-01T00:00:00Z", "endsAt": "0001-01-01T00:00:00Z", - "state": "", - "alertmanager": null, + "state": "suppressed", + "alertmanager": [ + { + "name": "", + "cluster": "fakeCluster", + "state": "", + "startsAt": "0001-01-01T00:00:00Z", + "endsAt": "0001-01-01T00:00:00Z", + "source": "", + "silencedBy": [ + "fakeSilenceID" + ], + "inhibitedBy": null + } + ], "receiver": "" }, { @@ -107,8 +145,21 @@ func TestDedupSharedMaps(t *testing.T) { }, "startsAt": "0001-01-01T00:00:00Z", "endsAt": "0001-01-01T00:00:00Z", - "state": "", - "alertmanager": null, + "state": "suppressed", + "alertmanager": [ + { + "name": "", + "cluster": "fakeCluster", + "state": "", + "startsAt": "0001-01-01T00:00:00Z", + "endsAt": "0001-01-01T00:00:00Z", + "source": "", + "silencedBy": [ + "fakeSilenceID" + ], + "inhibitedBy": null + } + ], "receiver": "" } ], @@ -125,13 +176,27 @@ func TestDedupSharedMaps(t *testing.T) { "isLink": false } ], - "labels": {} + "labels": {}, + "silences": { + "fakeCluster": "fakeSilenceID" + } } }` agJSON, _ := json.MarshalIndent(ag, "", " ") if string(agJSON) != expectedJSON { - t.Errorf("Expected: %s\nGot: %s\n", expectedJSON, string(agJSON)) + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(expectedJSON), + B: difflib.SplitLines(string(agJSON)), + FromFile: "Expected", + ToFile: "Current", + Context: 3, + } + text, err := difflib.GetUnifiedDiffString(diff) + if err != nil { + t.Error(err) + } + t.Errorf("JSON mismatch:\n%s", text) } }