From cec0a15a6addb951ae5214b99899965ba8ae93db Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Fri, 2 Jun 2023 09:55:19 +0200 Subject: [PATCH] [AO] Add alertDetailsUrl to APM rule types (#158657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #158268 ## Summary In this PR, I've added `alertDetailsUrl` as an action variable to the APM rule types. Also, the related functional tests were updated to ensure the URL was generated correctly. |Action|Result| |---|---| |![image](https://github.com/elastic/kibana/assets/12370520/680c575a-8bbc-4ead-9a74-d92d25780fcf)|![image](https://github.com/elastic/kibana/assets/12370520/c3b4fd1e-8ffd-43ab-8d58-130d70cc7767)| #### Filtered alert Note that, for the APM Latency threshold, we already had this variable pointing to the newly implemented alert details page. ## 🧪 How to test - Create an APM rule (besides the Latency threshold rule) - Create an action and add `context.alertDetailsUrl` variable in the message - After an alert is created, check the action message. The link should land on the `Alerts` page filtered for that specific alert instance --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/server/plugin.ts | 2 + .../routes/alerts/register_apm_rule_types.ts | 3 + .../register_anomaly_rule_type.test.ts | 1 + .../anomaly/register_anomaly_rule_type.ts | 80 ++++++++++------ .../register_error_count_rule_type.test.ts | 15 +++ .../register_error_count_rule_type.ts | 94 ++++++++++++------- ...register_transaction_duration_rule_type.ts | 62 ++++++------ ...r_transaction_error_rate_rule_type.test.ts | 4 + ...gister_transaction_error_rate_rule_type.ts | 85 +++++++++++------ .../server/routes/alerts/test_utils/index.ts | 9 +- x-pack/plugins/apm/server/types.ts | 3 + .../infra/server/lib/alerting/common/utils.ts | 30 ------ .../inventory_metric_threshold_executor.ts | 2 +- .../log_threshold/log_threshold_executor.ts | 3 +- .../metric_threshold_executor.ts | 4 +- x-pack/plugins/observability/common/index.ts | 2 + .../common/utils/alerting/alert_url.ts | 39 ++++++++ x-pack/plugins/observability/kibana.jsonc | 3 +- x-pack/plugins/observability/tsconfig.json | 3 +- .../test/apm_api_integration/configs/index.ts | 1 + .../alerts/error_count_threshold.spec.ts | 38 +++++--- .../alerts/transaction_error_rate.spec.ts | 32 +++++-- 22 files changed, 325 insertions(+), 190 deletions(-) create mode 100644 x-pack/plugins/observability/common/utils/alerting/alert_url.ts diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index b7ef807319163..9c3f31d2c4e1a 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -18,6 +18,7 @@ import { isEmpty, mapValues } from 'lodash'; import { Dataset } from '@kbn/rule-registry-plugin/server'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { mappingFromFieldMap } from '@kbn/alerting-plugin/common'; +import { alertsLocatorID } from '@kbn/observability-plugin/common'; import { APMConfig, APM_SERVER_FEATURE_ID } from '.'; import { APM_FEATURE, registerFeaturesUsage } from './feature'; import { @@ -187,6 +188,7 @@ export class APMPlugin ml: plugins.ml, observability: plugins.observability, ruleDataClient, + alertsLocator: plugins.share.url.locators.get(alertsLocatorID), }); } diff --git a/x-pack/plugins/apm/server/routes/alerts/register_apm_rule_types.ts b/x-pack/plugins/apm/server/routes/alerts/register_apm_rule_types.ts index e34957083d3e4..fb197daabe943 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_apm_rule_types.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_apm_rule_types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { AlertsLocatorParams } from '@kbn/observability-plugin/common'; +import { LocatorPublic } from '@kbn/share-plugin/common'; import { Observable } from 'rxjs'; import { IBasePath, Logger } from '@kbn/core/server'; import { @@ -89,6 +91,7 @@ export interface RegisterRuleDependencies { ml?: MlPluginSetup; observability: ObservabilityPluginSetup; ruleDataClient: IRuleDataClient; + alertsLocator?: LocatorPublic; } export function registerApmRuleTypes(dependencies: RegisterRuleDependencies) { diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.test.ts index ca504ec6b353b..611ca43499c6a 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.test.ts @@ -203,6 +203,7 @@ describe('Transaction duration anomaly alert', () => { 'critical anomaly with a score of 80 was detected in the last 5 mins for foo.', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo?transactionType=type-foo&environment=development', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts index ef37238ef3e35..937386e2c4928 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts @@ -8,7 +8,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWith import { KibanaRequest } from '@kbn/core/server'; import datemath from '@kbn/datemath'; import type { ESSearchResponse } from '@kbn/es-types'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { getAlertUrl, ProcessorEvent } from '@kbn/observability-plugin/common'; import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_EVALUATION_THRESHOLD, @@ -18,6 +18,7 @@ import { } from '@kbn/rule-data-utils'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; +import { asyncForEach } from '@kbn/std'; import { compact } from 'lodash'; import { getSeverity } from '../../../../../common/anomaly_detection'; import { @@ -55,6 +56,7 @@ const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.Anomaly]; export function registerAnomalyRuleType({ alerting, + alertsLocator, basePath, config$, logger, @@ -75,6 +77,7 @@ export function registerAnomalyRuleType({ validate: { params: anomalyParamsSchema }, actionVariables: { context: [ + apmActionVariables.alertDetailsUrl, apmActionVariables.environment, apmActionVariables.reason, apmActionVariables.serviceName, @@ -87,12 +90,17 @@ export function registerAnomalyRuleType({ producer: 'apm', minimumLicenseRequired: 'basic', isExportable: true, - executor: async ({ params, services, spaceId }) => { + executor: async ({ params, services, spaceId, startedAt }) => { if (!ml) { return { state: {} }; } - const { savedObjectsClient, scopedClusterClient } = services; + const { + getAlertUuid, + getAlertStartedDate, + savedObjectsClient, + scopedClusterClient, + } = services; const ruleParams = params; const request = {} as KibanaRequest; @@ -231,7 +239,7 @@ export function registerAnomalyRuleType({ anomaly ? anomaly.score >= threshold : false ) ?? []; - for (const anomaly of compact(anomalies)) { + await asyncForEach(compact(anomalies), async (anomaly) => { const { serviceName, environment, @@ -261,7 +269,7 @@ export function registerAnomalyRuleType({ windowUnit: params.windowUnit, }); - const id = [ + const alertId = [ ApmRuleType.Anomaly, serviceName, environment, @@ -270,43 +278,53 @@ export function registerAnomalyRuleType({ .filter((name) => name) .join('_'); + const alert = services.alertWithLifecycle({ + id: alertId, + fields: { + [SERVICE_NAME]: serviceName, + ...getEnvironmentEsField(environment), + [TRANSACTION_TYPE]: transactionType, + [PROCESSOR_EVENT]: ProcessorEvent.transaction, + [ALERT_SEVERITY]: severityLevel, + [ALERT_EVALUATION_VALUE]: score, + [ALERT_EVALUATION_THRESHOLD]: threshold, + [ALERT_REASON]: reasonMessage, + ...eventSourceFields, + }, + }); + const relativeViewInAppUrl = getAlertUrlTransaction( serviceName, getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT], transactionType ); - 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 + ); - services - .alertWithLifecycle({ - id, - fields: { - [SERVICE_NAME]: serviceName, - ...getEnvironmentEsField(environment), - [TRANSACTION_TYPE]: transactionType, - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - [ALERT_SEVERITY]: severityLevel, - [ALERT_EVALUATION_VALUE]: score, - [ALERT_EVALUATION_THRESHOLD]: threshold, - [ALERT_REASON]: reasonMessage, - ...eventSourceFields, - }, - }) - .scheduleActions(ruleTypeConfig.defaultActionGroupId, { - environment: getEnvironmentLabel(environment), - reason: reasonMessage, - serviceName, - threshold: selectedOption?.label, - transactionType, - triggerValue: severityLevel, - viewInAppUrl, - }); - } + alert.scheduleActions(ruleTypeConfig.defaultActionGroupId, { + alertDetailsUrl, + environment: getEnvironmentLabel(environment), + reason: reasonMessage, + serviceName, + threshold: selectedOption?.label, + transactionType, + triggerValue: severityLevel, + viewInAppUrl, + }); + }); return { state: {} }; }, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts index 781094ca55257..05539bd7ffdb4 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts @@ -147,6 +147,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { serviceName: 'foo', @@ -158,6 +159,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { serviceName: 'bar', @@ -169,6 +171,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); }); @@ -246,6 +249,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', transactionName: 'tx-name-foo', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { @@ -258,6 +262,7 @@ describe('Error count alert', () => { 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(scheduleActions).toHaveBeenCalledWith('threshold_met', { @@ -270,6 +275,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', transactionName: 'tx-name-bar', }); }); @@ -348,6 +354,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', errorGroupingKey: 'error-key-foo', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { @@ -360,6 +367,7 @@ describe('Error count alert', () => { 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(scheduleActions).toHaveBeenCalledWith('threshold_met', { @@ -372,6 +380,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', errorGroupingKey: 'error-key-bar', }); }); @@ -446,6 +455,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { serviceName: 'foo', @@ -457,6 +467,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { serviceName: 'bar', @@ -468,6 +479,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); }); @@ -545,6 +557,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=ENVIRONMENT_ALL', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { serviceName: 'foo', @@ -556,6 +569,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=ENVIRONMENT_ALL', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { serviceName: 'bar', @@ -567,6 +581,7 @@ describe('Error count alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts index 6ac9a87789136..812769a6a365a 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts @@ -7,6 +7,7 @@ import { formatDurationFromTimeUnitChar, + getAlertUrl, ProcessorEvent, TimeUnitChar, } from '@kbn/observability-plugin/common'; @@ -18,6 +19,7 @@ import { import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; import { termQuery } from '@kbn/observability-plugin/server'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; +import { asyncForEach } from '@kbn/std'; import { firstValueFrom } from 'rxjs'; import { getEnvironmentEsField } from '../../../../../common/environment_filter_values'; import { @@ -53,6 +55,7 @@ const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.ErrorCount]; export function registerErrorCountRuleType({ alerting, + alertsLocator, basePath, config$, logger, @@ -72,6 +75,7 @@ export function registerErrorCountRuleType({ validate: { params: errorCountParamsSchema }, actionVariables: { context: [ + apmActionVariables.alertDetailsUrl, apmActionVariables.environment, apmActionVariables.interval, apmActionVariables.reason, @@ -86,7 +90,12 @@ export function registerErrorCountRuleType({ producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, - executor: async ({ params: ruleParams, services, spaceId }) => { + executor: async ({ + params: ruleParams, + services, + spaceId, + startedAt, + }) => { const predefinedGroupby = [SERVICE_NAME, SERVICE_ENVIRONMENT]; const allGroupbyFields = Array.from( @@ -95,7 +104,12 @@ export function registerErrorCountRuleType({ const config = await firstValueFrom(config$); - const { savedObjectsClient, scopedClusterClient } = services; + const { + getAlertUuid, + getAlertStartedDate, + savedObjectsClient, + scopedClusterClient, + } = services; const indices = await getApmIndices({ config, @@ -166,11 +180,14 @@ export function registerErrorCountRuleType({ }; }) ?? []; - errorCountResults - .filter((result) => result.errorCount >= ruleParams.threshold) - .forEach((result) => { + 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, @@ -179,48 +196,59 @@ export function registerErrorCountRuleType({ 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 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); - services - .alertWithLifecycle({ - id: bucketKey.join('_'), - fields: { - [PROCESSOR_EVENT]: ProcessorEvent.error, - [ALERT_EVALUATION_VALUE]: errorCount, - [ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold, - [ERROR_GROUP_ID]: ruleParams.errorGroupingKey, - [ALERT_REASON]: alertReason, - ...sourceFields, - ...groupByFields, - }, - }) - .scheduleActions(ruleTypeConfig.defaultActionGroupId, { - interval: formatDurationFromTimeUnitChar( - ruleParams.windowSize, - ruleParams.windowUnit as TimeUnitChar - ), - reason: alertReason, - threshold: ruleParams.threshold, - errorGroupingKey: ruleParams.errorGroupingKey, // 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 - triggerValue: errorCount, - viewInAppUrl, - ...groupByActionVariables, - }); - }); + 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, + }); + } + ); return { state: {} }; }, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 768d7f8a9f781..39b3dabd21d76 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -6,13 +6,13 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; import { + asDuration, formatDurationFromTimeUnitChar, + getAlertDetailsUrl, ProcessorEvent, TimeUnitChar, } from '@kbn/observability-plugin/common'; -import { asDuration } from '@kbn/observability-plugin/common/utils/formatters'; import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_EVALUATION_THRESHOLD, @@ -251,14 +251,26 @@ export function registerTransactionDurationRuleType({ groupByFields, }); - const alertUuid = getAlertUuid(bucketKey.join('_')); + const alertId = bucketKey.join('_'); + const alert = services.alertWithLifecycle({ + id: alertId, + fields: { + [TRANSACTION_NAME]: ruleParams.transactionName, + [PROCESSOR_EVENT]: ProcessorEvent.transaction, + [ALERT_EVALUATION_VALUE]: transactionDuration, + [ALERT_EVALUATION_THRESHOLD]: thresholdMicroseconds, + [ALERT_REASON]: reason, + ...sourceFields, + ...groupByFields, + }, + }); + const alertUuid = getAlertUuid(alertId); const alertDetailsUrl = getAlertDetailsUrl( basePath, spaceId, alertUuid ); - const viewInAppUrl = addSpaceIdToPath( basePath.publicBaseUrl, spaceId, @@ -270,35 +282,21 @@ export function registerTransactionDurationRuleType({ groupByFields[TRANSACTION_TYPE] ) ); - const groupByActionVariables = getGroupByActionVariables(groupByFields); - - services - .alertWithLifecycle({ - id: bucketKey.join('_'), - fields: { - [TRANSACTION_NAME]: ruleParams.transactionName, - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - [ALERT_EVALUATION_VALUE]: transactionDuration, - [ALERT_EVALUATION_THRESHOLD]: thresholdMicroseconds, - [ALERT_REASON]: reason, - ...sourceFields, - ...groupByFields, - }, - }) - .scheduleActions(ruleTypeConfig.defaultActionGroupId, { - alertDetailsUrl, - interval: formatDurationFromTimeUnitChar( - ruleParams.windowSize, - ruleParams.windowUnit as TimeUnitChar - ), - reason, - transactionName: ruleParams.transactionName, // When group by doesn't include transaction.name, the context.transaction.name action variable will contain value of the Transaction Name filter - threshold: ruleParams.threshold, - triggerValue: transactionDurationFormatted, - viewInAppUrl, - ...groupByActionVariables, - }); + alert.scheduleActions(ruleTypeConfig.defaultActionGroupId, { + alertDetailsUrl, + interval: formatDurationFromTimeUnitChar( + ruleParams.windowSize, + ruleParams.windowUnit as TimeUnitChar + ), + reason, + // When group by doesn't include transaction.name, the context.transaction.name action variable will contain value of the Transaction Name filter + transactionName: ruleParams.transactionName, + threshold: ruleParams.threshold, + triggerValue: transactionDurationFormatted, + viewInAppUrl, + ...groupByActionVariables, + }); } return { state: {} }; diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts index 708d5c533ba6b..c50bd62210e32 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts @@ -135,6 +135,7 @@ describe('Transaction error rate alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo?transactionType=type-foo&environment=env-foo', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); }); @@ -235,6 +236,7 @@ describe('Transaction error rate alert', () => { viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo?transactionType=type-foo&environment=env-foo', transactionName: 'tx-name-foo', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); }); @@ -329,6 +331,7 @@ describe('Transaction error rate alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo?transactionType=type-foo&environment=env-foo', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); }); @@ -423,6 +426,7 @@ describe('Transaction error rate alert', () => { interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo?transactionType=type-foo&environment=ENVIRONMENT_ALL', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', }); }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 6fa9319b71753..3b3bcb6095c8a 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -7,6 +7,7 @@ import { formatDurationFromTimeUnitChar, + getAlertUrl, ProcessorEvent, TimeUnitChar, } from '@kbn/observability-plugin/common'; @@ -19,6 +20,7 @@ import { } from '@kbn/rule-data-utils'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; +import { asyncForEach } from '@kbn/std'; import { firstValueFrom } from 'rxjs'; import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; import { getEnvironmentEsField } from '../../../../../common/environment_filter_values'; @@ -62,6 +64,7 @@ const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionErrorRate]; export function registerTransactionErrorRateRuleType({ alerting, + alertsLocator, basePath, config$, logger, @@ -81,6 +84,7 @@ export function registerTransactionErrorRateRuleType({ validate: { params: transactionErrorRateParamsSchema }, actionVariables: { context: [ + apmActionVariables.alertDetailsUrl, apmActionVariables.environment, apmActionVariables.interval, apmActionVariables.reason, @@ -96,7 +100,12 @@ export function registerTransactionErrorRateRuleType({ producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, - executor: async ({ services, spaceId, params: ruleParams }) => { + executor: async ({ + services, + spaceId, + params: ruleParams, + startedAt, + }) => { const predefinedGroupby = [ SERVICE_NAME, SERVICE_ENVIRONMENT, @@ -109,7 +118,12 @@ export function registerTransactionErrorRateRuleType({ const config = await firstValueFrom(config$); - const { savedObjectsClient, scopedClusterClient } = services; + const { + getAlertUuid, + getAlertStartedDate, + savedObjectsClient, + scopedClusterClient, + } = services; const indices = await getApmIndices({ config, @@ -228,9 +242,9 @@ export function registerTransactionErrorRateRuleType({ } } - results.forEach((result) => { + await asyncForEach(results, async (result) => { const { errorRate, sourceFields, groupByFields, bucketKey } = result; - + const alertId = bucketKey.join('_'); const reasonMessage = formatTransactionErrorRateReason({ threshold: ruleParams.threshold, measured: errorRate, @@ -240,6 +254,19 @@ export function registerTransactionErrorRateRuleType({ groupByFields, }); + const alert = services.alertWithLifecycle({ + id: alertId, + fields: { + [TRANSACTION_NAME]: ruleParams.transactionName, + [PROCESSOR_EVENT]: ProcessorEvent.transaction, + [ALERT_EVALUATION_VALUE]: errorRate, + [ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold, + [ALERT_REASON]: reasonMessage, + ...sourceFields, + ...groupByFields, + }, + }); + const relativeViewInAppUrl = getAlertUrlTransaction( groupByFields[SERVICE_NAME], getEnvironmentEsField(groupByFields[SERVICE_ENVIRONMENT])?.[ @@ -247,41 +274,37 @@ export function registerTransactionErrorRateRuleType({ ], groupByFields[TRANSACTION_TYPE] ); - 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); - services - .alertWithLifecycle({ - id: bucketKey.join('_'), - fields: { - [TRANSACTION_NAME]: ruleParams.transactionName, - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - [ALERT_EVALUATION_VALUE]: errorRate, - [ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold, - [ALERT_REASON]: reasonMessage, - ...sourceFields, - ...groupByFields, - }, - }) - .scheduleActions(ruleTypeConfig.defaultActionGroupId, { - interval: formatDurationFromTimeUnitChar( - ruleParams.windowSize, - ruleParams.windowUnit as TimeUnitChar - ), - reason: reasonMessage, - threshold: ruleParams.threshold, - transactionName: ruleParams.transactionName, - triggerValue: asDecimalOrInteger(errorRate), - viewInAppUrl, - ...groupByActionVariables, - }); + alert.scheduleActions(ruleTypeConfig.defaultActionGroupId, { + alertDetailsUrl, + interval: formatDurationFromTimeUnitChar( + ruleParams.windowSize, + ruleParams.windowUnit as TimeUnitChar + ), + reason: reasonMessage, + threshold: ruleParams.threshold, + transactionName: ruleParams.transactionName, + triggerValue: asDecimalOrInteger(errorRate), + viewInAppUrl, + ...groupByActionVariables, + }); }); return { state: {} }; diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts index e0bb678a1be2f..fecb5c8fba19d 100644 --- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { IBasePath, Logger } from '@kbn/core/server'; import { of } from 'rxjs'; +import { IBasePath, Logger } from '@kbn/core/server'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import type { AlertsLocatorParams } from '@kbn/observability-plugin/common'; +import { LocatorPublic } from '@kbn/share-plugin/common'; import { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks'; import { PluginSetupContract as AlertingPluginSetupContract } from '@kbn/alerting-plugin/server'; @@ -70,6 +72,11 @@ export const createRuleTypeMocks = () => { ruleDataClient: ruleRegistryMocks.createRuleDataClient( '.alerts-observability.apm.alerts' ) as IRuleDataClient, + alertsLocator: { + getLocation: jest.fn().mockImplementation(() => ({ + path: 'mockedAlertsLocator > getLocation', + })), + } as any as LocatorPublic, }, services, scheduleActions, diff --git a/x-pack/plugins/apm/server/types.ts b/x-pack/plugins/apm/server/types.ts index 6353e0070160c..9424d5ec9049a 100644 --- a/x-pack/plugins/apm/server/types.ts +++ b/x-pack/plugins/apm/server/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SharePluginSetup } from '@kbn/share-plugin/server'; import { Observable } from 'rxjs'; import { KibanaRequest } from '@kbn/core/server'; import { @@ -76,6 +77,7 @@ export interface APMPluginSetupDependencies { ruleRegistry: RuleRegistryPluginSetupContract; infra: InfraPluginSetup; dataViews: {}; + share: SharePluginSetup; // optional dependencies actions?: ActionsPlugin['setup']; @@ -99,6 +101,7 @@ export interface APMPluginStartDependencies { ruleRegistry: RuleRegistryPluginStartContract; infra: InfraPluginStart; dataViews: DataViewsServerPluginStart; + share: undefined; // optional dependencies actions?: ActionsPlugin['start']; diff --git a/x-pack/plugins/infra/server/lib/alerting/common/utils.ts b/x-pack/plugins/infra/server/lib/alerting/common/utils.ts index 293de180bdac6..8377756654a9e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/utils.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/utils.ts @@ -5,9 +5,6 @@ * 2.0. */ -import moment from 'moment'; -import { AlertsLocatorParams } from '@kbn/observability-plugin/common'; -import { LocatorPublic } from '@kbn/share-plugin/common'; import { isEmpty, isError } from 'lodash'; import { schema } from '@kbn/config-schema'; import { Logger, LogMeta } from '@kbn/logging'; @@ -146,33 +143,6 @@ export const getViewInInventoryAppUrl = ({ export const getViewInMetricsAppUrl = (basePath: IBasePath, spaceId: string) => addSpaceIdToPath(basePath.publicBaseUrl, spaceId, LINK_TO_METRICS_EXPLORER); -export const getAlertUrl = async ( - alertUuid: string | null, - spaceId: string, - startedAt: string, - alertsLocator?: LocatorPublic, - publicBaseUrl?: string -) => { - if (!publicBaseUrl || !alertsLocator || !alertUuid) return ''; - - const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString(); - - return ( - await alertsLocator.getLocation({ - baseUrl: publicBaseUrl, - spaceId, - kuery: `kibana.alert.uuid: "${alertUuid}"`, - rangeFrom, - }) - ).path; -}; - -export const getAlertDetailsUrl = ( - basePath: IBasePath, - spaceId: string, - alertUuid: string | null -) => addSpaceIdToPath(basePath.publicBaseUrl, spaceId, `/app/observability/alerts/${alertUuid}`); - export const KUBERNETES_POD_UID = 'kubernetes.pod.uid'; export const NUMBER_OF_DOCUMENTS = 10; export const termsAggField: Record = { [KUBERNETES_POD_UID]: CONTAINER_ID }; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 51b88c4b91b3b..5a1dba9faae14 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -15,6 +15,7 @@ import { AlertInstanceState as AlertState, } from '@kbn/alerting-plugin/common'; import { Alert, RuleTypeState } from '@kbn/alerting-plugin/server'; +import { getAlertUrl } from '@kbn/observability-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates, InventoryMetricThresholdParams } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; @@ -35,7 +36,6 @@ import { AdditionalContext, createScopedLogger, flattenAdditionalContext, - getAlertUrl, getContextForRecoveredAlerts, getViewInInventoryAppUrl, UNGROUPED_FACTORY_KEY, diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 3e8a187a86303..d7c6871fafa03 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -7,7 +7,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { i18n } from '@kbn/i18n'; -import { AlertsLocatorParams } from '@kbn/observability-plugin/common'; +import { getAlertUrl, AlertsLocatorParams } from '@kbn/observability-plugin/common'; import { ALERT_CONTEXT, ALERT_EVALUATION_THRESHOLD, @@ -60,7 +60,6 @@ import { InfraBackendLibs } from '../../infra_types'; import { AdditionalContext, flattenAdditionalContext, - getAlertUrl, getContextForRecoveredAlerts, getGroupByObject, unflattenObject, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index a0a19f66bfdc6..cfe7a9cf94924 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -15,7 +15,8 @@ import { RecoveredActionGroup, } from '@kbn/alerting-plugin/common'; import { Alert, RuleTypeState } from '@kbn/alerting-plugin/server'; -import { TimeUnitChar } from '@kbn/observability-plugin/common/utils/formatters/duration'; +import type { TimeUnitChar } from '@kbn/observability-plugin/common'; +import { getAlertUrl } from '@kbn/observability-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates, Comparator } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; @@ -37,7 +38,6 @@ import { validGroupByForContext, flattenAdditionalContext, getGroupByObject, - getAlertUrl, } from '../common/utils'; import { EvaluatedRuleParams, evaluateRule } from './lib/evaluate_rule'; diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 816237ad8ed9a..4823e34dd5d06 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -11,8 +11,10 @@ export { formatDurationFromTimeUnitChar, asPercent, getDurationFormatter, + asDuration, } from './utils/formatters'; export { getInspectResponse } from './utils/get_inspect_response'; +export { getAlertDetailsUrl, getAlertUrl } from './utils/alerting/alert_url'; export { ProcessorEvent } from './processor_event'; diff --git a/x-pack/plugins/observability/common/utils/alerting/alert_url.ts b/x-pack/plugins/observability/common/utils/alerting/alert_url.ts new file mode 100644 index 0000000000000..cf3f81c2e9e08 --- /dev/null +++ b/x-pack/plugins/observability/common/utils/alerting/alert_url.ts @@ -0,0 +1,39 @@ +/* + * 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 { IBasePath } from '@kbn/core-http-server'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; +import moment from 'moment'; +import { AlertsLocatorParams } from '../..'; + +export const getAlertUrl = async ( + alertUuid: string | null, + spaceId: string, + startedAt: string, + alertsLocator?: LocatorPublic, + publicBaseUrl?: string +) => { + if (!publicBaseUrl || !alertsLocator || !alertUuid) return ''; + + const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString(); + + return ( + await alertsLocator.getLocation({ + baseUrl: publicBaseUrl, + spaceId, + kuery: `kibana.alert.uuid: "${alertUuid}"`, + rangeFrom, + }) + ).path; +}; + +export const getAlertDetailsUrl = ( + basePath: IBasePath, + spaceId: string, + alertUuid: string | null +) => addSpaceIdToPath(basePath.publicBaseUrl, spaceId, `/app/observability/alerts/${alertUuid}`); diff --git a/x-pack/plugins/observability/kibana.jsonc b/x-pack/plugins/observability/kibana.jsonc index 5284947ac2959..c60511b3ec6a5 100644 --- a/x-pack/plugins/observability/kibana.jsonc +++ b/x-pack/plugins/observability/kibana.jsonc @@ -25,10 +25,11 @@ "triggersActionsUi", "security", "share", + "spaces", "unifiedSearch", "visualizations" ], - "optionalPlugins": ["discover", "home", "licensing", "spaces", "usageCollection"], + "optionalPlugins": ["discover", "home", "licensing", "usageCollection"], "requiredBundles": ["data", "kibanaReact", "kibanaUtils", "unifiedSearch"], "extraPublicDirs": ["common"] } diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index e2e1d7694b705..502f14d985438 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -69,7 +69,8 @@ "@kbn/core-elasticsearch-server", "@kbn/observability-shared-plugin", "@kbn/exploratory-view-plugin", - "@kbn/rison" + "@kbn/rison", + "@kbn/core-http-server" ], "exclude": ["target/**/*"] } diff --git a/x-pack/test/apm_api_integration/configs/index.ts b/x-pack/test/apm_api_integration/configs/index.ts index 72dbace5e686b..2e45cca1c50ae 100644 --- a/x-pack/test/apm_api_integration/configs/index.ts +++ b/x-pack/test/apm_api_integration/configs/index.ts @@ -20,6 +20,7 @@ const apmFtrConfigs = { kibanaConfig: { 'xpack.apm.forceSyntheticSource': 'true', 'logging.loggers': [apmDebugLogger], + 'server.publicBaseUrl': 'http://mockedPublicBaseUrl', }, }, trial: { diff --git a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts index 64ced57167ada..bae143342bffd 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts @@ -5,6 +5,7 @@ * 2.0. */ +import moment from 'moment'; import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { errorCountMessage } from '@kbn/apm-plugin/common/rules/default_action_message'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; @@ -35,6 +36,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('error count threshold alert', { config: 'basic', archives: [] }, () => { let ruleId: string; + let alertId: string; + let startedAt: string; let actionId: string | undefined; const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-default'; @@ -117,7 +120,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { { message: `${errorCountMessage} - Transaction name: {{context.transactionName}} -- Error grouping key: {{context.errorGroupingKey}}`, +- Error grouping key: {{context.errorGroupingKey}} +- Alert URL: {{context.alertDetailsUrl}}`, }, ], }, @@ -142,7 +146,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(executionStatus.status).to.be('active'); }); + it('indexes alert document with all group-by fields', async () => { + const resp = await waitForAlertInIndex({ + es, + indexName: APM_ALERTS_INDEX, + ruleId, + }); + alertId = (resp.hits.hits[0]._source as any)['kibana.alert.uuid']; + startedAt = (resp.hits.hits[0]._source as any)['kibana.alert.start']; + + expect(resp.hits.hits[0]._source).property('service.name', 'opbeans-java'); + expect(resp.hits.hits[0]._source).property('service.environment', 'production'); + expect(resp.hits.hits[0]._source).property('transaction.name', 'tx-java'); + expect(resp.hits.hits[0]._source).property('error.grouping_key', errorGroupingKey); + }); + it('returns correct message', async () => { + const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString(); const resp = await waitForDocumentInIndex<{ message: string }>({ es, indexName: ALERT_ACTION_INDEX_NAME, @@ -156,23 +176,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { - Threshold: 1 - Triggered value: 15 errors over the last 1 hr - Transaction name: tx-java -- Error grouping key: ${errorGroupingKey}` +- Error grouping key: ${errorGroupingKey} +- Alert URL: http://mockedpublicbaseurl/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); }); - it('indexes alert document with all group-by fields', async () => { - const resp = await waitForAlertInIndex({ - es, - indexName: APM_ALERTS_INDEX, - ruleId, - }); - - expect(resp.hits.hits[0]._source).property('service.name', 'opbeans-java'); - expect(resp.hits.hits[0]._source).property('service.environment', 'production'); - expect(resp.hits.hits[0]._source).property('transaction.name', 'tx-java'); - expect(resp.hits.hits[0]._source).property('error.grouping_key', errorGroupingKey); - }); - it('shows the correct alert count for each service on service inventory', async () => { const serviceInventoryAlertCounts = await fetchServiceInventoryAlertCounts(apmApiClient); expect(serviceInventoryAlertCounts).to.eql({ diff --git a/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts index d4d6a0cf3585d..61caf5cd8c0f6 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts @@ -8,6 +8,7 @@ import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; +import moment from 'moment'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createApmRule, @@ -33,6 +34,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('transaction error rate alert', { config: 'basic', archives: [] }, () => { let ruleId: string; + let alertId: string; + let startedAt: string; let actionId: string | undefined; const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-default'; @@ -114,7 +117,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { group: 'threshold_met', id: actionId, params: { - documents: [{ message: 'Transaction Name: {{context.transactionName}}' }], + documents: [ + { + message: `Transaction Name: {{context.transactionName}} +- Alert URL: {{context.alertDetailsUrl}}`, + }, + ], }, frequency: { notify_when: 'onActionGroupChange', @@ -137,21 +145,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(executionStatus.status).to.be('active'); }); - it('returns correct message', async () => { - const resp = await waitForDocumentInIndex<{ message: string }>({ - es, - indexName: ALERT_ACTION_INDEX_NAME, - }); - - expect(resp.hits.hits[0]._source?.message).eql(`Transaction Name: tx-java`); - }); - it('indexes alert document with all group-by fields', async () => { const resp = await waitForAlertInIndex({ es, indexName: APM_ALERTS_INDEX, ruleId, }); + alertId = (resp.hits.hits[0]._source as any)['kibana.alert.uuid']; + startedAt = (resp.hits.hits[0]._source as any)['kibana.alert.start']; expect(resp.hits.hits[0]._source).property('service.name', 'opbeans-java'); expect(resp.hits.hits[0]._source).property('service.environment', 'production'); @@ -159,6 +160,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(resp.hits.hits[0]._source).property('transaction.name', 'tx-java'); }); + it('returns correct message', async () => { + const rangeFrom = moment(startedAt).subtract('5', 'minute').toISOString(); + const resp = await waitForDocumentInIndex<{ message: string }>({ + es, + indexName: ALERT_ACTION_INDEX_NAME, + }); + + expect(resp.hits.hits[0]._source?.message).eql(`Transaction Name: tx-java +- Alert URL: http://mockedpublicbaseurl/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)`); + }); + it('shows the correct alert count for each service on service inventory', async () => { const serviceInventoryAlertCounts = await fetchServiceInventoryAlertCounts(apmApiClient); expect(serviceInventoryAlertCounts).to.eql({