diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index ad826a446d823..a1161354e04f4 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; export enum AlertType { ErrorRate = 'apm.error_rate', TransactionDuration = 'apm.transaction_duration', + TransactionDurationAnomaly = 'apm.transaction_duration_anomaly', } export const ALERT_TYPES_CONFIG = { @@ -45,6 +46,24 @@ export const ALERT_TYPES_CONFIG = { defaultActionGroupId: 'threshold_met', producer: 'apm', }, + [AlertType.TransactionDurationAnomaly]: { + name: i18n.translate('xpack.apm.transactionDurationAnomalyAlert.name', { + defaultMessage: 'Transaction duration anomaly', + }), + actionGroups: [ + { + id: 'threshold_met', + name: i18n.translate( + 'xpack.apm.transactionDurationAlert.thresholdMet', + { + defaultMessage: 'Threshold met', + } + ), + }, + ], + defaultActionGroupId: 'threshold_met', + producer: 'apm', + }, }; export const TRANSACTION_ALERT_AGGREGATION_TYPES = { diff --git a/x-pack/plugins/apm/common/environment_filter_values.ts b/x-pack/plugins/apm/common/environment_filter_values.ts index 38b6f480ca3d3..e231f37a170ed 100644 --- a/x-pack/plugins/apm/common/environment_filter_values.ts +++ b/x-pack/plugins/apm/common/environment_filter_values.ts @@ -6,14 +6,30 @@ import { i18n } from '@kbn/i18n'; -export const ENVIRONMENT_ALL = 'ENVIRONMENT_ALL'; -export const ENVIRONMENT_NOT_DEFINED = 'ENVIRONMENT_NOT_DEFINED'; +const ENVIRONMENT_ALL_VALUE = 'ENVIRONMENT_ALL'; +const ENVIRONMENT_NOT_DEFINED_VALUE = 'ENVIRONMENT_NOT_DEFINED'; + +const environmentLabels: Record = { + [ENVIRONMENT_ALL_VALUE]: i18n.translate( + 'xpack.apm.filter.environment.allLabel', + { defaultMessage: 'All' } + ), + [ENVIRONMENT_NOT_DEFINED_VALUE]: i18n.translate( + 'xpack.apm.filter.environment.notDefinedLabel', + { defaultMessage: 'Not defined' } + ), +}; + +export const ENVIRONMENT_ALL = { + value: ENVIRONMENT_ALL_VALUE, + text: environmentLabels[ENVIRONMENT_ALL_VALUE], +}; + +export const ENVIRONMENT_NOT_DEFINED = { + value: ENVIRONMENT_NOT_DEFINED_VALUE, + text: environmentLabels[ENVIRONMENT_NOT_DEFINED_VALUE], +}; export function getEnvironmentLabel(environment: string) { - if (environment === ENVIRONMENT_NOT_DEFINED) { - return i18n.translate('xpack.apm.filter.environment.notDefinedLabel', { - defaultMessage: 'Not defined', - }); - } - return environment; + return environmentLabels[environment] || environment; } diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx index 80d5f739bea5a..27c4a37e09c00 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx @@ -18,27 +18,37 @@ import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; const alertLabel = i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.alerts', - { - defaultMessage: 'Alerts', - } + { defaultMessage: 'Alerts' } +); +const transactionDurationLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.transactionDuration', + { defaultMessage: 'Transaction duration' } +); +const errorRateLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.errorRate', + { defaultMessage: 'Error rate' } ); - const createThresholdAlertLabel = i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert', - { - defaultMessage: 'Create threshold alert', - } + { defaultMessage: 'Create threshold alert' } +); +const createAnomalyAlertAlertLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.createAnomalyAlert', + { defaultMessage: 'Create anomaly alert' } ); -const CREATE_THRESHOLD_ALERT_PANEL_ID = 'create_threshold'; +const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID = + 'create_transaction_duration'; +const CREATE_ERROR_RATE_ALERT_PANEL_ID = 'create_error_rate'; interface Props { canReadAlerts: boolean; canSaveAlerts: boolean; + canReadAnomalies: boolean; } export function AlertIntegrations(props: Props) { - const { canSaveAlerts, canReadAlerts } = props; + const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props; const plugin = useApmPluginContext(); @@ -52,9 +62,7 @@ export function AlertIntegrations(props: Props) { iconSide="right" onClick={() => setPopoverOpen(true)} > - {i18n.translate('xpack.apm.serviceDetails.alertsMenu.alerts', { - defaultMessage: 'Alerts', - })} + {alertLabel} ); @@ -66,10 +74,10 @@ export function AlertIntegrations(props: Props) { ...(canSaveAlerts ? [ { - name: createThresholdAlertLabel, - panel: CREATE_THRESHOLD_ALERT_PANEL_ID, - icon: 'bell', + name: transactionDurationLabel, + panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, }, + { name: errorRateLabel, panel: CREATE_ERROR_RATE_ALERT_PANEL_ID }, ] : []), ...(canReadAlerts @@ -77,9 +85,7 @@ export function AlertIntegrations(props: Props) { { name: i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts', - { - defaultMessage: 'View active alerts', - } + { defaultMessage: 'View active alerts' } ), href: plugin.core.http.basePath.prepend( '/app/management/insightsAndAlerting/triggersActions/alerts' @@ -91,29 +97,38 @@ export function AlertIntegrations(props: Props) { ], }, { - id: CREATE_THRESHOLD_ALERT_PANEL_ID, - title: createThresholdAlertLabel, + id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, + title: transactionDurationLabel, items: [ { - name: i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.transactionDuration', - { - defaultMessage: 'Transaction duration', - } - ), + name: createThresholdAlertLabel, onClick: () => { setAlertType(AlertType.TransactionDuration); + setPopoverOpen(false); }, }, + ...(canReadAnomalies + ? [ + { + name: createAnomalyAlertAlertLabel, + onClick: () => { + setAlertType(AlertType.TransactionDurationAnomaly); + setPopoverOpen(false); + }, + }, + ] + : []), + ], + }, + { + id: CREATE_ERROR_RATE_ALERT_PANEL_ID, + title: errorRateLabel, + items: [ { - name: i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.errorRate', - { - defaultMessage: 'Error rate', - } - ), + name: createThresholdAlertLabel, onClick: () => { setAlertType(AlertType.ErrorRate); + setPopoverOpen(false); }, }, ], diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx index 4488a962d0ba8..b5a4ca4799afd 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx @@ -26,19 +26,18 @@ export function ServiceDetails({ tab }: Props) { const plugin = useApmPluginContext(); const { urlParams } = useUrlParams(); const { serviceName } = urlParams; - - const canReadAlerts = !!plugin.core.application.capabilities.apm[ - 'alerting:show' - ]; - const canSaveAlerts = !!plugin.core.application.capabilities.apm[ - 'alerting:save' - ]; + const capabilities = plugin.core.application.capabilities; + const canReadAlerts = !!capabilities.apm['alerting:show']; + const canSaveAlerts = !!capabilities.apm['alerting:save']; const isAlertingPluginEnabled = 'alerts' in plugin.plugins; - const isAlertingAvailable = isAlertingPluginEnabled && (canReadAlerts || canSaveAlerts); - - const { core } = useApmPluginContext(); + const isMlPluginEnabled = 'ml' in plugin.plugins; + const canReadAnomalies = !!( + isMlPluginEnabled && + capabilities.ml.canAccessML && + capabilities.ml.canGetJobs + ); const ADD_DATA_LABEL = i18n.translate('xpack.apm.addDataButtonLabel', { defaultMessage: 'Add data', @@ -58,12 +57,15 @@ export function ServiceDetails({ tab }: Props) { )} , environment?: string ) { const nextEnvironmentQueryParam = - environment !== ENVIRONMENT_ALL ? environment : undefined; + environment !== ENVIRONMENT_ALL.value ? environment : undefined; history.push({ ...location, search: fromQuery({ @@ -32,13 +32,6 @@ function updateEnvironmentUrl( }); } -const NOT_DEFINED_OPTION = { - value: ENVIRONMENT_NOT_DEFINED, - text: i18n.translate('xpack.apm.filter.environment.notDefinedLabel', { - defaultMessage: 'Not defined', - }), -}; - const SEPARATOR_OPTION = { text: `- ${i18n.translate( 'xpack.apm.filter.environment.selectEnvironmentLabel', @@ -49,16 +42,16 @@ const SEPARATOR_OPTION = { function getOptions(environments: string[]) { const environmentOptions = environments - .filter((env) => env !== ENVIRONMENT_NOT_DEFINED) + .filter((env) => env !== ENVIRONMENT_NOT_DEFINED.value) .map((environment) => ({ value: environment, text: environment, })); return [ - ALL_OPTION, - ...(environments.includes(ENVIRONMENT_NOT_DEFINED) - ? [NOT_DEFINED_OPTION] + ENVIRONMENT_ALL, + ...(environments.includes(ENVIRONMENT_NOT_DEFINED.value) + ? [ENVIRONMENT_NOT_DEFINED] : []), ...(environmentOptions.length > 0 ? [SEPARATOR_OPTION] : []), ...environmentOptions, @@ -83,7 +76,7 @@ export function EnvironmentFilter() { defaultMessage: 'environment', })} options={getOptions(environments)} - value={environment || ENVIRONMENT_ALL} + value={environment || ENVIRONMENT_ALL.value} onChange={(event) => { updateEnvironmentUrl(location, event.target.value); }} diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx index b457fb1092bc6..7344839795955 100644 --- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx @@ -12,8 +12,12 @@ import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; -import { useEnvironments, ALL_OPTION } from '../../../hooks/useEnvironments'; +import { useEnvironments } from '../../../hooks/useEnvironments'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { + ENVIRONMENT_ALL, + getEnvironmentLabel, +} from '../../../../common/environment_filter_values'; export interface ErrorRateAlertTriggerParams { windowSize: number; @@ -39,7 +43,7 @@ export function ErrorRateAlertTrigger(props: Props) { threshold: 25, windowSize: 1, windowUnit: 'm', - environment: ALL_OPTION.value, + environment: urlParams.environment || ENVIRONMENT_ALL.value, }; const params = { @@ -51,11 +55,7 @@ export function ErrorRateAlertTrigger(props: Props) { const fields = [ + {label} + + ); +} + +const getOption = (value: SeverityScore) => { + return { + value: value.toString(10), + inputDisplay: , + dropdownDisplay: ( + <> + + + +

+ +

+
+ + ), + }; +}; + +interface Props { + onChange: (value: SeverityScore) => void; + value: SeverityScore; +} + +export function SelectAnomalySeverity({ onChange, value }: Props) { + const options = ANOMALY_SCORES.map((anomalyScore) => getOption(anomalyScore)); + + return ( + { + const selectedAnomalyScore = parseInt( + selectedValue, + 10 + ) as SeverityScore; + onChange(selectedAnomalyScore); + }} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx new file mode 100644 index 0000000000000..911c51013a844 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiExpression, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; +import { useEnvironments } from '../../../hooks/useEnvironments'; +import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; +import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; +import { + AnomalySeverity, + SelectAnomalySeverity, +} from './SelectAnomalySeverity'; +import { + ENVIRONMENT_ALL, + getEnvironmentLabel, +} from '../../../../common/environment_filter_values'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../../common/transaction_types'; + +interface Params { + windowSize: number; + windowUnit: string; + serviceName: string; + transactionType: string; + environment: string; + anomalyScore: 0 | 25 | 50 | 75; +} + +interface Props { + alertParams: Params; + setAlertParams: (key: string, value: any) => void; + setAlertProperty: (key: string, value: any) => void; +} + +export function TransactionDurationAnomalyAlertTrigger(props: Props) { + const { setAlertParams, alertParams, setAlertProperty } = props; + const { urlParams } = useUrlParams(); + const transactionTypes = useServiceTransactionTypes(urlParams); + const { serviceName, start, end } = urlParams; + const { environmentOptions } = useEnvironments({ serviceName, start, end }); + const supportedTransactionTypes = transactionTypes.filter((transactionType) => + [TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST].includes(transactionType) + ); + + if (!supportedTransactionTypes.length || !serviceName) { + return null; + } + + const defaults: Params = { + windowSize: 15, + windowUnit: 'm', + transactionType: supportedTransactionTypes[0], // 'page-load' for RUM, 'request' otherwise + serviceName, + environment: urlParams.environment || ENVIRONMENT_ALL.value, + anomalyScore: 75, + }; + + const params = { + ...defaults, + ...alertParams, + }; + + const fields = [ + , + + setAlertParams('environment', e.target.value)} + compressed + /> + , + } + title={i18n.translate( + 'xpack.apm.transactionDurationAnomalyAlertTrigger.anomalySeverity', + { + defaultMessage: 'Has anomaly with severity', + } + )} + > + { + setAlertParams('anomalyScore', value); + }} + /> + , + ]; + + return ( + + ); +} + +// Default export is required for React.lazy loading +// +// eslint-disable-next-line import/no-default-export +export default TransactionDurationAnomalyAlertTrigger; diff --git a/x-pack/plugins/apm/public/hooks/useEnvironments.tsx b/x-pack/plugins/apm/public/hooks/useEnvironments.tsx index b358608701fdf..9e01dde274ff7 100644 --- a/x-pack/plugins/apm/public/hooks/useEnvironments.tsx +++ b/x-pack/plugins/apm/public/hooks/useEnvironments.tsx @@ -5,30 +5,22 @@ */ import { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; import { useFetcher } from './useFetcher'; import { - ENVIRONMENT_NOT_DEFINED, ENVIRONMENT_ALL, + ENVIRONMENT_NOT_DEFINED, } from '../../common/environment_filter_values'; import { callApmApi } from '../services/rest/createCallApmApi'; -export const ALL_OPTION = { - value: ENVIRONMENT_ALL, - text: i18n.translate('xpack.apm.environment.allLabel', { - defaultMessage: 'All', - }), -}; - function getEnvironmentOptions(environments: string[]) { const environmentOptions = environments - .filter((env) => env !== ENVIRONMENT_NOT_DEFINED) + .filter((env) => env !== ENVIRONMENT_NOT_DEFINED.value) .map((environment) => ({ value: environment, text: environment, })); - return [ALL_OPTION, ...environmentOptions]; + return [ENVIRONMENT_ALL, ...environmentOptions]; } export function useEnvironments({ diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 27662585d01fa..f1acba76b0dc3 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -168,5 +168,20 @@ export class ApmPlugin implements Plugin { }), requiresAppContext: true, }); + + plugins.triggers_actions_ui.alertTypeRegistry.register({ + id: AlertType.TransactionDurationAnomaly, + name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', { + defaultMessage: 'Transaction duration anomaly', + }), + iconClass: 'bell', + alertParamsExpression: lazy(() => + import('./components/shared/TransactionDurationAnomalyAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); } } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts index 4b8e9cf937a2b..44ca80143bcd9 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts @@ -8,12 +8,15 @@ import { Observable } from 'rxjs'; import { AlertingPlugin } from '../../../../alerts/server'; import { ActionsPlugin } from '../../../../actions/server'; import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; +import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; import { registerErrorRateAlertType } from './register_error_rate_alert_type'; import { APMConfig } from '../..'; +import { MlPluginSetup } from '../../../../ml/server'; interface Params { alerts: AlertingPlugin['setup']; actions: ActionsPlugin['setup']; + ml?: MlPluginSetup; config$: Observable; } @@ -22,6 +25,11 @@ export function registerApmAlerts(params: Params) { alerts: params.alerts, config$: params.config$, }); + registerTransactionDurationAnomalyAlertType({ + alerts: params.alerts, + ml: params.ml, + config$: params.config$, + }); registerErrorRateAlertType({ alerts: params.alerts, config$: params.config$, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts index 53843b7f7412b..61e3dfee420a5 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts @@ -75,7 +75,7 @@ export function registerErrorRateAlertType({ }); const environmentTerm = - alertParams.environment === ENVIRONMENT_ALL + alertParams.environment === ENVIRONMENT_ALL.value ? [] : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }]; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index a922457b14cea..ead28c325692d 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -89,7 +89,7 @@ export function registerTransactionDurationAlertType({ }); const environmentTerm = - alertParams.environment === ENVIRONMENT_ALL + alertParams.environment === ENVIRONMENT_ALL.value ? [] : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }]; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts new file mode 100644 index 0000000000000..3abc89c470b21 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { Observable } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { KibanaRequest } from '../../../../../../src/core/server'; +import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { AlertingPlugin } from '../../../../alerts/server'; +import { APMConfig } from '../..'; +import { MlPluginSetup } from '../../../../ml/server'; +import { getMLJobIds } from '../service_map/get_service_anomalies'; + +interface RegisterAlertParams { + alerts: AlertingPlugin['setup']; + ml?: MlPluginSetup; + config$: Observable; +} + +const paramsSchema = schema.object({ + serviceName: schema.string(), + transactionType: schema.string(), + windowSize: schema.number(), + windowUnit: schema.string(), + environment: schema.string(), + anomalyScore: schema.number(), +}); + +const alertTypeConfig = + ALERT_TYPES_CONFIG[AlertType.TransactionDurationAnomaly]; + +export function registerTransactionDurationAnomalyAlertType({ + alerts, + ml, + config$, +}: RegisterAlertParams) { + alerts.registerType({ + id: AlertType.TransactionDurationAnomaly, + name: alertTypeConfig.name, + actionGroups: alertTypeConfig.actionGroups, + defaultActionGroupId: alertTypeConfig.defaultActionGroupId, + validate: { + params: paramsSchema, + }, + actionVariables: { + context: [ + { + description: i18n.translate( + 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.serviceName', + { + defaultMessage: 'Service name', + } + ), + name: 'serviceName', + }, + { + description: i18n.translate( + 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.transactionType', + { + defaultMessage: 'Transaction type', + } + ), + name: 'transactionType', + }, + ], + }, + producer: 'apm', + executor: async ({ services, params, state }) => { + if (!ml) { + return; + } + const alertParams = params as TypeOf; + const mlClient = services.getLegacyScopedClusterClient(ml.mlClient); + const request = { params: 'DummyKibanaRequest' } as KibanaRequest; + const { mlAnomalySearch } = ml.mlSystemProvider(mlClient, request); + const anomalyDetectors = ml.anomalyDetectorsProvider(mlClient, request); + + const mlJobIds = await getMLJobIds( + anomalyDetectors, + alertParams.environment + ); + const anomalySearchParams = { + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { result_type: 'record' } }, + { terms: { job_id: mlJobIds } }, + { + range: { + timestamp: { + gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`, + format: 'epoch_millis', + }, + }, + }, + { + term: { + partition_field_value: alertParams.serviceName, + }, + }, + { + range: { + record_score: { + gte: alertParams.anomalyScore, + }, + }, + }, + ], + }, + }, + }, + }; + + const response = ((await mlAnomalySearch( + anomalySearchParams + )) as unknown) as { hits: { total: { value: number } } }; + const hitCount = response.hits.total.value; + + if (hitCount > 0) { + const alertInstance = services.alertInstanceFactory( + AlertType.TransactionDurationAnomaly + ); + alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { + serviceName: alertParams.serviceName, + }); + } + + return {}; + }, + }); +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts index 05f41cdfdffd4..ead0c79a02836 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts @@ -22,7 +22,7 @@ export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } - const response = await getMlJobsWithAPMGroup(ml); + const response = await getMlJobsWithAPMGroup(ml.anomalyDetectors); return response.jobs .filter((job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2) .map((job) => { diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts index 5c0a3d17648aa..1c39892c3fd96 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../helpers/setup_request'; +import { MlPluginSetup } from '../../../../ml/server'; import { APM_ML_JOB_GROUP } from './constants'; // returns ml jobs containing "apm" group // workaround: the ML api returns 404 when no jobs are found. This is handled so instead of throwing an empty response is returned -export async function getMlJobsWithAPMGroup(ml: NonNullable) { +export async function getMlJobsWithAPMGroup( + anomalyDetectors: ReturnType +) { try { - return await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP); + return await anomalyDetectors.jobs(APM_ML_JOB_GROUP); } catch (e) { if (e.statusCode === 404) { return { count: 0, jobs: [] }; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts index ed66236726b9f..c1f346aa30e1f 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts @@ -23,7 +23,7 @@ export async function hasLegacyJobs(setup: Setup) { throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } - const response = await getMlJobsWithAPMGroup(ml); + const response = await getMlJobsWithAPMGroup(ml.anomalyDetectors); return response.jobs.some( (job) => job.job_id.endsWith('high_mean_response_time') && diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 423b87cb78c3c..29aaa98169fa5 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -48,7 +48,7 @@ export async function getAllEnvironments({ terms: { field: SERVICE_ENVIRONMENT, size: 100, - missing: includeMissing ? ENVIRONMENT_NOT_DEFINED : undefined, + missing: includeMissing ? ENVIRONMENT_NOT_DEFINED.value : undefined, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts index 800f809727eb6..a319bba1eabe1 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts @@ -21,7 +21,7 @@ describe('getEnvironmentUiFilterES', () => { }); it('should create a filter for missing service environments', () => { - const uiFilterES = getEnvironmentUiFilterES(ENVIRONMENT_NOT_DEFINED); + const uiFilterES = getEnvironmentUiFilterES(ENVIRONMENT_NOT_DEFINED.value); expect(uiFilterES).toHaveLength(1); expect(uiFilterES[0]).toHaveProperty( ['bool', 'must_not', 'exists', 'field'], diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts index 87bc8dc968373..6ff98a9be75f9 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts @@ -12,7 +12,7 @@ export function getEnvironmentUiFilterES(environment?: string): ESFilter[] { if (!environment) { return []; } - if (environment === ENVIRONMENT_NOT_DEFINED) { + if (environment === ENVIRONMENT_NOT_DEFINED.value) { return [{ bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } } }]; } return [{ term: { [SERVICE_ENVIRONMENT]: environment } }]; diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index ddad2eb2d22dc..a242a0adb6d4c 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -98,7 +98,11 @@ export async function setupRequest( context, request, }), - ml: getMlSetup(context, request), + ml: getMlSetup( + context.plugins.ml, + context.core.savedObjects.client, + request + ), config, }; @@ -110,20 +114,21 @@ export async function setupRequest( } as InferSetup; } -function getMlSetup(context: APMRequestHandlerContext, request: KibanaRequest) { - if (!context.plugins.ml) { +function getMlSetup( + ml: APMRequestHandlerContext['plugins']['ml'], + savedObjectsClient: APMRequestHandlerContext['core']['savedObjects']['client'], + request: KibanaRequest +) { + if (!ml) { return; } - const ml = context.plugins.ml; const mlClient = ml.mlClient.asScoped(request); + const mlSystem = ml.mlSystemProvider(mlClient, request); return { - mlSystem: ml.mlSystemProvider(mlClient, request), - anomalyDetectors: ml.anomalyDetectorsProvider(mlClient, request), - modules: ml.modulesProvider( - mlClient, - request, - context.core.savedObjects.client - ), mlClient, + mlSystem, + modules: ml.modulesProvider(mlClient, request, savedObjectsClient), + anomalyDetectors: ml.anomalyDetectorsProvider(mlClient, request), + mlAnomalySearch: mlSystem.mlAnomalySearch, }; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 03716382af859..ec274d20b6005 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -16,6 +16,8 @@ import { ML_ERRORS, } from '../../../common/anomaly_detection'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { MlPluginSetup } from '../../../../ml/server'; export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} }; @@ -43,7 +45,7 @@ export async function getServiceAnomalies({ throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } - const mlJobIds = await getMLJobIds(ml, environment); + const mlJobIds = await getMLJobIds(ml.anomalyDetectors, environment); const params = { body: { size: 0, @@ -136,16 +138,17 @@ function transformResponseToServiceAnomalies( } export async function getMLJobIds( - ml: Required['ml'], + anomalyDetectors: ReturnType, environment?: string ) { - const response = await getMlJobsWithAPMGroup(ml); + const response = await getMlJobsWithAPMGroup(anomalyDetectors); + // to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings` // and checking that it is compatable. const mlJobs = response.jobs.filter( (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2 ); - if (environment) { + if (environment && environment !== ENVIRONMENT_ALL.value) { const matchingMLJob = mlJobs.find( (job) => job.custom_settings?.job_tags?.environment === environment ); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index 072099bc9553c..596c3137ec19f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -66,7 +66,10 @@ export async function getAnomalySeries({ let mlJobIds: string[] = []; try { - mlJobIds = await getMLJobIds(setup.ml, uiFilters.environment); + mlJobIds = await getMLJobIds( + setup.ml.anomalyDetectors, + uiFilters.environment + ); } catch (error) { logger.error(error); return; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index 98f00bf8e6555..7d3af4caa2ca3 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -47,7 +47,7 @@ export async function getEnvironments( environments: { terms: { field: SERVICE_ENVIRONMENT, - missing: ENVIRONMENT_NOT_DEFINED, + missing: ENVIRONMENT_NOT_DEFINED.value, }, }, }, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index deafda67b806d..71202c62e6f6c 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -83,6 +83,7 @@ export class APMPlugin implements Plugin { registerApmAlerts({ alerts: plugins.alerts, actions: plugins.actions, + ml: plugins.ml, config$: mergedConfig$, }); } diff --git a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts index 1140af0b76404..603b4fba17adb 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts @@ -23,7 +23,13 @@ export function getAnomalyDetectorsProvider({ }: SharedServicesChecks): AnomalyDetectorsProvider { return { anomalyDetectorsProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - const hasMlCapabilities = getHasMlCapabilities(request); + // APM is using this service in anomaly alert, kibana alerting doesn't provide request object + // So we are adding a dummy request for now + // TODO: Remove this once kibana alerting provides request object + const hasMlCapabilities = + request.params !== 'DummyKibanaRequest' + ? getHasMlCapabilities(request) + : (_caps: string[]) => Promise.resolve(); return { async jobs(jobId?: string) { isFullLicense(); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d6e611e65154b..0e20e7281801f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4742,7 +4742,6 @@ "xpack.apm.customLink.empty": "カスタムリンクが見つかりません。独自のカスタムリンク、たとえば特定のダッシュボードまたは外部リンクへのリンクをセットアップします。", "xpack.apm.emptyMessage.noDataFoundDescription": "別の時間範囲を試すか検索フィルターをリセットしてください。", "xpack.apm.emptyMessage.noDataFoundLabel": "データが見つかりません。", - "xpack.apm.environment.allLabel": "すべて", "xpack.apm.error.prompt.body": "詳細はブラウザの開発者コンソールをご確認ください。", "xpack.apm.error.prompt.title": "申し訳ございませんが、エラーが発生しました :(", "xpack.apm.errorGroupDetails.avgLabel": "平均", @@ -4780,6 +4779,7 @@ "xpack.apm.fetcher.error.title": "リソースの取得中にエラーが発生しました", "xpack.apm.fetcher.error.url": "URL", "xpack.apm.filter.environment.label": "環境", + "xpack.apm.filter.environment.allLabel": "すべて", "xpack.apm.filter.environment.notDefinedLabel": "未定義", "xpack.apm.filter.environment.selectEnvironmentLabel": "環境を選択", "xpack.apm.formatters.hoursTimeUnitLabel": "h", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 54c69d849e3a9..2047468835024 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4743,7 +4743,6 @@ "xpack.apm.customLink.empty": "未找到定制链接。设置自己的定制链接,如特定仪表板的链接或外部链接。", "xpack.apm.emptyMessage.noDataFoundDescription": "尝试其他时间范围或重置搜索筛选。", "xpack.apm.emptyMessage.noDataFoundLabel": "未找到任何数据", - "xpack.apm.environment.allLabel": "全部", "xpack.apm.error.prompt.body": "有关详情,请查看您的浏览器开发者控制台。", "xpack.apm.error.prompt.title": "抱歉,发生错误 :(", "xpack.apm.errorGroupDetails.avgLabel": "平均", @@ -4781,6 +4780,7 @@ "xpack.apm.fetcher.error.title": "提取资源时出错", "xpack.apm.fetcher.error.url": "URL", "xpack.apm.filter.environment.label": "环境", + "xpack.apm.filter.environment.allLabel": "全部", "xpack.apm.filter.environment.notDefinedLabel": "未定义", "xpack.apm.filter.environment.selectEnvironmentLabel": "选择环境", "xpack.apm.formatters.hoursTimeUnitLabel": "h",