[ML] Replace APM error rate table with failed transactions correlations (#108441)

* [ML] Refactor with new table

* [ML] Fix types, rename var

* [ML] Remove duplicate action columns

* [ML] Finish renaming for consistency

* [ML] Add failure correlations help popover

* [ML] Add failure correlations help popover

* [ML] Extend correlation help

* Update message

* [ML] Delete old legacy correlations pages

* [ML] Address comments, rename

* [ML] Revert deletion of latency_correlations.tsx

* [ML] Add unit test for getFailedTransactionsCorrelationImpactLabel

* [ML] Rename & fix types

* [ML] Fix logic to note include 0.02 threshold

* [ML] Refactor to use state handler

* [ML] Fix hardcoded index, columns, popover

* [ML] Replace failed transaction tab

* [ML] Fix unused translations

* [ML] Delete empty files

* [ML] Move beta badge to be inside tab content

Co-authored-by: lcawl <lcawley@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen 2021-08-17 15:47:31 -05:00 committed by GitHub
parent 335393e875
commit 09e8cfd305
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1274 additions and 506 deletions

View file

@ -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;

View file

@ -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;
}

View file

@ -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<T extends CorrelationsTerm> = Pick<
T,
'fieldName' | 'fieldValue'
>;
@ -40,24 +25,22 @@ interface Props<T> {
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<EuiBasicTableColumn<T>>;
onFilter?: () => void;
columns: Array<EuiBasicTableColumn<T>>;
}
export function CorrelationsTable<T extends SignificantTerm>({
export function CorrelationsTable<T extends CorrelationsTerm>({
significantTerms,
status,
percentageColumnName,
setSelectedSignificantTerm,
onFilter,
columns,
selectedTerm,
}: Props<T>) {
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<T extends SignificantTerm>({
),
[trackApmEvent]
);
const history = useHistory();
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
@ -92,140 +74,6 @@ export function CorrelationsTable<T extends SignificantTerm>({
setPageSize(size);
}, []);
const tableColumns: Array<EuiBasicTableColumn<T>> = columns ?? [
{
width: '116px',
field: 'impact',
name: i18n.translate(
'xpack.apm.correlations.correlationsTable.impactLabel',
{ defaultMessage: 'Impact' }
),
render: (_: any, term: T) => {
return <ImpactBar size="m" value={term.impact * 100} />;
},
},
{
field: 'percentage',
name:
percentageColumnName ??
i18n.translate(
'xpack.apm.correlations.correlationsTable.percentageLabel',
{ defaultMessage: 'Percentage' }
),
render: (_: any, term: T) => {
return (
<EuiToolTip
position="right"
content={`${asInteger(term.valueCount)} / ${asInteger(
term.fieldCount
)}`}
>
<>{asPercent(term.valueCount, term.fieldCount)}</>
</EuiToolTip>
);
},
},
{
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 (
<>
<EuiLink
href={createHref(history, {
query: {
kuery: `${term.fieldName}:"${encodeURIComponent(
term.fieldValue
)}"`,
},
})}
>
<EuiIcon type="magnifyWithPlus" />
</EuiLink>
&nbsp;/&nbsp;
<EuiLink
href={createHref(history, {
query: {
kuery: `not ${term.fieldName}:"${encodeURIComponent(
term.fieldValue
)}"`,
},
})}
>
<EuiIcon type="magnifyWithMinus" />
</EuiLink>
</>
);
},
},
];
return (
<EuiBasicTable
items={pageOfItems ?? []}
@ -233,12 +81,12 @@ export function CorrelationsTable<T extends SignificantTerm>({
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:

View file

@ -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<SelectedSignificantTerm | null>(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 (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiText size="s" color="subdued">
<p>
{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.',
})}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xxs">
<h4>
{i18n.translate('xpack.apm.correlations.error.chart.title', {
defaultMessage: 'Error rate over time',
})}
</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<ErrorTimeseriesChart
overallData={overallData}
correlationsData={hasFieldNames ? correlationsData : undefined}
status={overallStatus}
selectedSignificantTerm={selectedSignificantTerm}
/>
</EuiFlexItem>
<EuiFlexItem>
<CorrelationsTable
percentageColumnName={i18n.translate(
'xpack.apm.correlations.error.percentageColumnName',
{ defaultMessage: '% of failed transactions' }
)}
significantTerms={
hasFieldNames && correlationsData?.significantTerms
? correlationsData.significantTerms
: []
}
status={correlationsStatus}
setSelectedSignificantTerm={setSelectedSignificantTerm}
onFilter={onFilter}
/>
</EuiFlexItem>
<EuiFlexItem>
<CustomFields fieldNames={fieldNames} setFieldNames={setFieldNames} />
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
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 (
<ChartContainer height={200} hasData={!!overallData} status={status}>
<Chart size={{ height: 200, width: '100%' }}>
<Settings showLegend legendPosition={Position.Bottom} />
<Axis
id="bottom"
position={Position.Bottom}
showOverlappingTicks
tickFormat={dateFormatter}
/>
<Axis
id="left"
position={Position.Left}
domain={{ min: 0, max: 1 }}
tickFormat={(d) => `${roundFloat(d * 100)}%`}
/>
<LineSeries
id={i18n.translate(
'xpack.apm.correlations.error.chart.overallErrorRateLabel',
{ defaultMessage: 'Overall error rate' }
)}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={'x'}
yAccessors={['y']}
data={overallData?.overall?.timeseries ?? []}
curve={CurveType.CURVE_MONOTONE_X}
color={theme.eui.euiColorVis7}
/>
{correlationsData && selectedSignificantTerm ? (
<LineSeries
id={i18n.translate(
'xpack.apm.correlations.error.chart.selectedTermErrorRateLabel',
{
defaultMessage: '{fieldName}:{fieldValue}',
values: {
fieldName: selectedSignificantTerm.fieldName,
fieldValue: selectedSignificantTerm.fieldValue,
},
}
)}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={'x'}
yAccessors={['y']}
color={theme.eui.euiColorAccent}
data={getSelectedTimeseries(
correlationsData.significantTerms,
selectedSignificantTerm
)}
curve={CurveType.CURVE_MONOTONE_X}
/>
) : null}
</Chart>
</ChartContainer>
);
}
function roundFloat(n: number, digits = 2) {
const factor = Math.pow(10, digits);
return Math.round(n * factor) / factor;
}

View file

@ -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<boolean>(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<FailedTransactionsCorrelationValue | null>(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<FailedTransactionsCorrelationValue>
> = useMemo(
() => [
{
width: '116px',
field: 'normalizedScore',
name: (
<>
{i18n.translate(
'xpack.apm.correlations.failedTransactions.correlationsTable.pValueLabel',
{
defaultMessage: 'Score',
}
)}
</>
),
render: (normalizedScore: number) => {
return (
<>
<ImpactBar size="m" value={normalizedScore * 100} />
</>
);
},
},
{
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 (
<>
<EuiLink
href={createHref(history, {
query: {
kuery: `${term.fieldName}:"${encodeURIComponent(
term.fieldValue
)}"`,
},
})}
>
<EuiIcon type="magnifyWithPlus" />
</EuiLink>
&nbsp;/&nbsp;
<EuiLink
href={createHref(history, {
query: {
kuery: `not ${term.fieldName}:"${encodeURIComponent(
term.fieldValue
)}"`,
},
})}
>
<EuiIcon type="magnifyWithMinus" />
</EuiLink>
</>
);
},
},
],
[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 (
<>
<EuiFlexGroup
data-test-subj="apmFailedTransactionsCorrelationsTabContent"
gutterSize="s"
>
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h5 data-test-subj="apmFailedTransactionsCorrelationsChartTitle">
{i18n.translate(
'xpack.apm.correlations.failedTransactions.panelTitle',
{
defaultMessage: 'Failed transactions',
}
)}
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={i18n.translate(
'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaLabel',
{
defaultMessage: 'Beta',
}
)}
title={i18n.translate(
'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaTitle',
{
defaultMessage: 'Failed transaction rate',
}
)}
tooltipContent={i18n.translate(
'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaDescription',
{
defaultMessage:
'Failed transaction rate is not GA. Please help us by reporting any bugs.',
}
)}
size="s"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{!isRunning && (
<EuiButton size="s" onClick={startFetch}>
<FormattedMessage
id="xpack.apm.correlations.failedTransactions.refreshButtonTitle"
defaultMessage="Refresh"
/>
</EuiButton>
)}
{isRunning && (
<EuiButton size="s" onClick={cancelFetch}>
<FormattedMessage
id="xpack.apm.correlations.failedTransactions.cancelButtonTitle"
defaultMessage="Cancel"
/>
</EuiButton>
)}
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem data-test-subj="apmCorrelationsFailedTransactionsCorrelationsProgressTitle">
<EuiText size="xs" color="subdued">
<FormattedMessage
data-test-subj="apmCorrelationsFailedTransactionsCorrelationsProgressMessage"
id="xpack.apm.correlations.failedTransactions.progressTitle"
defaultMessage="Progress: {progress}%"
values={{ progress: Math.round(progress * 100) }}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiProgress
aria-label={i18n.translate(
'xpack.apm.correlations.failedTransactions.progressAriaLabel',
{ defaultMessage: 'Progress' }
)}
value={Math.round(progress * 100)}
max={100}
size="m"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FailedTransactionsCorrelationsHelpPopover />
</EuiFlexItem>
</EuiFlexGroup>
{selectedTerm?.pValue != null ? (
<>
<EuiSpacer size="m" />
<Summary
items={[
<EuiBadge color="hollow">
{`${selectedTerm.fieldName}: ${selectedTerm.key}`}
</EuiBadge>,
<>{`p-value: ${selectedTerm.pValue.toPrecision(3)}`}</>,
]}
/>
<EuiSpacer size="m" />
</>
) : null}
<CorrelationsTable<FailedTransactionsCorrelationValue>
columns={failedTransactionsCorrelationsColumns}
significantTerms={result?.values}
status={FETCH_STATUS.SUCCESS}
setSelectedSignificantTerm={setSelectedSignificantTerm}
selectedTerm={selectedTerm}
/>
{ccsWarning && (
<>
<EuiSpacer size="m" />
<EuiCallOut
title={i18n.translate(
'xpack.apm.correlations.failedTransactions.ccsWarningCalloutTitle',
{
defaultMessage: 'Cross-cluster search compatibility',
}
)}
color="warning"
>
<p>
{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.',
}
)}
</p>
</EuiCallOut>
</>
)}
<EuiSpacer size="m" />
{log.length > 0 && displayLog && (
<EuiAccordion
id="apmFailedTransactionsCorrelationsLogAccordion"
buttonContent={i18n.translate(
'xpack.apm.correlations.failedTransactions.logButtonContent',
{
defaultMessage: 'Log',
}
)}
>
<EuiPanel color="subdued">
{log.map((d, i) => {
const splitItem = d.split(': ');
return (
<p key={i}>
<small>
<EuiCode>{splitItem[0]}</EuiCode> {splitItem[1]}
</small>
</p>
);
})}
</EuiPanel>
</EuiAccordion>
)}
</>
);
}

View file

@ -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 (
<HelpPopover
anchorPosition="leftUp"
button={
<HelpPopoverButton
onClick={() => {
setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen);
}}
/>
}
closePopover={() => setIsPopoverOpen(false)}
isOpen={isPopoverOpen}
title={i18n.translate('xpack.apm.correlations.failurePopoverTitle', {
defaultMessage: 'Failure correlations',
})}
>
<p>
<FormattedMessage
id="xpack.apm.correlations.failurePopoverBasicExplanation"
defaultMessage="Correlations help you discover which attributes are contributing to failed transactions."
/>
</p>
<p>
<FormattedMessage
id="xpack.apm.correlations.failurePopoverTableExplanation"
defaultMessage="The table is sorted by scores, which are mapped to high, medium, or low impact levels. Attributes with high impact levels are more likely to contribute to failed transactions."
/>
</p>
<p>
<FormattedMessage
id="xpack.apm.correlations.failurePopoverPerformanceExplanation"
defaultMessage="This analysis performs statistical searches across a large number of attributes. For large time ranges and services with high transaction throughput, this might take some time. Reduce the time range to improve performance."
/>
</p>
</HelpPopover>
);
}

View file

@ -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<SelectedSignificantTerm | null>(null);
] = useState<MlCorrelationsTerms | null>(null);
let selectedHistogram = histograms.length > 0 ? histograms[0] : undefined;
@ -376,10 +371,8 @@ export function LatencyCorrelations() {
<EuiSpacer size="m" />
<div data-test-subj="apmCorrelationsTable">
{histograms.length > 0 && selectedHistogram !== undefined && (
<CorrelationsTable
// @ts-ignore correlations don't have the same column format other tables have
<CorrelationsTable<MlCorrelationsTerms>
columns={mlCorrelationColumns}
// @ts-expect-error correlations don't have the same significant term other tables have
significantTerms={histogramTerms}
status={FETCH_STATUS.SUCCESS}
setSelectedSignificantTerm={setSelectedSignificantTerm}

View file

@ -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
);
});
});

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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;

