From 055f8d9f9a0ff83551789af3ebb3736100bc524c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 16 Jul 2021 18:15:22 +0100 Subject: [PATCH] fix(ui): don't rely on alert list for silences --- ui/src/Components/AlertAck/index.test.tsx | 4 + ui/src/Components/AlertAck/index.tsx | 2 +- ui/src/Models/APITypes.ts | 5 + ui/src/Stores/SilenceFormStore.test.ts | 198 ++++++++++++++++++++++ ui/src/Stores/SilenceFormStore.ts | 42 ++++- ui/src/__fixtures__/Alerts.ts | 5 + 6 files changed, 246 insertions(+), 10 deletions(-) diff --git a/ui/src/Components/AlertAck/index.test.tsx b/ui/src/Components/AlertAck/index.test.tsx index 561ae4eec..65bb02455 100644 --- a/ui/src/Components/AlertAck/index.test.tsx +++ b/ui/src/Components/AlertAck/index.test.tsx @@ -63,6 +63,10 @@ beforeEach(() => { MockAlert([], { foo: "ignore" }, "suppressed"), ]; group = MockAlertGroup({ alertname: "Fake Alert" }, alerts, [], {}, {}); + group.allLabels.active = { + alertname: ["Fake Alert"], + foo: ["bar", "baz"], + }; fetchMock.resetHistory(); fetchMock.mock( diff --git a/ui/src/Components/AlertAck/index.tsx b/ui/src/Components/AlertAck/index.tsx index 9f0af7da5..20a2a64a7 100644 --- a/ui/src/Components/AlertAck/index.tsx +++ b/ui/src/Components/AlertAck/index.tsx @@ -84,7 +84,7 @@ const AlertAck: FC<{ payload: GenerateAlertmanagerSilenceData( now, addSeconds(now, durationSeconds), - MatchersFromGroup(group, [], group.alerts, true), + MatchersFromGroup(group, [], true), author, comment ), diff --git a/ui/src/Models/APITypes.ts b/ui/src/Models/APITypes.ts index b4a4224a0..b4b87adc0 100644 --- a/ui/src/Models/APITypes.ts +++ b/ui/src/Models/APITypes.ts @@ -59,6 +59,11 @@ export interface APIAlertGroupT { labels: LabelsT; alerts: APIAlertT[]; totalAlerts: number; + allLabels: { + active: { [key: string]: string[] }; + suppressed: { [key: string]: string[] }; + unprocessed: { [key: string]: string[] }; + }; alertmanagerCount: { [key: string]: number }; stateCount: StateCountT; shared: { diff --git a/ui/src/Stores/SilenceFormStore.test.ts b/ui/src/Stores/SilenceFormStore.test.ts index 0c69f9780..c0215181c 100644 --- a/ui/src/Stores/SilenceFormStore.test.ts +++ b/ui/src/Stores/SilenceFormStore.test.ts @@ -126,6 +126,12 @@ describe("SilenceFormStore.data", () => { it("fillMatchersFromGroup() creates correct matcher object for a group", () => { const group = MockGroup(); + group.allLabels.active = { + alertname: ["FakeAlert"], + job: ["mock"], + instance: ["dev1", "prod1", "prod2"], + cluster: ["dev", "prod"], + }; store.data.fillMatchersFromGroup( group, [], @@ -172,6 +178,187 @@ describe("SilenceFormStore.data", () => { ); }); + it("fillMatchersFromGroup() creates correct matcher object for a list of alerts", () => { + const group = MockGroup(); + group.allLabels.active = { + alertname: ["FakeAlert"], + job: ["mock"], + instance: ["dev1", "prod1", "prod2"], + cluster: ["dev", "prod"], + }; + store.data.fillMatchersFromGroup( + group, + ["job"], + AlertmanagerClustersToOption({ ha: ["am1", "am2"] }), + group.alerts.slice(0, 2) + ); + expect(store.data.alertmanagers).toMatchObject([ + { label: "Cluster: ha", value: ["am1", "am2"] }, + ]); + expect(store.data.matchers).toHaveLength(3); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "alertname", + values: [{ label: "FakeAlert", value: "FakeAlert" }], + isRegex: false, + }) + ); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "instance", + values: [ + { label: "prod1", value: "prod1" }, + { label: "prod2", value: "prod2" }, + ], + isRegex: true, + }) + ); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "cluster", + values: [{ label: "prod", value: "prod" }], + isRegex: false, + }) + ); + }); + + it("fillMatchersFromGroup() creates correct matcher object for a list of alerts with common labels", () => { + const group = MockGroup(); + group.allLabels.active = { + alertname: ["FakeAlert"], + job: ["mock"], + instance: ["dev1", "prod1", "prod2"], + cluster: ["dev", "prod"], + }; + + store.data.fillMatchersFromGroup( + group, + ["job"], + AlertmanagerClustersToOption({ ha: ["am1", "am2"] }), + [group.alerts[0], group.alerts[2]] + ); + expect(store.data.alertmanagers).toMatchObject([ + { label: "Cluster: ha", value: ["am1", "am2"] }, + ]); + expect(store.data.matchers).toHaveLength(3); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "alertname", + values: [{ label: "FakeAlert", value: "FakeAlert" }], + isRegex: false, + }) + ); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "instance", + values: [ + { label: "dev1", value: "dev1" }, + { label: "prod1", value: "prod1" }, + ], + isRegex: true, + }) + ); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "cluster", + values: [ + { label: "dev", value: "dev" }, + { label: "prod", value: "prod" }, + ], + isRegex: true, + }) + ); + }); + + it("fillMatchersFromGroup() creates correct matcher object for a list of alerts with uncommon labels", () => { + const alerts = [ + MockAlert([], { instance: "1", banana: "ignore" }, "active"), + MockAlert([], { instance: "2" }, "suppressed"), + MockAlert([], { instance: "3" }, "active"), + ]; + const group = MockAlertGroup( + { alertname: "FakeAlert" }, + alerts, + [], + { + job: "mock", + }, + {} + ); + group.allLabels.active = { + alertname: ["FakeAlert"], + job: ["mock"], + instance: ["dev1", "prod1", "prod2"], + cluster: ["dev", "prod"], + }; + + store.data.fillMatchersFromGroup( + group, + ["job"], + AlertmanagerClustersToOption({ ha: ["am1", "am2"] }), + [group.alerts[0], group.alerts[2]] + ); + expect(store.data.alertmanagers).toMatchObject([ + { label: "Cluster: ha", value: ["am1", "am2"] }, + ]); + expect(store.data.matchers).toHaveLength(2); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "alertname", + values: [{ label: "FakeAlert", value: "FakeAlert" }], + isRegex: false, + }) + ); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "instance", + values: [ + { label: "1", value: "1" }, + { label: "3", value: "3" }, + ], + isRegex: true, + }) + ); + }); + + it("fillMatchersFromGroup() creates correct matcher object for a list of alerts with no labels", () => { + const alerts = [ + MockAlert([], {}, "active"), + MockAlert([], {}, "suppressed"), + MockAlert([], {}, "active"), + ]; + const group = MockAlertGroup( + { alertname: "FakeAlert" }, + alerts, + [], + { + job: "mock", + }, + {} + ); + group.allLabels.active = { + alertname: ["FakeAlert"], + }; + + store.data.fillMatchersFromGroup( + group, + ["job"], + AlertmanagerClustersToOption({ ha: ["am1", "am2"] }), + [group.alerts[0], group.alerts[2]] + ); + expect(store.data.alertmanagers).toMatchObject([ + { label: "Cluster: ha", value: ["am1", "am2"] }, + ]); + expect(store.data.matchers).toHaveLength(1); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "alertname", + values: [{ label: "FakeAlert", value: "FakeAlert" }], + isRegex: false, + }) + ); + }); + it("fillMatchersFromGroup() creates correct matcher object for a group with only a subset of alerts passed", () => { const group = MockGroup(); store.data.fillMatchersFromGroup( @@ -262,6 +449,11 @@ describe("SilenceFormStore.data", () => { }, {} ); + group.allLabels.active = { + alertname: ["Alert1", "Alert2", "Alert3"], + job: ["mock"], + region: ["AF"], + }; store.data.fillMatchersFromGroup(group, [], []); expect(store.data.matchers).toHaveLength(3); expect(store.data.matchers).toContainEqual( @@ -478,6 +670,12 @@ describe("SilenceFormStore.data", () => { it("toAlertmanagerPayload creates payload that matches snapshot", () => { const group = MockGroup(); + group.allLabels.active = { + alertname: ["FakeAlert"], + job: ["mock"], + instance: ["dev1", "prod1", "prod2"], + cluster: ["dev", "prod"], + }; store.data.fillMatchersFromGroup(group, [], []); // add empty matcher so we test empty string rendering store.data.addEmptyMatcher(); diff --git a/ui/src/Stores/SilenceFormStore.ts b/ui/src/Stores/SilenceFormStore.ts index 396b2f9f3..2f11f6d08 100644 --- a/ui/src/Stores/SilenceFormStore.ts +++ b/ui/src/Stores/SilenceFormStore.ts @@ -80,11 +80,37 @@ const AlertmanagerClustersToOption = (clusterDict: { const MatchersFromGroup = ( group: APIAlertGroupT, stripLabels: string[], - alerts?: APIAlertT[], onlyActive?: boolean ): MatcherWithIDT[] => { const matchers: MatcherWithIDT[] = []; + for (const [state, labels] of Object.entries(group.allLabels)) { + if (onlyActive === true && state !== "active") { + continue; + } + for (const [key, values] of Object.entries(labels).filter( + ([key, _]) => !stripLabels.includes(key) + )) { + matchers.push({ + id: uniqueId(), + name: key, + values: values.map((value) => StringToOption(value)), + isRegex: values.length > 1, + isEqual: true, + }); + } + } + + return matchers; +}; + +const MatchersFromAlerts = ( + group: APIAlertGroupT, + stripLabels: string[], + alerts: APIAlertT[] +): MatcherWithIDT[] => { + const matchers: MatcherWithIDT[] = []; + // add matchers for all shared labels in this group for (const [key, value] of Object.entries( Object.assign({}, group.labels, group.shared.labels) @@ -97,13 +123,8 @@ const MatchersFromGroup = ( } } - // this is the list of alerts we'll use to generate matchers - const filteredAlerts = (alerts ? alerts : group.alerts).filter( - (alert) => !onlyActive || alert.state === "active" - ); - // array of arrays with label keys for each alert - const allLabelKeys = filteredAlerts + const allLabelKeys = alerts .map((alert) => Object.keys(alert.labels)) .filter((a) => a.length > 0); @@ -125,7 +146,7 @@ const MatchersFromGroup = ( // add matchers for all unique labels in this group const labels: { [key: string]: Set } = {}; - for (const alert of filteredAlerts) { + for (const alert of alerts) { for (const [key, value] of Object.entries(alert.labels)) { if (sharedLabelKeys.includes(key) && !stripLabels.includes(key)) { if (!labels[key]) { @@ -486,7 +507,9 @@ class SilenceFormStore { ) { this.alertmanagers = alertmanagers; - this.matchers = MatchersFromGroup(group, stripLabels, alerts); + this.matchers = alerts + ? MatchersFromAlerts(group, stripLabels, alerts) + : MatchersFromGroup(group, stripLabels); // ensure that silenceID is nulled, since it's used to edit silences // and this is used to silence groups this.silenceID = null; @@ -652,6 +675,7 @@ export { NewEmptyMatcher, AlertmanagerClustersToOption, MatchersFromGroup, + MatchersFromAlerts, GenerateAlertmanagerSilenceData, NewClusterRequest, MatcherToOperator, diff --git a/ui/src/__fixtures__/Alerts.ts b/ui/src/__fixtures__/Alerts.ts index 8ec8198f2..95e21e248 100644 --- a/ui/src/__fixtures__/Alerts.ts +++ b/ui/src/__fixtures__/Alerts.ts @@ -58,6 +58,11 @@ const MockAlertGroup = ( labels: rootLabels, totalAlerts: alerts.length, alerts: alerts, + allLabels: { + active: {}, + suppressed: {}, + unprocessed: {}, + }, id: "099c5ca6d1c92f615b13056b935d0c8dee70f18c", alertmanagerCount: { default: 1,