diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index a6509c6779197..698abdfb14a9a 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -190,12 +190,13 @@ export class APMPlugin if (plugins.alerting) { registerApmRuleTypes({ - ruleDataClient, alerting: plugins.alerting, - ml: plugins.ml, + basePath: core.http.basePath, config$, logger: this.logger!.get('rule'), - basePath: core.http.basePath, + ml: plugins.ml, + observability: plugins.observability, + ruleDataClient, }); } diff --git a/x-pack/plugins/apm/server/routes/alerts/action_variables.ts b/x-pack/plugins/apm/server/routes/alerts/action_variables.ts index ce78dbc7bee6d..bf0453811b205 100644 --- a/x-pack/plugins/apm/server/routes/alerts/action_variables.ts +++ b/x-pack/plugins/apm/server/routes/alerts/action_variables.ts @@ -8,19 +8,15 @@ import { i18n } from '@kbn/i18n'; export const apmActionVariables = { - serviceName: { - description: i18n.translate( - 'xpack.apm.alerts.action_variables.serviceName', - { defaultMessage: 'The service the alert is created for' } - ), - name: 'serviceName' as const, - }, - transactionType: { + alertDetailsUrl: { description: i18n.translate( - 'xpack.apm.alerts.action_variables.transactionType', - { defaultMessage: 'The transaction type the alert is created for' } + 'xpack.apm.alerts.action_variables.alertDetailsUrl', + { + defaultMessage: + 'Link to the view within Elastic that shows further details and context surrounding this alert', + } ), - name: 'transactionType' as const, + name: 'alertDetailsUrl' as const, }, environment: { description: i18n.translate( @@ -29,23 +25,6 @@ export const apmActionVariables = { ), name: 'environment' as const, }, - threshold: { - description: i18n.translate('xpack.apm.alerts.action_variables.threshold', { - defaultMessage: - 'Any trigger value above this value will cause the alert to fire', - }), - name: 'threshold' as const, - }, - triggerValue: { - description: i18n.translate( - 'xpack.apm.alerts.action_variables.triggerValue', - { - defaultMessage: - 'The value that breached the threshold and triggered the alert', - } - ), - name: 'triggerValue' as const, - }, interval: { description: i18n.translate( 'xpack.apm.alerts.action_variables.intervalSize', @@ -65,6 +44,37 @@ export const apmActionVariables = { ), name: 'reason' as const, }, + serviceName: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.serviceName', + { defaultMessage: 'The service the alert is created for' } + ), + name: 'serviceName' as const, + }, + threshold: { + description: i18n.translate('xpack.apm.alerts.action_variables.threshold', { + defaultMessage: + 'Any trigger value above this value will cause the alert to fire', + }), + name: 'threshold' as const, + }, + transactionType: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.transactionType', + { defaultMessage: 'The transaction type the alert is created for' } + ), + name: 'transactionType' as const, + }, + triggerValue: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.triggerValue', + { + defaultMessage: + 'The value that breached the threshold and triggered the alert', + } + ), + name: 'triggerValue' as const, + }, viewInAppUrl: { description: i18n.translate( 'xpack.apm.alerts.action_variables.viewInAppUrl', 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 e327970198f80..b2abf6b7ed126 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 @@ -8,6 +8,7 @@ import { Observable } from 'rxjs'; import { IBasePath, Logger } from '@kbn/core/server'; import { PluginSetupContract as AlertingPluginSetupContract } from '@kbn/alerting-plugin/server'; +import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; import { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import { MlPluginSetup } from '@kbn/ml-plugin/server'; import { registerTransactionDurationRuleType } from './rule_types/transaction_duration/register_transaction_duration_rule_type'; @@ -17,12 +18,13 @@ import { APMConfig } from '../..'; import { registerTransactionErrorRateRuleType } from './rule_types/transaction_error_rate/register_transaction_error_rate_rule_type'; export interface RegisterRuleDependencies { - ruleDataClient: IRuleDataClient; - ml?: MlPluginSetup; alerting: AlertingPluginSetupContract; + basePath: IBasePath; config$: Observable; logger: Logger; - basePath: IBasePath; + ml?: MlPluginSetup; + observability: ObservabilityPluginSetup; + ruleDataClient: IRuleDataClient; } 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 875dce26c40fc..ecc93362f400d 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 @@ -194,6 +194,9 @@ describe('Transaction duration anomaly alert', () => { ); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + alertDetailsUrl: expect.stringContaining( + 'http://localhost:5601/eyr/app/observability/alerts/' + ), serviceName: 'foo', transactionType: 'type-foo', environment: 'development', 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 d85b8df2798fe..c2e4191fc49f5 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 @@ -4,27 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import datemath from '@kbn/datemath'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { schema } from '@kbn/config-schema'; +import { KibanaRequest } from '@kbn/core/server'; +import datemath from '@kbn/datemath'; +import type { ESSearchResponse } from '@kbn/es-types'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_REASON, ALERT_SEVERITY, } from '@kbn/rule-data-utils'; -import { compact } from 'lodash'; -import type { ESSearchResponse } from '@kbn/es-types'; -import { KibanaRequest } from '@kbn/core/server'; -import { termQuery } from '@kbn/observability-plugin/server'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { - ApmRuleType, - RULE_TYPES_CONFIG, - ANOMALY_ALERT_SEVERITY_TYPES, - formatAnomalyReason, -} from '../../../../../common/rules/apm_rule_types'; +import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; +import { compact } from 'lodash'; import { getSeverity } from '../../../../../common/anomaly_detection'; import { ApmMlDetectorType, @@ -41,6 +37,12 @@ import { getEnvironmentLabel, } from '../../../../../common/environment_filter_values'; import { ANOMALY_SEVERITY } from '../../../../../common/ml_constants'; +import { + ANOMALY_ALERT_SEVERITY_TYPES, + ApmRuleType, + formatAnomalyReason, + RULE_TYPES_CONFIG, +} from '../../../../../common/rules/apm_rule_types'; import { asMutableArray } from '../../../../../common/utils/as_mutable_array'; import { getAlertUrlTransaction } from '../../../../../common/utils/formatters'; import { getMLJobs } from '../../../service_map/get_service_anomalies'; @@ -65,12 +67,13 @@ const paramsSchema = schema.object({ const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.Anomaly]; export function registerAnomalyRuleType({ - logger, - ruleDataClient, - config$, alerting, - ml, basePath, + config$, + logger, + ml, + observability, + ruleDataClient, }: RegisterRuleDependencies) { const createLifecycleRuleType = createLifecycleRuleTypeFactory({ logger, @@ -88,32 +91,38 @@ export function registerAnomalyRuleType({ }, actionVariables: { context: [ - apmActionVariables.serviceName, - apmActionVariables.transactionType, + ...(observability.getAlertDetailsConfig()?.apm.enabled + ? [apmActionVariables.alertDetailsUrl] + : []), apmActionVariables.environment, + apmActionVariables.reason, + apmActionVariables.serviceName, apmActionVariables.threshold, + apmActionVariables.transactionType, apmActionVariables.triggerValue, - apmActionVariables.reason, apmActionVariables.viewInAppUrl, ], }, producer: 'apm', minimumLicenseRequired: 'basic', isExportable: true, - executor: async ({ services, params }) => { + executor: async ({ params, services, spaceId }) => { if (!ml) { return {}; } + const { savedObjectsClient, scopedClusterClient, getAlertUuid } = + services; + const ruleParams = params; const request = {} as KibanaRequest; const { mlAnomalySearch } = ml.mlSystemProvider( request, - services.savedObjectsClient + savedObjectsClient ); const anomalyDetectors = ml.anomalyDetectorsProvider( request, - services.savedObjectsClient + savedObjectsClient ); const mlJobs = await getMLJobs( @@ -254,8 +263,8 @@ export function registerAnomalyRuleType({ const eventSourceFields = await getServiceGroupFieldsForAnomaly({ config$, - scopedClusterClient: services.scopedClusterClient, - savedObjectsClient: services.savedObjectsClient, + scopedClusterClient, + savedObjectsClient, serviceName, environment, transactionType, @@ -272,28 +281,38 @@ export function registerAnomalyRuleType({ windowUnit: params.windowUnit, }); + const id = [ + ApmRuleType.Anomaly, + serviceName, + environment, + transactionType, + ] + .filter((name) => name) + .join('_'); + const relativeViewInAppUrl = getAlertUrlTransaction( serviceName, getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT], transactionType ); - const viewInAppUrl = basePath.publicBaseUrl - ? new URL( - basePath.prepend(relativeViewInAppUrl), - basePath.publicBaseUrl - ).toString() - : relativeViewInAppUrl; + const viewInAppUrl = addSpaceIdToPath( + basePath.publicBaseUrl, + spaceId, + relativeViewInAppUrl + ); + + const alertUuid = getAlertUuid(id); + + const alertDetailsUrl = getAlertDetailsUrl( + basePath, + spaceId, + alertUuid + ); + services .alertWithLifecycle({ - id: [ - ApmRuleType.Anomaly, - serviceName, - environment, - transactionType, - ] - .filter((name) => name) - .join('_'), + id, fields: { [SERVICE_NAME]: serviceName, ...getEnvironmentEsField(environment), @@ -307,12 +326,13 @@ export function registerAnomalyRuleType({ }, }) .scheduleActions(ruleTypeConfig.defaultActionGroupId, { - serviceName, - transactionType, + alertDetailsUrl, environment: getEnvironmentLabel(environment), + reason: reasonMessage, + serviceName, threshold: selectedOption?.label, + transactionType, triggerValue: severityLevel, - reason: reasonMessage, viewInAppUrl, }); } 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 342832b7e4099..705804cfa74f4 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 @@ -138,6 +138,9 @@ describe('Error count alert', () => { expect(scheduleActions).toHaveBeenCalledTimes(3); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + alertDetailsUrl: expect.stringContaining( + 'http://localhost:5601/eyr/app/observability/alerts/' + ), serviceName: 'foo', environment: 'env-foo', threshold: 2, @@ -148,6 +151,9 @@ describe('Error count alert', () => { 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + alertDetailsUrl: expect.stringContaining( + 'http://localhost:5601/eyr/app/observability/alerts/' + ), serviceName: 'foo', environment: 'env-foo-2', threshold: 2, @@ -158,6 +164,9 @@ describe('Error count alert', () => { 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + alertDetailsUrl: expect.stringContaining( + 'http://localhost:5601/eyr/app/observability/alerts/' + ), serviceName: 'bar', environment: 'env-bar', reason: 'Error count is 3 in the last 5 mins for bar. Alert when > 2.', 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 d9826aae392c8..648aa857870cc 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 @@ -15,6 +15,9 @@ import { import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; import { termQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; + import { ENVIRONMENT_NOT_DEFINED, getEnvironmentEsField, @@ -54,10 +57,11 @@ const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.ErrorCount]; export function registerErrorCountRuleType({ alerting, + basePath, + config$, logger, + observability, ruleDataClient, - config$, - basePath, }: RegisterRuleDependencies) { const createLifecycleRuleType = createLifecycleRuleTypeFactory({ ruleDataClient, @@ -75,24 +79,30 @@ export function registerErrorCountRuleType({ }, actionVariables: { context: [ - apmActionVariables.serviceName, + ...(observability.getAlertDetailsConfig()?.apm.enabled + ? [apmActionVariables.alertDetailsUrl] + : []), apmActionVariables.environment, - apmActionVariables.threshold, - apmActionVariables.triggerValue, apmActionVariables.interval, apmActionVariables.reason, + apmActionVariables.serviceName, + apmActionVariables.threshold, + apmActionVariables.triggerValue, apmActionVariables.viewInAppUrl, ], }, producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, - executor: async ({ services, params: ruleParams }) => { + executor: async ({ params: ruleParams, services, spaceId }) => { const config = await firstValueFrom(config$); + const { getAlertUuid, savedObjectsClient, scopedClusterClient } = + services; + const indices = await getApmIndices({ config, - savedObjectsClient: services.savedObjectsClient, + savedObjectsClient, }); const searchParams = { @@ -138,7 +148,7 @@ export function registerErrorCountRuleType({ }; const response = await alertingEsClient({ - scopedClusterClient: services.scopedClusterClient, + scopedClusterClient, params: searchParams, }); @@ -166,23 +176,31 @@ export function registerErrorCountRuleType({ windowUnit: ruleParams.windowUnit, }); + const id = [ApmRuleType.ErrorCount, serviceName, environment] + .filter((name) => name) + .join('_'); + const relativeViewInAppUrl = getAlertUrlErrorCount( serviceName, getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT] ); - const viewInAppUrl = basePath.publicBaseUrl - ? new URL( - basePath.prepend(relativeViewInAppUrl), - basePath.publicBaseUrl - ).toString() - : relativeViewInAppUrl; + const viewInAppUrl = addSpaceIdToPath( + basePath.publicBaseUrl, + spaceId, + relativeViewInAppUrl + ); + + const alertUuid = getAlertUuid(id); + const alertDetailsUrl = getAlertDetailsUrl( + basePath, + spaceId, + alertUuid + ); services .alertWithLifecycle({ - id: [ApmRuleType.ErrorCount, serviceName, environment] - .filter((name) => name) - .join('_'), + id, fields: { [SERVICE_NAME]: serviceName, ...getEnvironmentEsField(environment), @@ -194,12 +212,13 @@ export function registerErrorCountRuleType({ }, }) .scheduleActions(ruleTypeConfig.defaultActionGroupId, { - serviceName, + alertDetailsUrl, environment: getEnvironmentLabel(environment), - threshold: ruleParams.threshold, - triggerValue: errorCount, interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, reason: alertReason, + serviceName, + threshold: ruleParams.threshold, + triggerValue: errorCount, viewInAppUrl, }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts index 2b159e7acc0d2..1066eb8a987d8 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts @@ -9,7 +9,7 @@ import { registerTransactionDurationRuleType } from './register_transaction_dura import { createRuleTypeMocks } from '../../test_utils'; describe('registerTransactionDurationRuleType', () => { - it('sends alert when value is greater than threashold', async () => { + it('sends alert when value is greater than threshold', async () => { const { services, dependencies, executor, scheduleActions } = createRuleTypeMocks(); @@ -56,14 +56,17 @@ describe('registerTransactionDurationRuleType', () => { await executor({ params }); expect(scheduleActions).toHaveBeenCalledTimes(1); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { - transactionType: 'request', - serviceName: 'opbeans-java', + alertDetailsUrl: expect.stringContaining( + 'http://localhost:5601/eyr/app/observability/alerts/' + ), environment: 'Not defined', - threshold: 3000000, - triggerValue: '5,500 ms', interval: `5m`, reason: 'Avg. latency is 5,500 ms in the last 5 mins for opbeans-java. Alert when > 3,000 ms.', + transactionType: 'request', + serviceName: 'opbeans-java', + threshold: 3000000, + triggerValue: '5,500 ms', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/opbeans-java?transactionType=request&environment=ENVIRONMENT_ALL', }); 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 b4c7a6212b62d..304a752079e12 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 @@ -17,6 +17,8 @@ import { asDuration } from '@kbn/observability-plugin/common/utils/formatters'; import { termQuery } from '@kbn/observability-plugin/server'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; import { getAlertUrlTransaction } from '../../../../../common/utils/formatters'; import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; import { @@ -77,6 +79,7 @@ export function registerTransactionDurationRuleType({ ruleDataClient, config$, logger, + observability, basePath, }: RegisterRuleDependencies) { const createLifecycleRuleType = createLifecycleRuleTypeFactory({ @@ -94,25 +97,31 @@ export function registerTransactionDurationRuleType({ }, actionVariables: { context: [ + ...(observability.getAlertDetailsConfig()?.apm.enabled + ? [apmActionVariables.alertDetailsUrl] + : []), + apmActionVariables.environment, + apmActionVariables.interval, + apmActionVariables.reason, apmActionVariables.serviceName, apmActionVariables.transactionType, - apmActionVariables.environment, apmActionVariables.threshold, apmActionVariables.triggerValue, - apmActionVariables.interval, - apmActionVariables.reason, apmActionVariables.viewInAppUrl, ], }, producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, - executor: async ({ services, params }) => { + executor: async ({ params: ruleParams, services, spaceId }) => { const config = await firstValueFrom(config$); - const ruleParams = params; + + const { getAlertUuid, savedObjectsClient, scopedClusterClient } = + services; + const indices = await getApmIndices({ config, - savedObjectsClient: services.savedObjectsClient, + savedObjectsClient, }); // only query transaction events when set to 'never', @@ -185,7 +194,7 @@ export function registerTransactionDurationRuleType({ }; const response = await alertingEsClient({ - scopedClusterClient: services.scopedClusterClient, + scopedClusterClient, params: searchParams, }); @@ -197,22 +206,25 @@ export function registerTransactionDurationRuleType({ const thresholdMicroseconds = ruleParams.threshold * 1000; const triggeredBuckets = []; + for (const bucket of response.aggregations.series.buckets) { const [serviceName, environment, transactionType] = bucket.key; + const transactionDuration = 'avgLatency' in bucket // only true if ruleParams.aggregationType === 'avg' ? bucket.avgLatency.value : bucket.pctLatency.values[0].value; + if ( transactionDuration !== null && transactionDuration > thresholdMicroseconds ) { triggeredBuckets.push({ - serviceName, environment, + serviceName, + sourceFields: getServiceGroupFields(bucket), transactionType, transactionDuration, - sourceFields: getServiceGroupFields(bucket), }); } } @@ -224,36 +236,45 @@ export function registerTransactionDurationRuleType({ transactionDuration, sourceFields, } of triggeredBuckets) { + const environmentLabel = getEnvironmentLabel(environment); + const durationFormatter = getDurationFormatter(transactionDuration); const transactionDurationFormatted = durationFormatter(transactionDuration).formatted; - const reasonMessage = formatTransactionDurationReason({ + + const reason = formatTransactionDurationReason({ + aggregationType: String(ruleParams.aggregationType), + asDuration, measured: transactionDuration, serviceName, threshold: thresholdMicroseconds, - asDuration, - aggregationType: String(ruleParams.aggregationType), windowSize: ruleParams.windowSize, windowUnit: ruleParams.windowUnit, }); - const relativeViewInAppUrl = getAlertUrlTransaction( - serviceName, - getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT], - transactionType + const id = `${ApmRuleType.TransactionDuration}_${environmentLabel}`; + + const alertUuid = getAlertUuid(id); + + const alertDetailsUrl = getAlertDetailsUrl( + basePath, + spaceId, + alertUuid + ); + + const viewInAppUrl = addSpaceIdToPath( + basePath.publicBaseUrl, + spaceId, + getAlertUrlTransaction( + serviceName, + getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT], + transactionType + ) ); - const viewInAppUrl = basePath.publicBaseUrl - ? new URL( - basePath.prepend(relativeViewInAppUrl), - basePath.publicBaseUrl - ).toString() - : relativeViewInAppUrl; services .alertWithLifecycle({ - id: `${ApmRuleType.TransactionDuration}_${getEnvironmentLabel( - environment - )}`, + id, fields: { [SERVICE_NAME]: serviceName, ...getEnvironmentEsField(environment), @@ -261,18 +282,19 @@ export function registerTransactionDurationRuleType({ [PROCESSOR_EVENT]: ProcessorEvent.transaction, [ALERT_EVALUATION_VALUE]: transactionDuration, [ALERT_EVALUATION_THRESHOLD]: thresholdMicroseconds, - [ALERT_REASON]: reasonMessage, + [ALERT_REASON]: reason, ...sourceFields, }, }) .scheduleActions(ruleTypeConfig.defaultActionGroupId, { - transactionType, + alertDetailsUrl, + environment: environmentLabel, + interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, + reason, serviceName, - environment: getEnvironmentLabel(environment), threshold: thresholdMicroseconds, + transactionType, triggerValue: transactionDurationFormatted, - interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, - reason: reasonMessage, viewInAppUrl, }); } 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 f28493338ad0d..38de7d48cce4c 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 @@ -121,6 +121,9 @@ describe('Transaction error rate alert', () => { ); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + alertDetailsUrl: expect.stringContaining( + 'http://localhost:5601/eyr/app/observability/alerts/' + ), serviceName: 'foo', transactionType: 'type-foo', environment: 'env-foo', 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 73f7ccda26401..97f07e32566fd 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 @@ -15,7 +15,9 @@ import { import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; import { asPercent } from '@kbn/observability-plugin/common/utils/formatters'; import { termQuery } from '@kbn/observability-plugin/server'; +import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; import { ENVIRONMENT_NOT_DEFINED, getEnvironmentEsField, @@ -62,10 +64,11 @@ const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionErrorRate]; export function registerTransactionErrorRateRuleType({ alerting, - ruleDataClient, - logger, - config$, basePath, + config$, + logger, + observability, + ruleDataClient, }: RegisterRuleDependencies) { const createLifecycleRuleType = createLifecycleRuleTypeFactory({ ruleDataClient, @@ -83,24 +86,31 @@ export function registerTransactionErrorRateRuleType({ }, actionVariables: { context: [ - apmActionVariables.transactionType, - apmActionVariables.serviceName, + ...(observability.getAlertDetailsConfig()?.apm.enabled + ? [apmActionVariables.alertDetailsUrl] + : []), apmActionVariables.environment, - apmActionVariables.threshold, - apmActionVariables.triggerValue, apmActionVariables.interval, apmActionVariables.reason, + apmActionVariables.serviceName, + apmActionVariables.threshold, + apmActionVariables.transactionType, + apmActionVariables.triggerValue, apmActionVariables.viewInAppUrl, ], }, producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, - executor: async ({ services, params: ruleParams }) => { + executor: async ({ services, spaceId, params: ruleParams }) => { const config = await firstValueFrom(config$); + + const { getAlertUuid, savedObjectsClient, scopedClusterClient } = + services; + const indices = await getApmIndices({ config, - savedObjectsClient: services.savedObjectsClient, + savedObjectsClient, }); // only query transaction events when set to 'never', @@ -178,7 +188,7 @@ export function registerTransactionErrorRateRuleType({ }; const response = await alertingEsClient({ - scopedClusterClient: services.scopedClusterClient, + scopedClusterClient, params: searchParams, }); @@ -219,6 +229,7 @@ export function registerTransactionErrorRateRuleType({ errorRate, sourceFields, } = result; + const reasonMessage = formatTransactionErrorRateReason({ threshold: ruleParams.threshold, measured: errorRate, @@ -228,27 +239,38 @@ export function registerTransactionErrorRateRuleType({ windowUnit: ruleParams.windowUnit, }); + const id = [ + ApmRuleType.TransactionErrorRate, + serviceName, + transactionType, + environment, + ] + .filter((name) => name) + .join('_'); + + const alertUuid = getAlertUuid(id); + + const alertDetailsUrl = getAlertDetailsUrl( + basePath, + spaceId, + alertUuid + ); + const relativeViewInAppUrl = getAlertUrlTransaction( serviceName, getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT], transactionType ); - const viewInAppUrl = basePath.publicBaseUrl - ? new URL( - basePath.prepend(relativeViewInAppUrl), - basePath.publicBaseUrl - ).toString() - : relativeViewInAppUrl; + + const viewInAppUrl = addSpaceIdToPath( + basePath.publicBaseUrl, + spaceId, + relativeViewInAppUrl + ); + services .alertWithLifecycle({ - id: [ - ApmRuleType.TransactionErrorRate, - serviceName, - transactionType, - environment, - ] - .filter((name) => name) - .join('_'), + id, fields: { [SERVICE_NAME]: serviceName, ...getEnvironmentEsField(environment), @@ -261,13 +283,14 @@ export function registerTransactionErrorRateRuleType({ }, }) .scheduleActions(ruleTypeConfig.defaultActionGroupId, { - serviceName, - transactionType, + alertDetailsUrl, environment: getEnvironmentLabel(environment), - threshold: ruleParams.threshold, - triggerValue: asDecimalOrInteger(errorRate), interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, reason: reasonMessage, + serviceName, + threshold: ruleParams.threshold, + transactionType, + triggerValue: asDecimalOrInteger(errorRate), viewInAppUrl, }); }); 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 5302f632781f0..41d7385a2c3da 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 @@ -11,6 +11,7 @@ import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; 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'; +import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; import { APMConfig, APM_SERVER_FEATURE_ID } from '../../..'; export const createRuleTypeMocks = () => { @@ -51,16 +52,19 @@ export const createRuleTypeMocks = () => { return { dependencies: { alerting, + basePath: { + prepend: (path: string) => `http://localhost:5601/eyr${path}`, + publicBaseUrl: 'http://localhost:5601/eyr', + serverBasePath: '/eyr', + } as IBasePath, config$: mockedConfig$, + observability: { + getAlertDetailsConfig: jest.fn().mockReturnValue({ apm: true }), + } as unknown as ObservabilityPluginSetup, logger: loggerMock, ruleDataClient: ruleRegistryMocks.createRuleDataClient( '.alerts-observability.apm.alerts' ) as IRuleDataClient, - basePath: { - serverBasePath: '/eyr', - publicBaseUrl: 'http://localhost:5601/eyr', - prepend: (path: string) => `http://localhost:5601/eyr${path}`, - } as IBasePath, }, services, scheduleActions, diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index cf4b012633f96..131eb538f28aa 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -71,7 +71,7 @@ export interface LifecycleAlertServices< > { alertWithLifecycle: LifecycleAlertService; getAlertStartedDate: (alertInstanceId: string) => string | null; - getAlertUuid: (alertInstanceId: string) => string | null; + getAlertUuid: (alertInstanceId: string) => string; getAlertByAlertUuid: (alertUuid: string) => { [x: string]: any } | null; } @@ -176,13 +176,14 @@ export const createLifecycleExecutor = }, getAlertStartedDate: (alertId: string) => state.trackedAlerts[alertId]?.started ?? null, getAlertUuid: (alertId: string) => { - if (!state.trackedAlerts[alertId]) { - const alertUuid = v4(); - newAlertUuids[alertId] = alertUuid; - return alertUuid; + let existingUuid = state.trackedAlerts[alertId]?.alertUuid || newAlertUuids[alertId]; + + if (!existingUuid) { + existingUuid = v4(); + newAlertUuids[alertId] = existingUuid; } - return state.trackedAlerts[alertId].alertUuid; + return existingUuid; }, getAlertByAlertUuid: async (alertUuid: string) => { try { @@ -251,7 +252,7 @@ export const createLifecycleExecutor = const { alertUuid, started } = !isNew ? state.trackedAlerts[alertId] : { - alertUuid: newAlertUuids[alertId] || v4(), + alertUuid: lifecycleAlertServices.getAlertUuid(alertId), started: commonRuleFields[TIMESTAMP], }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts index 650b9563ec82e..b50912eda89b0 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts @@ -13,20 +13,24 @@ import { } from '@kbn/alerting-plugin/common'; import { IRuleDataClient } from '../rule_data_client'; import { AlertTypeWithExecutor } from '../types'; -import { LifecycleAlertService, createLifecycleExecutor } from './create_lifecycle_executor'; +import { createLifecycleExecutor, LifecycleAlertServices } from './create_lifecycle_executor'; import { createGetSummarizedAlertsFn } from './create_get_summarized_alerts_fn'; export const createLifecycleRuleTypeFactory = ({ logger, ruleDataClient }: { logger: Logger; ruleDataClient: IRuleDataClient }) => < TParams extends RuleTypeParams, + TAlertInstanceState extends AlertInstanceState, TAlertInstanceContext extends AlertInstanceContext, - TServices extends { - alertWithLifecycle: LifecycleAlertService, TAlertInstanceContext, string>; - } + TActionGroupIds extends string, + TServices extends LifecycleAlertServices< + TAlertInstanceState, + TAlertInstanceContext, + TActionGroupIds + > >( - type: AlertTypeWithExecutor, TParams, TAlertInstanceContext, TServices> - ): AlertTypeWithExecutor, TParams, TAlertInstanceContext, any> => { + type: AlertTypeWithExecutor + ): AlertTypeWithExecutor => { const createBoundLifecycleExecutor = createLifecycleExecutor(logger, ruleDataClient); const createGetSummarizedAlerts = createGetSummarizedAlertsFn({ ruleDataClient, diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts index ab0628f738286..721110db4d6af 100644 --- a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts +++ b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts @@ -36,6 +36,6 @@ export const createLifecycleAlertServicesMock = < ): LifecycleAlertServices => ({ alertWithLifecycle: ({ id }) => alertServices.alertFactory.create(id), getAlertStartedDate: jest.fn((id: string) => null), - getAlertUuid: jest.fn((id: string) => null), + getAlertUuid: jest.fn((id: string) => 'mock-alert-uuid'), getAlertByAlertUuid: jest.fn((id: string) => null), }); diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/types.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/types.ts index c57b0924ac373..c57f435b29a8a 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/types.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/types.ts @@ -5,8 +5,8 @@ * 2.0. */ import { AlertTypeWithExecutor } from '@kbn/rule-registry-plugin/server'; -import { AlertInstanceContext, RuleTypeState } from '@kbn/alerting-plugin/common'; -import { LifecycleAlertService } from '@kbn/rule-registry-plugin/server'; +import { AlertInstanceContext } from '@kbn/alerting-plugin/common'; +import { LifecycleAlertServices } from '@kbn/rule-registry-plugin/server'; import { UMServerLibs } from '../lib'; import { UptimeCorePluginsSetup, UptimeServerSetup } from '../adapters'; @@ -20,11 +20,7 @@ export type DefaultUptimeAlertInstance = AlertTy Record, Record, AlertInstanceContext, - { - alertWithLifecycle: LifecycleAlertService; - getAlertStartedDate: (alertId: string) => string | null; - getAlertUuid: (alertId: string) => string | null; - } + LifecycleAlertServices, AlertInstanceContext, TActionGroupIds> >; export type UptimeAlertTypeFactory = (