From 1f92577d5e11dfe156dfde94de90ca70c03d5cd0 Mon Sep 17 00:00:00 2001 From: Alexandra Doak Date: Fri, 22 Mar 2024 09:55:25 -0700 Subject: [PATCH 1/2] Initial commit onboarding the error count rule --- .../apm/common/rules/apm_rule_types.ts | 2 +- .../routes/alerts/register_apm_rule_types.ts | 1 + .../register_error_count_rule_type.test.ts | 787 ++++++++++++------ .../register_error_count_rule_type.ts | 389 +++++---- .../server/routes/alerts/test_utils/index.ts | 4 + 5 files changed, 755 insertions(+), 428 deletions(-) diff --git a/x-pack/plugins/observability_solution/apm/common/rules/apm_rule_types.ts b/x-pack/plugins/observability_solution/apm/common/rules/apm_rule_types.ts index 644191f1d4e82..c2712fb972234 100644 --- a/x-pack/plugins/observability_solution/apm/common/rules/apm_rule_types.ts +++ b/x-pack/plugins/observability_solution/apm/common/rules/apm_rule_types.ts @@ -38,7 +38,7 @@ export enum AggregationType { export const THRESHOLD_MET_GROUP_ID = 'threshold_met'; export type ThresholdMetActionGroupId = typeof THRESHOLD_MET_GROUP_ID; -const THRESHOLD_MET_GROUP: ActionGroup = { +export const THRESHOLD_MET_GROUP: ActionGroup = { id: THRESHOLD_MET_GROUP_ID, name: i18n.translate('xpack.apm.a.thresholdMet', { defaultMessage: 'Threshold met', diff --git a/x-pack/plugins/observability_solution/apm/server/routes/alerts/register_apm_rule_types.ts b/x-pack/plugins/observability_solution/apm/server/routes/alerts/register_apm_rule_types.ts index 54f4a824ee687..526fcc4b09701 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/alerts/register_apm_rule_types.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/alerts/register_apm_rule_types.ts @@ -90,6 +90,7 @@ export const ApmRuleTypeAlertDefinition: IRuleTypeAlerts = { context: APM_RULE_TYPE_ALERT_CONTEXT, mappings: { fieldMap: apmRuleTypeAlertFieldMap }, useLegacyAlerts: true, + shouldWrite: false, }; export interface RegisterRuleDependencies { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts b/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts index 1b56d89d91d3a..568ea3a93eca0 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts @@ -35,12 +35,11 @@ describe('Error count alert', () => { }); await executor({ params }); - expect(services.alertFactory.create).not.toBeCalled(); + expect(services.alertsClient.report).not.toBeCalled(); }); it('sends alerts with service name and environment for those that exceeded the threshold', async () => { - const { services, dependencies, executor, scheduleActions } = - createRuleTypeMocks(); + const { services, dependencies, executor } = createRuleTypeMocks(); registerErrorCountRuleType(dependencies); @@ -129,55 +128,100 @@ describe('Error count alert', () => { total: 1, }, }); + services.alertsClient.report.mockReturnValue({ uuid: 'test-uuid' }); await executor({ params }); ['foo_env-foo', 'foo_env-foo-2', 'bar_env-bar'].forEach((instanceName) => - expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'threshold_met', + id: instanceName, + }) ); - expect(scheduleActions).toHaveBeenCalledTimes(3); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(3); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo', - threshold: 2, - triggerValue: 5, - reason: - 'Error count is 5 in the last 5 mins for service: foo, env: env-foo. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 5, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + }, + id: 'foo_env-foo', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 5, + 'kibana.alert.reason': + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo-2', - threshold: 2, - triggerValue: 4, - reason: - 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo-2', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 4, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', + }, + id: 'foo_env-foo-2', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 4, + 'kibana.alert.reason': + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo-2', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'bar', - environment: 'env-bar', - reason: - 'Error count is 3 in the last 5 mins for service: bar, env: env-bar. Alert when > 2.', - threshold: 2, - triggerValue: 3, - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-bar', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar. Alert when > 2.', + serviceName: 'bar', + threshold: 2, + triggerValue: 3, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + }, + id: 'bar_env-bar', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 3, + 'kibana.alert.reason': + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-bar', + 'service.name': 'bar', + }, }); }); it('sends alert when rule is configured with group by on transaction.name', async () => { - const { services, dependencies, executor, scheduleActions } = - createRuleTypeMocks(); + const { services, dependencies, executor } = createRuleTypeMocks(); registerErrorCountRuleType(dependencies); @@ -227,6 +271,7 @@ describe('Error count alert', () => { total: 1, }, }); + services.alertsClient.report.mockReturnValue({ uuid: 'test-uuid' }); await executor({ params }); [ @@ -234,55 +279,102 @@ describe('Error count alert', () => { 'foo_env-foo-2_tx-name-foo-2', 'bar_env-bar_tx-name-bar', ].forEach((instanceName) => - expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'threshold_met', + id: instanceName, + }) ); - expect(scheduleActions).toHaveBeenCalledTimes(3); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(3); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo', - threshold: 2, - triggerValue: 5, - reason: - 'Error count is 5 in the last 5 mins for service: foo, env: env-foo, name: tx-name-foo. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', - transactionName: 'tx-name-foo', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo, name: tx-name-foo. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + transactionName: 'tx-name-foo', + triggerValue: 5, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + }, + id: 'foo_env-foo_tx-name-foo', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 5, + 'kibana.alert.reason': + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo, name: tx-name-foo. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo', + 'service.name': 'foo', + 'transaction.name': 'tx-name-foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo-2', - threshold: 2, - triggerValue: 4, - reason: - 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2, name: tx-name-foo-2. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', - transactionName: 'tx-name-foo-2', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo-2', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2, name: tx-name-foo-2. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + transactionName: 'tx-name-foo-2', + triggerValue: 4, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', + }, + id: 'foo_env-foo-2_tx-name-foo-2', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 4, + 'kibana.alert.reason': + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2, name: tx-name-foo-2. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo-2', + 'service.name': 'foo', + 'transaction.name': 'tx-name-foo-2', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'bar', - environment: 'env-bar', - reason: - 'Error count is 3 in the last 5 mins for service: bar, env: env-bar, name: tx-name-bar. Alert when > 2.', - threshold: 2, - triggerValue: 3, - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', - transactionName: 'tx-name-bar', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-bar', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar, name: tx-name-bar. Alert when > 2.', + serviceName: 'bar', + threshold: 2, + transactionName: 'tx-name-bar', + triggerValue: 3, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + }, + id: 'bar_env-bar_tx-name-bar', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 3, + 'kibana.alert.reason': + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar, name: tx-name-bar. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-bar', + 'service.name': 'bar', + 'transaction.name': 'tx-name-bar', + }, }); }); it('sends alert when rule is configured with group by on error.grouping_key', async () => { - const { services, dependencies, executor, scheduleActions } = - createRuleTypeMocks(); + const { services, dependencies, executor } = createRuleTypeMocks(); registerErrorCountRuleType(dependencies); @@ -332,6 +424,7 @@ describe('Error count alert', () => { total: 1, }, }); + services.alertsClient.report.mockReturnValue({ uuid: 'test-uuid' }); await executor({ params }); [ @@ -339,55 +432,96 @@ describe('Error count alert', () => { 'foo_env-foo-2_error-key-foo-2', 'bar_env-bar_error-key-bar', ].forEach((instanceName) => - expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'threshold_met', + id: instanceName, + }) ); - expect(scheduleActions).toHaveBeenCalledTimes(3); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(3); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo', - threshold: 2, - triggerValue: 5, - reason: - 'Error count is 5 in the last 5 mins for service: foo, env: env-foo, error key: error-key-foo. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', - errorGroupingKey: 'error-key-foo', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo', + errorGroupingKey: 'error-key-foo', + interval: '5 mins', + reason: + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo, error key: error-key-foo. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 5, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + }, + id: 'foo_env-foo_error-key-foo', + payload: { + 'error.grouping_key': 'error-key-foo', + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 5, + 'kibana.alert.reason': + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo, error key: error-key-foo. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo-2', - threshold: 2, - triggerValue: 4, - reason: - 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2, error key: error-key-foo-2. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', - errorGroupingKey: 'error-key-foo-2', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo-2', + errorGroupingKey: 'error-key-foo-2', + interval: '5 mins', + reason: + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2, error key: error-key-foo-2. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 4, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', + }, + id: 'foo_env-foo-2_error-key-foo-2', + payload: { + 'error.grouping_key': 'error-key-foo-2', + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 4, + 'kibana.alert.reason': + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2, error key: error-key-foo-2. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo-2', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'bar', - environment: 'env-bar', - reason: - 'Error count is 3 in the last 5 mins for service: bar, env: env-bar, error key: error-key-bar. Alert when > 2.', - threshold: 2, - triggerValue: 3, - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', - errorGroupingKey: 'error-key-bar', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-bar', + errorGroupingKey: 'error-key-bar', + interval: '5 mins', + reason: + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar, error key: error-key-bar. Alert when > 2.', + serviceName: 'bar', + threshold: 2, + triggerValue: 3, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + }, + id: 'bar_env-bar_error-key-bar', + payload: { + 'error.grouping_key': 'error-key-bar', + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 3, + 'kibana.alert.reason': + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar, error key: error-key-bar. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-bar', + 'service.name': 'bar', + }, }); }); it('sends alert when rule is configured with preselected group by', async () => { - const { services, dependencies, executor, scheduleActions } = - createRuleTypeMocks(); + const { services, dependencies, executor } = createRuleTypeMocks(); registerErrorCountRuleType(dependencies); @@ -437,55 +571,100 @@ describe('Error count alert', () => { total: 1, }, }); + services.alertsClient.report.mockReturnValue({ uuid: 'test-uuid' }); await executor({ params }); ['foo_env-foo', 'foo_env-foo-2', 'bar_env-bar'].forEach((instanceName) => - expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'threshold_met', + id: instanceName, + }) ); - expect(scheduleActions).toHaveBeenCalledTimes(3); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(3); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo', - threshold: 2, - triggerValue: 5, - reason: - 'Error count is 5 in the last 5 mins for service: foo, env: env-foo. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 5, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + }, + id: 'foo_env-foo', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 5, + 'kibana.alert.reason': + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo-2', - threshold: 2, - triggerValue: 4, - reason: - 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo-2', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 4, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', + }, + id: 'foo_env-foo-2', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 4, + 'kibana.alert.reason': + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo-2', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'bar', - environment: 'env-bar', - reason: - 'Error count is 3 in the last 5 mins for service: bar, env: env-bar. Alert when > 2.', - threshold: 2, - triggerValue: 3, - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-bar', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar. Alert when > 2.', + serviceName: 'bar', + threshold: 2, + triggerValue: 3, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + }, + id: 'bar_env-bar', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 3, + 'kibana.alert.reason': + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-bar', + 'service.name': 'bar', + }, }); }); it('sends alert when service.environment field does not exist in the source', async () => { - const { services, dependencies, executor, scheduleActions } = - createRuleTypeMocks(); + const { services, dependencies, executor } = createRuleTypeMocks(); registerErrorCountRuleType(dependencies); @@ -535,6 +714,7 @@ describe('Error count alert', () => { total: 1, }, }); + services.alertsClient.report.mockReturnValue({ uuid: 'test-uuid' }); await executor({ params }); [ @@ -542,52 +722,96 @@ describe('Error count alert', () => { 'foo_ENVIRONMENT_NOT_DEFINED', 'bar_env-bar', ].forEach((instanceName) => - expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'threshold_met', + id: instanceName, + }) ); - expect(scheduleActions).toHaveBeenCalledTimes(3); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(3); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'Not defined', - threshold: 2, - triggerValue: 5, - reason: - 'Error count is 5 in the last 5 mins for service: foo, env: Not defined. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=ENVIRONMENT_ALL', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'Not defined', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 5 in the last 5 mins for service: foo, env: Not defined. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 5, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=ENVIRONMENT_ALL', + }, + id: 'foo_ENVIRONMENT_NOT_DEFINED', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 5, + 'kibana.alert.reason': + 'Error count is 5 in the last 5 mins for service: foo, env: Not defined. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'ENVIRONMENT_NOT_DEFINED', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'Not defined', - threshold: 2, - triggerValue: 4, - reason: - 'Error count is 4 in the last 5 mins for service: foo, env: Not defined. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=ENVIRONMENT_ALL', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'Not defined', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 4 in the last 5 mins for service: foo, env: Not defined. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 4, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=ENVIRONMENT_ALL', + }, + id: 'foo_ENVIRONMENT_NOT_DEFINED', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 4, + 'kibana.alert.reason': + 'Error count is 4 in the last 5 mins for service: foo, env: Not defined. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'ENVIRONMENT_NOT_DEFINED', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'bar', - environment: 'env-bar', - reason: - 'Error count is 3 in the last 5 mins for service: bar, env: env-bar. Alert when > 2.', - threshold: 2, - triggerValue: 3, - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-bar', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar. Alert when > 2.', + serviceName: 'bar', + threshold: 2, + triggerValue: 3, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + }, + id: 'bar_env-bar', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 3, + 'kibana.alert.reason': + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-bar', + 'service.name': 'bar', + }, }); }); it('sends alert when rule is configured with group by on error.grouping_key and error.grouping_name', async () => { - const { services, dependencies, executor, scheduleActions } = - createRuleTypeMocks(); + const { services, dependencies, executor } = createRuleTypeMocks(); registerErrorCountRuleType(dependencies); @@ -642,6 +866,7 @@ describe('Error count alert', () => { total: 1, }, }); + services.alertsClient.report.mockReturnValue({ uuid: 'test-uuid' }); await executor({ params }); [ @@ -649,58 +874,102 @@ describe('Error count alert', () => { 'foo_env-foo-2_error-key-foo-2_error-name-foo2', 'bar_env-bar_error-key-bar_error-name-bar', ].forEach((instanceName) => - expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'threshold_met', + id: instanceName, + }) ); - expect(scheduleActions).toHaveBeenCalledTimes(3); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(3); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo', - threshold: 2, - triggerValue: 5, - reason: - 'Error count is 5 in the last 5 mins for service: foo, env: env-foo, error key: error-key-foo, error name: error-name-foo. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', - errorGroupingKey: 'error-key-foo', - errorGroupingName: 'error-name-foo', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo', + errorGroupingKey: 'error-key-foo', + errorGroupingName: 'error-name-foo', + interval: '5 mins', + reason: + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo, error key: error-key-foo, error name: error-name-foo. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 5, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + }, + id: 'foo_env-foo_error-key-foo_error-name-foo', + payload: { + 'error.grouping_key': 'error-key-foo', + 'error.grouping_name': 'error-name-foo', + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 5, + 'kibana.alert.reason': + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo, error key: error-key-foo, error name: error-name-foo. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo-2', - threshold: 2, - triggerValue: 4, - reason: - 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2, error key: error-key-foo-2, error name: error-name-foo2. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', - errorGroupingKey: 'error-key-foo-2', - errorGroupingName: 'error-name-foo2', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo-2', + errorGroupingKey: 'error-key-foo-2', + errorGroupingName: 'error-name-foo2', + interval: '5 mins', + reason: + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2, error key: error-key-foo-2, error name: error-name-foo2. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 4, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', + }, + id: 'foo_env-foo-2_error-key-foo-2_error-name-foo2', + payload: { + 'error.grouping_key': 'error-key-foo-2', + 'error.grouping_name': 'error-name-foo2', + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 4, + 'kibana.alert.reason': + 'Error count is 4 in the last 5 mins for service: foo, env: env-foo-2, error key: error-key-foo-2, error name: error-name-foo2. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo-2', + 'service.name': 'foo', + }, }); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'bar', - environment: 'env-bar', - reason: - 'Error count is 3 in the last 5 mins for service: bar, env: env-bar, error key: error-key-bar, error name: error-name-bar. Alert when > 2.', - threshold: 2, - triggerValue: 3, - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', - errorGroupingKey: 'error-key-bar', - errorGroupingName: 'error-name-bar', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-bar', + errorGroupingKey: 'error-key-bar', + errorGroupingName: 'error-name-bar', + interval: '5 mins', + reason: + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar, error key: error-key-bar, error name: error-name-bar. Alert when > 2.', + serviceName: 'bar', + threshold: 2, + triggerValue: 3, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + }, + id: 'bar_env-bar_error-key-bar_error-name-bar', + payload: { + 'error.grouping_key': 'error-key-bar', + 'error.grouping_name': 'error-name-bar', + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 3, + 'kibana.alert.reason': + 'Error count is 3 in the last 5 mins for service: bar, env: env-bar, error key: error-key-bar, error name: error-name-bar. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-bar', + 'service.name': 'bar', + }, }); }); it('sends alert when rule is configured with a filter query', async () => { - const { services, dependencies, executor, scheduleActions } = - createRuleTypeMocks(); + const { services, dependencies, executor } = createRuleTypeMocks(); registerErrorCountRuleType(dependencies); @@ -745,25 +1014,43 @@ describe('Error count alert', () => { total: 1, }, }); + services.alertsClient.report.mockReturnValue({ uuid: 'test-uuid' }); await executor({ params }); ['foo_env-foo'].forEach((instanceName) => - expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'threshold_met', + id: instanceName, + }) ); - expect(scheduleActions).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); - expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - serviceName: 'foo', - environment: 'env-foo', - threshold: 2, - triggerValue: 5, - reason: - 'Error count is 5 in the last 5 mins for service: foo, env: env-foo. Alert when > 2.', - interval: '5 mins', - viewInAppUrl: - 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + environment: 'env-foo', + errorGroupingKey: undefined, + interval: '5 mins', + reason: + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo. Alert when > 2.', + serviceName: 'foo', + threshold: 2, + triggerValue: 5, + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + }, + id: 'foo_env-foo', + payload: { + 'error.grouping_key': undefined, + 'kibana.alert.evaluation.threshold': 2, + 'kibana.alert.evaluation.value': 5, + 'kibana.alert.reason': + 'Error count is 5 in the last 5 mins for service: foo, env: env-foo. Alert when > 2.', + 'processor.event': 'error', + 'service.environment': 'env-foo', + 'service.name': 'foo', + }, }); }); }); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts index 5c3c2d3fe4fb2..f60be9b77f814 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts @@ -6,7 +6,16 @@ */ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; -import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; +import { + GetViewInAppRelativeUrlFnOpts, + ActionGroupIdsOf, + AlertInstanceContext as AlertContext, + AlertInstanceState as AlertState, + RuleTypeState, + RuleExecutorOptions, + AlertsClientError, + IRuleTypeAlerts, +} from '@kbn/alerting-plugin/server'; import { formatDurationFromTimeUnitChar, getAlertUrl, @@ -20,7 +29,7 @@ import { ALERT_REASON, ApmRuleType, } from '@kbn/rule-data-utils'; -import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; +import { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; import { getParsedFilterQuery, termQuery, @@ -38,8 +47,12 @@ import { APM_SERVER_FEATURE_ID, formatErrorCountReason, RULE_TYPES_CONFIG, + THRESHOLD_MET_GROUP, } from '../../../../../common/rules/apm_rule_types'; -import { errorCountParamsSchema } from '../../../../../common/rules/schema'; +import { + errorCountParamsSchema, + ApmRuleParamsType, +} from '../../../../../common/rules/schema'; import { environmentQuery } from '../../../../../common/utils/environment_query'; import { getAlertUrlErrorCount } from '../../../../../common/utils/formatters'; import { apmActionVariables } from '../../action_variables'; @@ -72,6 +85,13 @@ export const errorCountActionVariables = [ apmActionVariables.viewInAppUrl, ]; +type ErrorCountRuleTypeParams = ApmRuleParamsType[ApmRuleType.ErrorCount]; +type ErrorCountActionGroups = ActionGroupIdsOf; +type ErrorCountRuleTypeState = RuleTypeState; +type ErrorCountAlertState = AlertState; +type ErrorCountAlertContext = AlertContext; +type ErrorCountAlert = ObservabilityApmAlert; + export function registerErrorCountRuleType({ alerting, alertsLocator, @@ -80,204 +100,219 @@ export function registerErrorCountRuleType({ logger, ruleDataClient, }: RegisterRuleDependencies) { - const createLifecycleRuleType = createLifecycleRuleTypeFactory({ - ruleDataClient, - logger, - }); - - alerting.registerType( - createLifecycleRuleType({ - id: ApmRuleType.ErrorCount, - name: ruleTypeConfig.name, - actionGroups: ruleTypeConfig.actionGroups, - defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, - validate: { params: errorCountParamsSchema }, - schemas: { - params: { - type: 'config-schema', - schema: errorCountParamsSchema, - }, + if (!alerting) { + throw new Error( + 'Cannot register error count rule type. Both the actions and alerting plugins need to be enabled.' + ); + } + alerting.registerType({ + id: ApmRuleType.ErrorCount, + name: ruleTypeConfig.name, + actionGroups: ruleTypeConfig.actionGroups, + defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, + validate: { params: errorCountParamsSchema }, + schemas: { + params: { + type: 'config-schema', + schema: errorCountParamsSchema, }, - actionVariables: { - context: errorCountActionVariables, - }, - category: DEFAULT_APP_CATEGORIES.observability.id, - producer: APM_SERVER_FEATURE_ID, - minimumLicenseRequired: 'basic', - isExportable: true, - executor: async ({ + }, + actionVariables: { + context: errorCountActionVariables, + }, + category: DEFAULT_APP_CATEGORIES.observability.id, + producer: APM_SERVER_FEATURE_ID, + minimumLicenseRequired: 'basic', + isExportable: true, + executor: async ( + options: RuleExecutorOptions< + ErrorCountRuleTypeParams, + ErrorCountRuleTypeState, + ErrorCountAlertState, + ErrorCountAlertContext, + ErrorCountActionGroups, + ErrorCountAlert + > + ) => { + const { params: ruleParams, services, spaceId, startedAt, getTimeRange, - }) => { - const allGroupByFields = getAllGroupByFields( - ApmRuleType.ErrorCount, - ruleParams.groupBy - ); + } = options; + const { alertsClient, savedObjectsClient, scopedClusterClient } = + services; + if (!alertsClient) { + throw new AlertsClientError(); + } - const { - getAlertUuid, - getAlertStartedDate, - savedObjectsClient, - scopedClusterClient, - } = services; + const allGroupByFields = getAllGroupByFields( + ApmRuleType.ErrorCount, + ruleParams.groupBy + ); - const indices = await getApmIndices(savedObjectsClient); + const indices = await getApmIndices(savedObjectsClient); - const termFilterQuery = !ruleParams.searchConfiguration?.query?.query - ? [ - ...termQuery(SERVICE_NAME, ruleParams.serviceName, { - queryEmptyString: false, - }), - ...termQuery(ERROR_GROUP_ID, ruleParams.errorGroupingKey, { - queryEmptyString: false, - }), - ...environmentQuery(ruleParams.environment), - ] - : []; + const termFilterQuery = !ruleParams.searchConfiguration?.query?.query + ? [ + ...termQuery(SERVICE_NAME, ruleParams.serviceName, { + queryEmptyString: false, + }), + ...termQuery(ERROR_GROUP_ID, ruleParams.errorGroupingKey, { + queryEmptyString: false, + }), + ...environmentQuery(ruleParams.environment), + ] + : []; - const { dateStart } = getTimeRange( - `${ruleParams.windowSize}${ruleParams.windowUnit}` - ); + const { dateStart } = getTimeRange( + `${ruleParams.windowSize}${ruleParams.windowUnit}` + ); - const searchParams = { - index: indices.error, - body: { - track_total_hits: false, - size: 0, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: dateStart, - }, + const searchParams = { + index: indices.error, + body: { + track_total_hits: false, + size: 0, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: dateStart, }, }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, - ...termFilterQuery, - ...getParsedFilterQuery( - ruleParams.searchConfiguration?.query?.query as string - ), - ], - }, - }, - aggs: { - error_counts: { - multi_terms: { - terms: getGroupByTerms(allGroupByFields), - size: 1000, - order: { _count: 'desc' as const }, }, - aggs: getServiceGroupFieldsAgg(), + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + ...termFilterQuery, + ...getParsedFilterQuery( + ruleParams.searchConfiguration?.query?.query as string + ), + ], + }, + }, + aggs: { + error_counts: { + multi_terms: { + terms: getGroupByTerms(allGroupByFields), + size: 1000, + order: { _count: 'desc' as const }, }, + aggs: getServiceGroupFieldsAgg(), }, }, - }; + }, + }; - const response = await alertingEsClient({ - scopedClusterClient, - params: searchParams, - }); + const response = await alertingEsClient({ + scopedClusterClient, + params: searchParams, + }); - const errorCountResults = - response.aggregations?.error_counts.buckets.map((bucket) => { - const groupByFields = bucket.key.reduce( - (obj, bucketKey, bucketIndex) => { - obj[allGroupByFields[bucketIndex]] = bucketKey; - return obj; - }, - {} as Record - ); + const errorCountResults = + response.aggregations?.error_counts.buckets.map((bucket) => { + const groupByFields = bucket.key.reduce( + (obj, bucketKey, bucketIndex) => { + obj[allGroupByFields[bucketIndex]] = bucketKey; + return obj; + }, + {} as Record + ); - const bucketKey = bucket.key; + const bucketKey = bucket.key; - return { - errorCount: bucket.doc_count, - sourceFields: getServiceGroupFields(bucket), - groupByFields, - bucketKey, - }; - }) ?? []; + return { + errorCount: bucket.doc_count, + sourceFields: getServiceGroupFields(bucket), + groupByFields, + bucketKey, + }; + }) ?? []; - await asyncForEach( - errorCountResults.filter( - (result) => result.errorCount >= ruleParams.threshold - ), - async (result) => { - const { errorCount, sourceFields, groupByFields, bucketKey } = - result; - const alertId = bucketKey.join('_'); - const alertReason = formatErrorCountReason({ - threshold: ruleParams.threshold, - measured: errorCount, - windowSize: ruleParams.windowSize, - windowUnit: ruleParams.windowUnit, - groupByFields, - }); + await asyncForEach( + errorCountResults.filter( + (result) => result.errorCount >= ruleParams.threshold + ), + async (result) => { + const { errorCount, sourceFields, groupByFields, bucketKey } = result; + const alertId = bucketKey.join('_'); + const alertReason = formatErrorCountReason({ + threshold: ruleParams.threshold, + measured: errorCount, + windowSize: ruleParams.windowSize, + windowUnit: ruleParams.windowUnit, + groupByFields, + }); - const alert = services.alertWithLifecycle({ - id: alertId, - fields: { - [PROCESSOR_EVENT]: ProcessorEvent.error, - [ALERT_EVALUATION_VALUE]: errorCount, - [ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold, - [ERROR_GROUP_ID]: ruleParams.errorGroupingKey, - [ALERT_REASON]: alertReason, - ...sourceFields, - ...groupByFields, - }, - }); + const { uuid, start } = alertsClient.report({ + id: alertId, + actionGroup: ruleTypeConfig.defaultActionGroupId, + }); + const indexedStartedAt = start ?? startedAt.toISOString(); - const relativeViewInAppUrl = getAlertUrlErrorCount( - groupByFields[SERVICE_NAME], - getEnvironmentEsField(groupByFields[SERVICE_ENVIRONMENT])?.[ - SERVICE_ENVIRONMENT - ] - ); - const viewInAppUrl = addSpaceIdToPath( - basePath.publicBaseUrl, - spaceId, - relativeViewInAppUrl - ); - const indexedStartedAt = - getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const alertUuid = getAlertUuid(alertId); - const alertDetailsUrl = await getAlertUrl( - alertUuid, - spaceId, - indexedStartedAt, - alertsLocator, - basePath.publicBaseUrl - ); - const groupByActionVariables = - getGroupByActionVariables(groupByFields); + const relativeViewInAppUrl = getAlertUrlErrorCount( + groupByFields[SERVICE_NAME], + getEnvironmentEsField(groupByFields[SERVICE_ENVIRONMENT])?.[ + SERVICE_ENVIRONMENT + ] + ); + const viewInAppUrl = addSpaceIdToPath( + basePath.publicBaseUrl, + spaceId, + relativeViewInAppUrl + ); + const alertDetailsUrl = await getAlertUrl( + uuid, + spaceId, + indexedStartedAt, + alertsLocator, + basePath.publicBaseUrl + ); + const groupByActionVariables = + getGroupByActionVariables(groupByFields); - alert.scheduleActions(ruleTypeConfig.defaultActionGroupId, { - alertDetailsUrl, - interval: formatDurationFromTimeUnitChar( - ruleParams.windowSize, - ruleParams.windowUnit as TimeUnitChar - ), - reason: alertReason, - threshold: ruleParams.threshold, - // When group by doesn't include error.grouping_key, the context.error.grouping_key action variable will contain value of the Error Grouping Key filter - errorGroupingKey: ruleParams.errorGroupingKey, - triggerValue: errorCount, - viewInAppUrl, - ...groupByActionVariables, - }); - } - ); + const payload = { + [PROCESSOR_EVENT]: ProcessorEvent.error, + [ALERT_EVALUATION_VALUE]: errorCount, + [ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold, + [ERROR_GROUP_ID]: ruleParams.errorGroupingKey, + [ALERT_REASON]: alertReason, + ...sourceFields, + ...groupByFields, + }; - return { state: {} }; - }, - alerts: ApmRuleTypeAlertDefinition, - getViewInAppRelativeUrl: ({ rule }: GetViewInAppRelativeUrlFnOpts<{}>) => - observabilityPaths.ruleDetails(rule.id), - }) - ); + const context = { + alertDetailsUrl, + interval: formatDurationFromTimeUnitChar( + ruleParams.windowSize, + ruleParams.windowUnit as TimeUnitChar + ), + reason: alertReason, + threshold: ruleParams.threshold, + // When group by doesn't include error.grouping_key, the context.error.grouping_key action variable will contain value of the Error Grouping Key filter + errorGroupingKey: ruleParams.errorGroupingKey, + triggerValue: errorCount, + viewInAppUrl, + ...groupByActionVariables, + }; + + alertsClient.setAlertData({ + id: alertId, + payload, + context, + }); + } + ); + + return { state: {} }; + }, + alerts: { + ...ApmRuleTypeAlertDefinition, + shouldWrite: true, + } as IRuleTypeAlerts, + getViewInAppRelativeUrl: ({ rule }: GetViewInAppRelativeUrlFnOpts<{}>) => + observabilityPaths.ruleDetails(rule.id), + }); } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/alerts/test_utils/index.ts index a447ab2a75d4b..1f8ddeaff4620 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/alerts/test_utils/index.ts @@ -47,6 +47,10 @@ export const createRuleTypeMocks = () => { alertWithLifecycle: jest.fn(), logger: loggerMock, shouldWriteAlerts: () => true, + alertsClient: { + report: jest.fn(), + setAlertData: jest.fn(), + }, }; const dependencies = { From bcdfb041072e12fb97535586b3626f1a6ced0b06 Mon Sep 17 00:00:00 2001 From: Alexandra Doak Date: Wed, 27 Mar 2024 12:51:25 -0700 Subject: [PATCH 2/2] Updating the error message --- .../rule_types/error_count/register_error_count_rule_type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts index f60be9b77f814..786332a8f4196 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts @@ -102,7 +102,7 @@ export function registerErrorCountRuleType({ }: RegisterRuleDependencies) { if (!alerting) { throw new Error( - 'Cannot register error count rule type. Both the actions and alerting plugins need to be enabled.' + 'Cannot register error count rule type. The alerting plugin needs to be enabled.' ); } alerting.registerType({