diff --git a/Makefile b/Makefile index ffc99b703..76484c35c 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,7 @@ run: $(NAME) LABELS_COLOR_UNIQUE="@receiver instance cluster" \ LABELS_COLOR_STATIC="job" \ FILTERS_DEFAULT="@state=active @receiver=by-cluster-service" \ + SILENCEFORM_STRIP_LABELS="job" \ PORT=$(PORT) \ ./$(NAME) @@ -86,6 +87,7 @@ run-docker: docker-image -e LABELS_COLOR_UNIQUE="instance cluster" \ -e LABELS_COLOR_STATIC="job" \ -e FILTERS_DEFAULT="@state=active @receiver=by-cluster-service" \ + -e SILENCEFORM_STRIP_LABELS="job" \ -e PORT=$(PORT) \ -p $(PORT):$(PORT) \ $(NAME):$(VERSION) diff --git a/demo/karma.yaml b/demo/karma.yaml index ff97010cd..af9a93105 100644 --- a/demo/karma.yaml +++ b/demo/karma.yaml @@ -60,3 +60,7 @@ sentry: jira: - regex: DEVOPS-[0-9]+ uri: https://jira.example.com +silenceForm: + strip: + labels: + - job diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 2aa0e802b..d9db4dad6 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -599,6 +599,31 @@ sentry: public: https://:@sentry.io/ ``` +## Silence form + +`silenceForm` section allow customizing silence form behavior. +Syntax: + +```YAML +silenceForm: + strip: + labels: list of strings +``` + +- `strip:labels` - list of labels to ignore when populating silence form from + individual alerts or group of alerts. This allows to create silences matching + only unique labels, like `instance` or `host`, ignoring any common labels like + `job`. + +Example: + +```YAML +silenceForm: + strip: + labels: + - job +``` + ## Customizing karma In order to keep the core code simple karma doesn't support any way of extending diff --git a/docs/example.yaml b/docs/example.yaml index 38b2dd3e3..7fc9938cc 100644 --- a/docs/example.yaml +++ b/docs/example.yaml @@ -53,3 +53,7 @@ receivers: sentry: private: secret public: 123456789 +silenceForm: + strip: + labels: + - job diff --git a/internal/config/config.go b/internal/config/config.go index e2c433437..91d404b82 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -76,6 +76,8 @@ func init() { pflag.StringSlice("receivers.strip", []string{}, "List of receivers to not display alerts for") + pflag.StringSlice("silenceform.strip.labels", []string{}, "List of labels to ignore when auto-filling silence form from alerts") + pflag.String("listen.address", "", "IP/Hostname to listen on") pflag.Int("listen.port", 8080, "HTTP port to listen on") pflag.String("listen.prefix", "/", "URL prefix") @@ -161,6 +163,7 @@ func (config *configSchema) Read() { config.Receivers.Strip = v.GetStringSlice("receivers.strip") config.Sentry.Private = v.GetString("sentry.private") config.Sentry.Public = v.GetString("sentry.public") + config.SilenceForm.Strip.Labels = v.GetStringSlice("silenceform.strip.labels") err = v.UnmarshalKey("alertmanager.servers", &config.Alertmanager.Servers) if err != nil { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ddc561e28..882fd3f7a 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -119,6 +119,9 @@ receivers: sentry: private: secret key public: public key +silenceForm: + strip: + labels: [] ` configDump, err := yaml.Marshal(Config) diff --git a/internal/config/models.go b/internal/config/models.go index ac53968dd..f7831f818 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -92,4 +92,9 @@ type configSchema struct { Private string Public string } + SilenceForm struct { + Strip struct { + Labels []string + } + } `yaml:"silenceForm" mapstructure:"silenceForm"` } diff --git a/internal/models/api.go b/internal/models/api.go index 6fa98ad15..41999007d 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -219,13 +219,22 @@ type SortSettings struct { ValueMapping map[string]map[string]int `json:"valueMapping"` } +type SilenceFormStripSettings struct { + Labels []string `json:"labels"` +} + +type SilenceFormSettings struct { + Strip SilenceFormStripSettings `json:"strip"` +} + // Settings is used to export karma configuration that is used by UI type Settings struct { - StaticColorLabels []string `json:"staticColorLabels"` - AnnotationsDefaultHidden bool `json:"annotationsDefaultHidden"` - AnnotationsHidden []string `json:"annotationsHidden"` - AnnotationsVisible []string `json:"annotationsVisible"` - Sorting SortSettings `json:"sorting"` + StaticColorLabels []string `json:"staticColorLabels"` + AnnotationsDefaultHidden bool `json:"annotationsDefaultHidden"` + AnnotationsHidden []string `json:"annotationsHidden"` + AnnotationsVisible []string `json:"annotationsVisible"` + Sorting SortSettings `json:"sorting"` + SilenceForm SilenceFormSettings `json:"silenceForm"` } // AlertsResponse is the structure of JSON response UI will use to get alert data diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js index f90415016..aaff3bad1 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js @@ -17,13 +17,18 @@ import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash"; import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt"; import { APIAlert, APIGroup } from "Models/API"; +import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { FetchPauser } from "Components/FetchPauser"; import { DropdownSlide } from "Components/Animations/DropdownSlide"; -const onSilenceClick = (silenceFormStore, group, alert) => { +const onSilenceClick = (alertStore, silenceFormStore, group, alert) => { silenceFormStore.data.resetProgress(); - silenceFormStore.data.fillMatchersFromGroup(group, [alert]); + silenceFormStore.data.fillMatchersFromGroup( + group, + alertStore.settings.values.silenceForm.strip.labels, + [alert] + ); silenceFormStore.toggle.show(); }; @@ -35,6 +40,7 @@ const MenuContent = onClickOutside( group, alert, afterClick, + alertStore, silenceFormStore }) => { return ( @@ -62,7 +68,9 @@ const MenuContent = onClickOutside(
onSilenceClick(silenceFormStore, group, alert)} + onClick={() => + onSilenceClick(alertStore, silenceFormStore, group, alert) + } > Silence this alert @@ -86,6 +94,7 @@ const AlertMenu = observer( static propTypes = { group: APIGroup.isRequired, alert: APIAlert.isRequired, + alertStore: PropTypes.instanceOf(AlertStore).isRequired, silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired }; @@ -108,7 +117,7 @@ const AlertMenu = observer( }); render() { - const { group, alert, silenceFormStore } = this.props; + const { group, alert, alertStore, silenceFormStore } = this.props; const uniqueClass = `components-grid-alert-${group.id}-${hash( alert.labels @@ -148,6 +157,7 @@ const AlertMenu = observer( popperStyle={style} group={group} alert={alert} + alertStore={alertStore} silenceFormStore={silenceFormStore} afterClick={this.collapse.hide} handleClickOutside={this.collapse.hide} diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js index 4aee57a12..f63b215a3 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js @@ -29,6 +29,7 @@ const MountedAlertMenu = group => { @@ -70,6 +71,7 @@ const MountedMenuContent = group => { group={group} alert={alert} afterClick={MockAfterClick} + alertStore={alertStore} silenceFormStore={silenceFormStore} /> diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js index 72db47a43..393aef0d2 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js @@ -7,6 +7,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faVolumeMute } from "@fortawesome/free-solid-svg-icons/faVolumeMute"; import { APIAlert, APIGroup } from "Models/API"; +import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { BorderClassMap } from "Common/Colors"; import { StaticLabels } from "Common/Query"; @@ -26,6 +27,7 @@ const Alert = observer( showAlertmanagers: PropTypes.bool.isRequired, showReceiver: PropTypes.bool.isRequired, afterUpdate: PropTypes.func.isRequired, + alertStore: PropTypes.instanceOf(AlertStore).isRequired, silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired }; @@ -36,6 +38,7 @@ const Alert = observer( showAlertmanagers, showReceiver, afterUpdate, + alertStore, silenceFormStore } = this.props; @@ -87,6 +90,7 @@ const Alert = observer( {alert.alertmanager 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 047ed8aba..0b25cfe59 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js @@ -52,6 +52,7 @@ const MountedAlert = (alert, group, showAlertmanagers, showReceiver) => { showAlertmanagers={showAlertmanagers} showReceiver={showReceiver} afterUpdate={MockAfterUpdate} + alertStore={alertStore} silenceFormStore={silenceFormStore} /> diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js index cd27dd9cc..89255fcbf 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js @@ -16,14 +16,18 @@ import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash"; import { APIGroup } from "Models/API"; import { FormatAPIFilterQuery } from "Stores/AlertStore"; +import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { QueryOperators, StaticLabels, FormatQuery } from "Common/Query"; import { DropdownSlide } from "Components/Animations/DropdownSlide"; import { FetchPauser } from "Components/FetchPauser"; -const onSilenceClick = (silenceFormStore, group) => { +const onSilenceClick = (alertStore, silenceFormStore, group) => { silenceFormStore.data.resetProgress(); - silenceFormStore.data.fillMatchersFromGroup(group); + silenceFormStore.data.fillMatchersFromGroup( + group, + alertStore.settings.values.silenceForm.strip.labels + ); silenceFormStore.toggle.show(); }; @@ -34,6 +38,7 @@ const MenuContent = onClickOutside( popperStyle, group, afterClick, + alertStore, silenceFormStore }) => { let groupFilters = Object.keys(group.labels).map(name => @@ -65,7 +70,7 @@ const MenuContent = onClickOutside(
onSilenceClick(silenceFormStore, group)} + onClick={() => onSilenceClick(alertStore, silenceFormStore, group)} > Silence this group
@@ -86,6 +91,7 @@ const GroupMenu = observer( class GroupMenu extends Component { static propTypes = { group: APIGroup.isRequired, + alertStore: PropTypes.instanceOf(AlertStore).isRequired, silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, themed: PropTypes.bool.isRequired }; @@ -109,7 +115,7 @@ const GroupMenu = observer( }); render() { - const { group, silenceFormStore, themed } = this.props; + const { group, alertStore, silenceFormStore, themed } = this.props; return ( @@ -143,6 +149,7 @@ const GroupMenu = observer( popperRef={ref} popperStyle={style} group={group} + alertStore={alertStore} silenceFormStore={silenceFormStore} afterClick={this.collapse.hide} handleClickOutside={this.collapse.hide} diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js index 216ebaca4..ec6288ec7 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js @@ -26,6 +26,7 @@ const MountedGroupMenu = (group, themed) => { @@ -71,6 +72,7 @@ const MountedMenuContent = group => { popperStyle={{}} group={group} afterClick={MockAfterClick} + alertStore={alertStore} silenceFormStore={silenceFormStore} /> diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.js index 61c33bfc8..b82535b76 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.js @@ -8,6 +8,7 @@ import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp"; import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; import { APIGroup } from "Models/API"; +import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { FilteringLabel } from "Components/Labels/FilteringLabel"; import { FilteringCounterBadge } from "Components/Labels/FilteringCounterBadge"; @@ -22,6 +23,7 @@ const GroupHeader = observer( toggle: PropTypes.func.isRequired }).isRequired, group: APIGroup.isRequired, + alertStore: PropTypes.instanceOf(AlertStore).isRequired, silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, themedCounters: PropTypes.bool.isRequired }; @@ -30,6 +32,7 @@ const GroupHeader = observer( const { collapseStore, group, + alertStore, silenceFormStore, themedCounters } = this.props; @@ -43,6 +46,7 @@ const GroupHeader = observer( diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js index 581ad2621..f7f425d7b 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js @@ -12,6 +12,7 @@ import { faMinus } from "@fortawesome/free-solid-svg-icons/faMinus"; import { APIGroup } from "Models/API"; import { Settings } from "Stores/Settings"; +import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { IsMobile } from "Common/Device"; import { BackgroundClassMap } from "Common/Colors"; @@ -55,6 +56,7 @@ const AlertGroup = observer( afterUpdate: PropTypes.func.isRequired, group: APIGroup.isRequired, showAlertmanagers: PropTypes.bool.isRequired, + alertStore: PropTypes.instanceOf(AlertStore).isRequired, settingsStore: PropTypes.instanceOf(Settings).isRequired, silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, style: PropTypes.object @@ -157,6 +159,7 @@ const AlertGroup = observer( showAlertmanagers, afterUpdate, silenceFormStore, + alertStore, settingsStore, style } = this.props; @@ -198,6 +201,7 @@ const AlertGroup = observer( @@ -216,6 +220,7 @@ const AlertGroup = observer( } showReceiver={group.alerts.length === 1} afterUpdate={afterUpdate} + alertStore={alertStore} silenceFormStore={silenceFormStore} /> ))} diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js index 16ea8fb2c..0581a56c0 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js @@ -64,6 +64,7 @@ const MountedAlertGroup = (afterUpdate, showAlertmanagers) => { group={group} showAlertmanagers={showAlertmanagers} settingsStore={settingsStore} + alertStore={alertStore} silenceFormStore={silenceFormStore} /> diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js index ea15856fa..8cb15912d 100644 --- a/ui/src/Components/Grid/AlertGrid/index.js +++ b/ui/src/Components/Grid/AlertGrid/index.js @@ -240,6 +240,7 @@ const AlertGrid = observer( Object.keys(alertStore.data.upstreams.clusters).length > 1 } afterUpdate={this.masonryRepack} + alertStore={alertStore} settingsStore={settingsStore} silenceFormStore={silenceFormStore} style={{ diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index dfd75948b..015ed16e6 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -177,6 +177,11 @@ class AlertStore { label: "alertname" }, valueMapping: {} + }, + silenceForm: { + strip: { + labels: [] + } } } }, diff --git a/ui/src/Stores/SilenceFormStore.js b/ui/src/Stores/SilenceFormStore.js index 5bb676945..3f41736dd 100644 --- a/ui/src/Stores/SilenceFormStore.js +++ b/ui/src/Stores/SilenceFormStore.js @@ -114,17 +114,19 @@ class SilenceFormStore { }, // if alerts argument is not passed all group alerts will be used - fillMatchersFromGroup(group, alerts) { + fillMatchersFromGroup(group, stripLabels, alerts) { let matchers = []; // add matchers for all shared labels in this group for (const [key, value] of Object.entries( Object.assign({}, group.labels, group.shared.labels) )) { - const matcher = NewEmptyMatcher(); - matcher.name = key; - matcher.values = [MatcherValueToObject(value)]; - matchers.push(matcher); + if (!stripLabels.includes(key)) { + const matcher = NewEmptyMatcher(); + matcher.name = key; + matcher.values = [MatcherValueToObject(value)]; + matchers.push(matcher); + } } // add matchers for all unique labels in this group @@ -132,10 +134,12 @@ class SilenceFormStore { const allAlerts = alerts ? alerts : group.alerts; for (const alert of allAlerts) { for (const [key, value] of Object.entries(alert.labels)) { - if (!labels[key]) { - labels[key] = new Set(); + if (!stripLabels.includes(key)) { + if (!labels[key]) { + labels[key] = new Set(); + } + labels[key].add(value); } - labels[key].add(value); } } for (const [key, values] of Object.entries(labels)) { diff --git a/ui/src/Stores/SilenceFormStore.test.js b/ui/src/Stores/SilenceFormStore.test.js index 4f0e97b5e..f25ec17e2 100644 --- a/ui/src/Stores/SilenceFormStore.test.js +++ b/ui/src/Stores/SilenceFormStore.test.js @@ -120,7 +120,7 @@ describe("SilenceFormStore.data", () => { it("fillMatchersFromGroup() creates correct matcher object for a group", () => { const group = MockGroup(); - store.data.fillMatchersFromGroup(group); + store.data.fillMatchersFromGroup(group, []); expect(store.data.matchers).toHaveLength(4); expect(store.data.matchers).toContainEqual( expect.objectContaining({ @@ -161,7 +161,7 @@ describe("SilenceFormStore.data", () => { it("fillMatchersFromGroup() creates correct matcher object for a group with only a subset of alets passed", () => { const group = MockGroup(); - store.data.fillMatchersFromGroup(group, [group.alerts[0]]); + store.data.fillMatchersFromGroup(group, [], [group.alerts[0]]); expect(store.data.matchers).toHaveLength(4); expect(store.data.matchers).toContainEqual( expect.objectContaining({ @@ -193,10 +193,27 @@ describe("SilenceFormStore.data", () => { ); }); + it("fillMatchersFromGroup() ignores labels from stripLabels list", () => { + const group = MockGroup(); + store.data.fillMatchersFromGroup( + group, + ["job", "instance", "cluster"], + [group.alerts[0]] + ); + expect(store.data.matchers).toHaveLength(1); + expect(store.data.matchers).toContainEqual( + expect.objectContaining({ + name: "alertname", + values: [{ label: "FakeAlert", value: "FakeAlert" }], + isRegex: false + }) + ); + }); + it("fillMatchersFromGroup() resets silenceID if set", () => { store.data.silenceID = "12345"; const group = MockGroup(); - store.data.fillMatchersFromGroup(group, [group.alerts[0]]); + store.data.fillMatchersFromGroup(group, [], [group.alerts[0]]); expect(store.data.silenceID).toBeNull(); }); @@ -259,7 +276,7 @@ describe("SilenceFormStore.data", () => { it("toAlertmanagerPayload creates payload that matches snapshot", () => { const group = MockGroup(); - store.data.fillMatchersFromGroup(group); + store.data.fillMatchersFromGroup(group, []); // add empty matcher so we test empty string rendering store.data.addEmptyMatcher(); store.data.startsAt = moment.utc([2000, 1, 1, 0, 0, 0]); diff --git a/ui/src/__mocks__/Fetch.js b/ui/src/__mocks__/Fetch.js index 1c6430373..f074457a9 100644 --- a/ui/src/__mocks__/Fetch.js +++ b/ui/src/__mocks__/Fetch.js @@ -42,6 +42,11 @@ const EmptyAPIResponse = () => ({ } } }, + silenceForm: { + strip: { + labels: [] + } + }, staticColorLabels: ["job"], annotationsDefaultHidden: false, annotationsHidden: [], diff --git a/views.go b/views.go index 63c2c7899..47c69625c 100644 --- a/views.go +++ b/views.go @@ -95,6 +95,11 @@ func alerts(c *gin.Context) { AnnotationsDefaultHidden: config.Config.Annotations.Default.Hidden, AnnotationsHidden: config.Config.Annotations.Hidden, AnnotationsVisible: config.Config.Annotations.Visible, + SilenceForm: models.SilenceFormSettings{ + Strip: models.SilenceFormStripSettings{ + Labels: config.Config.SilenceForm.Strip.Labels, + }, + }, } if config.Config.Grid.Sorting.CustomValues.Labels != nil {