mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Refactor PromQL-style matcher parsing (#90129)
Co-authored-by: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com>
This commit is contained in:
@@ -14,8 +14,9 @@ import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces';
|
|||||||
import { usePagination } from './hooks/usePagination';
|
import { usePagination } from './hooks/usePagination';
|
||||||
import { useURLSearchParams } from './hooks/useURLSearchParams';
|
import { useURLSearchParams } from './hooks/useURLSearchParams';
|
||||||
import { fetchPromRulesAction, fetchRulerRulesAction } from './state/actions';
|
import { fetchPromRulesAction, fetchRulerRulesAction } from './state/actions';
|
||||||
import { combineMatcherStrings, labelsMatchMatchers, parseMatchers } from './utils/alertmanager';
|
import { combineMatcherStrings, labelsMatchMatchers } from './utils/alertmanager';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||||
|
import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers';
|
||||||
import { createViewLink } from './utils/misc';
|
import { createViewLink } from './utils/misc';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -168,7 +169,7 @@ function filterAndSortRules(
|
|||||||
labelFilter: string,
|
labelFilter: string,
|
||||||
sortOrder: SortOrder
|
sortOrder: SortOrder
|
||||||
) {
|
) {
|
||||||
const matchers = parseMatchers(labelFilter);
|
const matchers = parsePromQLStyleMatcherLooseSafe(labelFilter);
|
||||||
let rules = originalRules.filter(
|
let rules = originalRules.filter(
|
||||||
(rule) => rule.name.toLowerCase().includes(nameFilter.toLowerCase()) && labelsMatchMatchers(rule.labels, matchers)
|
(rule) => rule.name.toLowerCase().includes(nameFilter.toLowerCase()) && labelsMatchMatchers(rule.labels, matchers)
|
||||||
);
|
);
|
||||||
|
@@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { logInfo, LogMessages } from '../../Analytics';
|
import { logInfo, LogMessages } from '../../Analytics';
|
||||||
import { parseMatchers } from '../../utils/alertmanager';
|
import { parsePromQLStyleMatcherLoose } from '../../utils/matchers';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
defaultQueryString?: string;
|
defaultQueryString?: string;
|
||||||
@@ -28,13 +28,22 @@ export const MatcherFilter = ({ onFilterChange, defaultQueryString }: Props) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const searchIcon = <Icon name={'search'} />;
|
const searchIcon = <Icon name={'search'} />;
|
||||||
const inputInvalid = defaultQueryString ? parseMatchers(defaultQueryString).length === 0 : false;
|
let inputValid = Boolean(defaultQueryString && defaultQueryString.length >= 3);
|
||||||
|
try {
|
||||||
|
if (!defaultQueryString) {
|
||||||
|
inputValid = true;
|
||||||
|
} else {
|
||||||
|
parsePromQLStyleMatcherLoose(defaultQueryString);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
inputValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
className={styles.fixMargin}
|
className={styles.fixMargin}
|
||||||
invalid={inputInvalid || undefined}
|
invalid={!inputValid}
|
||||||
error={inputInvalid ? 'Query must use valid matcher syntax. See the examples in the help tooltip.' : null}
|
error={!inputValid ? 'Query must use valid matcher syntax. See the examples in the help tooltip.' : null}
|
||||||
label={
|
label={
|
||||||
<Label>
|
<Label>
|
||||||
<Stack gap={0.5} alignItems="center">
|
<Stack gap={0.5} alignItems="center">
|
||||||
|
@@ -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 { ObjectMatcher, Receiver, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
|
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
|
||||||
import { matcherToObjectMatcher, parseMatchers } from '../../utils/alertmanager';
|
import { matcherToObjectMatcher } from '../../utils/alertmanager';
|
||||||
import { normalizeMatchers } from '../../utils/matchers';
|
import {
|
||||||
|
normalizeMatchers,
|
||||||
|
parsePromQLStyleMatcherLoose,
|
||||||
|
parsePromQLStyleMatcherLooseSafe,
|
||||||
|
} from '../../utils/matchers';
|
||||||
|
|
||||||
interface NotificationPoliciesFilterProps {
|
interface NotificationPoliciesFilterProps {
|
||||||
receivers: Receiver[];
|
receivers: Receiver[];
|
||||||
@@ -35,7 +39,7 @@ const NotificationPoliciesFilter = ({
|
|||||||
}, [contactPoint, onChangeReceiver]);
|
}, [contactPoint, onChangeReceiver]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const matchers = parseMatchers(queryString ?? '').map(matcherToObjectMatcher);
|
const matchers = parsePromQLStyleMatcherLooseSafe(queryString ?? '').map(matcherToObjectMatcher);
|
||||||
handleChangeLabels()(matchers);
|
handleChangeLabels()(matchers);
|
||||||
}, [handleChangeLabels, queryString]);
|
}, [handleChangeLabels, queryString]);
|
||||||
|
|
||||||
@@ -50,7 +54,17 @@ const NotificationPoliciesFilter = ({
|
|||||||
const selectedContactPoint = receiverOptions.find((option) => option.value === contactPoint) ?? null;
|
const selectedContactPoint = receiverOptions.find((option) => option.value === contactPoint) ?? null;
|
||||||
|
|
||||||
const hasFilters = queryString || contactPoint;
|
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 (
|
return (
|
||||||
<Stack direction="row" alignItems="flex-end" gap={1}>
|
<Stack direction="row" alignItems="flex-end" gap={1}>
|
||||||
@@ -73,8 +87,8 @@ const NotificationPoliciesFilter = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
invalid={inputInvalid}
|
invalid={!inputValid}
|
||||||
error={inputInvalid ? 'Query must use valid matcher syntax' : null}
|
error={!inputValid ? 'Query must use valid matcher syntax' : null}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
|
@@ -10,13 +10,14 @@ import {
|
|||||||
AlertInstanceStateFilter,
|
AlertInstanceStateFilter,
|
||||||
InstanceStateFilter,
|
InstanceStateFilter,
|
||||||
} from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter';
|
} from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter';
|
||||||
import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
|
import { labelsMatchMatchers } from 'app/features/alerting/unified/utils/alertmanager';
|
||||||
import { createViewLink, sortAlerts } from 'app/features/alerting/unified/utils/misc';
|
import { createViewLink, sortAlerts } from 'app/features/alerting/unified/utils/misc';
|
||||||
import { SortOrder } from 'app/plugins/panel/alertlist/types';
|
import { SortOrder } from 'app/plugins/panel/alertlist/types';
|
||||||
import { Alert, CombinedRule, PaginationProps } from 'app/types/unified-alerting';
|
import { Alert, CombinedRule, PaginationProps } from 'app/types/unified-alerting';
|
||||||
import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
|
import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
|
import { parsePromQLStyleMatcherLooseSafe } from '../../utils/matchers';
|
||||||
import { isAlertingRule } from '../../utils/rules';
|
import { isAlertingRule } from '../../utils/rules';
|
||||||
|
|
||||||
import { AlertInstancesTable } from './AlertInstancesTable';
|
import { AlertInstancesTable } from './AlertInstancesTable';
|
||||||
@@ -148,7 +149,7 @@ function filterAlerts(
|
|||||||
): Alert[] {
|
): Alert[] {
|
||||||
let filteredAlerts = [...alerts];
|
let filteredAlerts = [...alerts];
|
||||||
if (alertInstanceLabel) {
|
if (alertInstanceLabel) {
|
||||||
const matchers = parseMatchers(alertInstanceLabel || '');
|
const matchers = alertInstanceLabel ? parsePromQLStyleMatcherLooseSafe(alertInstanceLabel) : [];
|
||||||
filteredAlerts = filteredAlerts.filter(({ labels }) => labelsMatchMatchers(labels, matchers));
|
filteredAlerts = filteredAlerts.filter(({ labels }) => labelsMatchMatchers(labels, matchers));
|
||||||
}
|
}
|
||||||
if (alertInstanceState) {
|
if (alertInstanceState) {
|
||||||
|
@@ -18,8 +18,9 @@ import {
|
|||||||
|
|
||||||
import { stateHistoryApi } from '../../../api/stateHistoryApi';
|
import { stateHistoryApi } from '../../../api/stateHistoryApi';
|
||||||
import { usePagination } from '../../../hooks/usePagination';
|
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 { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||||
|
import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
|
||||||
import { stringifyErrorLike } from '../../../utils/misc';
|
import { stringifyErrorLike } from '../../../utils/misc';
|
||||||
import { AlertLabels } from '../../AlertLabels';
|
import { AlertLabels } from '../../AlertLabels';
|
||||||
import { CollapseToggle } from '../../CollapseToggle';
|
import { CollapseToggle } from '../../CollapseToggle';
|
||||||
@@ -412,7 +413,7 @@ function useRuleHistoryRecords(stateHistory?: DataFrameJSON, filter?: string) {
|
|||||||
return { historyRecords: [] };
|
return { historyRecords: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterMatchers = filter ? parseMatchers(filter) : [];
|
const filterMatchers = filter ? parsePromQLStyleMatcherLooseSafe(filter) : [];
|
||||||
|
|
||||||
const [tsValues, lines] = stateHistory.data.values;
|
const [tsValues, lines] = stateHistory.data.values;
|
||||||
const timestamps = isNumbers(tsValues) ? tsValues : [];
|
const timestamps = isNumbers(tsValues) ? tsValues : [];
|
||||||
|
@@ -3,7 +3,8 @@ import { groupBy } from 'lodash';
|
|||||||
import { DataFrame, Field as DataFrameField, DataFrameJSON, Field, FieldType } from '@grafana/data';
|
import { DataFrame, Field as DataFrameField, DataFrameJSON, Field, FieldType } from '@grafana/data';
|
||||||
import { fieldIndexComparer } from '@grafana/data/src/field/fieldComparers';
|
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 { LogRecord } from '../state-history/common';
|
||||||
import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords';
|
import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords';
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ function groupDataFramesByTimeAndFilterByLabels(dataFrames: DataFrame[]): DataFr
|
|||||||
const filterValue = getFilterInQueryParams();
|
const filterValue = getFilterInQueryParams();
|
||||||
const dataframesFiltered = dataFrames.filter((frame) => {
|
const dataframesFiltered = dataFrames.filter((frame) => {
|
||||||
const labels = JSON.parse(frame.name ?? ''); // in name we store the labels stringified
|
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);
|
return labelsMatchMatchers(labels, matchers);
|
||||||
});
|
});
|
||||||
// Extract time fields from filtered data frames
|
// Extract time fields from filtered data frames
|
||||||
|
@@ -13,7 +13,8 @@ import { fieldIndexComparer } from '@grafana/data/src/field/fieldComparers';
|
|||||||
import { MappingType, ThresholdsMode } from '@grafana/schema';
|
import { MappingType, ThresholdsMode } from '@grafana/schema';
|
||||||
import { useTheme2 } from '@grafana/ui';
|
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';
|
import { extractCommonLabels, Line, LogRecord, omitLabels } from './common';
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ export function useRuleHistoryRecords(stateHistory?: DataFrameJSON, filter?: str
|
|||||||
|
|
||||||
const commonLabels = extractCommonLabels(groupLabelsArray);
|
const commonLabels = extractCommonLabels(groupLabelsArray);
|
||||||
|
|
||||||
const filterMatchers = filter ? parseMatchers(filter) : [];
|
const filterMatchers = filter ? parsePromQLStyleMatcherLooseSafe(filter) : [];
|
||||||
const filteredGroupedLines = Object.entries(logRecordsByInstance).filter(([key]) => {
|
const filteredGroupedLines = Object.entries(logRecordsByInstance).filter(([key]) => {
|
||||||
const labels = JSON.parse(key);
|
const labels = JSON.parse(key);
|
||||||
return labelsMatchMatchers(labels, filterMatchers);
|
return labelsMatchMatchers(labels, filterMatchers);
|
||||||
|
@@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui';
|
import { Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
|
||||||
import { parseMatchers } from '../../utils/alertmanager';
|
import { parsePromQLStyleMatcherLoose } from '../../utils/matchers';
|
||||||
import { getSilenceFiltersFromUrlParams } from '../../utils/misc';
|
import { getSilenceFiltersFromUrlParams } from '../../utils/misc';
|
||||||
|
|
||||||
const getQueryStringKey = () => uniqueId('query-string-');
|
const getQueryStringKey = () => uniqueId('query-string-');
|
||||||
@@ -30,7 +30,16 @@ export const SilencesFilter = () => {
|
|||||||
setTimeout(() => setQueryStringKey(getQueryStringKey()));
|
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 (
|
return (
|
||||||
<div className={styles.flexRow}>
|
<div className={styles.flexRow}>
|
||||||
@@ -53,8 +62,8 @@ export const SilencesFilter = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
invalid={inputInvalid}
|
invalid={!inputValid}
|
||||||
error={inputInvalid ? 'Query must use valid matcher syntax' : null}
|
error={!inputValid ? 'Query must use valid matcher syntax' : null}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
key={queryStringKey}
|
key={queryStringKey}
|
||||||
|
@@ -23,7 +23,7 @@ import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource
|
|||||||
|
|
||||||
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||||
import { parseMatchers } from '../../utils/alertmanager';
|
import { parsePromQLStyleMatcherLooseSafe } from '../../utils/matchers';
|
||||||
import { getSilenceFiltersFromUrlParams, makeAMLink, stringifyErrorLike } from '../../utils/misc';
|
import { getSilenceFiltersFromUrlParams, makeAMLink, stringifyErrorLike } from '../../utils/misc';
|
||||||
import { Authorize } from '../Authorize';
|
import { Authorize } from '../Authorize';
|
||||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||||
@@ -220,7 +220,7 @@ const useFilteredSilences = (silences: Silence[], expired = false) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
const matchers = parseMatchers(queryString);
|
const matchers = parsePromQLStyleMatcherLooseSafe(queryString);
|
||||||
const matchersMatch = matchers.every((matcher) =>
|
const matchersMatch = matchers.every((matcher) =>
|
||||||
silence.matchers?.some(
|
silence.matchers?.some(
|
||||||
({ name, value, isEqual, isRegex }) =>
|
({ name, value, isEqual, isRegex }) =>
|
||||||
|
@@ -3,19 +3,21 @@ import { useMemo } from 'react';
|
|||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
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';
|
import { getFiltersFromUrlParams } from '../utils/misc';
|
||||||
|
|
||||||
export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => {
|
export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => {
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
const filters = getFiltersFromUrlParams(queryParams);
|
const { queryString, alertState } = getFiltersFromUrlParams(queryParams);
|
||||||
const matchers = parseMatchers(filters.queryString || '');
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
const matchers = queryString ? parsePromQLStyleMatcherLooseSafe(queryString) : [];
|
||||||
|
|
||||||
return groups.reduce((filteredGroup: AlertmanagerGroup[], group) => {
|
return groups.reduce((filteredGroup: AlertmanagerGroup[], group) => {
|
||||||
const alerts = group.alerts.filter(({ labels, status }) => {
|
const alerts = group.alerts.filter(({ labels, status }) => {
|
||||||
const labelsMatch = labelsMatchMatchers(labels, matchers);
|
const labelsMatch = labelsMatchMatchers(labels, matchers);
|
||||||
const filtersMatch = filters.alertState ? status.state === filters.alertState : true;
|
const filtersMatch = alertState ? status.state === alertState : true;
|
||||||
return labelsMatch && filtersMatch;
|
return labelsMatch && filtersMatch;
|
||||||
});
|
});
|
||||||
if (alerts.length > 0) {
|
if (alerts.length > 0) {
|
||||||
@@ -28,5 +30,5 @@ export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => {
|
|||||||
}
|
}
|
||||||
return filteredGroup;
|
return filteredGroup;
|
||||||
}, []);
|
}, []);
|
||||||
}, [groups, filters, matchers]);
|
}, [queryString, groups, alertState]);
|
||||||
};
|
};
|
||||||
|
@@ -9,10 +9,10 @@ import { CombinedRuleGroup, CombinedRuleNamespace, Rule } from 'app/types/unifie
|
|||||||
import { isPromAlertingRuleState, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
import { isPromAlertingRuleState, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { applySearchFilterToQuery, getSearchFilterFromQuery, RulesFilter } from '../search/rulesSearchParser';
|
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 { Annotation } from '../utils/constants';
|
||||||
import { isCloudRulesSource } from '../utils/datasource';
|
import { isCloudRulesSource } from '../utils/datasource';
|
||||||
import { parseMatcher } from '../utils/matchers';
|
import { parseMatcher, parsePromQLStyleMatcherLoose } from '../utils/matchers';
|
||||||
import {
|
import {
|
||||||
getRuleHealth,
|
getRuleHealth,
|
||||||
isAlertingRule,
|
isAlertingRule,
|
||||||
@@ -71,7 +71,7 @@ export function useRulesFilter() {
|
|||||||
dataSource: queryParams.get('dataSource') ?? undefined,
|
dataSource: queryParams.get('dataSource') ?? undefined,
|
||||||
alertState: queryParams.get('alertState') ?? undefined,
|
alertState: queryParams.get('alertState') ?? undefined,
|
||||||
ruleType: queryParams.get('ruleType') ?? 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));
|
const hasLegacyFilters = Object.values(legacyFilters).some((legacyFilter) => !isEmpty(legacyFilter));
|
||||||
|
@@ -21,7 +21,6 @@ import { DataSourceSrv, GetDataSourceListFilters, config } from '@grafana/runtim
|
|||||||
import { defaultDashboard } from '@grafana/schema';
|
import { defaultDashboard } from '@grafana/schema';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { MOCK_GRAFANA_ALERT_RULE_TITLE } from 'app/features/alerting/unified/mocks/server/handlers/alertRules';
|
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 { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import {
|
import {
|
||||||
AlertManagerCortexConfig,
|
AlertManagerCortexConfig,
|
||||||
@@ -64,6 +63,8 @@ import {
|
|||||||
|
|
||||||
import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types';
|
import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types';
|
||||||
|
|
||||||
|
import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers';
|
||||||
|
|
||||||
let nextDataSourceId = 1;
|
let nextDataSourceId = 1;
|
||||||
|
|
||||||
export function mockDataSource<T extends DataSourceJsonData = DataSourceJsonData>(
|
export function mockDataSource<T extends DataSourceJsonData = DataSourceJsonData>(
|
||||||
@@ -328,12 +329,12 @@ export const mockSilences = [
|
|||||||
mockSilence({ id: MOCK_SILENCE_ID_EXISTING, comment: 'Happy path silence' }),
|
mockSilence({ id: MOCK_SILENCE_ID_EXISTING, comment: 'Happy path silence' }),
|
||||||
mockSilence({
|
mockSilence({
|
||||||
id: 'ce031625-61c7-47cd-9beb-8760bccf0ed7',
|
id: 'ce031625-61c7-47cd-9beb-8760bccf0ed7',
|
||||||
matchers: parseMatchers('foo!=bar'),
|
matchers: parsePromQLStyleMatcherLooseSafe('foo!=bar'),
|
||||||
comment: 'Silence with negated matcher',
|
comment: 'Silence with negated matcher',
|
||||||
}),
|
}),
|
||||||
mockSilence({
|
mockSilence({
|
||||||
id: MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID,
|
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',
|
comment: 'Silence with alert rule UID matcher',
|
||||||
metadata: {
|
metadata: {
|
||||||
rule_title: MOCK_GRAFANA_ALERT_RULE_TITLE,
|
rule_title: MOCK_GRAFANA_ALERT_RULE_TITLE,
|
||||||
@@ -341,7 +342,7 @@ export const mockSilences = [
|
|||||||
}),
|
}),
|
||||||
mockSilence({
|
mockSilence({
|
||||||
id: MOCK_SILENCE_ID_LACKING_PERMISSIONS,
|
id: MOCK_SILENCE_ID_LACKING_PERMISSIONS,
|
||||||
matchers: parseMatchers('something=else'),
|
matchers: parsePromQLStyleMatcherLooseSafe('something=else'),
|
||||||
comment: 'Silence without permissions to edit',
|
comment: 'Silence without permissions to edit',
|
||||||
accessControl: {},
|
accessControl: {},
|
||||||
}),
|
}),
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { Matcher, MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
|
import { Matcher, MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { Labels } from 'app/types/unified-alerting-dto';
|
import { Labels } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { parseMatchers, labelsMatchMatchers, removeMuteTimingFromRoute, matchersToString } from './alertmanager';
|
import { labelsMatchMatchers, removeMuteTimingFromRoute, matchersToString } from './alertmanager';
|
||||||
import { parseMatcher } from './matchers';
|
import { parseMatcher, parsePromQLStyleMatcherLooseSafe } from './matchers';
|
||||||
|
|
||||||
describe('Alertmanager utils', () => {
|
describe('Alertmanager utils', () => {
|
||||||
describe('parseMatcher', () => {
|
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<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(parseMatchers('{ foo=bar }')).toEqual<Matcher[]>([
|
|
||||||
{
|
|
||||||
name: 'foo',
|
|
||||||
value: 'bar',
|
|
||||||
isRegex: false,
|
|
||||||
isEqual: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse with spaces in the value', () => {
|
|
||||||
expect(parseMatchers('foo=bar bazz')).toEqual<Matcher[]>([
|
|
||||||
{
|
|
||||||
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<Matcher[]>([
|
|
||||||
{ 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<Matcher[]>([
|
|
||||||
{ name: 'foo.bar-baz', value: 'bar', isRegex: false, isEqual: true },
|
|
||||||
{ name: 'baz-bar.foo', value: 'bazz', isEqual: true, isRegex: false },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('labelsMatchMatchers', () => {
|
describe('labelsMatchMatchers', () => {
|
||||||
it('should return true for matching labels', () => {
|
it('should return true for matching labels', () => {
|
||||||
const labels: Labels = {
|
const labels: Labels = {
|
||||||
@@ -123,7 +72,7 @@ describe('Alertmanager utils', () => {
|
|||||||
bazz: 'buzz',
|
bazz: 'buzz',
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchers = parseMatchers('foo=bar,bar=bazz');
|
const matchers = parsePromQLStyleMatcherLooseSafe('foo=bar,bar=bazz');
|
||||||
expect(labelsMatchMatchers(labels, matchers)).toBe(true);
|
expect(labelsMatchMatchers(labels, matchers)).toBe(true);
|
||||||
});
|
});
|
||||||
it('should return false for no matching labels', () => {
|
it('should return false for no matching labels', () => {
|
||||||
@@ -131,7 +80,7 @@ describe('Alertmanager utils', () => {
|
|||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
bar: 'bazz',
|
bar: 'bazz',
|
||||||
};
|
};
|
||||||
const matchers = parseMatchers('foo=buzz');
|
const matchers = parsePromQLStyleMatcherLooseSafe('foo=buzz');
|
||||||
expect(labelsMatchMatchers(labels, matchers)).toBe(false);
|
expect(labelsMatchMatchers(labels, matchers)).toBe(false);
|
||||||
});
|
});
|
||||||
it('should match with different operators', () => {
|
it('should match with different operators', () => {
|
||||||
@@ -140,7 +89,7 @@ describe('Alertmanager utils', () => {
|
|||||||
bar: 'bazz',
|
bar: 'bazz',
|
||||||
email: 'admin@grafana.com',
|
email: 'admin@grafana.com',
|
||||||
};
|
};
|
||||||
const matchers = parseMatchers('foo!=bazz,bar=~ba.+');
|
const matchers = parsePromQLStyleMatcherLooseSafe('foo!=bazz,bar=~ba.+');
|
||||||
expect(labelsMatchMatchers(labels, matchers)).toBe(true);
|
expect(labelsMatchMatchers(labels, matchers)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -198,7 +147,7 @@ describe('Alertmanager utils', () => {
|
|||||||
|
|
||||||
const matchersString = matchersToString(matchers);
|
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" }');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -16,7 +16,7 @@ import { MatcherFieldValue } from '../types/silence-form';
|
|||||||
|
|
||||||
import { getAllDataSources } from './config';
|
import { getAllDataSources } from './config';
|
||||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './datasource';
|
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 {
|
export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
|
||||||
// add default receiver if it does not exist
|
// add default receiver if it does not exist
|
||||||
@@ -106,10 +106,10 @@ export function matchersToString(matchers: Matcher[]) {
|
|||||||
|
|
||||||
const combinedMatchers = matcherFields.reduce((acc, current) => {
|
const combinedMatchers = matcherFields.reduce((acc, current) => {
|
||||||
const currentMatcherString = `${current.name}${current.operator}"${current.value}"`;
|
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[] = [
|
export const matcherFieldOptions: SelectableValue[] = [
|
||||||
@@ -124,35 +124,6 @@ export function matcherToObjectMatcher(matcher: Matcher): ObjectMatcher {
|
|||||||
return [matcher.name, operator, matcher.value];
|
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 {
|
export function labelsMatchMatchers(labels: Labels, matchers: Matcher[]): boolean {
|
||||||
return matchers.every(({ name, value, isRegex, isEqual }) => {
|
return matchers.every(({ name, value, isRegex, isEqual }) => {
|
||||||
return Object.entries(labels).some(([labelKey, labelValue]) => {
|
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 {
|
export function combineMatcherStrings(...matcherStrings: string[]): string {
|
||||||
const matchers = matcherStrings.map(parseMatchers).flat();
|
const matchers = matcherStrings.map(parsePromQLStyleMatcherLooseSafe).flat();
|
||||||
const uniqueMatchers = uniqWith(matchers, isEqual);
|
const uniqueMatchers = uniqWith(matchers, isEqual);
|
||||||
return matchersToString(uniqueMatchers);
|
return matchersToString(uniqueMatchers);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types';
|
import { Matcher, MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
encodeMatcher,
|
encodeMatcher,
|
||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
normalizeMatchers,
|
normalizeMatchers,
|
||||||
parseMatcher,
|
parseMatcher,
|
||||||
parsePromQLStyleMatcher,
|
parsePromQLStyleMatcher,
|
||||||
|
parsePromQLStyleMatcherLoose,
|
||||||
|
parsePromQLStyleMatcherLooseSafe,
|
||||||
parseQueryParamMatchers,
|
parseQueryParamMatchers,
|
||||||
quoteWithEscape,
|
quoteWithEscape,
|
||||||
quoteWithEscapeIfRequired,
|
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<Matcher[]>([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
value: 'bar',
|
||||||
|
isRegex: false,
|
||||||
|
isEqual: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse with spaces in the value', () => {
|
||||||
|
expect(parsePromQLStyleMatcherLooseSafe('foo=bar bazz')).toEqual<Matcher[]>([
|
||||||
|
{
|
||||||
|
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<Matcher[]>([
|
||||||
|
{ 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<Matcher[]>([
|
||||||
|
{ 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' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@@ -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
|
* 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[] {
|
export function parseMatcherToArray(matcher: string): Matcher[] {
|
||||||
return isPromQLStyleMatcher(matcher) ? parsePromQLStyleMatcher(matcher) : [parseMatcher(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');
|
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
|
// split by `,` but not when it's used as a label value
|
||||||
const commaUnlessQuoted = /,(?=(?:[^"]*"[^"]*")*[^"]*$)/;
|
const commaUnlessQuoted = /,(?=(?:[^"]*"[^"]*")*[^"]*$)/;
|
||||||
const parts = matcher.replace(/^\{/, '').replace(/\}$/, '').trim().split(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[]
|
// Parses a list of entries like like "['foo=bar', 'baz=~bad*']" into SilenceMatcher[]
|
||||||
export function parseQueryParamMatchers(matcherPairs: string[]): Matcher[] {
|
export function parseQueryParamMatchers(matcherPairs: string[]): Matcher[] {
|
||||||
const parsedMatchers = matcherPairs.filter((x) => !!x.trim()).map((x) => parseMatcher(x));
|
const parsedMatchers = matcherPairs.filter((x) => !!x.trim()).map((x) => parseMatcher(x));
|
||||||
|
@@ -25,9 +25,9 @@ import {
|
|||||||
fetchAllPromAndRulerRulesAction,
|
fetchAllPromAndRulerRulesAction,
|
||||||
fetchPromAndRulerRulesAction,
|
fetchPromAndRulerRulesAction,
|
||||||
} from 'app/features/alerting/unified/state/actions';
|
} 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 { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||||
import { GRAFANA_DATASOURCE_NAME, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
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 {
|
import {
|
||||||
isAsyncRequestMapSlicePartiallyDispatched,
|
isAsyncRequestMapSlicePartiallyDispatched,
|
||||||
isAsyncRequestMapSlicePartiallyFulfilled,
|
isAsyncRequestMapSlicePartiallyFulfilled,
|
||||||
@@ -132,7 +132,7 @@ function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const matcherList = useMemo(
|
const matcherList = useMemo(
|
||||||
() => parseMatchers(parsedOptions.alertInstanceLabelFilter),
|
() => parsePromQLStyleMatcherLooseSafe(parsedOptions.alertInstanceLabelFilter),
|
||||||
[parsedOptions.alertInstanceLabelFilter]
|
[parsedOptions.alertInstanceLabelFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { Labels } from '@grafana/data';
|
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 { Alert, hasAlertState } from 'app/types/unified-alerting';
|
||||||
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { UnifiedAlertListOptions } from './types';
|
import { UnifiedAlertListOptions } from './types';
|
||||||
|
|
||||||
function hasLabelFilter(alertInstanceLabelFilter: string, labels: Labels) {
|
function hasLabelFilter(alertInstanceLabelFilter: string, labels: Labels) {
|
||||||
const matchers = parseMatchers(alertInstanceLabelFilter);
|
const matchers = parsePromQLStyleMatcherLooseSafe(alertInstanceLabelFilter);
|
||||||
return labelsMatchMatchers(labels, matchers);
|
return labelsMatchMatchers(labels, matchers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user