diff --git a/x-pack/plugins/apm/common/ml_job_constants.test.ts b/x-pack/plugins/apm/common/ml_job_constants.test.ts index 45bb7133e852ef..96e3ba826d2010 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.test.ts +++ b/x-pack/plugins/apm/common/ml_job_constants.test.ts @@ -4,45 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getMlJobId, - getMlPrefix, - getMlJobServiceName, - getSeverity, - severity, -} from './ml_job_constants'; +import { getSeverity, severity } from './ml_job_constants'; describe('ml_job_constants', () => { - it('getMlPrefix', () => { - expect(getMlPrefix('myServiceName')).toBe('myservicename-'); - expect(getMlPrefix('myServiceName', 'myTransactionType')).toBe( - 'myservicename-mytransactiontype-' - ); - }); - - it('getMlJobId', () => { - expect(getMlJobId('myServiceName')).toBe( - 'myservicename-high_mean_response_time' - ); - expect(getMlJobId('myServiceName', 'myTransactionType')).toBe( - 'myservicename-mytransactiontype-high_mean_response_time' - ); - expect(getMlJobId('my service name')).toBe( - 'my_service_name-high_mean_response_time' - ); - expect(getMlJobId('my service name', 'my transaction type')).toBe( - 'my_service_name-my_transaction_type-high_mean_response_time' - ); - }); - - describe('getMlJobServiceName', () => { - it('extracts the service name from a job id', () => { - expect( - getMlJobServiceName('opbeans-node-request-high_mean_response_time') - ).toEqual('opbeans-node'); - }); - }); - describe('getSeverity', () => { describe('when score is undefined', () => { it('returns undefined', () => { diff --git a/x-pack/plugins/apm/common/ml_job_constants.ts b/x-pack/plugins/apm/common/ml_job_constants.ts index f9b0119d8a107e..b8c2546bd0c84a 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.ts +++ b/x-pack/plugins/apm/common/ml_job_constants.ts @@ -11,25 +11,6 @@ export enum severity { warning = 'warning', } -export const APM_ML_JOB_GROUP_NAME = 'apm'; - -export function getMlPrefix(serviceName: string, transactionType?: string) { - const maybeTransactionType = transactionType ? `${transactionType}-` : ''; - return encodeForMlApi(`${serviceName}-${maybeTransactionType}`); -} - -export function getMlJobId(serviceName: string, transactionType?: string) { - return `${getMlPrefix(serviceName, transactionType)}high_mean_response_time`; -} - -export function getMlJobServiceName(jobId: string) { - return jobId.split('-').slice(0, -2).join('-'); -} - -export function encodeForMlApi(value: string) { - return value.replace(/\s+/g, '_').toLowerCase(); -} - export function getSeverity(score?: number) { if (typeof score !== 'number') { return undefined; diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 7d7a7811eeba2c..43f3585d0ebb2e 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -34,16 +34,6 @@ export interface Connection { destination: ConnectionNode; } -export interface ServiceAnomaly { - anomaly_score: number; - anomaly_severity: string; - actual_value: number; - typical_value: number; - ml_job_id: string; -} - -export type ServiceNode = ConnectionNode & Partial; - export interface ServiceNodeMetrics { avgMemoryUsage: number | null; avgCpuUsage: number | null; diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx deleted file mode 100644 index 42f7246b6ea359..00000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -interface TransactionSelectProps { - transactionTypes: string[]; - onChange: (value: string) => void; - selectedTransactionType: string; -} - -export function TransactionSelect({ - transactionTypes, - onChange, - selectedTransactionType, -}: TransactionSelectProps) { - return ( - - { - return { - value: transactionType, - inputDisplay: transactionType, - dropdownDisplay: ( - - - {transactionType} - - - ), - }; - })} - /> - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx deleted file mode 100644 index 91778b2940c6b5..00000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import React, { Component } from 'react'; -import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { startMLJob, MLError } from '../../../../../services/rest/ml'; -import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; -import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { MachineLearningFlyoutView } from './view'; -import { ApmPluginContext } from '../../../../../context/ApmPluginContext'; - -interface Props { - isOpen: boolean; - onClose: () => void; - urlParams: IUrlParams; -} - -interface State { - isCreatingJob: boolean; -} - -export class MachineLearningFlyout extends Component { - static contextType = ApmPluginContext; - - public state: State = { - isCreatingJob: false, - }; - - public onClickCreate = async ({ - transactionType, - }: { - transactionType: string; - }) => { - this.setState({ isCreatingJob: true }); - try { - const { http } = this.context.core; - const { serviceName } = this.props.urlParams; - if (!serviceName) { - throw new Error('Service name is required to create this ML job'); - } - const res = await startMLJob({ http, serviceName, transactionType }); - const didSucceed = res.datafeeds[0].success && res.jobs[0].success; - if (!didSucceed) { - throw new Error('Creating ML job failed'); - } - this.addSuccessToast({ transactionType }); - } catch (e) { - this.addErrorToast(e as MLError); - } - - this.setState({ isCreatingJob: false }); - this.props.onClose(); - }; - - public addErrorToast = (error: MLError) => { - const { core } = this.context; - - const { urlParams } = this.props; - const { serviceName } = urlParams; - - if (!serviceName) { - return; - } - - const errorDescription = error?.body?.message; - const errorText = errorDescription - ? `${error.message}: ${errorDescription}` - : error.message; - - core.notifications.toasts.addWarning({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle', - { - defaultMessage: 'Job creation failed', - } - ), - text: toMountPoint( - <> -

{errorText}

-

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText', - { - defaultMessage: - 'Your current license may not allow for creating machine learning jobs, or this job may already exist.', - } - )} -

- - ), - }); - }; - - public addSuccessToast = ({ - transactionType, - }: { - transactionType: string; - }) => { - const { core } = this.context; - const { urlParams } = this.props; - const { serviceName } = urlParams; - - if (!serviceName) { - return; - } - - core.notifications.toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle', - { - defaultMessage: 'Job successfully created', - } - ), - text: toMountPoint( -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText', - { - defaultMessage: - 'The analysis is now running for {serviceName} ({transactionType}). It might take a while before results are added to the response times graph.', - values: { - serviceName, - transactionType, - }, - } - )}{' '} - - - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText', - { - defaultMessage: 'View job', - } - )} - - -

- ), - }); - }; - - public render() { - const { isOpen, onClose, urlParams } = this.props; - const { serviceName } = urlParams; - const { isCreatingJob } = this.state; - - if (!isOpen || !serviceName) { - return null; - } - - return ( - - ); - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx deleted file mode 100644 index 72e8193ba2de27..00000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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 { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiFormRow, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useState, useEffect } from 'react'; -import { isEmpty } from 'lodash'; -import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher'; -import { getHasMLJob } from '../../../../../services/rest/ml'; -import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { MLLink } from '../../../../shared/Links/MachineLearningLinks/MLLink'; -import { TransactionSelect } from './TransactionSelect'; -import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; -import { useServiceTransactionTypes } from '../../../../../hooks/useServiceTransactionTypes'; -import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; - -interface Props { - isCreatingJob: boolean; - onClickCreate: ({ transactionType }: { transactionType: string }) => void; - onClose: () => void; - urlParams: IUrlParams; -} - -export function MachineLearningFlyoutView({ - isCreatingJob, - onClickCreate, - onClose, - urlParams, -}: Props) { - const { serviceName } = urlParams; - const transactionTypes = useServiceTransactionTypes(urlParams); - - const [selectedTransactionType, setSelectedTransactionType] = useState< - string | undefined - >(undefined); - - const { http } = useApmPluginContext().core; - - const { data: hasMLJob, status } = useFetcher( - () => { - if (serviceName && selectedTransactionType) { - return getHasMLJob({ - serviceName, - transactionType: selectedTransactionType, - http, - }); - } - }, - [serviceName, selectedTransactionType, http], - { showToastOnError: false } - ); - - // update selectedTransactionType when list of transaction types has loaded - useEffect(() => { - setSelectedTransactionType(transactionTypes[0]); - }, [transactionTypes]); - - if (!serviceName || !selectedTransactionType || isEmpty(transactionTypes)) { - return null; - } - - const isLoadingMLJob = status === FETCH_STATUS.LOADING; - const isMlAvailable = status !== FETCH_STATUS.FAILURE; - - return ( - - - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle', - { - defaultMessage: 'Enable anomaly detection', - } - )} -

-
- -
- - {!isMlAvailable && ( -
- -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.mlNotAvailableDescription', - { - defaultMessage: - 'Unable to connect to Machine learning. Make sure it is enabled in Kibana to use anomaly detection.', - } - )} -

-
- -
- )} - {hasMLJob && ( -
- -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription', - { - defaultMessage: - 'There is currently a job running for {serviceName} ({transactionType}).', - values: { - serviceName, - transactionType: selectedTransactionType, - }, - } - )}{' '} - - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText', - { - defaultMessage: 'View existing job', - } - )} - -

-
- -
- )} - -

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText', - { - defaultMessage: 'transaction duration', - } - )} - - ), - serviceMapAnnotationText: ( - - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.serviceMapAnnotationText', - { - defaultMessage: 'service maps', - } - )} - - ), - }} - /> -

