mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
335393e875
commit
09e8cfd305
22 changed files with 1274 additions and 506 deletions
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
/
|
||||
<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:
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
/
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
|
@ -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
|
||||
>;
|
|
@ -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;
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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
|
||||
)
|
||||
);
|
||||
})();
|
||||
|
|
|
@ -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の範囲。",
|
||||
|
|
|
@ -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。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue