From d99a7334d5f9a89106be9067b427f85d15ea5f97 Mon Sep 17 00:00:00 2001 From: Joe Blubaugh Date: Thu, 7 Jul 2022 16:06:25 +0800 Subject: [PATCH] Alerting: Fix RegExp matchers in frontend for Silences and other previews. (#51726) --- .../alerting/unified/utils/matchers.test.ts | 19 +++++++-- .../alerting/unified/utils/matchers.ts | 41 +++++++++++++------ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/public/app/features/alerting/unified/utils/matchers.test.ts b/public/app/features/alerting/unified/utils/matchers.test.ts index 0a073dde7c4..09a54217fb7 100644 --- a/public/app/features/alerting/unified/utils/matchers.test.ts +++ b/public/app/features/alerting/unified/utils/matchers.test.ts @@ -56,27 +56,38 @@ describe('Unified Alerting matchers', () => { }); it('should match for regex', () => { - const matchers = [{ name: 'foo', value: 'bar', operator: MatcherOperator.regex }]; + const matchers = [{ name: 'foo', value: 'b{1}a.*', operator: MatcherOperator.regex }]; const alerts = [ + mockPromAlert({ labels: { foo: 'bbr' } }), + mockPromAlert({ labels: { foo: 'aba' } }), // This does not match because the regex is implicitly anchored. + mockPromAlert({ labels: { foo: 'ba' } }), mockPromAlert({ labels: { foo: 'bar' } }), mockPromAlert({ labels: { foo: 'baz' } }), mockPromAlert({ labels: { foo: 'bas' } }), ]; const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers); - expect(matchedAlerts).toHaveLength(1); + expect(matchedAlerts).toHaveLength(4); + expect(matchedAlerts.map((instance) => instance.data.matchedInstance.labels.foo)).toEqual([ + 'ba', + 'bar', + 'baz', + 'bas', + ]); }); it('should not match regex', () => { - const matchers = [{ name: 'foo', value: 'bar', operator: MatcherOperator.notRegex }]; + const matchers = [{ name: 'foo', value: 'ba{3}', operator: MatcherOperator.notRegex }]; const alerts = [ mockPromAlert({ labels: { foo: 'bar' } }), mockPromAlert({ labels: { foo: 'baz' } }), + mockPromAlert({ labels: { foo: 'baaa' } }), mockPromAlert({ labels: { foo: 'bas' } }), ]; const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers); - expect(matchedAlerts).toHaveLength(2); + expect(matchedAlerts).toHaveLength(3); + expect(matchedAlerts.map((instance) => instance.data.matchedInstance.labels.foo)).toEqual(['bar', 'baz', 'bas']); }); }); }); diff --git a/public/app/features/alerting/unified/utils/matchers.ts b/public/app/features/alerting/unified/utils/matchers.ts index 66fe014b2bc..d8889638f58 100644 --- a/public/app/features/alerting/unified/utils/matchers.ts +++ b/public/app/features/alerting/unified/utils/matchers.ts @@ -41,29 +41,44 @@ export const findAlertInstancesWithMatchers = ( instances: Alert[], matchers: MatcherFieldValue[] ): MatchedInstance[] => { - const hasMatcher = (instance: Alert, matcher: MatcherFieldValue) => { + const anchorRegex = (regexpString: string): RegExp => { + // Silence matchers are always fully anchored in the Alertmanager: https://github.com/prometheus/alertmanager/pull/748 + if (!regexpString.startsWith('^')) { + regexpString = '^' + regexpString; + } + if (!regexpString.endsWith('$')) { + regexpString = regexpString + '$'; + } + return new RegExp(regexpString); + }; + + const matchesInstance = (instance: Alert, matcher: MatcherFieldValue) => { return Object.entries(instance.labels).some(([key, value]) => { if (!matcher.name || !matcher.value) { return false; } - if (matcher.operator === MatcherOperator.equal) { - return matcher.name === key && matcher.value === value; + if (matcher.name !== key) { + return false; } - if (matcher.operator === MatcherOperator.notEqual) { - return matcher.name === key && matcher.value !== value; + switch (matcher.operator) { + case MatcherOperator.equal: + return matcher.value === value; + case MatcherOperator.notEqual: + return matcher.value !== value; + case MatcherOperator.regex: + const regex = anchorRegex(matcher.value); + return regex.test(value); + case MatcherOperator.notRegex: + const negregex = anchorRegex(matcher.value); + return !negregex.test(value); + default: + return false; } - if (matcher.operator === MatcherOperator.regex) { - return matcher.name === key && matcher.value.match(value); - } - if (matcher.operator === MatcherOperator.notRegex) { - return matcher.name === key && !matcher.value.match(value); - } - return false; }); }; const filteredInstances = instances.filter((instance) => { - return matchers.every((matcher) => hasMatcher(instance, matcher)); + return matchers.every((matcher) => matchesInstance(instance, matcher)); }); const mappedInstances = filteredInstances.map((instance) => ({ id: `${instance.activeAt}-${instance.value}`,