diff --git a/public/app/features/alerting/unified/components/notification-policies/Filters.tsx b/public/app/features/alerting/unified/components/notification-policies/Filters.tsx
index 1352563ed67..4989cf15a83 100644
--- a/public/app/features/alerting/unified/components/notification-policies/Filters.tsx
+++ b/public/app/features/alerting/unified/components/notification-policies/Filters.tsx
@@ -7,8 +7,12 @@ import { Button, Field, Icon, Input, Label, Select, Stack, Text, Tooltip, useSty
import { ObjectMatcher, Receiver, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
-import { matcherToObjectMatcher, parseMatchers } from '../../utils/alertmanager';
-import { normalizeMatchers } from '../../utils/matchers';
+import { matcherToObjectMatcher } from '../../utils/alertmanager';
+import {
+ normalizeMatchers,
+ parsePromQLStyleMatcherLoose,
+ parsePromQLStyleMatcherLooseSafe,
+} from '../../utils/matchers';
interface NotificationPoliciesFilterProps {
receivers: Receiver[];
@@ -35,7 +39,7 @@ const NotificationPoliciesFilter = ({
}, [contactPoint, onChangeReceiver]);
useEffect(() => {
- const matchers = parseMatchers(queryString ?? '').map(matcherToObjectMatcher);
+ const matchers = parsePromQLStyleMatcherLooseSafe(queryString ?? '').map(matcherToObjectMatcher);
handleChangeLabels()(matchers);
}, [handleChangeLabels, queryString]);
@@ -50,7 +54,17 @@ const NotificationPoliciesFilter = ({
const selectedContactPoint = receiverOptions.find((option) => option.value === contactPoint) ?? null;
const hasFilters = queryString || contactPoint;
- const inputInvalid = queryString && queryString.length > 3 ? parseMatchers(queryString).length === 0 : false;
+
+ let inputValid = Boolean(queryString && queryString.length > 3);
+ try {
+ if (!queryString) {
+ inputValid = true;
+ } else {
+ parsePromQLStyleMatcherLoose(queryString);
+ }
+ } catch (err) {
+ inputValid = false;
+ }
return (
@@ -73,8 +87,8 @@ const NotificationPoliciesFilter = ({
}
- invalid={inputInvalid}
- error={inputInvalid ? 'Query must use valid matcher syntax' : null}
+ invalid={!inputValid}
+ error={!inputValid ? 'Query must use valid matcher syntax' : null}
>
labelsMatchMatchers(labels, matchers));
}
if (alertInstanceState) {
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx
index 175c7a304fc..3474049887d 100644
--- a/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx
@@ -18,8 +18,9 @@ import {
import { stateHistoryApi } from '../../../api/stateHistoryApi';
import { usePagination } from '../../../hooks/usePagination';
-import { labelsMatchMatchers, parseMatchers } from '../../../utils/alertmanager';
+import { labelsMatchMatchers } from '../../../utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
import { stringifyErrorLike } from '../../../utils/misc';
import { AlertLabels } from '../../AlertLabels';
import { CollapseToggle } from '../../CollapseToggle';
@@ -412,7 +413,7 @@ function useRuleHistoryRecords(stateHistory?: DataFrameJSON, filter?: string) {
return { historyRecords: [] };
}
- const filterMatchers = filter ? parseMatchers(filter) : [];
+ const filterMatchers = filter ? parsePromQLStyleMatcherLooseSafe(filter) : [];
const [tsValues, lines] = stateHistory.data.values;
const timestamps = isNumbers(tsValues) ? tsValues : [];
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts b/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts
index 02cd7a7dc8d..324849c6da5 100644
--- a/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts
@@ -3,7 +3,8 @@ import { groupBy } from 'lodash';
import { DataFrame, Field as DataFrameField, DataFrameJSON, Field, FieldType } from '@grafana/data';
import { fieldIndexComparer } from '@grafana/data/src/field/fieldComparers';
-import { labelsMatchMatchers, parseMatchers } from '../../../utils/alertmanager';
+import { labelsMatchMatchers } from '../../../utils/alertmanager';
+import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
import { LogRecord } from '../state-history/common';
import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords';
@@ -61,7 +62,7 @@ function groupDataFramesByTimeAndFilterByLabels(dataFrames: DataFrame[]): DataFr
const filterValue = getFilterInQueryParams();
const dataframesFiltered = dataFrames.filter((frame) => {
const labels = JSON.parse(frame.name ?? ''); // in name we store the labels stringified
- const matchers = Boolean(filterValue) ? parseMatchers(filterValue) : [];
+ const matchers = Boolean(filterValue) ? parsePromQLStyleMatcherLooseSafe(filterValue) : [];
return labelsMatchMatchers(labels, matchers);
});
// Extract time fields from filtered data frames
diff --git a/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx b/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx
index 67efa185b3a..8187fb06e8a 100644
--- a/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx
+++ b/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx
@@ -13,7 +13,8 @@ import { fieldIndexComparer } from '@grafana/data/src/field/fieldComparers';
import { MappingType, ThresholdsMode } from '@grafana/schema';
import { useTheme2 } from '@grafana/ui';
-import { labelsMatchMatchers, parseMatchers } from '../../../utils/alertmanager';
+import { labelsMatchMatchers } from '../../../utils/alertmanager';
+import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
import { extractCommonLabels, Line, LogRecord, omitLabels } from './common';
@@ -50,7 +51,7 @@ export function useRuleHistoryRecords(stateHistory?: DataFrameJSON, filter?: str
const commonLabels = extractCommonLabels(groupLabelsArray);
- const filterMatchers = filter ? parseMatchers(filter) : [];
+ const filterMatchers = filter ? parsePromQLStyleMatcherLooseSafe(filter) : [];
const filteredGroupedLines = Object.entries(logRecordsByInstance).filter(([key]) => {
const labels = JSON.parse(key);
return labelsMatchMatchers(labels, filterMatchers);
diff --git a/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx
index 7cb57c91055..a15698fc893 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx
@@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
-import { parseMatchers } from '../../utils/alertmanager';
+import { parsePromQLStyleMatcherLoose } from '../../utils/matchers';
import { getSilenceFiltersFromUrlParams } from '../../utils/misc';
const getQueryStringKey = () => uniqueId('query-string-');
@@ -30,7 +30,16 @@ export const SilencesFilter = () => {
setTimeout(() => setQueryStringKey(getQueryStringKey()));
};
- const inputInvalid = queryString && queryString.length > 3 ? parseMatchers(queryString).length === 0 : false;
+ let inputValid = queryString && queryString.length > 3;
+ try {
+ if (!queryString) {
+ inputValid = true;
+ } else {
+ parsePromQLStyleMatcherLoose(queryString);
+ }
+ } catch (err) {
+ inputValid = false;
+ }
return (
@@ -53,8 +62,8 @@ export const SilencesFilter = () => {
}
- invalid={inputInvalid}
- error={inputInvalid ? 'Query must use valid matcher syntax' : null}
+ invalid={!inputValid}
+ error={!inputValid ? 'Query must use valid matcher syntax' : null}
>
{
}
}
if (queryString) {
- const matchers = parseMatchers(queryString);
+ const matchers = parsePromQLStyleMatcherLooseSafe(queryString);
const matchersMatch = matchers.every((matcher) =>
silence.matchers?.some(
({ name, value, isEqual, isRegex }) =>
diff --git a/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts b/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts
index 3aa49042d93..3d88eafd006 100644
--- a/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts
+++ b/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts
@@ -3,19 +3,21 @@ import { useMemo } from 'react';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
-import { labelsMatchMatchers, parseMatchers } from '../utils/alertmanager';
+import { labelsMatchMatchers } from '../utils/alertmanager';
+import { parsePromQLStyleMatcherLooseSafe } from '../utils/matchers';
import { getFiltersFromUrlParams } from '../utils/misc';
export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => {
const [queryParams] = useQueryParams();
- const filters = getFiltersFromUrlParams(queryParams);
- const matchers = parseMatchers(filters.queryString || '');
+ const { queryString, alertState } = getFiltersFromUrlParams(queryParams);
return useMemo(() => {
+ const matchers = queryString ? parsePromQLStyleMatcherLooseSafe(queryString) : [];
+
return groups.reduce((filteredGroup: AlertmanagerGroup[], group) => {
const alerts = group.alerts.filter(({ labels, status }) => {
const labelsMatch = labelsMatchMatchers(labels, matchers);
- const filtersMatch = filters.alertState ? status.state === filters.alertState : true;
+ const filtersMatch = alertState ? status.state === alertState : true;
return labelsMatch && filtersMatch;
});
if (alerts.length > 0) {
@@ -28,5 +30,5 @@ export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => {
}
return filteredGroup;
}, []);
- }, [groups, filters, matchers]);
+ }, [queryString, groups, alertState]);
};
diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
index eaa13b43840..f7674c92500 100644
--- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts
+++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
@@ -9,10 +9,10 @@ import { CombinedRuleGroup, CombinedRuleNamespace, Rule } from 'app/types/unifie
import { isPromAlertingRuleState, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
import { applySearchFilterToQuery, getSearchFilterFromQuery, RulesFilter } from '../search/rulesSearchParser';
-import { labelsMatchMatchers, matcherToMatcherField, parseMatchers } from '../utils/alertmanager';
+import { labelsMatchMatchers, matcherToMatcherField } from '../utils/alertmanager';
import { Annotation } from '../utils/constants';
import { isCloudRulesSource } from '../utils/datasource';
-import { parseMatcher } from '../utils/matchers';
+import { parseMatcher, parsePromQLStyleMatcherLoose } from '../utils/matchers';
import {
getRuleHealth,
isAlertingRule,
@@ -71,7 +71,7 @@ export function useRulesFilter() {
dataSource: queryParams.get('dataSource') ?? undefined,
alertState: queryParams.get('alertState') ?? undefined,
ruleType: queryParams.get('ruleType') ?? undefined,
- labels: parseMatchers(queryParams.get('queryString') ?? '').map(matcherToMatcherField),
+ labels: parsePromQLStyleMatcherLoose(queryParams.get('queryString') ?? '').map(matcherToMatcherField),
};
const hasLegacyFilters = Object.values(legacyFilters).some((legacyFilter) => !isEmpty(legacyFilter));
diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts
index 34c1a5ead12..8af7b141bff 100644
--- a/public/app/features/alerting/unified/mocks.ts
+++ b/public/app/features/alerting/unified/mocks.ts
@@ -21,7 +21,6 @@ import { DataSourceSrv, GetDataSourceListFilters, config } from '@grafana/runtim
import { defaultDashboard } from '@grafana/schema';
import { contextSrv } from 'app/core/services/context_srv';
import { MOCK_GRAFANA_ALERT_RULE_TITLE } from 'app/features/alerting/unified/mocks/server/handlers/alertRules';
-import { parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import {
AlertManagerCortexConfig,
@@ -64,6 +63,8 @@ import {
import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types';
+import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers';
+
let nextDataSourceId = 1;
export function mockDataSource(
@@ -328,12 +329,12 @@ export const mockSilences = [
mockSilence({ id: MOCK_SILENCE_ID_EXISTING, comment: 'Happy path silence' }),
mockSilence({
id: 'ce031625-61c7-47cd-9beb-8760bccf0ed7',
- matchers: parseMatchers('foo!=bar'),
+ matchers: parsePromQLStyleMatcherLooseSafe('foo!=bar'),
comment: 'Silence with negated matcher',
}),
mockSilence({
id: MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID,
- matchers: parseMatchers(`__alert_rule_uid__=${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}`),
+ matchers: parsePromQLStyleMatcherLooseSafe(`__alert_rule_uid__=${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}`),
comment: 'Silence with alert rule UID matcher',
metadata: {
rule_title: MOCK_GRAFANA_ALERT_RULE_TITLE,
@@ -341,7 +342,7 @@ export const mockSilences = [
}),
mockSilence({
id: MOCK_SILENCE_ID_LACKING_PERMISSIONS,
- matchers: parseMatchers('something=else'),
+ matchers: parsePromQLStyleMatcherLooseSafe('something=else'),
comment: 'Silence without permissions to edit',
accessControl: {},
}),
diff --git a/public/app/features/alerting/unified/utils/alertmanager.test.ts b/public/app/features/alerting/unified/utils/alertmanager.test.ts
index d8f3cbe59c1..3c0293aef94 100644
--- a/public/app/features/alerting/unified/utils/alertmanager.test.ts
+++ b/public/app/features/alerting/unified/utils/alertmanager.test.ts
@@ -1,8 +1,8 @@
import { Matcher, MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
import { Labels } from 'app/types/unified-alerting-dto';
-import { parseMatchers, labelsMatchMatchers, removeMuteTimingFromRoute, matchersToString } from './alertmanager';
-import { parseMatcher } from './matchers';
+import { labelsMatchMatchers, removeMuteTimingFromRoute, matchersToString } from './alertmanager';
+import { parseMatcher, parsePromQLStyleMatcherLooseSafe } from './matchers';
describe('Alertmanager utils', () => {
describe('parseMatcher', () => {
@@ -64,57 +64,6 @@ describe('Alertmanager utils', () => {
});
});
- describe('parseMatchers', () => {
- it('should parse all operators', () => {
- expect(parseMatchers('foo=bar, bar=~ba.+, severity!=warning, email!~@grafana.com')).toEqual([
- { name: 'foo', value: 'bar', isRegex: false, isEqual: true },
- { name: 'bar', value: 'ba.+', isEqual: true, isRegex: true },
- { name: 'severity', value: 'warning', isRegex: false, isEqual: false },
- { name: 'email', value: '@grafana.com', isRegex: true, isEqual: false },
- ]);
- });
-
- it('should parse with spaces and brackets', () => {
- expect(parseMatchers('{ foo=bar }')).toEqual([
- {
- name: 'foo',
- value: 'bar',
- isRegex: false,
- isEqual: true,
- },
- ]);
- });
-
- it('should parse with spaces in the value', () => {
- expect(parseMatchers('foo=bar bazz')).toEqual([
- {
- name: 'foo',
- value: 'bar bazz',
- isRegex: false,
- isEqual: true,
- },
- ]);
- });
-
- it('should return nothing for invalid operator', () => {
- expect(parseMatchers('foo=!bar')).toEqual([]);
- });
-
- it('should parse matchers with or without quotes', () => {
- expect(parseMatchers('foo="bar",bar=bazz')).toEqual([
- { name: 'foo', value: 'bar', isRegex: false, isEqual: true },
- { name: 'bar', value: 'bazz', isEqual: true, isRegex: false },
- ]);
- });
-
- it('should parse matchers for key with special characters', () => {
- expect(parseMatchers('foo.bar-baz="bar",baz-bar.foo=bazz')).toEqual([
- { name: 'foo.bar-baz', value: 'bar', isRegex: false, isEqual: true },
- { name: 'baz-bar.foo', value: 'bazz', isEqual: true, isRegex: false },
- ]);
- });
- });
-
describe('labelsMatchMatchers', () => {
it('should return true for matching labels', () => {
const labels: Labels = {
@@ -123,7 +72,7 @@ describe('Alertmanager utils', () => {
bazz: 'buzz',
};
- const matchers = parseMatchers('foo=bar,bar=bazz');
+ const matchers = parsePromQLStyleMatcherLooseSafe('foo=bar,bar=bazz');
expect(labelsMatchMatchers(labels, matchers)).toBe(true);
});
it('should return false for no matching labels', () => {
@@ -131,7 +80,7 @@ describe('Alertmanager utils', () => {
foo: 'bar',
bar: 'bazz',
};
- const matchers = parseMatchers('foo=buzz');
+ const matchers = parsePromQLStyleMatcherLooseSafe('foo=buzz');
expect(labelsMatchMatchers(labels, matchers)).toBe(false);
});
it('should match with different operators', () => {
@@ -140,7 +89,7 @@ describe('Alertmanager utils', () => {
bar: 'bazz',
email: 'admin@grafana.com',
};
- const matchers = parseMatchers('foo!=bazz,bar=~ba.+');
+ const matchers = parsePromQLStyleMatcherLooseSafe('foo!=bazz,bar=~ba.+');
expect(labelsMatchMatchers(labels, matchers)).toBe(true);
});
});
@@ -198,7 +147,7 @@ describe('Alertmanager utils', () => {
const matchersString = matchersToString(matchers);
- expect(matchersString).toBe('{severity="critical",resource=~"cpu",rule_uid!="2Otf8canzz",cluster!~"prom"}');
+ expect(matchersString).toBe('{ severity="critical", resource=~"cpu", rule_uid!="2Otf8canzz", cluster!~"prom" }');
});
});
});
diff --git a/public/app/features/alerting/unified/utils/alertmanager.ts b/public/app/features/alerting/unified/utils/alertmanager.ts
index 2738b703844..8a73dd2685c 100644
--- a/public/app/features/alerting/unified/utils/alertmanager.ts
+++ b/public/app/features/alerting/unified/utils/alertmanager.ts
@@ -16,7 +16,7 @@ import { MatcherFieldValue } from '../types/silence-form';
import { getAllDataSources } from './config';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './datasource';
-import { MatcherFormatter, unquoteWithUnescape } from './matchers';
+import { MatcherFormatter, parsePromQLStyleMatcherLooseSafe, unquoteWithUnescape } from './matchers';
export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
// add default receiver if it does not exist
@@ -106,10 +106,10 @@ export function matchersToString(matchers: Matcher[]) {
const combinedMatchers = matcherFields.reduce((acc, current) => {
const currentMatcherString = `${current.name}${current.operator}"${current.value}"`;
- return acc ? `${acc},${currentMatcherString}` : currentMatcherString;
+ return acc ? `${acc}, ${currentMatcherString}` : currentMatcherString;
}, '');
- return `{${combinedMatchers}}`;
+ return `{ ${combinedMatchers} }`;
}
export const matcherFieldOptions: SelectableValue[] = [
@@ -124,35 +124,6 @@ export function matcherToObjectMatcher(matcher: Matcher): ObjectMatcher {
return [matcher.name, operator, matcher.value];
}
-export function parseMatchers(matcherQueryString: string): Matcher[] {
- const matcherRegExp = /\b([\w.-]+)(=~|!=|!~|=(?="?\w))"?([^"\n,}]*)"?/g;
- const matchers: Matcher[] = [];
-
- matcherQueryString.replace(matcherRegExp, (_, key, operator, value) => {
- const isEqual = operator === MatcherOperator.equal || operator === MatcherOperator.regex;
- const isRegex = operator === MatcherOperator.regex || operator === MatcherOperator.notRegex;
- matchers.push({
- name: key,
- value: isRegex ? getValidRegexString(value.trim()) : value.trim(),
- isEqual,
- isRegex,
- });
- return '';
- });
-
- return matchers;
-}
-
-function getValidRegexString(regex: string): string {
- // Regexes provided by users might be invalid, so we need to catch the error
- try {
- new RegExp(regex);
- return regex;
- } catch (error) {
- return '';
- }
-}
-
export function labelsMatchMatchers(labels: Labels, matchers: Matcher[]): boolean {
return matchers.every(({ name, value, isRegex, isEqual }) => {
return Object.entries(labels).some(([labelKey, labelValue]) => {
@@ -177,7 +148,7 @@ export function labelsMatchMatchers(labels: Labels, matchers: Matcher[]): boolea
}
export function combineMatcherStrings(...matcherStrings: string[]): string {
- const matchers = matcherStrings.map(parseMatchers).flat();
+ const matchers = matcherStrings.map(parsePromQLStyleMatcherLooseSafe).flat();
const uniqueMatchers = uniqWith(matchers, isEqual);
return matchersToString(uniqueMatchers);
}
diff --git a/public/app/features/alerting/unified/utils/matchers.test.ts b/public/app/features/alerting/unified/utils/matchers.test.ts
index c6caad93097..69487604a8c 100644
--- a/public/app/features/alerting/unified/utils/matchers.test.ts
+++ b/public/app/features/alerting/unified/utils/matchers.test.ts
@@ -1,4 +1,4 @@
-import { MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types';
+import { Matcher, MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types';
import {
encodeMatcher,
@@ -8,6 +8,8 @@ import {
normalizeMatchers,
parseMatcher,
parsePromQLStyleMatcher,
+ parsePromQLStyleMatcherLoose,
+ parsePromQLStyleMatcherLooseSafe,
parseQueryParamMatchers,
quoteWithEscape,
quoteWithEscapeIfRequired,
@@ -193,3 +195,86 @@ describe('parsePromQLStyleMatcher', () => {
);
});
});
+
+describe('parsePromQLStyleMatcherLooseSafe', () => {
+ it('should parse all operators', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo=bar, bar=~ba.+, severity!=warning, email!~@grafana.com')).toEqual<
+ Matcher[]
+ >([
+ { name: 'foo', value: 'bar', isRegex: false, isEqual: true },
+ { name: 'bar', value: 'ba.+', isEqual: true, isRegex: true },
+ { name: 'severity', value: 'warning', isRegex: false, isEqual: false },
+ { name: 'email', value: '@grafana.com', isRegex: true, isEqual: false },
+ ]);
+ });
+
+ it('should parse with spaces and brackets', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('{ foo=bar }')).toEqual([
+ {
+ name: 'foo',
+ value: 'bar',
+ isRegex: false,
+ isEqual: true,
+ },
+ ]);
+ });
+
+ it('should parse with spaces in the value', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo=bar bazz')).toEqual([
+ {
+ name: 'foo',
+ value: 'bar bazz',
+ isRegex: false,
+ isEqual: true,
+ },
+ ]);
+ });
+
+ it('should return nothing for invalid operator', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo=!bar')).toEqual([
+ {
+ name: 'foo',
+ value: '!bar',
+ isRegex: false,
+ isEqual: true,
+ },
+ ]);
+ });
+
+ it('should parse matchers with or without quotes', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo="bar",bar=bazz')).toEqual([
+ { name: 'foo', value: 'bar', isRegex: false, isEqual: true },
+ { name: 'bar', value: 'bazz', isEqual: true, isRegex: false },
+ ]);
+ });
+
+ it('should parse matchers for key with special characters', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo.bar-baz="bar",baz-bar.foo=bazz')).toEqual([
+ { name: 'foo.bar-baz', value: 'bar', isRegex: false, isEqual: true },
+ { name: 'baz-bar.foo', value: 'bazz', isEqual: true, isRegex: false },
+ ]);
+ });
+});
+
+describe('parsePromQLStyleMatcherLoose', () => {
+ it('should throw on invalid matcher', () => {
+ expect(() => {
+ parsePromQLStyleMatcherLoose('foo');
+ }).toThrow();
+
+ expect(() => {
+ parsePromQLStyleMatcherLoose('foo;bar');
+ }).toThrow();
+ });
+
+ it('should return empty array for empty input', () => {
+ expect(parsePromQLStyleMatcherLoose('')).toStrictEqual([]);
+ });
+
+ it('should also accept { } syntax', () => {
+ expect(parsePromQLStyleMatcherLoose('{ foo=bar, bar=baz }')).toStrictEqual([
+ { isEqual: true, isRegex: false, name: 'foo', value: 'bar' },
+ { isEqual: true, isRegex: false, name: 'bar', value: 'baz' },
+ ]);
+ });
+});
diff --git a/public/app/features/alerting/unified/utils/matchers.ts b/public/app/features/alerting/unified/utils/matchers.ts
index e5aea380b62..917e055e3bf 100644
--- a/public/app/features/alerting/unified/utils/matchers.ts
+++ b/public/app/features/alerting/unified/utils/matchers.ts
@@ -58,6 +58,8 @@ export function parseMatcher(matcher: string): Matcher {
/**
* This function combines parseMatcher and parsePromQLStyleMatcher, always returning an array of Matcher[] regardless of input syntax
+ * 1. { foo=bar, bar=baz }
+ * 2. foo=bar
*/
export function parseMatcherToArray(matcher: string): Matcher[] {
return isPromQLStyleMatcher(matcher) ? parsePromQLStyleMatcher(matcher) : [parseMatcher(matcher)];
@@ -71,6 +73,15 @@ export function parsePromQLStyleMatcher(matcher: string): Matcher[] {
throw new Error('not a PromQL style matcher');
}
+ return parsePromQLStyleMatcherLoose(matcher);
+}
+
+/**
+ * This function behaves the same as "parsePromQLStyleMatcher" but does not check if the matcher is formatted with { }
+ * In other words; it accepts both "{ foo=bar, bar=baz }" and "foo=bar,bar=baz"
+ * @throws
+ */
+export function parsePromQLStyleMatcherLoose(matcher: string): Matcher[] {
// split by `,` but not when it's used as a label value
const commaUnlessQuoted = /,(?=(?:[^"]*"[^"]*")*[^"]*$)/;
const parts = matcher.replace(/^\{/, '').replace(/\}$/, '').trim().split(commaUnlessQuoted);
@@ -84,6 +95,18 @@ export function parsePromQLStyleMatcher(matcher: string): Matcher[] {
}));
}
+/**
+ * This function behaves the same as "parsePromQLStyleMatcherLoose" but instead of throwing an error for incorrect syntax
+ * it returns an empty Array of matchers instead.
+ */
+export function parsePromQLStyleMatcherLooseSafe(matcher: string): Matcher[] {
+ try {
+ return parsePromQLStyleMatcherLoose(matcher);
+ } catch {
+ return [];
+ }
+}
+
// Parses a list of entries like like "['foo=bar', 'baz=~bad*']" into SilenceMatcher[]
export function parseQueryParamMatchers(matcherPairs: string[]): Matcher[] {
const parsedMatchers = matcherPairs.filter((x) => !!x.trim()).map((x) => parseMatcher(x));
diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
index 0e0b3ce4fc6..2e615646324 100644
--- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
+++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
@@ -25,9 +25,9 @@ import {
fetchAllPromAndRulerRulesAction,
fetchPromAndRulerRulesAction,
} from 'app/features/alerting/unified/state/actions';
-import { parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
import { Annotation } from 'app/features/alerting/unified/utils/constants';
import { GRAFANA_DATASOURCE_NAME, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
+import { parsePromQLStyleMatcherLooseSafe } from 'app/features/alerting/unified/utils/matchers';
import {
isAsyncRequestMapSlicePartiallyDispatched,
isAsyncRequestMapSlicePartiallyFulfilled,
@@ -132,7 +132,7 @@ function UnifiedAlertList(props: PanelProps) {
};
const matcherList = useMemo(
- () => parseMatchers(parsedOptions.alertInstanceLabelFilter),
+ () => parsePromQLStyleMatcherLooseSafe(parsedOptions.alertInstanceLabelFilter),
[parsedOptions.alertInstanceLabelFilter]
);
diff --git a/public/app/plugins/panel/alertlist/util.ts b/public/app/plugins/panel/alertlist/util.ts
index 5acf905d27c..a7984904696 100644
--- a/public/app/plugins/panel/alertlist/util.ts
+++ b/public/app/plugins/panel/alertlist/util.ts
@@ -1,14 +1,15 @@
import { isEmpty } from 'lodash';
import { Labels } from '@grafana/data';
-import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
+import { labelsMatchMatchers } from 'app/features/alerting/unified/utils/alertmanager';
+import { parsePromQLStyleMatcherLooseSafe } from 'app/features/alerting/unified/utils/matchers';
import { Alert, hasAlertState } from 'app/types/unified-alerting';
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { UnifiedAlertListOptions } from './types';
function hasLabelFilter(alertInstanceLabelFilter: string, labels: Labels) {
- const matchers = parseMatchers(alertInstanceLabelFilter);
+ const matchers = parsePromQLStyleMatcherLooseSafe(alertInstanceLabelFilter);
return labelsMatchMatchers(labels, matchers);
}