[ML] APM Correlations: Chart for failed transactions correlations tab. (#110172)

* [ML] Fix tooltip text.

* Revert "[ML] Fix tooltip text."

This reverts commit ca86f769d7.

* [ML] Chart prototype.

* [ML] Hover support for failed transactions chart.

* [ML] Add p-value column.

* [ML] Code consolidation.

* [ML] Fix naming inconsistencies.

* [ML] Fix naming inconsistencies.

* [ML] Fix naming inconsistencies.

* [ML] Consolidate hooks.

* [ML] Consolidate hooks.

* [ML] Consolidate hooks.

* [ML] Use function overloads.

* [ML] Fix naming inconsistencies.

* [ML] Fix jest test.

* [ML] Fix chart loading behavior.

* [ML] Rename values to latencyCorrelations.

* [ML] Clean up types.

* [ML] Add function overloads.

* [Ml] Update API integration tests.

* [ML] Rename values to failedTransactionsCorrelations.

* [ML] Fix naming inconsistencies.

* [ML] Fix naming inconsistencies.

* [ML] Fix naming inconsistencies.

* [ML] Fix jest test.

* [ML] Fix API integration test

* [ML] Clean up chart data.

* [ML] Fix chart props.

* [ML] Tweak types for failed correlations.

* [ML] Improve FieldValuePair type usage.

* [ML] Remove 'async' from variable names.

* [ML] Fix typo.

* [ML] Simplify mock.

* [ML] Refactor code that used type guards.

* [ML] Comment about feature availability.

* [ML] Simplify check.

* [ML] Simplify selectedHistogram.

* [ML] Improve column type safety.

* [ML] Simplify selectedTerm.

* [ML] Simplify sorting.

* [ML] Fix regresssion when there's no data for failed transactions.

* [ML] Rename fieldFilter to termFilters.

* [ML] Update api integration test assertions.

* [ML] Fix failed transactions params.

* [ML] Tweak chart title.

* [ML] Tweak chart colors.

* [ML] Add translation.

* [ML] Tweak selectedTerm if statement.

* [ML] Fix types.

* [ML] Fix assertion text.

* [ML] Refactor replaceHistogramDotsWithBars.

* [ML] Refactor fetchFailedTransactionsCorrelationPValues.

* [ML] Fix score column width.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Walter Rafelsberger 2021-09-13 18:08:43 +02:00 committed by GitHub
parent 4dc72140be
commit 065701a0c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 427 additions and 183 deletions

View file

@ -7,6 +7,7 @@
import {
FieldValuePair,
HistogramItem,
RawResponseBase,
SearchStrategyClientParams,
} from '../types';
@ -21,14 +22,23 @@ export interface FailedTransactionsCorrelation extends FieldValuePair {
normalizedScore: number;
failurePercentage: number;
successPercentage: number;
histogram: HistogramItem[];
}
export type FailedTransactionsCorrelationsImpactThreshold = typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD[keyof typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD];
export type FailedTransactionsCorrelationsParams = SearchStrategyClientParams;
export interface FailedTransactionsCorrelationsParams {
percentileThreshold: number;
}
export type FailedTransactionsCorrelationsRequestParams = FailedTransactionsCorrelationsParams &
SearchStrategyClientParams;
export interface FailedTransactionsCorrelationsRawResponse
extends RawResponseBase {
log: string[];
failedTransactionsCorrelations: FailedTransactionsCorrelation[];
failedTransactionsCorrelations?: FailedTransactionsCorrelation[];
percentileThresholdValue?: number;
overallHistogram?: HistogramItem[];
errorHistogram?: HistogramItem[];
}

View file

@ -27,11 +27,14 @@ export interface LatencyCorrelationSearchServiceProgress {
loadedHistograms: number;
}
export interface LatencyCorrelationsParams extends SearchStrategyClientParams {
export interface LatencyCorrelationsParams {
percentileThreshold: number;
analyzeCorrelations: boolean;
}
export type LatencyCorrelationsRequestParams = LatencyCorrelationsParams &
SearchStrategyClientParams;
export interface LatencyCorrelationsRawResponse extends RawResponseBase {
log: string[];
overallHistogram?: HistogramItem[];

View file

@ -33,7 +33,10 @@ import {
import { asPercent } from '../../../../common/utils/formatters';
import { FailedTransactionsCorrelation } from '../../../../common/search_strategies/failed_transactions_correlations/types';
import { APM_SEARCH_STRATEGIES } from '../../../../common/search_strategies/constants';
import {
APM_SEARCH_STRATEGIES,
DEFAULT_PERCENTILE_THRESHOLD,
} from '../../../../common/search_strategies/constants';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
@ -47,6 +50,11 @@ import { CorrelationsTable } from './correlations_table';
import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover';
import { isErrorMessage } from './utils/is_error_message';
import { getFailedTransactionsCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label';
import { getOverallHistogram } from './utils/get_overall_histogram';
import {
TransactionDistributionChart,
TransactionDistributionChartData,
} from '../../shared/charts/transaction_distribution_chart';
import { CorrelationsLog } from './correlations_log';
import { CorrelationsEmptyStatePrompt } from './empty_state_prompt';
import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning';
@ -65,9 +73,16 @@ export function FailedTransactionsCorrelations({
const inspectEnabled = uiSettings.get<boolean>(enableInspectEsQueries);
const { progress, response, startFetch, cancelFetch } = useSearchStrategy(
APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS
APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS,
{
percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD,
}
);
const progressNormalized = progress.loaded / progress.total;
const { overallHistogram, hasData, status } = getOverallHistogram(
response,
progress.isRunning
);
const [
selectedSignificantTerm,
@ -86,6 +101,13 @@ export function FailedTransactionsCorrelations({
EuiBasicTableColumn<FailedTransactionsCorrelation>
> = inspectEnabled
? [
{
width: '100px',
field: 'pValue',
name: 'p-value',
render: (pValue: number) => pValue.toPrecision(3),
sortable: true,
},
{
width: '100px',
field: 'failurePercentage',
@ -157,6 +179,7 @@ export function FailedTransactionsCorrelations({
: [];
return [
{
width: '116px',
field: 'normalizedScore',
name: (
<>
@ -350,6 +373,35 @@ export function FailedTransactionsCorrelations({
const showSummaryBadge =
inspectEnabled && (progress.isRunning || correlationTerms.length > 0);
const transactionDistributionChartData: TransactionDistributionChartData[] = [];
if (Array.isArray(overallHistogram)) {
transactionDistributionChartData.push({
id: i18n.translate(
'xpack.apm.transactionDistribution.chart.allTransactionsLabel',
{ defaultMessage: 'All transactions' }
),
histogram: overallHistogram,
});
}
if (Array.isArray(response.errorHistogram)) {
transactionDistributionChartData.push({
id: i18n.translate(
'xpack.apm.transactionDistribution.chart.allFailedTransactionsLabel',
{ defaultMessage: 'All failed transactions' }
),
histogram: response.errorHistogram,
});
}
if (selectedTerm && Array.isArray(selectedTerm.histogram)) {
transactionDistributionChartData.push({
id: `${selectedTerm.fieldName}:${selectedTerm.fieldValue}`,
histogram: selectedTerm.histogram,
});
}
return (
<div data-test-subj="apmFailedTransactionsCorrelationsTabContent">
<EuiFlexItem style={{ flexDirection: 'row', alignItems: 'center' }}>
@ -363,7 +415,7 @@ export function FailedTransactionsCorrelations({
{i18n.translate(
'xpack.apm.correlations.failedTransactions.panelTitle',
{
defaultMessage: 'Failed transactions',
defaultMessage: 'Failed transactions latency distribution',
}
)}
</h5>
@ -402,6 +454,16 @@ export function FailedTransactionsCorrelations({
<EuiSpacer size="s" />
<TransactionDistributionChart
markerPercentile={DEFAULT_PERCENTILE_THRESHOLD}
markerValue={response.percentileThresholdValue ?? 0}
data={transactionDistributionChartData}
hasData={hasData}
status={status}
/>
<EuiSpacer size="s" />
<EuiTitle size="xs">
<span data-test-subj="apmFailedTransactionsCorrelationsTablePanelTitle">
{i18n.translate(

View file

@ -39,7 +39,10 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { useSearchStrategy } from '../../../hooks/use_search_strategy';
import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart';
import {
TransactionDistributionChart,
TransactionDistributionChartData,
} from '../../shared/charts/transaction_distribution_chart';
import { push } from '../../shared/Links/url_helpers';
import { CorrelationsTable } from './correlations_table';
@ -239,6 +242,25 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) {
histogramTerms.length < 1 &&
(progressNormalized === 1 || !progress.isRunning);
const transactionDistributionChartData: TransactionDistributionChartData[] = [];
if (Array.isArray(overallHistogram)) {
transactionDistributionChartData.push({
id: i18n.translate(
'xpack.apm.transactionDistribution.chart.allTransactionsLabel',
{ defaultMessage: 'All transactions' }
),
histogram: overallHistogram,
});
}
if (selectedHistogram && Array.isArray(selectedHistogram.histogram)) {
transactionDistributionChartData.push({
id: `${selectedHistogram.fieldName}:${selectedHistogram.fieldValue}`,
histogram: selectedHistogram.histogram,
});
}
return (
<div data-test-subj="apmLatencyCorrelationsTabContent">
<EuiFlexGroup>
@ -264,8 +286,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) {
<TransactionDistributionChart
markerPercentile={DEFAULT_PERCENTILE_THRESHOLD}
markerValue={response.percentileThresholdValue ?? 0}
{...selectedHistogram}
overallHistogram={overallHistogram}
data={transactionDistributionChartData}
hasData={hasData}
status={status}
/>

View file

@ -17,18 +17,24 @@ import {
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useUiTracker } from '../../../../../../observability/public';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
import {
APM_SEARCH_STRATEGIES,
DEFAULT_PERCENTILE_THRESHOLD,
} from '../../../../../common/search_strategies/constants';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useSearchStrategy } from '../../../../hooks/use_search_strategy';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { TransactionDistributionChart } from '../../../shared/charts/transaction_distribution_chart';
import { useUiTracker } from '../../../../../../observability/public';
import {
TransactionDistributionChart,
TransactionDistributionChartData,
} from '../../../shared/charts/transaction_distribution_chart';
import { isErrorMessage } from '../../correlations/utils/is_error_message';
import { getOverallHistogram } from '../../correlations/utils/get_overall_histogram';
@ -132,6 +138,18 @@ export function TransactionDistribution({
trackApmEvent({ metric: 'transaction_distribution_chart_clear_selection' });
};
const transactionDistributionChartData: TransactionDistributionChartData[] = [];
if (Array.isArray(overallHistogram)) {
transactionDistributionChartData.push({
id: i18n.translate(
'xpack.apm.transactionDistribution.chart.allTransactionsLabel',
{ defaultMessage: 'All transactions' }
),
histogram: overallHistogram,
});
}
return (
<div data-test-subj="apmTransactionDistributionTabContent">
<EuiFlexGroup style={{ minHeight: MIN_TAB_TITLE_HEIGHT }}>
@ -193,10 +211,10 @@ export function TransactionDistribution({
<EuiSpacer size="s" />
<TransactionDistributionChart
data={transactionDistributionChartData}
markerCurrentTransaction={markerCurrentTransaction}
markerPercentile={DEFAULT_PERCENTILE_THRESHOLD}
markerValue={response.percentileThresholdValue ?? 0}
overallHistogram={overallHistogram}
onChartSelection={onTrackedChartSelection}
hasData={hasData}
selection={selection}

View file

@ -0,0 +1,42 @@
/*
* 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 { HistogramItem } from '../../../../../common/search_strategies/types';
import { replaceHistogramDotsWithBars } from './index';
describe('TransactionDistributionChart', () => {
describe('replaceHistogramDotsWithBars', () => {
it('does the thing', () => {
const mockHistogram = [
{ doc_count: 10 },
{ doc_count: 10 },
{ doc_count: 0 },
{ doc_count: 0 },
{ doc_count: 0 },
{ doc_count: 10 },
{ doc_count: 10 },
{ doc_count: 0 },
{ doc_count: 10 },
{ doc_count: 10 },
] as HistogramItem[];
expect(replaceHistogramDotsWithBars(mockHistogram)).toEqual([
{ doc_count: 10 },
{ doc_count: 10 },
{ doc_count: 0.0001 },
{ doc_count: 0 },
{ doc_count: 0 },
{ doc_count: 10 },
{ doc_count: 10 },
{ doc_count: 0.0001 },
{ doc_count: 10 },
{ doc_count: 10 },
]);
});
});
});

View file

@ -5,7 +5,9 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import React from 'react';
import { flatten } from 'lodash';
import {
AnnotationDomainType,
AreaSeries,
@ -30,25 +32,24 @@ import { i18n } from '@kbn/i18n';
import { useChartTheme } from '../../../../../../observability/public';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
import type {
FieldValuePair,
HistogramItem,
} from '../../../../../common/search_strategies/types';
import type { HistogramItem } from '../../../../../common/search_strategies/types';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { useTheme } from '../../../../hooks/use_theme';
import { ChartContainer } from '../chart_container';
export interface TransactionDistributionChartData {
id: string;
histogram: HistogramItem[];
}
interface TransactionDistributionChartProps {
fieldName?: FieldValuePair['fieldName'];
fieldValue?: FieldValuePair['fieldValue'];
data: TransactionDistributionChartData[];
hasData: boolean;
histogram?: HistogramItem[];
markerCurrentTransaction?: number;
markerValue: number;
markerPercentile: number;
overallHistogram?: HistogramItem[];
onChartSelection?: BrushEndListener;
selection?: [number, number];
status: FETCH_STATUS;
@ -69,29 +70,24 @@ const getAnnotationsStyle = (color = 'gray'): LineAnnotationStyle => ({
},
});
// TODO Revisit this approach since it actually manipulates the numbers
// showing in the chart and its tooltips.
const CHART_PLACEHOLDER_VALUE = 0.0001;
// Elastic charts will show any lone bin (i.e. a populated bin followed by empty bin)
// as a circular marker instead of a bar
// This provides a workaround by making the next bin not empty
export const replaceHistogramDotsWithBars = (
originalHistogram: HistogramItem[] | undefined
) => {
if (originalHistogram === undefined) return;
const histogram = [...originalHistogram];
{
for (let i = 0; i < histogram.length - 1; i++) {
if (
histogram[i].doc_count > 0 &&
histogram[i].doc_count !== CHART_PLACEHOLDER_VALUE &&
histogram[i + 1].doc_count === 0
) {
histogram[i + 1].doc_count = CHART_PLACEHOLDER_VALUE;
}
export const replaceHistogramDotsWithBars = (histogramItems: HistogramItem[]) =>
histogramItems.reduce((histogramItem, _, i) => {
if (
histogramItem[i - 1]?.doc_count > 0 &&
histogramItem[i - 1]?.doc_count !== CHART_PLACEHOLDER_VALUE &&
histogramItem[i].doc_count === 0
) {
histogramItem[i].doc_count = CHART_PLACEHOLDER_VALUE;
}
return histogram;
}
};
return histogramItem;
}, histogramItems);
// Create and call a duration formatter for every value since the durations for the
// x axis might have a wide range of values e.g. from low milliseconds to large seconds.
@ -100,14 +96,11 @@ const xAxisTickFormat: TickFormatter<number> = (d) =>
getDurationFormatter(d, 0.9999)(d).formatted;
export function TransactionDistributionChart({
fieldName,
fieldValue,
data,
hasData,
histogram: originalHistogram,
markerCurrentTransaction,
markerValue,
markerPercentile,
overallHistogram,
onChartSelection,
selection,
status,
@ -115,10 +108,11 @@ export function TransactionDistributionChart({
const chartTheme = useChartTheme();
const euiTheme = useTheme();
const patchedOverallHistogram = useMemo(
() => replaceHistogramDotsWithBars(overallHistogram),
[overallHistogram]
);
const areaSeriesColors = [
euiTheme.eui.euiColorVis1,
euiTheme.eui.euiColorVis2,
euiTheme.eui.euiColorVis5,
];
const annotationsDataValues: LineAnnotationDatum[] = [
{
@ -137,15 +131,15 @@ export function TransactionDistributionChart({
// This will create y axis ticks for 1, 10, 100, 1000 ...
const yMax =
Math.max(...(overallHistogram ?? []).map((d) => d.doc_count)) ?? 0;
Math.max(
...flatten(data.map((d) => d.histogram)).map((d) => d.doc_count)
) ?? 0;
const yTicks = Math.ceil(Math.log10(yMax));
const yAxisDomain = {
min: 0.9,
max: Math.pow(10, yTicks),
};
const histogram = replaceHistogramDotsWithBars(originalHistogram);
const selectionAnnotation =
selection !== undefined
? [
@ -260,35 +254,19 @@ export function TransactionDistributionChart({
ticks={yTicks}
gridLine={{ visible: true }}
/>
<AreaSeries
id={i18n.translate(
'xpack.apm.transactionDistribution.chart.allTransactionsLabel',
{ defaultMessage: 'All transactions' }
)}
xScaleType={ScaleType.Log}
yScaleType={ScaleType.Log}
data={patchedOverallHistogram ?? []}
curve={CurveType.CURVE_STEP_AFTER}
xAccessor="key"
yAccessors={['doc_count']}
color={euiTheme.eui.euiColorVis1}
fit="lookahead"
/>
{Array.isArray(histogram) &&
fieldName !== undefined &&
fieldValue !== undefined && (
<AreaSeries
// id is used as the label for the legend
id={`${fieldName}:${fieldValue}`}
xScaleType={ScaleType.Log}
yScaleType={ScaleType.Log}
data={histogram}
curve={CurveType.CURVE_STEP_AFTER}
xAccessor="key"
yAccessors={['doc_count']}
color={euiTheme.eui.euiColorVis2}
/>
)}
{data.map((d, i) => (
<AreaSeries
id={d.id}
xScaleType={ScaleType.Log}
yScaleType={ScaleType.Log}
data={replaceHistogramDotsWithBars(d.histogram)}
curve={CurveType.CURVE_STEP_AFTER}
xAccessor="key"
yAccessors={['doc_count']}
color={areaSeriesColors[i]}
fit="lookahead"
/>
))}
</Chart>
</ChartContainer>
</div>

View file

@ -18,8 +18,14 @@ import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import type { SearchStrategyClientParams } from '../../common/search_strategies/types';
import type { RawResponseBase } from '../../common/search_strategies/types';
import type { LatencyCorrelationsRawResponse } from '../../common/search_strategies/latency_correlations/types';
import type { FailedTransactionsCorrelationsRawResponse } from '../../common/search_strategies/failed_transactions_correlations/types';
import type {
LatencyCorrelationsParams,
LatencyCorrelationsRawResponse,
} from '../../common/search_strategies/latency_correlations/types';
import type {
FailedTransactionsCorrelationsParams,
FailedTransactionsCorrelationsRawResponse,
} from '../../common/search_strategies/failed_transactions_correlations/types';
import {
ApmSearchStrategies,
APM_SEARCH_STRATEGIES,
@ -58,8 +64,9 @@ const getReducer = <T>() => (prev: T, update: Partial<T>): T => ({
...update,
});
interface SearchStrategyReturnBase {
interface SearchStrategyReturnBase<TRawResponse extends RawResponseBase> {
progress: SearchStrategyProgress;
response: TRawResponse;
startFetch: () => void;
cancelFetch: () => void;
}
@ -67,25 +74,22 @@ interface SearchStrategyReturnBase {
// Function overload for Latency Correlations
export function useSearchStrategy(
searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS,
options: {
percentileThreshold: number;
analyzeCorrelations: boolean;
}
): {
response: LatencyCorrelationsRawResponse;
} & SearchStrategyReturnBase;
searchStrategyParams: LatencyCorrelationsParams
): SearchStrategyReturnBase<LatencyCorrelationsRawResponse>;
// Function overload for Failed Transactions Correlations
export function useSearchStrategy(
searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS
): {
response: FailedTransactionsCorrelationsRawResponse;
} & SearchStrategyReturnBase;
searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS,
searchStrategyParams: FailedTransactionsCorrelationsParams
): SearchStrategyReturnBase<FailedTransactionsCorrelationsRawResponse>;
export function useSearchStrategy<
TRawResponse extends RawResponseBase,
TOptions = unknown
>(searchStrategyName: ApmSearchStrategies, options?: TOptions): unknown {
TParams = unknown
>(
searchStrategyName: ApmSearchStrategies,
searchStrategyParams?: TParams
): SearchStrategyReturnBase<TRawResponse> {
const {
services: { data },
} = useKibana<ApmPluginStartDeps>();
@ -110,7 +114,7 @@ export function useSearchStrategy<
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef<Subscription>();
const optionsRef = useRef(options);
const searchStrategyParamsRef = useRef(searchStrategyParams);
const startFetch = useCallback(() => {
searchSubscription$.current?.unsubscribe();
@ -130,14 +134,16 @@ export function useSearchStrategy<
kuery,
start,
end,
...(optionsRef.current ? { ...optionsRef.current } : {}),
...(searchStrategyParamsRef.current
? { ...searchStrategyParamsRef.current }
: {}),
},
};
// Submit the search request using the `data.search` service.
searchSubscription$.current = data.search
.search<
IKibanaSearchRequest<SearchStrategyClientParams & (TOptions | {})>,
IKibanaSearchRequest<SearchStrategyClientParams & (TParams | {})>,
IKibanaSearchResponse<TRawResponse>
>(request, {
strategy: searchStrategyName,

View file

@ -16,9 +16,10 @@ import {
} from '../../../../../../../src/plugins/data/common';
import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames';
import type { SearchStrategyParams } from '../../../../common/search_strategies/types';
import { EventOutcome } from '../../../../common/event_outcome';
import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types';
import type {
FailedTransactionsCorrelationsParams,
FailedTransactionsCorrelationsRequestParams,
FailedTransactionsCorrelationsRawResponse,
} from '../../../../common/search_strategies/failed_transactions_correlations/types';
import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
@ -26,6 +27,9 @@ import { searchServiceLogProvider } from '../search_service_log';
import {
fetchFailedTransactionsCorrelationPValues,
fetchTransactionDurationFieldCandidates,
fetchTransactionDurationPercentiles,
fetchTransactionDurationRanges,
fetchTransactionDurationHistogramRangeSteps,
} from '../queries';
import type { SearchServiceProvider } from '../search_strategy_provider';
@ -34,19 +38,19 @@ import { failedTransactionsCorrelationsSearchServiceStateProvider } from './fail
import { ERROR_CORRELATION_THRESHOLD } from '../constants';
export type FailedTransactionsCorrelationsSearchServiceProvider = SearchServiceProvider<
FailedTransactionsCorrelationsParams,
FailedTransactionsCorrelationsRequestParams,
FailedTransactionsCorrelationsRawResponse
>;
export type FailedTransactionsCorrelationsSearchStrategy = ISearchStrategy<
IKibanaSearchRequest<FailedTransactionsCorrelationsParams>,
IKibanaSearchRequest<FailedTransactionsCorrelationsRequestParams>,
IKibanaSearchResponse<FailedTransactionsCorrelationsRawResponse>
>;
export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider = (
esClient: ElasticsearchClient,
getApmIndices: () => Promise<ApmIndicesConfig>,
searchServiceParams: FailedTransactionsCorrelationsParams,
searchServiceParams: FailedTransactionsCorrelationsRequestParams,
includeFrozen: boolean
) => {
const { addLogMessage, getLogMessages } = searchServiceLogProvider();
@ -56,12 +60,66 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact
async function fetchErrorCorrelations() {
try {
const indices = await getApmIndices();
const params: SearchStrategyParams = {
const params: FailedTransactionsCorrelationsRequestParams &
SearchStrategyServerParams = {
...searchServiceParams,
index: indices['apm_oss.transactionIndices'],
includeFrozen,
};
// 95th percentile to be displayed as a marker in the log log chart
const {
totalDocs,
percentiles: percentilesResponseThresholds,
} = await fetchTransactionDurationPercentiles(
esClient,
params,
params.percentileThreshold ? [params.percentileThreshold] : undefined
);
const percentileThresholdValue =
percentilesResponseThresholds[`${params.percentileThreshold}.0`];
state.setPercentileThresholdValue(percentileThresholdValue);
addLogMessage(
`Fetched ${params.percentileThreshold}th percentile value of ${percentileThresholdValue} based on ${totalDocs} documents.`
);
// finish early if we weren't able to identify the percentileThresholdValue.
if (percentileThresholdValue === undefined) {
addLogMessage(
`Abort service since percentileThresholdValue could not be determined.`
);
state.setProgress({
loadedFieldCandidates: 1,
loadedErrorCorrelations: 1,
loadedOverallHistogram: 1,
loadedFailedTransactionsCorrelations: 1,
});
state.setIsRunning(false);
return;
}
const histogramRangeSteps = await fetchTransactionDurationHistogramRangeSteps(
esClient,
params
);
const overallLogHistogramChartData = await fetchTransactionDurationRanges(
esClient,
params,
histogramRangeSteps
);
const errorLogHistogramChartData = await fetchTransactionDurationRanges(
esClient,
params,
histogramRangeSteps,
[{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }]
);
state.setProgress({ loadedOverallHistogram: 1 });
state.setErrorHistogram(errorLogHistogramChartData);
state.setOverallHistogram(overallLogHistogramChartData);
const {
fieldCandidates: candidates,
} = await fetchTransactionDurationFieldCandidates(esClient, params);
@ -82,6 +140,7 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact
fetchFailedTransactionsCorrelationPValues(
esClient,
params,
histogramRangeSteps,
fieldName
)
)
@ -139,7 +198,15 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact
fetchErrorCorrelations();
return () => {
const { ccsWarning, error, isRunning, progress } = state.getState();
const {
ccsWarning,
error,
isRunning,
overallHistogram,
errorHistogram,
percentileThresholdValue,
progress,
} = state.getState();
return {
cancel: () => {
@ -158,6 +225,9 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact
log: getLogMessages(),
took: Date.now() - progress.started,
failedTransactionsCorrelations: state.getFailedTransactionsCorrelationsSortedByScore(),
overallHistogram,
errorHistogram,
percentileThresholdValue,
},
};
};

View file

@ -7,11 +7,16 @@
import { FailedTransactionsCorrelation } from '../../../../common/search_strategies/failed_transactions_correlations/types';
import type { HistogramItem } from '../../../../common/search_strategies/types';
interface Progress {
started: number;
loadedFieldCandidates: number;
loadedErrorCorrelations: number;
loadedOverallHistogram: number;
loadedFailedTransactionsCorrelations: number;
}
export const failedTransactionsCorrelationsSearchServiceStateProvider = () => {
let ccsWarning = false;
function setCcsWarning(d: boolean) {
@ -33,9 +38,26 @@ export const failedTransactionsCorrelationsSearchServiceStateProvider = () => {
isRunning = d;
}
let errorHistogram: HistogramItem[] | undefined;
function setErrorHistogram(d: HistogramItem[]) {
errorHistogram = d;
}
let overallHistogram: HistogramItem[] | undefined;
function setOverallHistogram(d: HistogramItem[]) {
overallHistogram = d;
}
let percentileThresholdValue: number;
function setPercentileThresholdValue(d: number) {
percentileThresholdValue = d;
}
let progress: Progress = {
started: Date.now(),
loadedFieldCandidates: 0,
loadedErrorCorrelations: 0,
loadedOverallHistogram: 0,
loadedFailedTransactionsCorrelations: 0,
};
function getOverallProgress() {
@ -71,6 +93,9 @@ export const failedTransactionsCorrelationsSearchServiceStateProvider = () => {
error,
isCancelled,
isRunning,
overallHistogram,
errorHistogram,
percentileThresholdValue,
progress,
failedTransactionsCorrelations,
};
@ -86,6 +111,9 @@ export const failedTransactionsCorrelationsSearchServiceStateProvider = () => {
setError,
setIsCancelled,
setIsRunning,
setOverallHistogram,
setErrorHistogram,
setPercentileThresholdValue,
setProgress,
};
};

View file

@ -16,7 +16,7 @@ import {
import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types';
import type {
LatencyCorrelationsParams,
LatencyCorrelationsRequestParams,
LatencyCorrelationsRawResponse,
} from '../../../../common/search_strategies/latency_correlations/types';
@ -38,19 +38,19 @@ import type { SearchServiceProvider } from '../search_strategy_provider';
import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state';
export type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider<
LatencyCorrelationsParams,
LatencyCorrelationsRequestParams,
LatencyCorrelationsRawResponse
>;
export type LatencyCorrelationsSearchStrategy = ISearchStrategy<
IKibanaSearchRequest<LatencyCorrelationsParams>,
IKibanaSearchRequest<LatencyCorrelationsRequestParams>,
IKibanaSearchResponse<LatencyCorrelationsRawResponse>
>;
export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearchServiceProvider = (
esClient: ElasticsearchClient,
getApmIndices: () => Promise<ApmIndicesConfig>,
searchServiceParams: LatencyCorrelationsParams,
searchServiceParams: LatencyCorrelationsRequestParams,
includeFrozen: boolean
) => {
const { addLogMessage, getLogMessages } = searchServiceLogProvider();
@ -59,7 +59,7 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch
async function fetchCorrelations() {
let params:
| (LatencyCorrelationsParams & SearchStrategyServerParams)
| (LatencyCorrelationsRequestParams & SearchStrategyServerParams)
| undefined;
try {

View file

@ -99,8 +99,12 @@ describe('correlations', () => {
environment: ENVIRONMENT_ALL.value,
kuery: '',
},
fieldName: 'actualFieldName',
fieldValue: 'actualFieldValue',
termFilters: [
{
fieldName: 'actualFieldName',
fieldValue: 'actualFieldValue',
},
],
});
expect(query).toEqual({
bool: {

View file

@ -18,23 +18,15 @@ import { rangeRt } from '../../../routes/default_api_types';
import { getCorrelationsFilters } from '../../correlations/get_filters';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
export const getTermsQuery = (
fieldName: FieldValuePair['fieldName'] | undefined,
fieldValue: FieldValuePair['fieldValue'] | undefined
) => {
return fieldName && fieldValue ? [{ term: { [fieldName]: fieldValue } }] : [];
export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => {
return { term: { [fieldName]: fieldValue } };
};
interface QueryParams {
params: SearchStrategyParams;
fieldName?: FieldValuePair['fieldName'];
fieldValue?: FieldValuePair['fieldValue'];
termFilters?: FieldValuePair[];
}
export const getQueryWithParams = ({
params,
fieldName,
fieldValue,
}: QueryParams) => {
export const getQueryWithParams = ({ params, termFilters }: QueryParams) => {
const {
environment,
kuery,
@ -53,7 +45,7 @@ export const getQueryWithParams = ({
})
) as Setup & SetupTimeRange;
const filters = getCorrelationsFilters({
const correlationFilters = getCorrelationsFilters({
setup,
environment,
kuery,
@ -65,8 +57,8 @@ export const getQueryWithParams = ({
return {
bool: {
filter: [
...filters,
...getTermsQuery(fieldName, fieldValue),
...correlationFilters,
...(Array.isArray(termFilters) ? termFilters.map(getTermsQuery) : []),
] as estypes.QueryDslQueryContainer[],
},
};

View file

@ -38,10 +38,9 @@ export const getTransactionDurationCorrelationRequest = (
ranges: estypes.AggregationsAggregationRange[],
fractions: number[],
totalDocCount: number,
fieldName?: FieldValuePair['fieldName'],
fieldValue?: FieldValuePair['fieldValue']
termFilters?: FieldValuePair[]
): estypes.SearchRequest => {
const query = getQueryWithParams({ params, fieldName, fieldValue });
const query = getQueryWithParams({ params, termFilters });
const bucketCorrelation: BucketCorrelation = {
buckets_path: 'latency_ranges>_count',
@ -93,8 +92,7 @@ export const fetchTransactionDurationCorrelation = async (
ranges: estypes.AggregationsAggregationRange[],
fractions: number[],
totalDocCount: number,
fieldName?: FieldValuePair['fieldName'],
fieldValue?: FieldValuePair['fieldValue']
termFilters?: FieldValuePair[]
): Promise<{
ranges: unknown[];
correlation: number | null;
@ -107,8 +105,7 @@ export const fetchTransactionDurationCorrelation = async (
ranges,
fractions,
totalDocCount,
fieldName,
fieldValue
termFilters
)
);

View file

@ -9,6 +9,7 @@ import { ElasticsearchClient } from 'kibana/server';
import { SearchStrategyParams } from '../../../../common/search_strategies/types';
import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames';
import { EventOutcome } from '../../../../common/event_outcome';
import { fetchTransactionDurationRanges } from './query_ranges';
import { getQueryWithParams, getTermsQuery } from './get_query_with_params';
import { getRequestBase } from './get_request_base';
@ -26,7 +27,12 @@ export const getFailureCorrelationRequest = (
...query.bool,
filter: [
...query.bool.filter,
...getTermsQuery(EVENT_OUTCOME, EventOutcome.failure),
...[
getTermsQuery({
fieldName: EVENT_OUTCOME,
fieldValue: EventOutcome.failure,
}),
],
],
},
};
@ -60,6 +66,7 @@ export const getFailureCorrelationRequest = (
export const fetchFailedTransactionsCorrelationPValues = async (
esClient: ElasticsearchClient,
params: SearchStrategyParams,
histogramRangeSteps: number[],
fieldName: string
) => {
const resp = await esClient.search(
@ -79,7 +86,10 @@ export const fetchFailedTransactionsCorrelationPValues = async (
bg_count: number;
score: number;
}>;
const result = overallResult.buckets.map((bucket) => {
// Using for of to sequentially augment the results with histogram data.
const result = [];
for (const bucket of overallResult.buckets) {
// Scale the score into a value from 0 - 1
// using a concave piecewise linear function in -log(p-value)
const normalizedScore =
@ -87,7 +97,17 @@ export const fetchFailedTransactionsCorrelationPValues = async (
0.25 * Math.min(Math.max((bucket.score - 6.908) / 6.908, 0), 1) +
0.25 * Math.min(Math.max((bucket.score - 13.816) / 101.314, 0), 1);
return {
const histogram = await fetchTransactionDurationRanges(
esClient,
params,
histogramRangeSteps,
[
{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure },
{ fieldName, fieldValue: bucket.key },
]
);
result.push({
fieldName,
fieldValue: bucket.key,
doc_count: bucket.doc_count,
@ -101,8 +121,9 @@ export const fetchFailedTransactionsCorrelationPValues = async (
successPercentage:
(bucket.bg_count - bucket.doc_count) /
(overallResult.bg_count - overallResult.doc_count),
};
});
histogram,
});
}
return result;
};

View file

@ -23,12 +23,11 @@ import { getRequestBase } from './get_request_base';
export const getTransactionDurationHistogramRequest = (
params: SearchStrategyParams,
interval: number,
fieldName?: FieldValuePair['fieldName'],
fieldValue?: FieldValuePair['fieldValue']
termFilters?: FieldValuePair[]
): estypes.SearchRequest => ({
...getRequestBase(params),
body: {
query: getQueryWithParams({ params, fieldName, fieldValue }),
query: getQueryWithParams({ params, termFilters }),
size: 0,
aggs: {
transaction_duration_histogram: {
@ -42,16 +41,10 @@ export const fetchTransactionDurationHistogram = async (
esClient: ElasticsearchClient,
params: SearchStrategyParams,
interval: number,
fieldName?: FieldValuePair['fieldName'],
fieldValue?: FieldValuePair['fieldValue']
termFilters?: FieldValuePair[]
): Promise<HistogramItem[]> => {
const resp = await esClient.search<ResponseHit>(
getTransactionDurationHistogramRequest(
params,
interval,
fieldName,
fieldValue
)
getTransactionDurationHistogramRequest(params, interval, termFilters)
);
if (resp.body.aggregations === undefined) {

View file

@ -43,15 +43,17 @@ export async function* fetchTransactionDurationHistograms(
// If one of the fields have an error
// We don't want to stop the whole process
try {
const { correlation, ksTest } = await fetchTransactionDurationCorrelation(
const {
correlation,
ksTest,
} = await fetchTransactionDurationCorrelation(
esClient,
params,
expectations,
ranges,
fractions,
totalDocCount,
item.fieldName,
item.fieldValue
[item]
);
if (state.getIsCancelled()) {
@ -69,8 +71,7 @@ export async function* fetchTransactionDurationHistograms(
esClient,
params,
histogramRangeSteps,
item.fieldName,
item.fieldValue
[item]
);
yield {
...item,

View file

@ -23,10 +23,9 @@ import { SIGNIFICANT_VALUE_DIGITS } from '../constants';
export const getTransactionDurationPercentilesRequest = (
params: SearchStrategyParams,
percents?: number[],
fieldName?: FieldValuePair['fieldName'],
fieldValue?: FieldValuePair['fieldValue']
termFilters?: FieldValuePair[]
): estypes.SearchRequest => {
const query = getQueryWithParams({ params, fieldName, fieldValue });
const query = getQueryWithParams({ params, termFilters });
return {
...getRequestBase(params),
@ -53,16 +52,10 @@ export const fetchTransactionDurationPercentiles = async (
esClient: ElasticsearchClient,
params: SearchStrategyParams,
percents?: number[],
fieldName?: FieldValuePair['fieldName'],
fieldValue?: FieldValuePair['fieldValue']
termFilters?: FieldValuePair[]
): Promise<{ totalDocs: number; percentiles: Record<string, number> }> => {
const resp = await esClient.search<ResponseHit>(
getTransactionDurationPercentilesRequest(
params,
percents,
fieldName,
fieldValue
)
getTransactionDurationPercentilesRequest(params, percents, termFilters)
);
// return early with no results if the search didn't return any documents

View file

@ -22,10 +22,9 @@ import { getRequestBase } from './get_request_base';
export const getTransactionDurationRangesRequest = (
params: SearchStrategyParams,
rangesSteps: number[],
fieldName?: FieldValuePair['fieldName'],
fieldValue?: FieldValuePair['fieldValue']
termFilters?: FieldValuePair[]
): estypes.SearchRequest => {
const query = getQueryWithParams({ params, fieldName, fieldValue });
const query = getQueryWithParams({ params, termFilters });
const ranges = rangesSteps.reduce(
(p, to) => {
@ -60,16 +59,10 @@ export const fetchTransactionDurationRanges = async (
esClient: ElasticsearchClient,
params: SearchStrategyParams,
rangesSteps: number[],
fieldName?: FieldValuePair['fieldName'],
fieldValue?: FieldValuePair['fieldValue']
termFilters?: FieldValuePair[]
): Promise<Array<{ key: number; doc_count: number }>> => {
const resp = await esClient.search<ResponseHit>(
getTransactionDurationRangesRequest(
params,
rangesSteps,
fieldName,
fieldValue
)
getTransactionDurationRangesRequest(params, rangesSteps, termFilters)
);
if (resp.body.aggregations === undefined) {

View file

@ -13,6 +13,7 @@ import { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common'
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
import type { LatencyCorrelationsParams } from '../../../common/search_strategies/latency_correlations/types';
import type { SearchStrategyClientParams } from '../../../common/search_strategies/types';
import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices';
@ -124,7 +125,9 @@ describe('APM Correlations search strategy', () => {
let mockGetApmIndicesMock: jest.Mock;
let mockDeps: SearchStrategyDependencies;
let params: Required<
IKibanaSearchRequest<LatencyCorrelationsParams>
IKibanaSearchRequest<
LatencyCorrelationsParams & SearchStrategyClientParams
>
>['params'];
beforeEach(() => {

View file

@ -10,6 +10,7 @@ import expect from '@kbn/expect';
import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common';
import type { FailedTransactionsCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/failed_transactions_correlations/types';
import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types';
import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@ -21,12 +22,15 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('legacySupertestAsApmReadUser');
const getRequestBody = () => {
const request: IKibanaSearchRequest<FailedTransactionsCorrelationsParams> = {
const request: IKibanaSearchRequest<
FailedTransactionsCorrelationsParams & SearchStrategyClientParams
> = {
params: {
environment: 'ENVIRONMENT_ALL',
start: '2020',
end: '2021',
kuery: '',
percentileThreshold: 95,
},
};
@ -210,8 +214,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const { rawResponse: finalRawResponse } = followUpResult;
expect(typeof finalRawResponse?.took).to.be('number');
expect(finalRawResponse?.percentileThresholdValue).to.be(undefined);
expect(finalRawResponse?.overallHistogram).to.be(undefined);
expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875);
expect(finalRawResponse?.errorHistogram.length).to.be(101);
expect(finalRawResponse?.overallHistogram.length).to.be(101);
expect(finalRawResponse?.failedTransactionsCorrelations.length).to.eql(
30,
@ -219,6 +224,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
);
expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([
'Fetched 95th percentile value of 1309695.875 based on 1244 documents.',
'Identified 68 fieldCandidates.',
'Identified correlations for 68 fields out of 68 candidates.',
'Identified 30 significant correlations relating to failed transactions.',

View file

@ -10,6 +10,7 @@ import expect from '@kbn/expect';
import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common';
import type { LatencyCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/latency_correlations/types';
import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types';
import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@ -21,7 +22,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('legacySupertestAsApmReadUser');
const getRequestBody = () => {
const request: IKibanaSearchRequest<LatencyCorrelationsParams> = {
const request: IKibanaSearchRequest<LatencyCorrelationsParams & SearchStrategyClientParams> = {
params: {
environment: 'ENVIRONMENT_ALL',
start: '2020',
@ -138,7 +139,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
);
registry.when(
'Correlations latency_ml with data and opbeans-node args',
'correlations latency with data and opbeans-node args',
{ config: 'trial', archives: ['8.0.0'] },
() => {
// putting this into a single `it` because the responses depend on each other

View file

@ -135,7 +135,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const apmFailedTransactionsCorrelationsTabTitle = await testSubjects.getVisibleText(
'apmFailedTransactionsCorrelationsTabTitle'
);
expect(apmFailedTransactionsCorrelationsTabTitle).to.be('Failed transactions');
expect(apmFailedTransactionsCorrelationsTabTitle).to.be(
'Failed transactions latency distribution'
);
// Assert that the data fully loaded to 100%
const apmFailedTransactionsCorrelationsProgressTitle = await testSubjects.getVisibleText(