View file

@ -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 ? (
<ErrorCorrelations />
<FailedTransactionsCorrelations />
) : (
<LicensePrompt
text={i18n.translate(
@ -65,29 +63,7 @@ export const failedTransactionsCorrelationsTab = {
{
defaultMessage: 'Failed transaction correlations',
}
)}{' '}
<EuiBetaBadge
label={i18n.translate(
'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaLabel',
{
defaultMessage: 'Beta',
}
)}
title={i18n.translate(
'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaTitle',
{
defaultMessage: 'Failed transaction rate',
}
)}
tooltipContent={i18n.translate(
'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaDescription',
{
defaultMessage:
'Failed transaction rate is not GA. Please help us by reporting any bugs.',
}
)}
size="s"
/>
)}
</>
),
component: FailedTransactionsCorrelationsTab,

View file

@ -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<SearchServiceParams, 'analyzeCorrelations'>
) => {
const {
services: { data },
} = useKibana<ApmPluginStartDeps>();
const [
fetchState,
setFetchState,
] = useState<FailedTransactionsCorrelationsFetcherState>({
isComplete: false,
isRunning: false,
loaded: 0,
ccsWarning: false,
values: [],
log: [],
total: 100,
});
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef<Subscription>();
function setResponse(response: IKibanaSearchResponse<RawResponse>) {
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<IKibanaSearchRequest, IKibanaSearchResponse<RawResponse>>(req, {
strategy: FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY,
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (res: IKibanaSearchResponse<RawResponse>) => {
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,
};
};

View file

@ -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<ApmIndicesConfig>,
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);
},
};
};
};

View file

@ -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<Omit<Progress, 'started'>>) {
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
>;

View file

@ -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;

View file

@ -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';

View file

@ -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;
};

View file

@ -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<SearchServiceParams>;
export type PartialSearchResponse = IKibanaSearchResponse<{
values: FailedTransactionsCorrelationValue[];
}>;
export const apmFailedTransactionsCorrelationsSearchStrategyProvider = (
getApmIndices: () => Promise<ApmIndicesConfig>,
includeFrozen: boolean
): ISearchStrategy<PartialSearchRequest, PartialSearchResponse> => {
const asyncSearchServiceMap = new Map<
string,
ReturnType<typeof asyncErrorCorrelationSearchServiceProvider>
>();
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);
}
},
};
};

View file

@ -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
)
);
})();

View file

@ -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": "サービスの遅延に対するフィールドの影響。01の範囲。",

View file

@ -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。",