-

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText', - { - defaultMessage: 'Machine Learning Job Management page', - } - )} - - ), - }} - />{' '} - - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.noteText', - { - defaultMessage: - 'Note: It might take a few minutes for the job to begin calculating results.', - } - )} - -

-
- - -
- - - - {transactionTypes.length > 1 ? ( - { - setSelectedTransactionType(value); - }} - /> - ) : null} - - - - - onClickCreate({ transactionType: selectedTransactionType }) - } - fill - disabled={isCreatingJob || hasMLJob || isLoadingMLJob} - > - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel', - { - defaultMessage: 'Create job', - } - )} - - - - - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx index 321617ed8496af..0a7dcbd0be3dfc 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx @@ -4,18 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - EuiContextMenu, - EuiContextMenuPanelItemDescriptor, - EuiPopover, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { memoize } from 'lodash'; -import React, { Fragment } from 'react'; +import React from 'react'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { LicenseContext } from '../../../../context/LicenseContext'; -import { MachineLearningFlyout } from './MachineLearningFlyout'; import { WatcherFlyout } from './WatcherFlyout'; import { ApmPluginContext } from '../../../../context/ApmPluginContext'; @@ -26,7 +18,7 @@ interface State { isPopoverOpen: boolean; activeFlyout: FlyoutName; } -type FlyoutName = null | 'ML' | 'Watcher'; +type FlyoutName = null | 'Watcher'; export class ServiceIntegrations extends React.Component { static contextType = ApmPluginContext; @@ -34,38 +26,6 @@ export class ServiceIntegrations extends React.Component { public state: State = { isPopoverOpen: false, activeFlyout: null }; - public getPanelItems = memoize((mlAvailable: boolean | undefined) => { - let panelItems: EuiContextMenuPanelItemDescriptor[] = []; - if (mlAvailable) { - panelItems = panelItems.concat(this.getMLPanelItems()); - } - return panelItems.concat(this.getWatcherPanelItems()); - }); - - public getMLPanelItems = () => { - return [ - { - name: i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonLabel', - { - defaultMessage: 'Enable ML anomaly detection', - } - ), - icon: 'machineLearningApp', - toolTipContent: i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonTooltip', - { - defaultMessage: 'Set up a machine learning job for this service', - } - ), - onClick: () => { - this.closePopover(); - this.openFlyout('ML'); - }, - }, - ]; - }; - public getWatcherPanelItems = () => { const { core } = this.context; @@ -132,42 +92,31 @@ export class ServiceIntegrations extends React.Component { ); return ( - - {(license) => ( - - - - - - - - )} - + <> + + + + + ); } } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index ff68288916af47..78779bdcc2052e 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -15,8 +15,6 @@ import React, { MouseEvent } from 'react'; import { Buttons } from './Buttons'; import { Info } from './Info'; import { ServiceMetricFetcher } from './ServiceMetricFetcher'; -import { AnomalyDetection } from './anomaly_detection'; -import { ServiceNode } from '../../../../../common/service_map'; import { popoverMinWidth } from '../cytoscapeOptions'; interface ContentsProps { @@ -70,12 +68,13 @@ export function Contents({ - {isService && ( + {/* //TODO [APM ML] add service health stats here: + isService && ( - + - )} + )*/} {isService ? ( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx deleted file mode 100644 index 531bbb139d58b6..00000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import React from 'react'; -import styled from 'styled-components'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiIconTip, - EuiHealth, -} from '@elastic/eui'; -import { useTheme } from '../../../../hooks/useTheme'; -import { fontSize, px } from '../../../../style/variables'; -import { asInteger } from '../../../../utils/formatters'; -import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { getSeverityColor, popoverMinWidth } from '../cytoscapeOptions'; -import { getMetricChangeDescription } from '../../../../../../ml/public'; -import { ServiceNode } from '../../../../../common/service_map'; - -const HealthStatusTitle = styled(EuiTitle)` - display: inline; - text-transform: uppercase; -`; - -const VerticallyCentered = styled.div` - display: flex; - align-items: center; -`; - -const SubduedText = styled.span` - color: ${({ theme }) => theme.eui.euiTextSubduedColor}; -`; - -const EnableText = styled.section` - color: ${({ theme }) => theme.eui.euiTextSubduedColor}; - line-height: 1.4; - font-size: ${fontSize}; - width: ${px(popoverMinWidth)}; -`; - -export const ContentLine = styled.section` - line-height: 2; -`; - -interface AnomalyDetectionProps { - serviceNodeData: cytoscape.NodeDataDefinition & ServiceNode; -} - -export function AnomalyDetection({ serviceNodeData }: AnomalyDetectionProps) { - const theme = useTheme(); - const anomalySeverity = serviceNodeData.anomaly_severity; - const anomalyScore = serviceNodeData.anomaly_score; - const actualValue = serviceNodeData.actual_value; - const typicalValue = serviceNodeData.typical_value; - const mlJobId = serviceNodeData.ml_job_id; - const hasAnomalyDetectionScore = - anomalySeverity !== undefined && anomalyScore !== undefined; - const anomalyDescription = - hasAnomalyDetectionScore && - actualValue !== undefined && - typicalValue !== undefined - ? getMetricChangeDescription(actualValue, typicalValue).message - : null; - - return ( - <> -
- -

{ANOMALY_DETECTION_TITLE}

