From d5af2e031c95cdefcf3ded7be3456819584cb674 Mon Sep 17 00:00:00 2001 From: Umberto Pepato Date: Wed, 18 Dec 2024 14:23:29 +0100 Subject: [PATCH] [ResponseOps][Alerts] Fix muted alerts query using wrong filter (#204182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Reverts the `getMutedAlerts` filter to use rule ids instead of rule type ids that caused the muted alerts state to be always empty. ## To verify 1. Create a rule that fire alerts 2. Navigate to `Stack management > Alerts` 3. Click on any alert actions menu (•••) 4. Toggle on and off the alert muted state and check that the action updates accordingly and the slashed bell icon appears in the status cell when muted ## Release note Fix alert mute/unmute action --- .../hooks/apis/get_rules_muted_alerts.test.ts | 23 +++- .../hooks/apis/get_rules_muted_alerts.ts | 6 +- .../tests/alerting/group4/index.ts | 1 + .../tests/alerting/group4/muted_alerts.ts | 122 ++++++++++++++++++ 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/muted_alerts.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/get_rules_muted_alerts.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/get_rules_muted_alerts.test.ts index d0f1a39f7cede..f6b3d23c2e289 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/get_rules_muted_alerts.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/get_rules_muted_alerts.test.ts @@ -37,7 +37,28 @@ describe('getMutedAlerts', () => { Array [ "/internal/alerting/rules/_find", Object { - "body": "{\\"rule_type_ids\\":[\\"foo\\"],\\"fields\\":[\\"id\\",\\"mutedInstanceIds\\"],\\"page\\":1,\\"per_page\\":1}", + "body": "{\\"filter\\":\\"{\\\\\\"type\\\\\\":\\\\\\"function\\\\\\",\\\\\\"function\\\\\\":\\\\\\"is\\\\\\",\\\\\\"arguments\\\\\\":[{\\\\\\"type\\\\\\":\\\\\\"literal\\\\\\",\\\\\\"value\\\\\\":\\\\\\"alert.id\\\\\\",\\\\\\"isQuoted\\\\\\":false},{\\\\\\"type\\\\\\":\\\\\\"literal\\\\\\",\\\\\\"value\\\\\\":\\\\\\"alert:foo\\\\\\",\\\\\\"isQuoted\\\\\\":false}]}\\",\\"fields\\":[\\"id\\",\\"mutedInstanceIds\\"],\\"page\\":1,\\"per_page\\":1}", + "signal": undefined, + }, + ] + `); + }); + + test('should call find API with multiple ruleIds', async () => { + const result = await getMutedAlerts(http, { ruleIds: ['foo', 'bar'] }); + + expect(result).toEqual({ + page: 1, + per_page: 10, + total: 0, + data: [], + }); + + expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_find", + Object { + "body": "{\\"filter\\":\\"{\\\\\\"type\\\\\\":\\\\\\"function\\\\\\",\\\\\\"function\\\\\\":\\\\\\"or\\\\\\",\\\\\\"arguments\\\\\\":[{\\\\\\"type\\\\\\":\\\\\\"function\\\\\\",\\\\\\"function\\\\\\":\\\\\\"is\\\\\\",\\\\\\"arguments\\\\\\":[{\\\\\\"type\\\\\\":\\\\\\"literal\\\\\\",\\\\\\"value\\\\\\":\\\\\\"alert.id\\\\\\",\\\\\\"isQuoted\\\\\\":false},{\\\\\\"type\\\\\\":\\\\\\"literal\\\\\\",\\\\\\"value\\\\\\":\\\\\\"alert:foo\\\\\\",\\\\\\"isQuoted\\\\\\":false}]},{\\\\\\"type\\\\\\":\\\\\\"function\\\\\\",\\\\\\"function\\\\\\":\\\\\\"is\\\\\\",\\\\\\"arguments\\\\\\":[{\\\\\\"type\\\\\\":\\\\\\"literal\\\\\\",\\\\\\"value\\\\\\":\\\\\\"alert.id\\\\\\",\\\\\\"isQuoted\\\\\\":false},{\\\\\\"type\\\\\\":\\\\\\"literal\\\\\\",\\\\\\"value\\\\\\":\\\\\\"alert:bar\\\\\\",\\\\\\"isQuoted\\\\\\":false}]}]}\\",\\"fields\\":[\\"id\\",\\"mutedInstanceIds\\"],\\"page\\":1,\\"per_page\\":2}", "signal": undefined, }, ] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/get_rules_muted_alerts.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/get_rules_muted_alerts.ts index d9baef548dafc..4c13c0c7b5ef6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/get_rules_muted_alerts.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/get_rules_muted_alerts.ts @@ -6,6 +6,7 @@ */ import { HttpStart } from '@kbn/core-http-browser'; +import { nodeBuilder } from '@kbn/es-query'; const INTERNAL_FIND_RULES_URL = '/internal/alerting/rules/_find'; @@ -23,9 +24,12 @@ export const getMutedAlerts = async ( params: { ruleIds: string[] }, signal?: AbortSignal ) => { + const filterNode = nodeBuilder.or( + params.ruleIds.map((id) => nodeBuilder.is('alert.id', `alert:${id}`)) + ); return http.post(INTERNAL_FIND_RULES_URL, { body: JSON.stringify({ - rule_type_ids: params.ruleIds, + filter: JSON.stringify(filterNode), fields: ['id', 'mutedInstanceIds'], page: 1, per_page: params.ruleIds.length, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts index 1da03fbbcc54c..b2753cb17245d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts @@ -18,6 +18,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./builtin_alert_types')); loadTestFile(require.resolve('./mustache_templates.ts')); loadTestFile(require.resolve('./notify_when')); + loadTestFile(require.resolve('./muted_alerts')); loadTestFile(require.resolve('./event_log_alerts')); loadTestFile(require.resolve('./snooze')); loadTestFile(require.resolve('./unsnooze')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/muted_alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/muted_alerts.ts new file mode 100644 index 0000000000000..475a36872e8c7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/muted_alerts.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; +import { ALERT_INSTANCE_ID, ALERT_RULE_UUID, ALERT_STATUS } from '@kbn/rule-data-utils'; +import { nodeBuilder } from '@kbn/es-query'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; + +const alertAsDataIndex = '.internal.alerts-observability.test.alerts.alerts-default-000001'; + +// eslint-disable-next-line import/no-default-export +export default function createDisableRuleTests({ getService }: FtrProviderContext) { + const es = getService('es'); + const retry = getService('retry'); + const supertest = getService('supertest'); + + describe('mutedAlerts', () => { + const objectRemover = new ObjectRemover(supertest); + + const createRule = async () => { + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.always-firing-alert-as-data', + schedule: { interval: '24h' }, + throttle: undefined, + notify_when: undefined, + params: { + index: ES_TEST_INDEX_NAME, + reference: 'test', + }, + }) + ) + .expect(200); + + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + return createdRule.id; + }; + + const getAlerts = async () => { + const { + hits: { hits: alerts }, + } = await es.search({ + index: alertAsDataIndex, + body: { query: { match_all: {} } }, + }); + + return alerts; + }; + + afterEach(async () => { + await es.deleteByQuery({ + index: alertAsDataIndex, + query: { + match_all: {}, + }, + conflicts: 'proceed', + ignore_unavailable: true, + }); + await objectRemover.removeAll(); + }); + + it('should reflect muted alert instance ids in rule', async () => { + const createdRule1 = await createRule(); + const createdRule2 = await createRule(); + + let alerts: any[] = []; + + await retry.try(async () => { + alerts = await getAlerts(); + + expect(alerts.length).greaterThan(2); + alerts.forEach((activeAlert: any) => { + expect(activeAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + + const alertFromRule1 = alerts.find( + (alert: any) => + alert._source[ALERT_STATUS] === 'active' && + alert._source[ALERT_RULE_UUID] === createdRule1 + ); + + await supertest + .post( + `${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${encodeURIComponent( + createdRule1 + )}/alert/${encodeURIComponent(alertFromRule1._source['kibana.alert.instance.id'])}/_mute` + ) + .set('kbn-xsrf', 'foo') + .expect(204); + + const ruleUuids = [createdRule1, createdRule2]; + + const filterNode = nodeBuilder.or( + ruleUuids.map((id) => nodeBuilder.is('alert.id', `alert:${id}`)) + ); + const { body: rules } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + filter: JSON.stringify(filterNode), + fields: ['id', 'mutedInstanceIds'], + page: 1, + per_page: ruleUuids.length, + }); + + expect(rules.data.length).to.be(2); + const mutedRule = rules.data.find((rule: { id: string }) => rule.id === createdRule1); + expect(mutedRule.muted_alert_ids).to.contain(alertFromRule1._source[ALERT_INSTANCE_ID]); + }); + }); +}