Merge pull request #662 from prymitive/silenceform-strip-labels

feat(ui): add options for ignoring labels when populating silences from alerts
This commit is contained in:
Łukasz Mierzwa
2019-04-29 21:37:49 +01:00
committed by GitHub
23 changed files with 153 additions and 25 deletions

View File

@@ -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)

View File

@@ -60,3 +60,7 @@ sentry:
jira:
- regex: DEVOPS-[0-9]+
uri: https://jira.example.com
silenceForm:
strip:
labels:
- job

View File

@@ -599,6 +599,31 @@ sentry:
public: https://<key>:<secret>@sentry.io/<project>
```
## 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

View File

@@ -53,3 +53,7 @@ receivers:
sentry:
private: secret
public: 123456789
silenceForm:
strip:
labels:
- job

View File

@@ -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 {

View File

@@ -119,6 +119,9 @@ receivers:
sentry:
private: secret key
public: public key
silenceForm:
strip:
labels: []
`
configDump, err := yaml.Marshal(Config)

View File

@@ -92,4 +92,9 @@ type configSchema struct {
Private string
Public string
}
SilenceForm struct {
Strip struct {
Labels []string
}
} `yaml:"silenceForm" mapstructure:"silenceForm"`
}

View File

@@ -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

View File

@@ -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(
<div className="dropdown-divider" />
<div
className="dropdown-item cursor-pointer"
onClick={() => onSilenceClick(silenceFormStore, group, alert)}
onClick={() =>
onSilenceClick(alertStore, silenceFormStore, group, alert)
}
>
<FontAwesomeIcon className="mr-1" icon={faBellSlash} />
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}

View File

@@ -29,6 +29,7 @@ const MountedAlertMenu = group => {
<AlertMenu
group={group}
alert={alert}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
</Provider>
@@ -70,6 +71,7 @@ const MountedMenuContent = group => {
group={group}
alert={alert}
afterClick={MockAfterClick}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
</Provider>

View File

@@ -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(
<AlertMenu
group={group}
alert={alert}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
{alert.alertmanager

View File

@@ -52,6 +52,7 @@ const MountedAlert = (alert, group, showAlertmanagers, showReceiver) => {
showAlertmanagers={showAlertmanagers}
showReceiver={showReceiver}
afterUpdate={MockAfterUpdate}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
</Provider>

View File

@@ -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(
</div>
<div
className="dropdown-item cursor-pointer"
onClick={() => onSilenceClick(silenceFormStore, group)}
onClick={() => onSilenceClick(alertStore, silenceFormStore, group)}
>
<FontAwesomeIcon icon={faBellSlash} /> Silence this group
</div>
@@ -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 (
<Manager>
@@ -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}

View File

@@ -26,6 +26,7 @@ const MountedGroupMenu = (group, themed) => {
<Provider alertStore={alertStore}>
<GroupMenu
group={group}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
themed={themed}
/>
@@ -71,6 +72,7 @@ const MountedMenuContent = group => {
popperStyle={{}}
group={group}
afterClick={MockAfterClick}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
</Provider>

View File

@@ -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(
<span className="flex-shrink-0 flex-grow-0">
<GroupMenu
group={group}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
themed={!themedCounters}
/>

View File

@@ -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(
<GroupHeader
collapseStore={this.collapse}
group={group}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
themedCounters={themedCounters}
/>
@@ -216,6 +220,7 @@ const AlertGroup = observer(
}
showReceiver={group.alerts.length === 1}
afterUpdate={afterUpdate}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
))}

View File

@@ -64,6 +64,7 @@ const MountedAlertGroup = (afterUpdate, showAlertmanagers) => {
group={group}
showAlertmanagers={showAlertmanagers}
settingsStore={settingsStore}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
</Provider>

View File

@@ -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={{

View File

@@ -177,6 +177,11 @@ class AlertStore {
label: "alertname"
},
valueMapping: {}
},
silenceForm: {
strip: {
labels: []
}
}
}
},

View File

@@ -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)) {

View File

@@ -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]);

View File

@@ -42,6 +42,11 @@ const EmptyAPIResponse = () => ({
}
}
},
silenceForm: {
strip: {
labels: []
}
},
staticColorLabels: ["job"],
annotationsDefaultHidden: false,
annotationsHidden: [],

View File

@@ -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 {