-
-   - - {!mlJobId && {ANOMALY_DETECTION_DISABLED_TEXT}} -
- {hasAnomalyDetectionScore && ( - - - - - - {ANOMALY_DETECTION_SCORE_METRIC} - - - -
- {getDisplayedAnomalyScore(anomalyScore as number)} - {anomalyDescription && ( -  ({anomalyDescription}) - )} -
-
-
-
- )} - {mlJobId && !hasAnomalyDetectionScore && ( - {ANOMALY_DETECTION_NO_DATA_TEXT} - )} - {mlJobId && ( - - - {ANOMALY_DETECTION_LINK} - - - )} - - ); -} - -function getDisplayedAnomalyScore(score: number) { - if (score > 0 && score < 1) { - return '< 1'; - } - return asInteger(score); -} - -const ANOMALY_DETECTION_TITLE = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverTitle', - { defaultMessage: 'Anomaly Detection' } -); - -const ANOMALY_DETECTION_TOOLTIP = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverTooltip', - { - defaultMessage: - 'Service health indicators are powered by the anomaly detection feature in machine learning', - } -); - -const ANOMALY_DETECTION_SCORE_METRIC = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric', - { defaultMessage: 'Score (max.)' } -); - -const ANOMALY_DETECTION_LINK = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverLink', - { defaultMessage: 'View anomalies' } -); - -const ANOMALY_DETECTION_DISABLED_TEXT = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverDisabled', - { - defaultMessage: - 'Display service health indicators by enabling anomaly detection from the Integrations menu in the Service details view.', - } -); - -const ANOMALY_DETECTION_NO_DATA_TEXT = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverNoData', - { - defaultMessage: `We couldn't find an anomaly score within the selected time range. See details in the anomaly explorer.`, - } -); diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 9018fbb2bc410b..fc5347d081316a 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -22,8 +22,6 @@ import { TransactionCharts } from '../../shared/charts/TransactionCharts'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { TransactionList } from './List'; import { useRedirect } from './useRedirect'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { getHasMLJob } from '../../../services/rest/ml'; import { history } from '../../../utils/history'; import { useLocation } from '../../../hooks/useLocation'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; @@ -34,7 +32,6 @@ import { PROJECTION } from '../../../../common/projections/typings'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; function getRedirectLocation({ urlParams, @@ -86,18 +83,6 @@ export function TransactionOverview() { status: transactionListStatus, } = useTransactionList(urlParams); - const { http } = useApmPluginContext().core; - - const { data: hasMLJob = false } = useFetcher( - () => { - if (serviceName && transactionType) { - return getHasMLJob({ serviceName, transactionType, http }); - } - }, - [http, serviceName, transactionType], - { showToastOnError: false } - ); - const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: [ @@ -140,7 +125,8 @@ export function TransactionOverview() { { - it('should produce the correct URL with serviceName', async () => { - const href = await getRenderedHref( - () => ( - - ), - { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location - ); - - expect(href).toEqual( - `/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))` - ); - }); it('should produce the correct URL with jobId', async () => { const href = await getRenderedHref( () => ( diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx index 346748964d529b..1e1f9ea5f23b72 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx @@ -5,28 +5,16 @@ */ import React from 'react'; -import { getMlJobId } from '../../../../../common/ml_job_constants'; import { MLLink } from './MLLink'; -interface PropsServiceName { - serviceName: string; - transactionType?: string; -} -interface PropsJobId { +interface Props { jobId: string; -} - -type Props = (PropsServiceName | PropsJobId) & { external?: boolean; -}; +} export const MLJobLink: React.FC = (props) => { - const jobId = - 'jobId' in props - ? props.jobId - : getMlJobId(props.serviceName, props.transactionType); const query = { - ml: { jobIds: [jobId] }, + ml: { jobIds: [props.jobId] }, }; return ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 4821e06419e341..00ff6f9969725a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -101,11 +101,13 @@ export class TransactionCharts extends Component { return null; } - const { serviceName, transactionType, kuery } = this.props.urlParams; + const { serviceName, kuery } = this.props.urlParams; if (!serviceName) { return null; } + const linkedJobId = ''; // TODO [APM ML] link to ML job id for the selected environment + const hasKuery = !isEmpty(kuery); const icon = hasKuery ? ( { } )}{' '} - - View Job - + View Job ); diff --git a/x-pack/plugins/apm/public/services/rest/ml.ts b/x-pack/plugins/apm/public/services/rest/ml.ts deleted file mode 100644 index 47032501d9fbe1..00000000000000 --- a/x-pack/plugins/apm/public/services/rest/ml.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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 { HttpSetup } from 'kibana/public'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_TYPE, -} from '../../../common/elasticsearch_fieldnames'; -import { - APM_ML_JOB_GROUP_NAME, - getMlJobId, - getMlPrefix, - encodeForMlApi, -} from '../../../common/ml_job_constants'; -import { callApi } from './callApi'; -import { ESFilter } from '../../../typings/elasticsearch'; -import { callApmApi } from './createCallApmApi'; - -interface MlResponseItem { - id: string; - success: boolean; - error?: { - msg: string; - body: string; - path: string; - response: string; - statusCode: number; - }; -} - -interface StartedMLJobApiResponse { - datafeeds: MlResponseItem[]; - jobs: MlResponseItem[]; -} - -async function getTransactionIndices() { - const indices = await callApmApi({ - method: 'GET', - pathname: `/api/apm/settings/apm-indices`, - }); - return indices['apm_oss.transactionIndices']; -} - -export async function startMLJob({ - serviceName, - transactionType, - http, -}: { - serviceName: string; - transactionType: string; - http: HttpSetup; -}) { - const transactionIndices = await getTransactionIndices(); - const groups = [ - APM_ML_JOB_GROUP_NAME, - encodeForMlApi(serviceName), - encodeForMlApi(transactionType), - ]; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ]; - return callApi(http, { - method: 'POST', - pathname: `/api/ml/modules/setup/apm_transaction`, - body: { - prefix: getMlPrefix(serviceName, transactionType), - groups, - indexPatternName: transactionIndices, - startDatafeed: true, - query: { - bool: { - filter, - }, - }, - }, - }); -} - -// https://www.elastic.co/guide/en/elasticsearch/reference/6.5/ml-get-job.html -export interface MLJobApiResponse { - count: number; - jobs: Array<{ - job_id: string; - }>; -} - -export type MLError = Error & { body?: { message?: string } }; - -export async function getHasMLJob({ - serviceName, - transactionType, - http, -}: { - serviceName: string; - transactionType: string; - http: HttpSetup; -}) { - try { - await callApi(http, { - method: 'GET', - pathname: `/api/ml/anomaly_detectors/${getMlJobId( - serviceName, - transactionType - )}`, - }); - return true; - } catch (error) { - if ( - error?.body?.statusCode === 404 && - error?.body?.attributes?.body?.error?.type === - 'resource_not_found_exception' - ) { - return false; // false only if ML api responds with resource_not_found_exception - } - throw error; - } -} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts deleted file mode 100644 index aefd074c373f95..00000000000000 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { getApmMlJobCategory } from './get_service_anomalies'; -import { Job as AnomalyDetectionJob } from '../../../../ml/server'; - -describe('getApmMlJobCategory', () => { - it('should match service names with different casings', () => { - const mlJob = { - job_id: 'testservice-request-high_mean_response_time', - groups: ['apm', 'testservice', 'request'], - } as AnomalyDetectionJob; - const serviceNames = ['testService']; - const apmMlJobCategory = getApmMlJobCategory(mlJob, serviceNames); - - expect(apmMlJobCategory).toEqual({ - jobId: 'testservice-request-high_mean_response_time', - serviceName: 'testService', - transactionType: 'request', - }); - }); - - it('should match service names with spaces', () => { - const mlJob = { - job_id: 'test_service-request-high_mean_response_time', - groups: ['apm', 'test_service', 'request'], - } as AnomalyDetectionJob; - const serviceNames = ['Test Service']; - const apmMlJobCategory = getApmMlJobCategory(mlJob, serviceNames); - - expect(apmMlJobCategory).toEqual({ - jobId: 'test_service-request-high_mean_response_time', - serviceName: 'Test Service', - transactionType: 'request', - }); - }); -}); 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 deleted file mode 100644 index 900141e9040ae1..00000000000000 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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 { intersection } from 'lodash'; -import { leftJoin } from '../../../common/utils/left_join'; -import { Job as AnomalyDetectionJob } from '../../../../ml/server'; -import { PromiseReturnType } from '../../../typings/common'; -import { IEnvOptions } from './get_service_map'; -import { Setup } from '../helpers/setup_request'; -import { - APM_ML_JOB_GROUP_NAME, - encodeForMlApi, -} from '../../../common/ml_job_constants'; - -async function getApmAnomalyDetectionJobs( - setup: Setup -): Promise { - const { ml } = setup; - - if (!ml) { - return []; - } - try { - const { jobs } = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP_NAME); - return jobs; - } catch (error) { - if (error.statusCode === 404) { - return []; - } - throw error; - } -} - -type ApmMlJobCategory = NonNullable>; - -export const getApmMlJobCategory = ( - mlJob: AnomalyDetectionJob, - serviceNames: string[] -) => { - const serviceByGroupNameMap = new Map( - serviceNames.map((serviceName) => [ - encodeForMlApi(serviceName), - serviceName, - ]) - ); - if (!mlJob.groups.includes(APM_ML_JOB_GROUP_NAME)) { - // ML job missing "apm" group name - return; - } - const apmJobGroups = mlJob.groups.filter( - (groupName) => groupName !== APM_ML_JOB_GROUP_NAME - ); - const apmJobServiceNames = apmJobGroups.map( - (groupName) => serviceByGroupNameMap.get(groupName) || groupName - ); - const [serviceName] = intersection(apmJobServiceNames, serviceNames); - if (!serviceName) { - // APM ML job service was not found - return; - } - const serviceGroupName = encodeForMlApi(serviceName); - const [transactionType] = apmJobGroups.filter( - (groupName) => groupName !== serviceGroupName - ); - if (!transactionType) { - // APM ML job transaction type was not found. - return; - } - return { jobId: mlJob.job_id, serviceName, transactionType }; -}; - -export type ServiceAnomalies = PromiseReturnType; - -export async function getServiceAnomalies( - options: IEnvOptions, - serviceNames: string[] -) { - const { start, end, ml } = options.setup; - - if (!ml || serviceNames.length === 0) { - return []; - } - - const apmMlJobs = await getApmAnomalyDetectionJobs(options.setup); - if (apmMlJobs.length === 0) { - return []; - } - const apmMlJobCategories = apmMlJobs - .map((job) => getApmMlJobCategory(job, serviceNames)) - .filter( - (apmJobCategory) => apmJobCategory !== undefined - ) as ApmMlJobCategory[]; - const apmJobIds = apmMlJobs.map((job) => job.job_id); - const params = { - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { result_type: 'record' } }, - { - terms: { - job_id: apmJobIds, - }, - }, - { - range: { - timestamp: { gte: start, lte: end, format: 'epoch_millis' }, - }, - }, - ], - }, - }, - aggs: { - jobs: { - terms: { field: 'job_id', size: apmJobIds.length }, - aggs: { - top_score_hits: { - top_hits: { - sort: [{ record_score: { order: 'desc' as const } }], - _source: ['record_score', 'timestamp', 'typical', 'actual'], - size: 1, - }, - }, - }, - }, - }, - }, - }; - - const response = (await ml.mlSystem.mlAnomalySearch(params)) as { - aggregations: { - jobs: { - buckets: Array<{ - key: string; - top_score_hits: { - hits: { - hits: Array<{ - _source: { - record_score: number; - timestamp: number; - typical: number[]; - actual: number[]; - }; - }>; - }; - }; - }>; - }; - }; - }; - const anomalyScores = response.aggregations.jobs.buckets.map((jobBucket) => { - const jobId = jobBucket.key; - const bucketSource = jobBucket.top_score_hits.hits.hits?.[0]?._source; - return { - jobId, - anomalyScore: bucketSource.record_score, - timestamp: bucketSource.timestamp, - typical: bucketSource.typical[0], - actual: bucketSource.actual[0], - }; - }); - return leftJoin(apmMlJobCategories, 'jobId', anomalyScores); -} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 9f3ded82d7cbd8..4d488cd1a55096 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -13,14 +13,9 @@ import { getServicesProjection } from '../../../common/projections/services'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { - transformServiceMapResponses, - getAllNodes, - getServiceNodes, -} from './transform_service_map_responses'; +import { transformServiceMapResponses } from './transform_service_map_responses'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; -import { getServiceAnomalies, ServiceAnomalies } from './get_service_anomalies'; export interface IEnvOptions { setup: Setup & SetupTimeRange; @@ -132,7 +127,6 @@ async function getServicesData(options: IEnvOptions) { ); } -export { ServiceAnomalies }; export type ConnectionsResponse = PromiseReturnType; export type ServicesResponse = PromiseReturnType; export type ServiceMapAPIResponse = PromiseReturnType; @@ -143,19 +137,8 @@ export async function getServiceMap(options: IEnvOptions) { getServicesData(options), ]); - // Derive all related service names from connection and service data - const allNodes = getAllNodes(servicesData, connectionData.connections); - const serviceNodes = getServiceNodes(allNodes); - const serviceNames = serviceNodes.map( - (serviceData) => serviceData[SERVICE_NAME] - ); - - // Get related service anomalies - const serviceAnomalies = await getServiceAnomalies(options, serviceNames); - return transformServiceMapResponses({ ...connectionData, - anomalies: serviceAnomalies, services: servicesData, }); } diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts deleted file mode 100644 index f07b575cc0a35a..00000000000000 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 { ServiceAnomalies } from './get_service_map'; -import { addAnomaliesDataToNodes } from './ml_helpers'; - -describe('addAnomaliesDataToNodes', () => { - it('adds anomalies to nodes', () => { - const nodes = [ - { - 'service.name': 'opbeans-ruby', - 'agent.name': 'ruby', - 'service.environment': null, - }, - { - 'service.name': 'opbeans-java', - 'agent.name': 'java', - 'service.environment': null, - }, - ]; - - const serviceAnomalies: ServiceAnomalies = [ - { - jobId: 'opbeans-ruby-request-high_mean_response_time', - serviceName: 'opbeans-ruby', - transactionType: 'request', - anomalyScore: 50, - timestamp: 1591351200000, - actual: 2000, - typical: 1000, - }, - { - jobId: 'opbeans-java-request-high_mean_response_time', - serviceName: 'opbeans-java', - transactionType: 'request', - anomalyScore: 100, - timestamp: 1591351200000, - actual: 9000, - typical: 3000, - }, - ]; - - const result = [ - { - 'service.name': 'opbeans-ruby', - 'agent.name': 'ruby', - 'service.environment': null, - anomaly_score: 50, - anomaly_severity: 'major', - actual_value: 2000, - typical_value: 1000, - ml_job_id: 'opbeans-ruby-request-high_mean_response_time', - }, - { - 'service.name': 'opbeans-java', - 'agent.name': 'java', - 'service.environment': null, - anomaly_score: 100, - anomaly_severity: 'critical', - actual_value: 9000, - typical_value: 3000, - ml_job_id: 'opbeans-java-request-high_mean_response_time', - }, - ]; - - expect( - addAnomaliesDataToNodes( - nodes, - (serviceAnomalies as unknown) as ServiceAnomalies - ) - ).toEqual(result); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts deleted file mode 100644 index 8162417616b6cc..00000000000000 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; -import { getSeverity } from '../../../common/ml_job_constants'; -import { ConnectionNode, ServiceNode } from '../../../common/service_map'; -import { ServiceAnomalies } from './get_service_map'; - -export function addAnomaliesDataToNodes( - nodes: ConnectionNode[], - serviceAnomalies: ServiceAnomalies -) { - const anomaliesMap = serviceAnomalies.reduce( - (acc, anomalyJob) => { - const serviceAnomaly: typeof acc[string] | undefined = - acc[anomalyJob.serviceName]; - const hasAnomalyJob = serviceAnomaly !== undefined; - const hasAnomalyScore = serviceAnomaly?.anomaly_score !== undefined; - const hasNewAnomalyScore = anomalyJob.anomalyScore !== undefined; - const hasNewMaxAnomalyScore = - hasNewAnomalyScore && - (!hasAnomalyScore || - (anomalyJob?.anomalyScore ?? 0) > - (serviceAnomaly?.anomaly_score ?? 0)); - - if (!hasAnomalyJob || hasNewMaxAnomalyScore) { - acc[anomalyJob.serviceName] = { - anomaly_score: anomalyJob.anomalyScore, - actual_value: anomalyJob.actual, - typical_value: anomalyJob.typical, - ml_job_id: anomalyJob.jobId, - }; - } - - return acc; - }, - {} as { - [serviceName: string]: { - anomaly_score?: number; - actual_value?: number; - typical_value?: number; - ml_job_id: string; - }; - } - ); - - const servicesDataWithAnomalies: ServiceNode[] = nodes.map((service) => { - const serviceAnomaly = anomaliesMap[service[SERVICE_NAME]]; - if (serviceAnomaly) { - const anomalyScore = serviceAnomaly.anomaly_score; - return { - ...service, - anomaly_score: anomalyScore, - anomaly_severity: getSeverity(anomalyScore), - actual_value: serviceAnomaly.actual_value, - typical_value: serviceAnomaly.typical_value, - ml_job_id: serviceAnomaly.ml_job_id, - }; - } - return service; - }); - - return servicesDataWithAnomalies; -} diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 6c9880c2dc4dfb..1e26634bdf0f12 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -12,7 +12,6 @@ import { SPAN_SUBTYPE, SPAN_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { ServiceAnomalies } from './get_service_map'; import { transformServiceMapResponses, ServiceMapResponse, @@ -36,12 +35,9 @@ const javaService = { [AGENT_NAME]: 'java', }; -const serviceAnomalies: ServiceAnomalies = []; - describe('transformServiceMapResponses', () => { it('maps external destinations to internal services', () => { const response: ServiceMapResponse = { - anomalies: serviceAnomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -73,7 +69,6 @@ describe('transformServiceMapResponses', () => { it('collapses external destinations based on span.destination.resource.name', () => { const response: ServiceMapResponse = { - anomalies: serviceAnomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -109,7 +104,6 @@ describe('transformServiceMapResponses', () => { it('picks the first span.type/subtype in an alphabetically sorted list', () => { const response: ServiceMapResponse = { - anomalies: serviceAnomalies, services: [javaService], discoveredServices: [], connections: [ @@ -148,7 +142,6 @@ describe('transformServiceMapResponses', () => { it('processes connections without a matching "service" aggregation', () => { const response: ServiceMapResponse = { - anomalies: serviceAnomalies, services: [javaService], discoveredServices: [], connections: [ diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 53abf54cbcf313..835c00b8df239e 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -17,12 +17,7 @@ import { ServiceConnectionNode, ExternalConnectionNode, } from '../../../common/service_map'; -import { - ConnectionsResponse, - ServicesResponse, - ServiceAnomalies, -} from './get_service_map'; -import { addAnomaliesDataToNodes } from './ml_helpers'; +import { ConnectionsResponse, ServicesResponse } from './get_service_map'; function getConnectionNodeId(node: ConnectionNode): string { if ('span.destination.service.resource' in node) { @@ -67,12 +62,11 @@ export function getServiceNodes(allNodes: ConnectionNode[]) { } export type ServiceMapResponse = ConnectionsResponse & { - anomalies: ServiceAnomalies; services: ServicesResponse; }; export function transformServiceMapResponses(response: ServiceMapResponse) { - const { anomalies, discoveredServices, services, connections } = response; + const { discoveredServices, services, connections } = response; const allNodes = getAllNodes(services, connections); const serviceNodes = getServiceNodes(allNodes); @@ -214,18 +208,10 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { return prev.concat(connection); }, []); - // Add anomlies data - const dedupedNodesWithAnomliesData = addAnomaliesDataToNodes( - dedupedNodes, - anomalies - ); - // Put everything together in elements, with everything in the "data" property - const elements = [...dedupedConnections, ...dedupedNodesWithAnomliesData].map( - (element) => ({ - data: element, - }) - ); + const elements = [...dedupedConnections, ...dedupedNodes].map((element) => ({ + data: element, + })); return { elements }; } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap deleted file mode 100644 index cf3fdac221b597..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap +++ /dev/null @@ -1,68 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`anomalyAggsFetcher when ES returns valid response should call client with correct query 1`] = ` -Array [ - Array [ - Object { - "body": Object { - "aggs": Object { - "ml_avg_response_times": Object { - "aggs": Object { - "anomaly_score": Object { - "max": Object { - "field": "anomaly_score", - }, - }, - "lower": Object { - "min": Object { - "field": "model_lower", - }, - }, - "upper": Object { - "max": Object { - "field": "model_upper", - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 200000, - "min": 90000, - }, - "field": "timestamp", - "fixed_interval": "myInterval", - "min_doc_count": 0, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "job_id": "myservicename-mytransactiontype-high_mean_response_time", - }, - }, - Object { - "exists": Object { - "field": "bucket_span", - }, - }, - Object { - "range": Object { - "timestamp": Object { - "format": "epoch_millis", - "gte": 90000, - "lte": 200000, - }, - }, - }, - ], - }, - }, - "size": 0, - }, - }, - ], -] -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap deleted file mode 100644 index 971fa3b92cc83f..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`getAnomalySeries should match snapshot 1`] = ` -Object { - "anomalyBoundaries": Array [ - Object { - "x": 5000, - "y": 200, - "y0": 20, - }, - Object { - "x": 15000, - "y": 100, - "y0": 20, - }, - Object { - "x": 25000, - "y": 50, - "y0": 10, - }, - Object { - "x": 30000, - "y": 50, - "y0": 10, - }, - ], - "anomalyScore": Array [ - Object { - "x": 25000, - "x0": 15000, - }, - Object { - "x": 35000, - "x0": 25000, - }, - ], -} -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap deleted file mode 100644 index 8cf471cb34ed26..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`anomalySeriesTransform should match snapshot 1`] = ` -Object { - "anomalyBoundaries": Array [ - Object { - "x": 10000, - "y": 200, - "y0": 20, - }, - Object { - "x": 15000, - "y": 100, - "y0": 20, - }, - Object { - "x": 25000, - "y": 50, - "y0": 10, - }, - ], - "anomalyScore": Array [ - Object { - "x": 25000, - "x0": 15000, - }, - Object { - "x": 25000, - "x0": 25000, - }, - ], -} -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts deleted file mode 100644 index 313cf818a322da..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 { anomalySeriesFetcher, ESResponse } from './fetcher'; - -describe('anomalyAggsFetcher', () => { - describe('when ES returns valid response', () => { - let response: ESResponse | undefined; - let clientSpy: jest.Mock; - - beforeEach(async () => { - clientSpy = jest.fn().mockReturnValue('ES Response'); - response = await anomalySeriesFetcher({ - serviceName: 'myServiceName', - transactionType: 'myTransactionType', - intervalString: 'myInterval', - mlBucketSize: 10, - setup: { - ml: { - mlSystem: { - mlAnomalySearch: clientSpy, - }, - } as any, - start: 100000, - end: 200000, - } as any, - }); - }); - - it('should call client with correct query', () => { - expect(clientSpy.mock.calls).toMatchSnapshot(); - }); - - it('should return correct response', () => { - expect(response).toBe('ES Response'); - }); - }); - - it('should swallow HTTP errors', () => { - const httpError = new Error('anomaly lookup failed') as any; - httpError.statusCode = 418; - const failedRequestSpy = jest.fn(() => Promise.reject(httpError)); - - return expect( - anomalySeriesFetcher({ - setup: { - ml: { - mlSystem: { - mlAnomalySearch: failedRequestSpy, - }, - } as any, - }, - } as any) - ).resolves.toEqual(undefined); - }); - - it('should throw other errors', () => { - const otherError = new Error('anomaly lookup ASPLODED') as any; - const failedRequestSpy = jest.fn(() => Promise.reject(otherError)); - - return expect( - anomalySeriesFetcher({ - setup: { - ml: { - mlSystem: { - mlAnomalySearch: failedRequestSpy, - }, - } as any, - }, - } as any) - ).rejects.toThrow(otherError); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts deleted file mode 100644 index 8ee078de7f3ce1..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 { getMlJobId } from '../../../../../common/ml_job_constants'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; - -export type ESResponse = Exclude< - PromiseReturnType, - undefined ->; - -export async function anomalySeriesFetcher({ - serviceName, - transactionType, - intervalString, - mlBucketSize, - setup, -}: { - serviceName: string; - transactionType: string; - intervalString: string; - mlBucketSize: number; - setup: Setup & SetupTimeRange; -}) { - const { ml, start, end } = setup; - if (!ml) { - return; - } - - // move the start back with one bucket size, to ensure to get anomaly data in the beginning - // this is required because ML has a minimum bucket size (default is 900s) so if our buckets are smaller, we might have several null buckets in the beginning - const newStart = start - mlBucketSize * 1000; - const jobId = getMlJobId(serviceName, transactionType); - - const params = { - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { job_id: jobId } }, - { exists: { field: 'bucket_span' } }, - { - range: { - timestamp: { - gte: newStart, - lte: end, - format: 'epoch_millis', - }, - }, - }, - ], - }, - }, - aggs: { - ml_avg_response_times: { - date_histogram: { - field: 'timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { - min: newStart, - max: end, - }, - }, - aggs: { - anomaly_score: { max: { field: 'anomaly_score' } }, - lower: { min: { field: 'model_lower' } }, - upper: { max: { field: 'model_upper' } }, - }, - }, - }, - }, - }; - - try { - const response = await ml.mlSystem.mlAnomalySearch(params); - return response; - } catch (err) { - const isHttpError = 'statusCode' in err; - if (isHttpError) { - return; - } - throw err; - } -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts deleted file mode 100644 index d649bfb1927390..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 { getMlJobId } from '../../../../../common/ml_job_constants'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; - -interface IOptions { - serviceName: string; - transactionType: string; - setup: Setup & SetupTimeRange; -} - -interface ESResponse { - bucket_span: number; -} - -export async function getMlBucketSize({ - serviceName, - transactionType, - setup, -}: IOptions): Promise { - const { ml, start, end } = setup; - if (!ml) { - return 0; - } - const jobId = getMlJobId(serviceName, transactionType); - - const params = { - body: { - _source: 'bucket_span', - size: 1, - query: { - bool: { - filter: [ - { term: { job_id: jobId } }, - { exists: { field: 'bucket_span' } }, - { - range: { - timestamp: { - gte: start, - lte: end, - format: 'epoch_millis', - }, - }, - }, - ], - }, - }, - }, - }; - - try { - const resp = await ml.mlSystem.mlAnomalySearch(params); - return resp.hits.hits[0]?._source.bucket_span || 0; - } catch (err) { - const isHttpError = 'statusCode' in err; - if (isHttpError) { - return 0; - } - throw err; - } -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts deleted file mode 100644 index fb87f1b5707d14..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 { getAnomalySeries } from '.'; -import { mlAnomalyResponse } from './mock_responses/ml_anomaly_response'; -import { mlBucketSpanResponse } from './mock_responses/ml_bucket_span_response'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; -import { APMConfig } from '../../../..'; - -describe('getAnomalySeries', () => { - let avgAnomalies: PromiseReturnType; - beforeEach(async () => { - const clientSpy = jest - .fn() - .mockResolvedValueOnce(mlBucketSpanResponse) - .mockResolvedValueOnce(mlAnomalyResponse); - - avgAnomalies = await getAnomalySeries({ - serviceName: 'myServiceName', - transactionType: 'myTransactionType', - transactionName: undefined, - timeSeriesDates: [100, 100000], - setup: { - start: 0, - end: 500000, - client: { search: () => {} } as any, - internalClient: { search: () => {} } as any, - config: new Proxy( - {}, - { - get: () => 'myIndex', - } - ) as APMConfig, - uiFiltersES: [], - indices: { - 'apm_oss.sourcemapIndices': 'myIndex', - 'apm_oss.errorIndices': 'myIndex', - 'apm_oss.onboardingIndices': 'myIndex', - 'apm_oss.spanIndices': 'myIndex', - 'apm_oss.transactionIndices': 'myIndex', - 'apm_oss.metricsIndices': 'myIndex', - apmAgentConfigurationIndex: 'myIndex', - apmCustomLinkIndex: 'myIndex', - }, - dynamicIndexPattern: null as any, - ml: { - mlSystem: { - mlAnomalySearch: clientSpy, - mlCapabilities: async () => ({ isPlatinumOrTrialLicense: true }), - }, - } as any, - }, - }); - }); - - it('should remove buckets lower than threshold and outside date range from anomalyScore', () => { - expect(avgAnomalies!.anomalyScore).toEqual([ - { x0: 15000, x: 25000 }, - { x0: 25000, x: 35000 }, - ]); - }); - - it('should remove buckets outside date range from anomalyBoundaries', () => { - expect( - avgAnomalies!.anomalyBoundaries!.filter( - (bucket) => bucket.x < 100 || bucket.x > 100000 - ).length - ).toBe(0); - }); - - it('should remove buckets with null from anomalyBoundaries', () => { - expect( - avgAnomalies!.anomalyBoundaries!.filter((p) => p.y === null).length - ).toBe(0); - }); - - it('should match snapshot', async () => { - expect(avgAnomalies).toMatchSnapshot(); - }); -}); 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 6f44cfa1df9f06..b2d11f2ffe19a6 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 @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getBucketSize } from '../../../helpers/get_bucket_size'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../../../helpers/setup_request'; -import { anomalySeriesFetcher } from './fetcher'; -import { getMlBucketSize } from './get_ml_bucket_size'; -import { anomalySeriesTransform } from './transform'; +import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries'; + +interface AnomalyTimeseries { + anomalyBoundaries: Coordinate[]; + anomalyScore: RectCoordinate[]; +} export async function getAnomalySeries({ serviceName, @@ -26,7 +28,7 @@ export async function getAnomalySeries({ transactionName: string | undefined; timeSeriesDates: number[]; setup: Setup & SetupTimeRange & SetupUIFilters; -}) { +}): Promise { // don't fetch anomalies for transaction details page if (transactionName) { return; @@ -53,29 +55,6 @@ export async function getAnomalySeries({ return; } - const mlBucketSize = await getMlBucketSize({ - serviceName, - transactionType, - setup, - }); - - const { start, end } = setup; - const { intervalString, bucketSize } = getBucketSize(start, end, 'auto'); - - const esResponse = await anomalySeriesFetcher({ - serviceName, - transactionType, - intervalString, - mlBucketSize, - setup, - }); - - return esResponse - ? anomalySeriesTransform( - esResponse, - mlBucketSize, - bucketSize, - timeSeriesDates - ) - : undefined; + // TODO [APM ML] return a series of anomaly scores, upper & lower bounds for the given timeSeriesDates + return; } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts deleted file mode 100644 index 523161ec102759..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 { ESResponse } from '../fetcher'; - -export const mlAnomalyResponse: ESResponse = ({ - took: 3, - timed_out: false, - _shards: { - total: 5, - successful: 5, - skipped: 0, - failed: 0, - }, - hits: { - total: 10, - max_score: 0, - hits: [], - }, - aggregations: { - ml_avg_response_times: { - buckets: [ - { - key_as_string: '2018-07-02T09:16:40.000Z', - key: 0, - doc_count: 0, - anomaly_score: { - value: null, - }, - upper: { - value: 200, - }, - lower: { - value: 20, - }, - }, - { - key_as_string: '2018-07-02T09:25:00.000Z', - key: 5000, - doc_count: 4, - anomaly_score: { - value: null, - }, - upper: { - value: null, - }, - lower: { - value: null, - }, - }, - { - key_as_string: '2018-07-02T09:33:20.000Z', - key: 10000, - doc_count: 0, - anomaly_score: { - value: null, - }, - upper: { - value: null, - }, - lower: { - value: null, - }, - }, - { - key_as_string: '2018-07-02T09:41:40.000Z', - key: 15000, - doc_count: 2, - anomaly_score: { - value: 90, - }, - upper: { - value: 100, - }, - lower: { - value: 20, - }, - }, - { - key_as_string: '2018-07-02T09:50:00.000Z', - key: 20000, - doc_count: 0, - anomaly_score: { - value: null, - }, - upper: { - value: null, - }, - lower: { - value: null, - }, - }, - { - key_as_string: '2018-07-02T09:58:20.000Z', - key: 25000, - doc_count: 2, - anomaly_score: { - value: 100, - }, - upper: { - value: 50, - }, - lower: { - value: 10, - }, - }, - { - key_as_string: '2018-07-02T10:15:00.000Z', - key: 30000, - doc_count: 2, - anomaly_score: { - value: 0, - }, - upper: { - value: null, - }, - lower: { - value: null, - }, - }, - ], - }, - }, -} as unknown) as ESResponse; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts deleted file mode 100644 index 3689529a07c4a9..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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. - */ - -export const mlBucketSpanResponse = { - took: 1, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: 192, - max_score: 1.0, - hits: [ - { - _index: '.ml-anomalies-shared', - _id: - 'opbeans-go-request-high_mean_response_time_model_plot_1542636000000_900_0_29791_0', - _score: 1.0, - _source: { - bucket_span: 10, - }, - }, - ], - }, -}; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts deleted file mode 100644 index eb94c83e92576d..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -/* - * 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 { ESResponse } from './fetcher'; -import { mlAnomalyResponse } from './mock_responses/ml_anomaly_response'; -import { anomalySeriesTransform, replaceFirstAndLastBucket } from './transform'; - -describe('anomalySeriesTransform', () => { - it('should match snapshot', () => { - const getMlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [10000, 25000]; - const anomalySeries = anomalySeriesTransform( - mlAnomalyResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - expect(anomalySeries).toMatchSnapshot(); - }); - - describe('anomalyScoreSeries', () => { - it('should only returns bucket within range and above threshold', () => { - const esResponse = getESResponse([ - { - key: 0, - anomaly_score: { value: 90 }, - }, - { - key: 5000, - anomaly_score: { value: 0 }, - }, - { - key: 10000, - anomaly_score: { value: 90 }, - }, - { - key: 15000, - anomaly_score: { value: 0 }, - }, - { - key: 20000, - anomaly_score: { value: 90 }, - }, - ]); - - const getMlBucketSize = 5; - const bucketSize = 5; - const timeSeriesDates = [5000, 15000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyScore; - expect(buckets).toEqual([{ x0: 10000, x: 15000 }]); - }); - - it('should decrease the x-value to avoid going beyond last date', () => { - const esResponse = getESResponse([ - { - key: 0, - anomaly_score: { value: 0 }, - }, - { - key: 5000, - anomaly_score: { value: 90 }, - }, - ]); - - const getMlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [0, 10000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyScore; - expect(buckets).toEqual([{ x0: 5000, x: 10000 }]); - }); - }); - - describe('anomalyBoundariesSeries', () => { - it('should trim buckets to time range', () => { - const esResponse = getESResponse([ - { - key: 0, - upper: { value: 15 }, - lower: { value: 10 }, - }, - { - key: 5000, - upper: { value: 25 }, - lower: { value: 20 }, - }, - { - key: 10000, - upper: { value: 35 }, - lower: { value: 30 }, - }, - { - key: 15000, - upper: { value: 45 }, - lower: { value: 40 }, - }, - ]); - - const mlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [5000, 10000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - mlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyBoundaries; - expect(buckets).toEqual([ - { x: 5000, y: 25, y0: 20 }, - { x: 10000, y: 35, y0: 30 }, - ]); - }); - - it('should replace first bucket in range', () => { - const esResponse = getESResponse([ - { - key: 0, - anomaly_score: { value: 0 }, - upper: { value: 15 }, - lower: { value: 10 }, - }, - { - key: 5000, - anomaly_score: { value: 0 }, - upper: { value: null }, - lower: { value: null }, - }, - { - key: 10000, - anomaly_score: { value: 0 }, - upper: { value: 25 }, - lower: { value: 20 }, - }, - ]); - - const getMlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [5000, 10000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyBoundaries; - expect(buckets).toEqual([ - { x: 5000, y: 15, y0: 10 }, - { x: 10000, y: 25, y0: 20 }, - ]); - }); - - it('should replace last bucket in range', () => { - const esResponse = getESResponse([ - { - key: 0, - anomaly_score: { value: 0 }, - upper: { value: 15 }, - lower: { value: 10 }, - }, - { - key: 5000, - anomaly_score: { value: 0 }, - upper: { value: null }, - lower: { value: null }, - }, - { - key: 10000, - anomaly_score: { value: 0 }, - upper: { value: null }, - lower: { value: null }, - }, - ]); - - const getMlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [5000, 10000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyBoundaries; - expect(buckets).toEqual([ - { x: 5000, y: 15, y0: 10 }, - { x: 10000, y: 15, y0: 10 }, - ]); - }); - }); -}); - -describe('replaceFirstAndLastBucket', () => { - it('should extend the first bucket', () => { - const buckets = [ - { - x: 0, - lower: 10, - upper: 20, - }, - { - x: 5, - lower: null, - upper: null, - }, - { - x: 10, - lower: null, - upper: null, - }, - { - x: 15, - lower: 30, - upper: 40, - }, - ]; - - const timeSeriesDates = [10, 15]; - expect(replaceFirstAndLastBucket(buckets as any, timeSeriesDates)).toEqual([ - { x: 10, lower: 10, upper: 20 }, - { x: 15, lower: 30, upper: 40 }, - ]); - }); - - it('should extend the last bucket', () => { - const buckets = [ - { - x: 10, - lower: 30, - upper: 40, - }, - { - x: 15, - lower: null, - upper: null, - }, - { - x: 20, - lower: null, - upper: null, - }, - ] as any; - - const timeSeriesDates = [10, 15, 20]; - expect(replaceFirstAndLastBucket(buckets, timeSeriesDates)).toEqual([ - { x: 10, lower: 30, upper: 40 }, - { x: 15, lower: null, upper: null }, - { x: 20, lower: 30, upper: 40 }, - ]); - }); -}); - -function getESResponse(buckets: any): ESResponse { - return ({ - took: 3, - timed_out: false, - _shards: { - total: 5, - successful: 5, - skipped: 0, - failed: 0, - }, - hits: { - total: 10, - max_score: 0, - hits: [], - }, - aggregations: { - ml_avg_response_times: { - buckets: buckets.map((bucket: any) => { - return { - ...bucket, - lower: { value: bucket?.lower?.value || null }, - upper: { value: bucket?.upper?.value || null }, - anomaly_score: { - value: bucket?.anomaly_score?.value || null, - }, - }; - }), - }, - }, - } as unknown) as ESResponse; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts deleted file mode 100644 index 454a6add3e2562..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 { first, last } from 'lodash'; -import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries'; -import { ESResponse } from './fetcher'; - -type IBucket = ReturnType; -function getBucket( - bucket: Required< - ESResponse - >['aggregations']['ml_avg_response_times']['buckets'][0] -) { - return { - x: bucket.key, - anomalyScore: bucket.anomaly_score.value, - lower: bucket.lower.value, - upper: bucket.upper.value, - }; -} - -export type AnomalyTimeSeriesResponse = ReturnType< - typeof anomalySeriesTransform ->; -export function anomalySeriesTransform( - response: ESResponse, - mlBucketSize: number, - bucketSize: number, - timeSeriesDates: number[] -) { - const buckets = - response.aggregations?.ml_avg_response_times.buckets.map(getBucket) || []; - - const bucketSizeInMillis = Math.max(bucketSize, mlBucketSize) * 1000; - - return { - anomalyScore: getAnomalyScoreDataPoints( - buckets, - timeSeriesDates, - bucketSizeInMillis - ), - anomalyBoundaries: getAnomalyBoundaryDataPoints(buckets, timeSeriesDates), - }; -} - -export function getAnomalyScoreDataPoints( - buckets: IBucket[], - timeSeriesDates: number[], - bucketSizeInMillis: number -): RectCoordinate[] { - const ANOMALY_THRESHOLD = 75; - const firstDate = first(timeSeriesDates); - const lastDate = last(timeSeriesDates); - - return buckets - .filter( - (bucket) => - bucket.anomalyScore !== null && bucket.anomalyScore > ANOMALY_THRESHOLD - ) - .filter(isInDateRange(firstDate, lastDate)) - .map((bucket) => { - return { - x0: bucket.x, - x: Math.min(bucket.x + bucketSizeInMillis, lastDate), // don't go beyond last date - }; - }); -} - -export function getAnomalyBoundaryDataPoints( - buckets: IBucket[], - timeSeriesDates: number[] -): Coordinate[] { - return replaceFirstAndLastBucket(buckets, timeSeriesDates) - .filter((bucket) => bucket.lower !== null) - .map((bucket) => { - return { - x: bucket.x, - y0: bucket.lower, - y: bucket.upper, - }; - }); -} - -export function replaceFirstAndLastBucket( - buckets: IBucket[], - timeSeriesDates: number[] -) { - const firstDate = first(timeSeriesDates); - const lastDate = last(timeSeriesDates); - - const preBucketWithValue = buckets - .filter((p) => p.x <= firstDate) - .reverse() - .find((p) => p.lower !== null); - - const bucketsInRange = buckets.filter(isInDateRange(firstDate, lastDate)); - - // replace first bucket if it is null - const firstBucket = first(bucketsInRange); - if (preBucketWithValue && firstBucket && firstBucket.lower === null) { - firstBucket.lower = preBucketWithValue.lower; - firstBucket.upper = preBucketWithValue.upper; - } - - const lastBucketWithValue = [...buckets] - .reverse() - .find((p) => p.lower !== null); - - // replace last bucket if it is null - const lastBucket = last(bucketsInRange); - if (lastBucketWithValue && lastBucket && lastBucket.lower === null) { - lastBucket.lower = lastBucketWithValue.lower; - lastBucket.upper = lastBucketWithValue.upper; - } - - return bucketsInRange; -} - -// anomaly time series contain one or more buckets extra in the beginning -// these extra buckets should be removed -function isInDateRange(firstDate: number, lastDate: number) { - return (p: IBucket) => p.x >= firstDate && p.x <= lastDate; -} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 14a58ec595abc8..0d85960807f93b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4296,21 +4296,6 @@ "xpack.apm.serviceDetails.alertsMenu.errorRate": "エラー率", "xpack.apm.serviceDetails.alertsMenu.transactionDuration": "トランザクション期間", "xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "アクティブアラートを表示", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "現在 {serviceName} ({transactionType}) の実行中のジョブがあります。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "既存のジョブを表示", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle": "ジョブが既に存在します", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText": "トランザクション時間のグラフ", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel": "ジョブを作成", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "異常検知を有効にする", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText": "現在 {serviceName} ({transactionType}) の分析を実行中です。応答時間グラフに結果が追加されるまで少し時間がかかる場合があります。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText": "ジョブを表示", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle": "ジョブが作成されました", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText": "現在のライセンスでは機械学習ジョブの作成が許可されていないか、ジョブが既に存在する可能性があります。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle": "ジョブの作成に失敗", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription": "ジョブはそれぞれのサービス + トランザクションタイプの組み合わせに対して作成できます。ジョブの作成後、{mlJobsPageLink} で管理と詳細の確認ができます。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText": "機械学習ジョブの管理ページ", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.noteText": "注:ジョブが結果の計算を開始するまでに少し時間がかかる場合があります。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.selectTransactionTypeLabel": "このジョブのトランザクションタイプを選択してください", "xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription": "レポートはメールで送信するか Slack チャンネルに投稿できます。各レポートにはオカランス別のトップ 10 のエラーが含まれます。", "xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle": "アクション", "xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle": "コンディション", @@ -4346,8 +4331,6 @@ "xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText": "ユーザーにウォッチ作成のパーミッションがあることを確認してください。", "xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle": "ウォッチの作成に失敗", "xpack.apm.serviceDetails.errorsTabLabel": "エラー", - "xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonLabel": "ML 異常検知を有効にする", - "xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonTooltip": "このサービスの機械学習ジョブをセットアップします", "xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel": "ウォッチエラーレポートを有効にする", "xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel": "統合", "xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel": "既存のウォッチを表示", @@ -4357,9 +4340,6 @@ "xpack.apm.serviceDetails.metricsTabLabel": "メトリック", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "トランザクション", - "xpack.apm.serviceMap.anomalyDetectionPopoverLink": "異常を表示", - "xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric": "スコア(最大)", - "xpack.apm.serviceMap.anomalyDetectionPopoverTitle": "異常検知", "xpack.apm.serviceMap.avgCpuUsagePopoverMetric": "CPU使用状況 (平均)", "xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric": "1分あたりのエラー(平均)", "xpack.apm.serviceMap.avgMemoryUsagePopoverMetric": "メモリー使用状況(平均)", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9c58aeba1dbaa1..85167e11b28baf 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4299,21 +4299,6 @@ "xpack.apm.serviceDetails.alertsMenu.errorRate": "错误率", "xpack.apm.serviceDetails.alertsMenu.transactionDuration": "事务持续时间", "xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "查看活动的告警", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "当前有 {serviceName}({transactionType})的作业正在运行。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "查看现有作业", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle": "作业已存在", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText": "事务持续时间图表", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel": "创建作业", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "启用异常检测", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText": "现在正在运行对 {serviceName}({transactionType})的分析。可能要花费点时间,才会将结果添加响应时间图表。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText": "查看作业", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle": "作业已成功创建", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText": "您当前的许可可能不允许创建 Machine Learning 作业,或者此作业可能已存在。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle": "作业创建失败", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription": "可以创建每个服务 + 事务类型组合的作业。创建作业后,可以在 {mlJobsPageLink}中管理作业以及查看更多详细信息。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText": "Machine Learning 作业管理页面", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.noteText": "注意:可能要过几分钟后,作业才会开始计算结果。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.selectTransactionTypeLabel": "为此作业选择事务类型", "xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription": "可以通过电子邮件发送报告或将报告发布到 Slack 频道。每个报告将包括按发生次数排序的前 10 个错误。", "xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle": "操作", "xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle": "条件", @@ -4349,8 +4334,6 @@ "xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText": "确保您的用户有权创建监视。", "xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle": "监视创建失败", "xpack.apm.serviceDetails.errorsTabLabel": "错误", - "xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonLabel": "启用 ML 异常检测", - "xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonTooltip": "为此服务设置 Machine Learning 作业", "xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel": "启用 Watcher 错误报告", "xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel": "集成", "xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel": "查看现有监视", @@ -4360,9 +4343,6 @@ "xpack.apm.serviceDetails.metricsTabLabel": "指标", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "事务", - "xpack.apm.serviceMap.anomalyDetectionPopoverLink": "查看异常", - "xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric": "分数(最大)", - "xpack.apm.serviceMap.anomalyDetectionPopoverTitle": "异常检测", "xpack.apm.serviceMap.avgCpuUsagePopoverMetric": "CPU 使用(平均)", "xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric": "每分钟错误数(平均)", "xpack.apm.serviceMap.avgMemoryUsagePopoverMetric": "内存使用(平均)",