diff --git a/x-pack/plugins/apm/common/search_strategies/failure_correlations/constants.ts b/x-pack/plugins/apm/common/search_strategies/failure_correlations/constants.ts new file mode 100644 index 00000000000000..a80918f0e399e2 --- /dev/null +++ b/x-pack/plugins/apm/common/search_strategies/failure_correlations/constants.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY = + 'apmFailedTransactionsCorrelationsSearchStrategy'; + +export const FAILED_TRANSACTIONS_IMPACT_THRESHOLD = { + HIGH: i18n.translate( + 'xpack.apm.correlations.failedTransactions.highImpactText', + { + defaultMessage: 'High', + } + ), + MEDIUM: i18n.translate( + 'xpack.apm.correlations.failedTransactions.mediumImpactText', + { + defaultMessage: 'Medium', + } + ), + LOW: i18n.translate( + 'xpack.apm.correlations.failedTransactions.lowImpactText', + { + defaultMessage: 'Low', + } + ), +} as const; diff --git a/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts new file mode 100644 index 00000000000000..08e05d46ba0136 --- /dev/null +++ b/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants'; + +export interface FailedTransactionsCorrelationValue { + key: string; + doc_count: number; + bg_count: number; + score: number; + pValue: number | null; + fieldName: string; + fieldValue: string; +} + +export type FailureCorrelationImpactThreshold = typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD[keyof typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD]; + +export interface CorrelationsTerm { + fieldName: string; + fieldValue: string; +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx index 62d566963699de..28f671183ed876 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx @@ -7,32 +7,17 @@ import React, { useCallback, useMemo, useState } from 'react'; import { debounce } from 'lodash'; -import { - EuiIcon, - EuiLink, - EuiBasicTable, - EuiBasicTableColumn, - EuiToolTip, -} from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useHistory } from 'react-router-dom'; -import { asInteger, asPercent } from '../../../../common/utils/formatters'; -import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { createHref, push } from '../../shared/Links/url_helpers'; -import { ImpactBar } from '../../shared/ImpactBar'; import { useUiTracker } from '../../../../../observability/public'; import { useTheme } from '../../../hooks/use_theme'; +import { CorrelationsTerm } from '../../../../common/search_strategies/failure_correlations/types'; const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50]; -type CorrelationsApiResponse = - | APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> - | APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'>; -export type SignificantTerm = CorrelationsApiResponse['significantTerms'][0]; - -export type SelectedSignificantTerm = Pick< - SignificantTerm, +export type SelectedCorrelationTerm = Pick< + T, 'fieldName' | 'fieldValue' >; @@ -40,24 +25,22 @@ interface Props { significantTerms?: T[]; status: FETCH_STATUS; percentageColumnName?: string; - setSelectedSignificantTerm: (term: SelectedSignificantTerm | null) => void; + setSelectedSignificantTerm: (term: T | null) => void; selectedTerm?: { fieldName: string; fieldValue: string }; - onFilter: () => void; - columns?: Array>; + onFilter?: () => void; + columns: Array>; } -export function CorrelationsTable({ +export function CorrelationsTable({ significantTerms, status, - percentageColumnName, setSelectedSignificantTerm, - onFilter, columns, selectedTerm, }: Props) { const euiTheme = useTheme(); const trackApmEvent = useUiTracker({ app: 'apm' }); - const trackSelectSignificantTerm = useCallback( + const trackSelectSignificantCorrelationTerm = useCallback( () => debounce( () => trackApmEvent({ metric: 'select_significant_term' }), @@ -65,7 +48,6 @@ export function CorrelationsTable({ ), [trackApmEvent] ); - const history = useHistory(); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); @@ -92,140 +74,6 @@ export function CorrelationsTable({ setPageSize(size); }, []); - const tableColumns: Array> = columns ?? [ - { - width: '116px', - field: 'impact', - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.impactLabel', - { defaultMessage: 'Impact' } - ), - render: (_: any, term: T) => { - return ; - }, - }, - { - field: 'percentage', - name: - percentageColumnName ?? - i18n.translate( - 'xpack.apm.correlations.correlationsTable.percentageLabel', - { defaultMessage: 'Percentage' } - ), - render: (_: any, term: T) => { - return ( - - <>{asPercent(term.valueCount, term.fieldCount)} - - ); - }, - }, - { - field: 'fieldName', - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.fieldNameLabel', - { defaultMessage: 'Field name' } - ), - }, - { - field: 'fieldValue', - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.fieldValueLabel', - { defaultMessage: 'Field value' } - ), - render: (_: any, term: T) => String(term.fieldValue).slice(0, 50), - }, - { - width: '100px', - actions: [ - { - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.filterLabel', - { defaultMessage: 'Filter' } - ), - description: i18n.translate( - 'xpack.apm.correlations.correlationsTable.filterDescription', - { defaultMessage: 'Filter by value' } - ), - icon: 'plusInCircle', - type: 'icon', - onClick: (term: T) => { - push(history, { - query: { - kuery: `${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onFilter(); - trackApmEvent({ metric: 'correlations_term_include_filter' }); - }, - }, - { - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.excludeLabel', - { defaultMessage: 'Exclude' } - ), - description: i18n.translate( - 'xpack.apm.correlations.correlationsTable.excludeDescription', - { defaultMessage: 'Filter out value' } - ), - icon: 'minusInCircle', - type: 'icon', - onClick: (term: T) => { - push(history, { - query: { - kuery: `not ${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onFilter(); - trackApmEvent({ metric: 'correlations_term_exclude_filter' }); - }, - }, - ], - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.actionsLabel', - { defaultMessage: 'Filter' } - ), - render: (_: any, term: T) => { - return ( - <> - - - -  /  - - - - - ); - }, - }, - ]; - return ( ({ status === FETCH_STATUS.LOADING ? loadingText : noDataText } loading={status === FETCH_STATUS.LOADING} - columns={tableColumns} + columns={columns} rowProps={(term) => { return { onMouseEnter: () => { setSelectedSignificantTerm(term); - trackSelectSignificantTerm(); + trackSelectSignificantCorrelationTerm(); }, onMouseLeave: () => setSelectedSignificantTerm(null), style: diff --git a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx deleted file mode 100644 index 2e5887cab9918c..00000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx +++ /dev/null @@ -1,281 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Axis, - Chart, - CurveType, - LineSeries, - Position, - ScaleType, - Settings, - timeFormatter, -} from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { useUiTracker } from '../../../../../observability/public'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { useLocalStorage } from '../../../hooks/useLocalStorage'; -import { useApmParams } from '../../../hooks/use_apm_params'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; -import { useTheme } from '../../../hooks/use_theme'; -import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { ChartContainer } from '../../shared/charts/chart_container'; -import { - CorrelationsTable, - SelectedSignificantTerm, -} from './correlations_table'; -import { CustomFields } from './custom_fields'; -import { useFieldNames } from './use_field_names'; - -type OverallErrorsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/errors/overall_timeseries'> ->; - -type CorrelationsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> ->; - -export function ErrorCorrelations() { - const [ - selectedSignificantTerm, - setSelectedSignificantTerm, - ] = useState(null); - - const { serviceName } = useApmServiceContext(); - const { urlParams } = useUrlParams(); - const { transactionName, transactionType, start, end } = urlParams; - const { defaultFieldNames } = useFieldNames(); - const [fieldNames, setFieldNames] = useLocalStorage( - `apm.correlations.errors.fields:${serviceName}`, - defaultFieldNames - ); - const hasFieldNames = fieldNames.length > 0; - - const { - query: { environment, kuery }, - } = useApmParams('/services/:serviceName'); - - const { data: overallData, status: overallStatus } = useFetcher( - (callApmApi) => { - if (start && end) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - }, - }, - }); - } - }, - [ - environment, - kuery, - serviceName, - start, - end, - transactionName, - transactionType, - ] - ); - - const { data: correlationsData, status: correlationsStatus } = useFetcher( - (callApmApi) => { - if (start && end && hasFieldNames) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/errors/failed_transactions', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - fieldNames: fieldNames.join(','), - }, - }, - }); - } - }, - [ - environment, - kuery, - serviceName, - start, - end, - transactionName, - transactionType, - fieldNames, - hasFieldNames, - ] - ); - - const trackApmEvent = useUiTracker({ app: 'apm' }); - trackApmEvent({ metric: 'view_failed_transactions' }); - - const onFilter = () => {}; - - return ( - <> - - - -

- {i18n.translate('xpack.apm.correlations.error.description', { - defaultMessage: - 'Why are some transactions failing and returning errors? Correlations will help discover a possible culprit in a particular cohort of your data. Either by host, version, or other custom fields.', - })} -

-
-
- - -

- {i18n.translate('xpack.apm.correlations.error.chart.title', { - defaultMessage: 'Error rate over time', - })} -

-
-
- - - - - - - - - -
- - ); -} - -function getSelectedTimeseries( - significantTerms: CorrelationsApiResponse['significantTerms'], - selectedSignificantTerm: SelectedSignificantTerm -) { - if (!significantTerms) { - return []; - } - return ( - significantTerms.find( - ({ fieldName, fieldValue }) => - selectedSignificantTerm.fieldName === fieldName && - selectedSignificantTerm.fieldValue === fieldValue - )?.timeseries || [] - ); -} - -function ErrorTimeseriesChart({ - overallData, - correlationsData, - selectedSignificantTerm, - status, -}: { - overallData?: OverallErrorsApiResponse; - correlationsData?: CorrelationsApiResponse; - selectedSignificantTerm: SelectedSignificantTerm | null; - status: FETCH_STATUS; -}) { - const theme = useTheme(); - const dateFormatter = timeFormatter('HH:mm:ss'); - - return ( - - - - - - `${roundFloat(d * 100)}%`} - /> - - - - {correlationsData && selectedSignificantTerm ? ( - - ) : null} - - - ); -} - -function roundFloat(n: number, digits = 2) { - const factor = Math.pow(10, digits); - return Math.round(n * factor) / factor; -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx new file mode 100644 index 00000000000000..3ec663ba368482 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -0,0 +1,437 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useMemo, useState } from 'react'; +import { + EuiCallOut, + EuiCode, + EuiAccordion, + EuiPanel, + EuiBasicTableColumn, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, + EuiText, + EuiBadge, + EuiIcon, + EuiLink, + EuiTitle, + EuiBetaBadge, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory } from 'react-router-dom'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { CorrelationsTable } from './correlations_table'; +import { enableInspectEsQueries } from '../../../../../observability/public'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; +import { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; +import { ImpactBar } from '../../shared/ImpactBar'; +import { isErrorMessage } from './utils/is_error_message'; +import { Summary } from '../../shared/Summary'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { getFailedTransactionsCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label'; +import { createHref, push } from '../../shared/Links/url_helpers'; +import { useUiTracker } from '../../../../../observability/public'; +import { useFailedTransactionsCorrelationsFetcher } from '../../../hooks/use_failed_transactions_correlations_fetcher'; +import { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import { useApmParams } from '../../../hooks/use_apm_params'; + +export function FailedTransactionsCorrelations() { + const { + core: { notifications, uiSettings }, + } = useApmPluginContext(); + const trackApmEvent = useUiTracker({ app: 'apm' }); + + const { serviceName, transactionType } = useApmServiceContext(); + + const { + query: { kuery, environment }, + } = useApmParams('/services/:serviceName'); + + const { urlParams } = useUrlParams(); + const { transactionName, start, end } = urlParams; + + const displayLog = uiSettings.get(enableInspectEsQueries); + + const searchServicePrams: SearchServiceParams = { + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + }; + + const result = useFailedTransactionsCorrelationsFetcher(searchServicePrams); + + const { + ccsWarning, + log, + error, + isRunning, + progress, + startFetch, + cancelFetch, + } = result; + // start fetching on load + // we want this effect to execute exactly once after the component mounts + useEffect(() => { + startFetch(); + + return () => { + // cancel any running async partial request when unmounting the component + // we want this effect to execute exactly once after the component mounts + cancelFetch(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [ + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); + + const selectedTerm = useMemo(() => { + if (selectedSignificantTerm) return selectedSignificantTerm; + return result?.values && + Array.isArray(result.values) && + result.values.length > 0 + ? result?.values[0] + : undefined; + }, [selectedSignificantTerm, result]); + + const history = useHistory(); + + const failedTransactionsCorrelationsColumns: Array< + EuiBasicTableColumn + > = useMemo( + () => [ + { + width: '116px', + field: 'normalizedScore', + name: ( + <> + {i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.pValueLabel', + { + defaultMessage: 'Score', + } + )} + + ), + render: (normalizedScore: number) => { + return ( + <> + + + ); + }, + }, + { + width: '116px', + field: 'pValue', + name: ( + <> + {i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.impactLabel', + { + defaultMessage: 'Impact', + } + )} + + ), + render: getFailedTransactionsCorrelationImpactLabel, + }, + { + field: 'fieldName', + name: i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.fieldNameLabel', + { defaultMessage: 'Field name' } + ), + }, + { + field: 'key', + name: i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.fieldValueLabel', + { defaultMessage: 'Field value' } + ), + render: (fieldValue: string) => String(fieldValue).slice(0, 50), + }, + { + width: '100px', + actions: [ + { + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.filterLabel', + { defaultMessage: 'Filter' } + ), + description: i18n.translate( + 'xpack.apm.correlations.correlationsTable.filterDescription', + { defaultMessage: 'Filter by value' } + ), + icon: 'plusInCircle', + type: 'icon', + onClick: (term: FailedTransactionsCorrelationValue) => { + push(history, { + query: { + kuery: `${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_include_filter' }); + }, + }, + { + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.excludeLabel', + { defaultMessage: 'Exclude' } + ), + description: i18n.translate( + 'xpack.apm.correlations.correlationsTable.excludeDescription', + { defaultMessage: 'Filter out value' } + ), + icon: 'minusInCircle', + type: 'icon', + onClick: (term: FailedTransactionsCorrelationValue) => { + push(history, { + query: { + kuery: `not ${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_exclude_filter' }); + }, + }, + ], + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.actionsLabel', + { defaultMessage: 'Filter' } + ), + render: (_: unknown, term: FailedTransactionsCorrelationValue) => { + return ( + <> + + + +  /  + + + + + ); + }, + }, + ], + [history, trackApmEvent] + ); + + useEffect(() => { + if (isErrorMessage(error)) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.correlations.failedTransactions.errorTitle', + { + defaultMessage: + 'An error occurred performing correlations on failed transactions', + } + ), + text: error.toString(), + }); + } + }, [error, notifications.toasts]); + return ( + <> + + + +
+ {i18n.translate( + 'xpack.apm.correlations.failedTransactions.panelTitle', + { + defaultMessage: 'Failed transactions', + } + )} +
+
+
+ + + + +
+ + + + {!isRunning && ( + + + + )} + {isRunning && ( + + + + )} + + + + + + + + + + + + + + + + + + {selectedTerm?.pValue != null ? ( + <> + + + {`${selectedTerm.fieldName}: ${selectedTerm.key}`} + , + <>{`p-value: ${selectedTerm.pValue.toPrecision(3)}`}, + ]} + /> + + + ) : null} + + columns={failedTransactionsCorrelationsColumns} + significantTerms={result?.values} + status={FETCH_STATUS.SUCCESS} + setSelectedSignificantTerm={setSelectedSignificantTerm} + selectedTerm={selectedTerm} + /> + + {ccsWarning && ( + <> + + +

+ {i18n.translate( + 'xpack.apm.correlations.failedTransactions.ccsWarningCalloutBody', + { + defaultMessage: + 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.15 and later versions.', + } + )} +

+
+ + )} + + + {log.length > 0 && displayLog && ( + + + {log.map((d, i) => { + const splitItem = d.split(': '); + return ( +

+ + {splitItem[0]} {splitItem[1]} + +

+ ); + })} +
+
+ )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx new file mode 100644 index 00000000000000..bebc889cc4ed97 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { HelpPopover, HelpPopoverButton } from '../help_popover/help_popover'; + +export function FailedTransactionsCorrelationsHelpPopover() { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + { + setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen); + }} + /> + } + closePopover={() => setIsPopoverOpen(false)} + isOpen={isPopoverOpen} + title={i18n.translate('xpack.apm.correlations.failurePopoverTitle', { + defaultMessage: 'Failure correlations', + })} + > +

+ +

+

+ +

+

+ +

+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 0a534ba1b945b4..bcf4d21baefd9c 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -30,10 +30,7 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_ import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTransactionLatencyCorrelationsFetcher } from '../../../hooks/use_transaction_latency_correlations_fetcher'; import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; -import { - CorrelationsTable, - SelectedSignificantTerm, -} from './correlations_table'; +import { CorrelationsTable } from './correlations_table'; import { push } from '../../shared/Links/url_helpers'; import { enableInspectEsQueries, @@ -43,11 +40,9 @@ import { asPreciseDecimal } from '../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { LatencyCorrelationsHelpPopover } from './latency_correlations_help_popover'; import { useApmParams } from '../../../hooks/use_apm_params'; +import { isErrorMessage } from './utils/is_error_message'; const DEFAULT_PERCENTILE_THRESHOLD = 95; -const isErrorMessage = (arg: unknown): arg is Error => { - return arg instanceof Error; -}; interface MlCorrelationsTerms { correlation: number; @@ -126,7 +121,7 @@ export function LatencyCorrelations() { const [ selectedSignificantTerm, setSelectedSignificantTerm, - ] = useState(null); + ] = useState(null); let selectedHistogram = histograms.length > 0 ? histograms[0] : undefined; @@ -376,10 +371,8 @@ export function LatencyCorrelations() {
{histograms.length > 0 && selectedHistogram !== undefined && ( - columns={mlCorrelationColumns} - // @ts-expect-error correlations don't have the same significant term other tables have significantTerms={histogramTerms} status={FETCH_STATUS.SUCCESS} setSelectedSignificantTerm={setSelectedSignificantTerm} diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts new file mode 100644 index 00000000000000..d133ed1060ebe6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failure_correlations/constants'; + +describe('getFailedTransactionsCorrelationImpactLabel', () => { + it('returns null if value is invalid ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(-0.03)).toBe(null); + expect(getFailedTransactionsCorrelationImpactLabel(NaN)).toBe(null); + expect(getFailedTransactionsCorrelationImpactLabel(Infinity)).toBe(null); + }); + + it('returns null if value is greater than or equal to the threshold ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(0.02)).toBe(null); + expect(getFailedTransactionsCorrelationImpactLabel(0.1)).toBe(null); + }); + + it('returns High if value is within [0, 1e-6) ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(0)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH + ); + expect(getFailedTransactionsCorrelationImpactLabel(1e-7)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH + ); + }); + + it('returns Medium if value is within [1e-6, 1e-3) ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(1e-6)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM + ); + expect(getFailedTransactionsCorrelationImpactLabel(1e-5)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM + ); + expect(getFailedTransactionsCorrelationImpactLabel(1e-4)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM + ); + }); + + it('returns Low if value is within [1e-3, 0.02) ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(1e-3)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW + ); + expect(getFailedTransactionsCorrelationImpactLabel(0.009)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW + ); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts new file mode 100644 index 00000000000000..af64c506170192 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FailureCorrelationImpactThreshold } from '../../../../../common/search_strategies/failure_correlations/types'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failure_correlations/constants'; + +export function getFailedTransactionsCorrelationImpactLabel( + pValue: number +): FailureCorrelationImpactThreshold | null { + // The lower the p value, the higher the impact + if (pValue >= 0 && pValue < 1e-6) + return FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH; + if (pValue >= 1e-6 && pValue < 0.001) + return FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM; + if (pValue >= 0.001 && pValue < 0.02) + return FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW; + + return null; +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/is_error_message.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/is_error_message.ts new file mode 100644 index 00000000000000..06eb75d6b33147 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/is_error_message.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const isErrorMessage = (arg: unknown): arg is Error => { + return arg instanceof Error; +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index c21d292c05c857..afb784dde55939 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -23,11 +23,9 @@ import { TransactionDistributionChart } from '../../../shared/charts/transaction import { useUiTracker } from '../../../../../../observability/public'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useApmParams } from '../../../../hooks/use_apm_params'; +import { isErrorMessage } from '../../correlations/utils/is_error_message'; const DEFAULT_PERCENTILE_THRESHOLD = 95; -const isErrorMessage = (arg: unknown): arg is Error => { - return arg instanceof Error; -}; interface Props { markerCurrentTransaction?: number; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx index e727aa4dfc5fd8..c1c74965ed27d5 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiBetaBadge } from '@elastic/eui'; - import { METRIC_TYPE, useTrackMetric, @@ -22,7 +20,7 @@ import { useLicenseContext } from '../../../context/license/use_license_context' import { LicensePrompt } from '../../shared/license_prompt'; -import { ErrorCorrelations } from '../correlations/error_correlations'; +import { FailedTransactionsCorrelations } from '../correlations/failed_transactions_correlations'; import type { TabContentProps } from './types'; @@ -42,7 +40,7 @@ function FailedTransactionsCorrelationsTab({}: TabContentProps) { useTrackMetric({ ...metric, delay: 15000 }); return hasActivePlatinumLicense ? ( - + ) : ( + )} ), component: FailedTransactionsCorrelationsTab, diff --git a/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts new file mode 100644 index 00000000000000..3841419e860fc7 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef, useState } from 'react'; +import type { Subscription } from 'rxjs'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + isCompleteResponse, + isErrorResponse, +} from '../../../../../src/plugins/data/public'; +import type { SearchServiceParams } from '../../common/search_strategies/correlations/types'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../plugin'; +import { FailedTransactionsCorrelationValue } from '../../common/search_strategies/failure_correlations/types'; +import { FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY } from '../../common/search_strategies/failure_correlations/constants'; + +interface RawResponse { + took: number; + values: FailedTransactionsCorrelationValue[]; + log: string[]; + ccsWarning: boolean; +} + +interface FailedTransactionsCorrelationsFetcherState { + error?: Error; + isComplete: boolean; + isRunning: boolean; + loaded: number; + ccsWarning: RawResponse['ccsWarning']; + values: RawResponse['values']; + log: RawResponse['log']; + timeTook?: number; + total: number; +} + +export const useFailedTransactionsCorrelationsFetcher = ( + params: Omit +) => { + const { + services: { data }, + } = useKibana(); + + const [ + fetchState, + setFetchState, + ] = useState({ + isComplete: false, + isRunning: false, + loaded: 0, + ccsWarning: false, + values: [], + log: [], + total: 100, + }); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + + function setResponse(response: IKibanaSearchResponse) { + setFetchState((prevState) => ({ + ...prevState, + isRunning: response.isRunning || false, + ccsWarning: response.rawResponse?.ccsWarning ?? false, + values: response.rawResponse?.values ?? [], + log: response.rawResponse?.log ?? [], + loaded: response.loaded!, + total: response.total!, + timeTook: response.rawResponse.took, + })); + } + + const startFetch = () => { + setFetchState((prevState) => ({ + ...prevState, + error: undefined, + isComplete: false, + })); + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + const req = { params }; + + // Submit the search request using the `data.search` service. + searchSubscription$.current = data.search + .search>(req, { + strategy: FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY, + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (res: IKibanaSearchResponse) => { + setResponse(res); + if (isCompleteResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + isRunnning: false, + isComplete: true, + })); + } else if (isErrorResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + error: (res as unknown) as Error, + setIsRunning: false, + })); + } + }, + error: (error: Error) => { + setFetchState((prevState) => ({ + ...prevState, + error, + setIsRunning: false, + })); + }, + }); + }; + + const cancelFetch = () => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + setFetchState((prevState) => ({ + ...prevState, + setIsRunning: false, + })); + }; + + return { + ...fetchState, + progress: fetchState.loaded / fetchState.total, + startFetch, + cancelFetch, + }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts new file mode 100644 index 00000000000000..9afe9d916b38e4 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from 'src/core/server'; +import { chunk } from 'lodash'; +import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; +import { asyncSearchServiceLogProvider } from '../correlations/async_search_service_log'; +import { asyncErrorCorrelationsSearchServiceStateProvider } from './async_search_service_state'; +import { fetchTransactionDurationFieldCandidates } from '../correlations/queries'; +import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { fetchFailedTransactionsCorrelationPValues } from './queries/query_failure_correlation'; +import { ERROR_CORRELATION_THRESHOLD } from './constants'; + +export const asyncErrorCorrelationSearchServiceProvider = ( + esClient: ElasticsearchClient, + getApmIndices: () => Promise, + searchServiceParams: SearchServiceParams, + includeFrozen: boolean +) => { + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + + const state = asyncErrorCorrelationsSearchServiceStateProvider(); + + async function fetchErrorCorrelations() { + try { + const indices = await getApmIndices(); + const params: SearchServiceFetchParams = { + ...searchServiceParams, + index: indices['apm_oss.transactionIndices'], + includeFrozen, + }; + + const { fieldCandidates } = await fetchTransactionDurationFieldCandidates( + esClient, + params + ); + + addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); + + state.setProgress({ loadedFieldCandidates: 1 }); + + let fieldCandidatesFetchedCount = 0; + if (params !== undefined && fieldCandidates.length > 0) { + const batches = chunk(fieldCandidates, 10); + for (let i = 0; i < batches.length; i++) { + try { + const results = await Promise.allSettled( + batches[i].map((fieldName) => + fetchFailedTransactionsCorrelationPValues( + esClient, + params, + fieldName + ) + ) + ); + + results.forEach((result, idx) => { + if (result.status === 'fulfilled') { + state.addValues( + result.value.filter( + (record) => + record && + typeof record.pValue === 'number' && + record.pValue < ERROR_CORRELATION_THRESHOLD + ) + ); + } else { + // If one of the fields in the batch had an error + addLogMessage( + `Error getting error correlation for field ${batches[i][idx]}: ${result.reason}.` + ); + } + }); + } catch (e) { + state.setError(e); + + if (params?.index.includes(':')) { + state.setCcsWarning(true); + } + } finally { + fieldCandidatesFetchedCount += batches[i].length; + state.setProgress({ + loadedErrorCorrelations: + fieldCandidatesFetchedCount / fieldCandidates.length, + }); + } + } + + addLogMessage( + `Identified correlations for ${fieldCandidatesFetchedCount} fields out of ${fieldCandidates.length} candidates.` + ); + } + } catch (e) { + state.setError(e); + } + + addLogMessage( + `Identified ${ + state.getState().values.length + } significant correlations relating to failed transactions.` + ); + + state.setIsRunning(false); + } + + fetchErrorCorrelations(); + + return () => { + const { ccsWarning, error, isRunning, progress } = state.getState(); + + return { + ccsWarning, + error, + log: getLogMessages(), + isRunning, + loaded: Math.round(state.getOverallProgress() * 100), + started: progress.started, + total: 100, + values: state.getValuesSortedByScore(), + cancel: () => { + addLogMessage(`Service cancelled.`); + state.setIsCancelled(true); + }, + }; + }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service_state.ts new file mode 100644 index 00000000000000..fb0c6fea4879a6 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service_state.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; + +interface Progress { + started: number; + loadedFieldCandidates: number; + loadedErrorCorrelations: number; +} +export const asyncErrorCorrelationsSearchServiceStateProvider = () => { + let ccsWarning = false; + function setCcsWarning(d: boolean) { + ccsWarning = d; + } + + let error: Error; + function setError(d: Error) { + error = d; + } + + let isCancelled = false; + function setIsCancelled(d: boolean) { + isCancelled = d; + } + + let isRunning = true; + function setIsRunning(d: boolean) { + isRunning = d; + } + + let progress: Progress = { + started: Date.now(), + loadedFieldCandidates: 0, + loadedErrorCorrelations: 0, + }; + function getOverallProgress() { + return ( + progress.loadedFieldCandidates * 0.025 + + progress.loadedErrorCorrelations * (1 - 0.025) + ); + } + function setProgress(d: Partial>) { + progress = { + ...progress, + ...d, + }; + } + + const values: FailedTransactionsCorrelationValue[] = []; + function addValue(d: FailedTransactionsCorrelationValue) { + values.push(d); + } + function addValues(d: FailedTransactionsCorrelationValue[]) { + values.push(...d); + } + + function getValuesSortedByScore() { + return values.sort((a, b) => b.score - a.score); + } + + function getState() { + return { + ccsWarning, + error, + isCancelled, + isRunning, + progress, + values, + }; + } + + return { + addValue, + addValues, + getOverallProgress, + getState, + getValuesSortedByScore, + setCcsWarning, + setError, + setIsCancelled, + setIsRunning, + setProgress, + }; +}; + +export type AsyncSearchServiceState = ReturnType< + typeof asyncErrorCorrelationsSearchServiceStateProvider +>; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/constants.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/constants.ts new file mode 100644 index 00000000000000..711c5f736d7745 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ERROR_CORRELATION_THRESHOLD = 0.02; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts new file mode 100644 index 00000000000000..f7e24ac6e1335e --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { apmFailedTransactionsCorrelationsSearchStrategyProvider } from './search_strategy'; +export { FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY } from '../../../../common/search_strategies/failure_correlations/constants'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts new file mode 100644 index 00000000000000..22424d68f07ff9 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { estypes } from '@elastic/elasticsearch'; +import { ElasticsearchClient } from 'kibana/server'; +import { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { + getQueryWithParams, + getTermsQuery, +} from '../../correlations/queries/get_query_with_params'; +import { getRequestBase } from '../../correlations/queries/get_request_base'; +import { EVENT_OUTCOME } from '../../../../../common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../../../common/event_outcome'; + +export const getFailureCorrelationRequest = ( + params: SearchServiceFetchParams, + fieldName: string +): estypes.SearchRequest => { + const query = getQueryWithParams({ + params, + }); + + const queryWithFailure = { + ...query, + bool: { + ...query.bool, + filter: [ + ...query.bool.filter, + ...getTermsQuery(EVENT_OUTCOME, EventOutcome.failure), + ], + }, + }; + + const body = { + query: queryWithFailure, + size: 0, + aggs: { + failure_p_value: { + significant_terms: { + field: fieldName, + background_filter: { + // Important to have same query as above here + // without it, we would be comparing sets of different filtered elements + ...query, + }, + // No need to have must_not "event.outcome": "failure" clause + // if background_is_superset is set to true + p_value: { background_is_superset: true }, + }, + }, + }, + }; + + return { + ...getRequestBase(params), + body, + }; +}; + +export const fetchFailedTransactionsCorrelationPValues = async ( + esClient: ElasticsearchClient, + params: SearchServiceFetchParams, + fieldName: string +) => { + const resp = await esClient.search( + getFailureCorrelationRequest(params, fieldName) + ); + + if (resp.body.aggregations === undefined) { + throw new Error( + 'fetchErrorCorrelation failed, did not return aggregations.' + ); + } + + const result = (resp.body.aggregations + .failure_p_value as estypes.AggregationsMultiBucketAggregate<{ + key: string; + doc_count: number; + bg_count: number; + score: number; + }>).buckets.map((b) => { + const score = b.score; + + // Scale the score into a value from 0 - 1 + // using a concave piecewise linear function in -log(p-value) + const normalizedScore = + 0.5 * Math.min(Math.max((score - 3.912) / 2.995, 0), 1) + + 0.25 * Math.min(Math.max((score - 6.908) / 6.908, 0), 1) + + 0.25 * Math.min(Math.max((score - 13.816) / 101.314, 0), 1); + + return { + ...b, + fieldName, + fieldValue: b.key, + pValue: Math.exp(-score), + normalizedScore, + }; + }); + + return result; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/search_strategy.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/search_strategy.ts new file mode 100644 index 00000000000000..415f19e892741b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/search_strategy.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; +import { of } from 'rxjs'; + +import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../../../src/plugins/data/common'; + +import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; + +import { asyncErrorCorrelationSearchServiceProvider } from './async_search_service'; +import { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; + +export type PartialSearchRequest = IKibanaSearchRequest; +export type PartialSearchResponse = IKibanaSearchResponse<{ + values: FailedTransactionsCorrelationValue[]; +}>; + +export const apmFailedTransactionsCorrelationsSearchStrategyProvider = ( + getApmIndices: () => Promise, + includeFrozen: boolean +): ISearchStrategy => { + const asyncSearchServiceMap = new Map< + string, + ReturnType + >(); + + return { + search: (request, options, deps) => { + if (request.params === undefined) { + throw new Error('Invalid request parameters.'); + } + + // The function to fetch the current state of the async search service. + // This will be either an existing service for a follow up fetch or a new one for new requests. + let getAsyncSearchServiceState: ReturnType< + typeof asyncErrorCorrelationSearchServiceProvider + >; + + // If the request includes an ID, we require that the async search service already exists + // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. + // This also avoids instantiating async search services when the service gets called with random IDs. + if (typeof request.id === 'string') { + const existingGetAsyncSearchServiceState = asyncSearchServiceMap.get( + request.id + ); + + if (typeof existingGetAsyncSearchServiceState === 'undefined') { + throw new Error( + `AsyncSearchService with ID '${request.id}' does not exist.` + ); + } + + getAsyncSearchServiceState = existingGetAsyncSearchServiceState; + } else { + getAsyncSearchServiceState = asyncErrorCorrelationSearchServiceProvider( + deps.esClient.asCurrentUser, + getApmIndices, + request.params, + includeFrozen + ); + } + + // Reuse the request's id or create a new one. + const id = request.id ?? uuid(); + + const { + ccsWarning, + error, + log, + isRunning, + loaded, + started, + total, + values, + } = getAsyncSearchServiceState(); + + if (error instanceof Error) { + asyncSearchServiceMap.delete(id); + throw error; + } else if (isRunning) { + asyncSearchServiceMap.set(id, getAsyncSearchServiceState); + } else { + asyncSearchServiceMap.delete(id); + } + + const took = Date.now() - started; + + return of({ + id, + loaded, + total, + isRunning, + isPartial: isRunning, + rawResponse: { + ccsWarning, + log, + took, + values, + }, + }); + }, + cancel: async (id, options, deps) => { + const getAsyncSearchServiceState = asyncSearchServiceMap.get(id); + if (getAsyncSearchServiceState !== undefined) { + getAsyncSearchServiceState().cancel(); + asyncSearchServiceMap.delete(id); + } + }, + }; +}; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 807d21768a50c9..1e0e61bc2bf3a3 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -51,6 +51,10 @@ import { TRANSACTION_TYPE, } from '../common/elasticsearch_fieldnames'; import { tutorialProvider } from './tutorial'; +import { + apmFailedTransactionsCorrelationsSearchStrategyProvider, + FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY, +} from './lib/search_strategies/failed_transactions_correlations'; export class APMPlugin implements @@ -219,13 +223,25 @@ export class APMPlugin coreStart.savedObjects.createInternalRepository() ); + const includeFrozen = await coreStart.uiSettings + .asScopedToClient(savedObjectsClient) + .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); + + // Register APM latency correlations search strategy plugins.data.search.registerSearchStrategy( 'apmCorrelationsSearchStrategy', apmCorrelationsSearchStrategyProvider( boundGetApmIndices, - await coreStart.uiSettings - .asScopedToClient(savedObjectsClient) - .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN) + includeFrozen + ) + ); + + // Register APM failed transactions correlations search strategy + plugins.data.search.registerSearchStrategy( + FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY, + apmFailedTransactionsCorrelationsSearchStrategyProvider( + boundGetApmIndices, + includeFrozen ) ); })(); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 673b2b331b2a2a..d070252d19c78f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5502,14 +5502,10 @@ "xpack.apm.correlations.correlationsTable.actionsLabel": "フィルター", "xpack.apm.correlations.correlationsTable.excludeDescription": "値を除外", "xpack.apm.correlations.correlationsTable.excludeLabel": "除外", - "xpack.apm.correlations.correlationsTable.fieldNameLabel": "フィールド名", - "xpack.apm.correlations.correlationsTable.fieldValueLabel": "フィールド値", "xpack.apm.correlations.correlationsTable.filterDescription": "値でフィルタリング", "xpack.apm.correlations.correlationsTable.filterLabel": "フィルター", - "xpack.apm.correlations.correlationsTable.impactLabel": "インパクト", "xpack.apm.correlations.correlationsTable.loadingText": "読み込み中", "xpack.apm.correlations.correlationsTable.noDataText": "データなし", - "xpack.apm.correlations.correlationsTable.percentageLabel": "割合 (%) ", "xpack.apm.correlations.customize.buttonLabel": "フィールドのカスタマイズ", "xpack.apm.correlations.customize.fieldHelpText": "相関関係を分析するフィールドをカスタマイズまたは{reset}します。{docsLink}", "xpack.apm.correlations.customize.fieldHelpTextDocsLink": "デフォルトフィールドの詳細。", @@ -5518,11 +5514,6 @@ "xpack.apm.correlations.customize.fieldPlaceholder": "オプションを選択または作成", "xpack.apm.correlations.customize.thresholdLabel": "しきい値", "xpack.apm.correlations.customize.thresholdPercentile": "{percentile}パーセンタイル", - "xpack.apm.correlations.error.chart.overallErrorRateLabel": "全体のエラー率", - "xpack.apm.correlations.error.chart.selectedTermErrorRateLabel": "{fieldName}:{fieldValue}", - "xpack.apm.correlations.error.chart.title": "経時的なエラー率", - "xpack.apm.correlations.error.description": "一部のトランザクションが失敗してエラーが返される理由。相関関係は、データの特定のコホートで想定される原因を検出するのに役立ちます。ホスト、バージョン、または他のカスタムフィールドのいずれか。", - "xpack.apm.correlations.error.percentageColumnName": "失敗したトランザクションの%", "xpack.apm.correlations.latencyCorrelations.cancelButtonTitle": "キャンセル", "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "フィルター", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "サービスの遅延に対するフィールドの影響。0~1の範囲。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f51e9b2de0007d..aa3840f392a2b5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5527,14 +5527,10 @@ "xpack.apm.correlations.correlationsTable.actionsLabel": "筛选", "xpack.apm.correlations.correlationsTable.excludeDescription": "筛除值", "xpack.apm.correlations.correlationsTable.excludeLabel": "排除", - "xpack.apm.correlations.correlationsTable.fieldNameLabel": "字段名称", - "xpack.apm.correlations.correlationsTable.fieldValueLabel": "字段值", "xpack.apm.correlations.correlationsTable.filterDescription": "按值筛选", "xpack.apm.correlations.correlationsTable.filterLabel": "筛选", - "xpack.apm.correlations.correlationsTable.impactLabel": "影响", "xpack.apm.correlations.correlationsTable.loadingText": "正在加载", "xpack.apm.correlations.correlationsTable.noDataText": "无数据", - "xpack.apm.correlations.correlationsTable.percentageLabel": "百分比", "xpack.apm.correlations.customize.buttonLabel": "定制字段", "xpack.apm.correlations.customize.fieldHelpText": "定制或{reset}要针对相关性分析的字段。{docsLink}", "xpack.apm.correlations.customize.fieldHelpTextDocsLink": "详细了解默认字段。", @@ -5543,11 +5539,6 @@ "xpack.apm.correlations.customize.fieldPlaceholder": "选择或创建选项", "xpack.apm.correlations.customize.thresholdLabel": "阈值", "xpack.apm.correlations.customize.thresholdPercentile": "第 {percentile} 个百分位数", - "xpack.apm.correlations.error.chart.overallErrorRateLabel": "总错误率", - "xpack.apm.correlations.error.chart.selectedTermErrorRateLabel": "{fieldName}:{fieldValue}", - "xpack.apm.correlations.error.chart.title": "时移错误率", - "xpack.apm.correlations.error.description": "为什么某些事务失败并返回错误?相关性将有助于在您数据的特定群组中发现可能的原因。按主机、版本或其他定制字段。", - "xpack.apm.correlations.error.percentageColumnName": "失败事务 %", "xpack.apm.correlations.latencyCorrelations.cancelButtonTitle": "取消", "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "筛选", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "字段对服务延迟的影响,范围从 0 到 1。",