[APM] Correlations Beta (#86477) (#89952)

* [APM] Correlations GA (#86477)

* polish and improvements to correlations UI

* more improvements and polish

* added impact bar

* added descriptions

* make custom field persistence be unique per service

* make custom threshold unique per service in latency correlations

* adds telemetry for apm correlations feature. Events:
 - 'show_correlations_flyout'
 - 'customize_correlations_fields'
 - 'select_significant_term'

* adds more telemetry for correlations (#90622)

* removes the raw score column

* replaces experiemental callout with beta badge

* replaces threshold number input with percentile option selector

* improvements to latency correlations scoring and percentage reporting

* removes the 'apm:enableCorrelations' UI setting

* - rename useFieldNames.ts -> use_field_names.ts
- filter out fields that are not type 'keyword'
- feedback improvements

* Fixes casing issue for the 'correlations' dir

* [APM] Moves correlations button to service details tabslist row (#91080)

* [APM] Adds license check for correlations (#90766)

* [APM] Adds metrics tracking for correlations views and license prompts (#90622)

* Updated the API integration tests to check for new default fields and 15 buckets

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Oliver Gupte 2021-02-16 23:21:47 -05:00 committed by GitHub
parent 8ce6ed42a3
commit d7d2b15cdb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 794 additions and 350 deletions

View file

@ -127,5 +127,4 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
'securitySolution:rulesTableRefresh': { type: 'text' },
'apm:enableSignificantTerms': { type: 'boolean' },
'apm:enableServiceOverview': { type: 'boolean' },
'apm:enableCorrelations': { type: 'boolean' },
};

View file

@ -30,7 +30,6 @@ export interface UsageStats {
'securitySolution:rulesTableRefresh': string;
'apm:enableSignificantTerms': boolean;
'apm:enableServiceOverview': boolean;
'apm:enableCorrelations': boolean;
'visualize:enableLabs': boolean;
'visualization:heatmap:maxBuckets': number;
'visualization:colorMapping': string;

View file

@ -4384,9 +4384,6 @@
},
"apm:enableServiceOverview": {
"type": "boolean"
},
"apm:enableCorrelations": {
"type": "boolean"
}
}
},

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export const enableCorrelations = 'apm:enableCorrelations';
export const enableServiceOverview = 'apm:enableServiceOverview';

View file

@ -1,108 +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 React, { useState } from 'react';
import {
EuiButtonEmpty,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiPortal,
EuiCode,
EuiLink,
EuiCallOut,
EuiButton,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { EuiSpacer } from '@elastic/eui';
import { isActivePlatinumLicense } from '../../../../common/license_check';
import { enableCorrelations } from '../../../../common/ui_settings_keys';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { LatencyCorrelations } from './LatencyCorrelations';
import { ErrorCorrelations } from './ErrorCorrelations';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { createHref } from '../../shared/Links/url_helpers';
import { useLicenseContext } from '../../../context/license/use_license_context';
export function Correlations() {
const { uiSettings } = useApmPluginContext().core;
const { urlParams } = useUrlParams();
const license = useLicenseContext();
const history = useHistory();
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
if (
!uiSettings.get(enableCorrelations) ||
!isActivePlatinumLicense(license)
) {
return null;
}
return (
<>
<EuiButton
onClick={() => {
setIsFlyoutVisible(true);
}}
>
View correlations
</EuiButton>
<EuiSpacer size="s" />
{isFlyoutVisible && (
<EuiPortal>
<EuiFlyout
size="m"
ownFocus
onClose={() => setIsFlyoutVisible(false)}
>
<EuiFlyoutHeader hasBorder aria-labelledby="correlations-flyout">
<EuiTitle>
<h2 id="correlations-flyout">Correlations</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{urlParams.kuery ? (
<>
<EuiCallOut size="m">
<span>Filtering by</span>
<EuiCode>{urlParams.kuery}</EuiCode>
<EuiLink
href={createHref(history, { query: { kuery: '' } })}
>
<EuiButtonEmpty iconType="cross">Clear</EuiButtonEmpty>
</EuiLink>
</EuiCallOut>
<EuiSpacer />
</>
) : null}
<EuiCallOut
size="s"
title="Experimental"
color="warning"
iconType="alert"
>
<p>
Correlations is an experimental feature and in active
development. Bugs and surprises are to be expected but let us
know your feedback so we can improve it.
</p>
</EuiCallOut>
<EuiSpacer />
<LatencyCorrelations />
<ErrorCorrelations />
</EuiFlyoutBody>
</EuiFlyout>
</EuiPortal>
)}
</>
);
}

View file

@ -5,16 +5,23 @@
* 2.0.
*/
import React from 'react';
import { EuiIcon, EuiLink } from '@elastic/eui';
import React, { useCallback } from 'react';
import { debounce } from 'lodash';
import {
EuiIcon,
EuiLink,
EuiBasicTable,
EuiBasicTableColumn,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useHistory } from 'react-router-dom';
import { EuiBasicTable } from '@elastic/eui';
import { EuiBasicTableColumn } from '@elastic/eui';
import { EuiCode } from '@elastic/eui';
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';
type CorrelationsApiResponse =
| APIReturnType<'GET /api/apm/correlations/failed_transactions'>
@ -27,49 +34,83 @@ type SignificantTerm = NonNullable<
interface Props<T> {
significantTerms?: T[];
status: FETCH_STATUS;
cardinalityColumnName: string;
percentageColumnName: string;
setSelectedSignificantTerm: (term: T | null) => void;
onFilter: () => void;
}
export function SignificantTermsTable<T extends SignificantTerm>({
export function CorrelationsTable<T extends SignificantTerm>({
significantTerms,
status,
cardinalityColumnName,
percentageColumnName,
setSelectedSignificantTerm,
onFilter,
}: Props<T>) {
const trackApmEvent = useUiTracker({ app: 'apm' });
const trackSelectSignificantTerm = useCallback(
() =>
debounce(
() => trackApmEvent({ metric: 'select_significant_term' }),
1000
),
[trackApmEvent]
);
const history = useHistory();
const columns: Array<EuiBasicTableColumn<T>> = [
{
width: '100px',
field: 'score',
name: 'Score',
field: 'impact',
name: i18n.translate(
'xpack.apm.correlations.correlationsTable.impactLabel',
{ defaultMessage: 'Impact' }
),
render: (_: any, term: T) => {
return <EuiCode>{Math.round(term.score)}</EuiCode>;
return <ImpactBar value={term.impact * 100} />;
},
},
{
field: 'cardinality',
name: cardinalityColumnName,
field: 'percentage',
name: percentageColumnName,
render: (_: any, term: T) => {
const matches = asPercent(term.fgCount, term.bgCount);
return `${asInteger(term.fgCount)} (${matches})`;
return (
<EuiToolTip
position="right"
content={`${asInteger(term.valueCount)} / ${asInteger(
term.fieldCount
)}`}
>
<>{asPercent(term.valueCount, term.fieldCount)}</>
</EuiToolTip>
);
},
},
{
field: 'fieldName',
name: 'Field name',
name: i18n.translate(
'xpack.apm.correlations.correlationsTable.fieldNameLabel',
{ defaultMessage: 'Field name' }
),
},
{
field: 'fieldValue',
name: 'Field value',
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: 'Focus',
description: 'Focus on this term',
name: i18n.translate(
'xpack.apm.correlations.correlationsTable.filterLabel',
{ defaultMessage: 'Filter' }
),
description: i18n.translate(
'xpack.apm.correlations.correlationsTable.filterDescription',
{ defaultMessage: 'Filter by value' }
),
icon: 'magnifyWithPlus',
type: 'icon',
onClick: (term: T) => {
@ -80,11 +121,19 @@ export function SignificantTermsTable<T extends SignificantTerm>({
)}"`,
},
});
onFilter();
trackApmEvent({ metric: 'correlations_term_include_filter' });
},
},
{
name: 'Exclude',
description: 'Exclude this term',
name: i18n.translate(
'xpack.apm.correlations.correlationsTable.excludeLabel',
{ defaultMessage: 'Exclude' }
),
description: i18n.translate(
'xpack.apm.correlations.correlationsTable.excludeDescription',
{ defaultMessage: 'Filter out value' }
),
icon: 'magnifyWithMinus',
type: 'icon',
onClick: (term: T) => {
@ -95,10 +144,15 @@ export function SignificantTermsTable<T extends SignificantTerm>({
)}"`,
},
});
onFilter();
trackApmEvent({ metric: 'correlations_term_exclude_filter' });
},
},
],
name: 'Actions',
name: i18n.translate(
'xpack.apm.correlations.correlationsTable.actionsLabel',
{ defaultMessage: 'Actions' }
),
render: (_: any, term: T) => {
return (
<>
@ -134,15 +188,30 @@ export function SignificantTermsTable<T extends SignificantTerm>({
return (
<EuiBasicTable
items={significantTerms ?? []}
noItemsMessage={status === FETCH_STATUS.LOADING ? 'Loading' : 'No data'}
noItemsMessage={
status === FETCH_STATUS.LOADING ? loadingText : noDataText
}
loading={status === FETCH_STATUS.LOADING}
columns={columns}
rowProps={(term) => {
return {
onMouseEnter: () => setSelectedSignificantTerm(term),
onMouseEnter: () => {
setSelectedSignificantTerm(term);
trackSelectSignificantTerm();
},
onMouseLeave: () => setSelectedSignificantTerm(null),
};
}}
/>
);
}
const loadingText = i18n.translate(
'xpack.apm.correlations.correlationsTable.loadingText',
{ defaultMessage: 'Loading' }
);
const noDataText = i18n.translate(
'xpack.apm.correlations.correlationsTable.noDataText',
{ defaultMessage: 'No data' }
);

View file

@ -0,0 +1,165 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiAccordion,
EuiComboBox,
EuiFormRow,
EuiLink,
EuiSelect,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useEffect, useState } from 'react';
import { useFieldNames } from './use_field_names';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { useUiTracker } from '../../../../../observability/public';
interface Props {
fieldNames: string[];
setFieldNames: (fieldNames: string[]) => void;
setDurationPercentile?: (value: PercentileOption) => void;
showThreshold?: boolean;
durationPercentile?: PercentileOption;
}
export type PercentileOption = 50 | 75 | 99;
const percentilOptions: PercentileOption[] = [50, 75, 99];
export function CustomFields({
fieldNames,
setFieldNames,
setDurationPercentile = () => {},
showThreshold = false,
durationPercentile = 75,
}: Props) {
const trackApmEvent = useUiTracker({ app: 'apm' });
const { defaultFieldNames, getSuggestions } = useFieldNames();
const [suggestedFieldNames, setSuggestedFieldNames] = useState(
getSuggestions('')
);
useEffect(() => {
if (suggestedFieldNames.length) {
return;
}
setSuggestedFieldNames(getSuggestions(''));
}, [getSuggestions, suggestedFieldNames]);
return (
<EuiAccordion
id="accordion"
buttonContent={i18n.translate(
'xpack.apm.correlations.customize.buttonLabel',
{ defaultMessage: 'Customize fields' }
)}
>
<EuiSpacer />
<EuiFlexGroup direction="column">
{showThreshold && (
<EuiFlexItem grow={false}>
<EuiFormRow
label={i18n.translate(
'xpack.apm.correlations.customize.thresholdLabel',
{ defaultMessage: 'Threshold' }
)}
helpText="Default threshold is 75th percentile."
>
<EuiSelect
value={durationPercentile}
options={percentilOptions.map((percentile) => ({
value: percentile,
text: i18n.translate(
'xpack.apm.correlations.customize.thresholdPercentile',
{
defaultMessage: '{percentile}th percentile',
values: { percentile },
}
),
}))}
onChange={(e) => {
setDurationPercentile(
parseInt(e.target.value, 10) as PercentileOption
);
}}
/>
</EuiFormRow>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiFormRow
fullWidth={true}
label={i18n.translate(
'xpack.apm.correlations.customize.fieldLabel',
{ defaultMessage: 'Field' }
)}
helpText={
<FormattedMessage
id="xpack.apm.correlations.customize.fieldHelpText"
defaultMessage="Customize or {reset} fields to analyze for correlations. {docsLink}"
values={{
reset: (
<EuiLink
type="reset"
onClick={() => {
setFieldNames(defaultFieldNames);
}}
>
{i18n.translate(
'xpack.apm.correlations.customize.fieldHelpTextReset',
{ defaultMessage: 'reset' }
)}
</EuiLink>
),
docsLink: (
<ElasticDocsLink
section="/kibana"
path="/advanced-queries.html"
>
{i18n.translate(
'xpack.apm.correlations.customize.fieldHelpTextDocsLink',
{
defaultMessage:
'Learn more about the default fields.',
}
)}
</ElasticDocsLink>
),
}}
/>
}
>
<EuiComboBox
fullWidth={true}
placeholder={i18n.translate(
'xpack.apm.correlations.customize.fieldPlaceholder',
{ defaultMessage: 'Select or create options' }
)}
selectedOptions={fieldNames.map((label) => ({ label }))}
onChange={(options) => {
const nextFieldNames = options.map((option) => option.label);
setFieldNames(nextFieldNames);
trackApmEvent({ metric: 'customize_correlations_fields' });
}}
onCreateOption={(term) => {
const nextFieldNames = [...fieldNames, term];
setFieldNames(nextFieldNames);
}}
onSearchChange={(searchValue) => {
setSuggestedFieldNames(getSuggestions(searchValue));
}}
options={suggestedFieldNames.map((label) => ({ label }))}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
);
}

View file

@ -17,19 +17,19 @@ import {
} from '@elastic/charts';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import {
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiComboBox,
EuiAccordion,
} from '@elastic/eui';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { px } from '../../../style/variables';
import { SignificantTermsTable } from './SignificantTermsTable';
import { CorrelationsTable } from './correlations_table';
import { ChartContainer } from '../../shared/charts/chart_container';
import { useTheme } from '../../../hooks/use_theme';
import { CustomFields } from './custom_fields';
import { useFieldNames } from './use_field_names';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { useUiTracker } from '../../../../../observability/public';
type CorrelationsApiResponse = NonNullable<
APIReturnType<'GET /api/apm/correlations/failed_transactions'>
@ -39,29 +39,24 @@ type SignificantTerm = NonNullable<
CorrelationsApiResponse['significantTerms']
>[0];
const initialFieldNames = [
'transaction.name',
'user.username',
'user.id',
'host.ip',
'user_agent.name',
'kubernetes.pod.uuid',
'kubernetes.pod.name',
'url.domain',
'container.id',
'service.node.name',
].map((label) => ({ label }));
interface Props {
onClose: () => void;
}
export function ErrorCorrelations() {
export function ErrorCorrelations({ onClose }: Props) {
const [
selectedSignificantTerm,
setSelectedSignificantTerm,
] = useState<SignificantTerm | null>(null);
const [fieldNames, setFieldNames] = useState(initialFieldNames);
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
const { transactionName, transactionType, start, end } = urlParams;
const { defaultFieldNames } = useFieldNames();
const [fieldNames, setFieldNames] = useLocalStorage(
`apm.correlations.errors.fields:${serviceName}`,
defaultFieldNames
);
const { data, status } = useFetcher(
(callApmApi) => {
@ -76,7 +71,7 @@ export function ErrorCorrelations() {
start,
end,
uiFilters: JSON.stringify(uiFilters),
fieldNames: fieldNames.map((field) => field.label).join(','),
fieldNames: fieldNames.join(','),
},
},
});
@ -93,12 +88,29 @@ export function ErrorCorrelations() {
]
);
const trackApmEvent = useUiTracker({ app: 'apm' });
trackApmEvent({ metric: 'view_errors_correlations' });
return (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiTitle size="s">
<h4>Error rate over time</h4>
<EuiText size="s">
<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>
@ -109,26 +121,20 @@ export function ErrorCorrelations() {
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiAccordion id="accordion" buttonContent="Customize">
<EuiComboBox
fullWidth={true}
placeholder="Select or create options"
selectedOptions={fieldNames}
onChange={setFieldNames}
onCreateOption={(term) =>
setFieldNames((names) => [...names, { label: term }])
}
/>
</EuiAccordion>
</EuiFlexItem>
<EuiFlexItem>
<SignificantTermsTable
cardinalityColumnName="# of failed transactions"
<CorrelationsTable
percentageColumnName={i18n.translate(
'xpack.apm.correlations.error.percentageColumnName',
{ defaultMessage: '% of failed transactions' }
)}
significantTerms={data?.significantTerms}
status={status}
setSelectedSignificantTerm={setSelectedSignificantTerm}
onFilter={onClose}
/>
</EuiFlexItem>
<EuiFlexItem>
<CustomFields fieldNames={fieldNames} setFieldNames={setFieldNames} />
</EuiFlexItem>
</EuiFlexGroup>
</>
);
@ -143,6 +149,7 @@ function ErrorTimeseriesChart({
selectedSignificantTerm: SignificantTerm | null;
status: FETCH_STATUS;
}) {
const theme = useTheme();
const dateFormatter = timeFormatter('HH:mm:ss');
return (
@ -164,7 +171,10 @@ function ErrorTimeseriesChart({
/>
<LineSeries
id="Overall error rate"
id={i18n.translate(
'xpack.apm.correlations.error.chart.overallErrorRateLabel',
{ defaultMessage: 'Overall error rate' }
)}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={'x'}
@ -175,12 +185,21 @@ function ErrorTimeseriesChart({
{selectedSignificantTerm !== null ? (
<LineSeries
id="Error rate for selected term"
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="red"
color={theme.eui.euiColorAccent}
data={selectedSignificantTerm.timeseries}
curve={CurveType.CURVE_MONOTONE_X}
/>

View file

@ -0,0 +1,191 @@
/*
* 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 {
EuiButtonEmpty,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiPortal,
EuiCode,
EuiLink,
EuiCallOut,
EuiButton,
EuiTab,
EuiTabs,
EuiSpacer,
EuiBetaBadge,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useHistory } from 'react-router-dom';
import { LatencyCorrelations } from './latency_correlations';
import { ErrorCorrelations } from './error_correlations';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { createHref } from '../../shared/Links/url_helpers';
import {
METRIC_TYPE,
useTrackMetric,
} from '../../../../../observability/public';
import { isActivePlatinumLicense } from '../../../../common/license_check';
import { useLicenseContext } from '../../../context/license/use_license_context';
import { LicensePrompt } from '../../shared/LicensePrompt';
const latencyTab = {
key: 'latency',
label: i18n.translate('xpack.apm.correlations.tabs.latencyLabel', {
defaultMessage: 'Latency',
}),
component: LatencyCorrelations,
};
const errorRateTab = {
key: 'errorRate',
label: i18n.translate('xpack.apm.correlations.tabs.errorRateLabel', {
defaultMessage: 'Error rate',
}),
component: ErrorCorrelations,
};
const tabs = [latencyTab, errorRateTab];
export function Correlations() {
const { urlParams } = useUrlParams();
const history = useHistory();
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const [currentTab, setCurrentTab] = useState(latencyTab.key);
const { component: TabContent } =
tabs.find((tab) => tab.key === currentTab) ?? latencyTab;
return (
<>
<EuiButton
onClick={() => {
setIsFlyoutVisible(true);
}}
iconType="visTagCloud"
>
{i18n.translate('xpack.apm.correlations.buttonLabel', {
defaultMessage: 'View correlations',
})}
</EuiButton>
{isFlyoutVisible && (
<EuiPortal>
<EuiFlyout
size="m"
ownFocus
onClose={() => setIsFlyoutVisible(false)}
>
<EuiFlyoutHeader hasBorder aria-labelledby="correlations-flyout">
<EuiTitle>
<h2 id="correlations-flyout">
{CORRELATIONS_TITLE}
&nbsp;
<EuiBetaBadge
label={i18n.translate('xpack.apm.correlations.betaLabel', {
defaultMessage: 'Beta',
})}
title={CORRELATIONS_TITLE}
tooltipContent={i18n.translate(
'xpack.apm.correlations.betaDescription',
{
defaultMessage:
'Correlations is not GA. Please help us by reporting any bugs.',
}
)}
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<CorrelationsMetricsLicenseCheck>
{urlParams.kuery ? (
<>
<EuiCallOut size="m">
<span>
{i18n.translate(
'xpack.apm.correlations.filteringByLabel',
{ defaultMessage: 'Filtering by' }
)}
</span>
<EuiCode>{urlParams.kuery}</EuiCode>
<EuiLink
href={createHref(history, { query: { kuery: '' } })}
>
<EuiButtonEmpty iconType="cross">
{i18n.translate(
'xpack.apm.correlations.clearFiltersLabel',
{ defaultMessage: 'Clear' }
)}
</EuiButtonEmpty>
</EuiLink>
</EuiCallOut>
<EuiSpacer />
</>
) : null}
<EuiSpacer />
<EuiTabs>
{tabs.map(({ key, label }) => (
<EuiTab
key={key}
isSelected={key === currentTab}
onClick={() => {
setCurrentTab(key);
}}
>
{label}
</EuiTab>
))}
</EuiTabs>
<EuiSpacer />
<TabContent onClose={() => setIsFlyoutVisible(false)} />
</CorrelationsMetricsLicenseCheck>
</EuiFlyoutBody>
</EuiFlyout>
</EuiPortal>
)}
</>
);
}
const CORRELATIONS_TITLE = i18n.translate('xpack.apm.correlations.title', {
defaultMessage: 'Correlations',
});
function CorrelationsMetricsLicenseCheck({
children,
}: {
children: React.ReactNode;
}) {
const license = useLicenseContext();
const hasActivePlatinumLicense = isActivePlatinumLicense(license);
const metric = {
app: 'apm' as const,
metric: hasActivePlatinumLicense
? 'correlations_flyout_view'
: 'correlations_license_prompt',
metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT,
};
useTrackMetric(metric);
useTrackMetric({ ...metric, delay: 15000 });
return (
<>
{hasActivePlatinumLicense ? (
children
) : (
<LicensePrompt
text={i18n.translate('xpack.apm.correlations.licenseCheckText', {
defaultMessage: `To use correlations, you must be subscribed to an Elastic Platinum license. With it, you'll be able to discover which fields are correlated with poor performance.`,
})}
/>
)}
</>
);
}

View file

@ -15,21 +15,19 @@ import {
} from '@elastic/charts';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import {
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiComboBox,
EuiAccordion,
EuiFormRow,
EuiFieldNumber,
} from '@elastic/eui';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getDurationFormatter } from '../../../../common/utils/formatters';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { SignificantTermsTable } from './SignificantTermsTable';
import { CorrelationsTable } from './correlations_table';
import { ChartContainer } from '../../shared/charts/chart_container';
import { useTheme } from '../../../hooks/use_theme';
import { CustomFields, PercentileOption } from './custom_fields';
import { useFieldNames } from './use_field_names';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { useUiTracker } from '../../../../../observability/public';
type CorrelationsApiResponse = NonNullable<
APIReturnType<'GET /api/apm/correlations/slow_transactions'>
@ -39,29 +37,31 @@ type SignificantTerm = NonNullable<
CorrelationsApiResponse['significantTerms']
>[0];
const initialFieldNames = [
'user.username',
'user.id',
'host.ip',
'user_agent.name',
'kubernetes.pod.uuid',
'kubernetes.pod.name',
'url.domain',
'container.id',
'service.node.name',
].map((label) => ({ label }));
interface Props {
onClose: () => void;
}
export function LatencyCorrelations() {
export function LatencyCorrelations({ onClose }: Props) {
const [
selectedSignificantTerm,
setSelectedSignificantTerm,
] = useState<SignificantTerm | null>(null);
const [fieldNames, setFieldNames] = useState(initialFieldNames);
const [durationPercentile, setDurationPercentile] = useState('50');
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
const { transactionName, transactionType, start, end } = urlParams;
const { defaultFieldNames } = useFieldNames();
const [fieldNames, setFieldNames] = useLocalStorage(
`apm.correlations.latency.fields:${serviceName}`,
defaultFieldNames
);
const [
durationPercentile,
setDurationPercentile,
] = useLocalStorage<PercentileOption>(
`apm.correlations.latency.threshold:${serviceName}`,
75
);
const { data, status } = useFetcher(
(callApmApi) => {
@ -76,8 +76,8 @@ export function LatencyCorrelations() {
start,
end,
uiFilters: JSON.stringify(uiFilters),
durationPercentile,
fieldNames: fieldNames.map((field) => field.label).join(','),
durationPercentile: durationPercentile.toString(10),
fieldNames: fieldNames.join(','),
},
},
});
@ -95,14 +95,32 @@ export function LatencyCorrelations() {
]
);
const trackApmEvent = useUiTracker({ app: 'apm' });
trackApmEvent({ metric: 'view_latency_correlations' });
return (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiText size="s">
<p>
{i18n.translate('xpack.apm.correlations.latency.description', {
defaultMessage:
'What is slowing down my service? Correlations will help discover a slower performance in a particular cohort of your data. Either by host, version, or other custom fields.',
})}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiTitle size="s">
<h4>Latency distribution</h4>
<EuiTitle size="xxs">
<h4>
{i18n.translate(
'xpack.apm.correlations.latency.chart.title',
{ defaultMessage: 'Latency distribution' }
)}
</h4>
</EuiTitle>
<LatencyDistributionChart
data={data}
@ -113,44 +131,24 @@ export function LatencyCorrelations() {
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiAccordion id="accordion" buttonContent="Customize">
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiFormRow label="Threshold">
<EuiFieldNumber
value={durationPercentile}
onChange={(e) =>
setDurationPercentile(e.currentTarget.value)
}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={4}>
<EuiFormRow
fullWidth={true}
label="Field"
helpText="Fields to analyse for correlations"
>
<EuiComboBox
fullWidth={true}
placeholder="Select or create options"
selectedOptions={fieldNames}
onChange={setFieldNames}
onCreateOption={(term) => {
setFieldNames((names) => [...names, { label: term }]);
}}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
</EuiFlexItem>
<EuiFlexItem>
<SignificantTermsTable
cardinalityColumnName="# of slow transactions"
<CorrelationsTable
percentageColumnName={i18n.translate(
'xpack.apm.correlations.latency.percentageColumnName',
{ defaultMessage: '% of slow transactions' }
)}
significantTerms={data?.significantTerms}
status={status}
setSelectedSignificantTerm={setSelectedSignificantTerm}
onFilter={onClose}
/>
</EuiFlexItem>
<EuiFlexItem>
<CustomFields
fieldNames={fieldNames}
setFieldNames={setFieldNames}
showThreshold
setDurationPercentile={setDurationPercentile}
durationPercentile={durationPercentile}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -181,6 +179,7 @@ function LatencyDistributionChart({
selectedSignificantTerm: SignificantTerm | null;
status: FETCH_STATUS;
}) {
const theme = useTheme();
const xMax = Math.max(
...(data?.overall?.distribution.map((p) => p.x ?? 0) ?? [])
);
@ -218,7 +217,10 @@ function LatencyDistributionChart({
/>
<BarSeries
id="Overall latency distribution"
id={i18n.translate(
'xpack.apm.correlations.latency.chart.overallLatencyDistributionLabel',
{ defaultMessage: 'Overall latency distribution' }
)}
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor={'x'}
@ -230,12 +232,21 @@ function LatencyDistributionChart({
{selectedSignificantTerm !== null ? (
<BarSeries
id="Latency distribution for selected term"
id={i18n.translate(
'xpack.apm.correlations.latency.chart.selectedTermLatencyDistributionLabel',
{
defaultMessage: '{fieldName}:{fieldValue}',
values: {
fieldName: selectedSignificantTerm.fieldName,
fieldValue: selectedSignificantTerm.fieldValue,
},
}
)}
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor={'x'}
yAccessors={['y']}
color="red"
color={theme.eui.euiColorAccent}
data={selectedSignificantTerm.distribution}
minBarHeight={5}
tickFormat={(d) => `${roundFloat(d)}%`}

View file

@ -0,0 +1,74 @@
/*
* 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 { memoize } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import { isRumAgentName } from '../../../../common/agent_name';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useDynamicIndexPatternFetcher } from '../../../hooks/use_dynamic_index_pattern';
interface IndexPattern {
fields: Array<{ name: string; esTypes: string[] }>;
}
export function useFieldNames() {
const { agentName } = useApmServiceContext();
const isRumAgent = isRumAgentName(agentName);
const { indexPattern } = useDynamicIndexPatternFetcher();
const [defaultFieldNames, setDefaultFieldNames] = useState(
getDefaultFieldNames(indexPattern, isRumAgent)
);
const getSuggestions = useMemo(
() =>
memoize((searchValue: string) =>
getMatchingFieldNames(indexPattern, searchValue)
),
[indexPattern]
);
useEffect(() => {
setDefaultFieldNames(getDefaultFieldNames(indexPattern, isRumAgent));
}, [indexPattern, isRumAgent]);
return { defaultFieldNames, getSuggestions };
}
function getMatchingFieldNames(
indexPattern: IndexPattern | undefined,
inputValue: string
) {
if (!indexPattern) {
return [];
}
return indexPattern.fields
.filter(
({ name, esTypes }) =>
name.startsWith(inputValue) && esTypes[0] === 'keyword' // only show fields of type 'keyword'
)
.map(({ name }) => name);
}
function getDefaultFieldNames(
indexPattern: IndexPattern | undefined,
isRumAgent: boolean
) {
const labelFields = getMatchingFieldNames(indexPattern, 'labels.').slice(
0,
6
);
return isRumAgent
? [
...labelFields,
'user_agent.name',
'user_agent.os.name',
'url.original',
...getMatchingFieldNames(indexPattern, 'user.').slice(0, 6),
]
: [...labelFields, 'service.version', 'service.node.name', 'host.ip'];
}

View file

@ -26,6 +26,7 @@ import { ServiceNodeOverview } from '../service_node_overview';
import { ServiceMetrics } from '../service_metrics';
import { ServiceOverview } from '../service_overview';
import { TransactionOverview } from '../transaction_overview';
import { Correlations } from '../correlations';
interface Tab {
key: string;
@ -137,6 +138,9 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) {
{text}
</EuiTab>
))}
<div style={{ marginLeft: 'auto' }}>
<Correlations />
</div>
</MainTabs>
{selectedTab ? selectedTab.render() : null}
</>

View file

@ -57,7 +57,11 @@ export function ServiceOverview({
return (
<AnnotationsContextProvider>
<ChartPointerEventContextProvider>
<SearchBar prepend={transactionTypeLabel} showTimeComparison />
<SearchBar
prepend={transactionTypeLabel}
showTimeComparison
showCorrelations
/>
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>

View file

@ -5,14 +5,13 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel } from '@elastic/eui';
import { EuiFlexGroup, EuiPage, EuiPanel } from '@elastic/eui';
import React from 'react';
import { useTrackPageview } from '../../../../../observability/public';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { SearchBar } from '../../shared/search_bar';
import { Correlations } from '../Correlations';
import { TraceList } from './TraceList';
type TracesAPIResponse = APIReturnType<'GET /api/apm/traces'>;
@ -48,14 +47,9 @@ export function TraceOverview() {
return (
<>
<SearchBar />
<SearchBar showCorrelations />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<Correlations />
</EuiFlexItem>
</EuiFlexGroup>
<EuiPanel>
<TraceList
items={data.items}

View file

@ -7,7 +7,6 @@
import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPage,
EuiPanel,
@ -27,7 +26,6 @@ import { TransactionCharts } from '../../shared/charts/transaction_charts';
import { HeightRetainer } from '../../shared/HeightRetainer';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
import { SearchBar } from '../../shared/search_bar';
import { Correlations } from '../Correlations';
import { TransactionDistribution } from './Distribution';
import { useWaterfallFetcher } from './use_waterfall_fetcher';
import { WaterfallWithSummmary } from './WaterfallWithSummmary';
@ -97,15 +95,9 @@ export function TransactionDetails({
<h1>{transactionName}</h1>
</EuiTitle>
</ApmHeader>
<SearchBar showTimeComparison />
<SearchBar showTimeComparison showCorrelations />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<Correlations />
</EuiFlexItem>
</EuiFlexGroup>
<ChartPointerEventContextProvider>
<TransactionCharts />
</ChartPointerEventContextProvider>

View file

@ -29,7 +29,6 @@ import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
import { SearchBar } from '../../shared/search_bar';
import { TransactionTypeSelect } from '../../shared/transaction_type_select';
import { Correlations } from '../Correlations';
import { TransactionList } from './TransactionList';
import { useRedirect } from './useRedirect';
import { useTransactionListFetcher } from './use_transaction_list';
@ -83,7 +82,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
return (
<>
<SearchBar showTimeComparison />
<SearchBar showTimeComparison showCorrelations />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
@ -110,9 +109,6 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
</EuiFlexGroup>
<EuiSpacer size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Correlations />
</EuiFlexItem>
</EuiFlexGroup>
<TransactionCharts />
<EuiSpacer size="s" />

View file

@ -22,15 +22,21 @@ const SearchBarFlexGroup = euiStyled(EuiFlexGroup)`
interface Props {
prepend?: React.ReactNode | string;
showTimeComparison?: boolean;
showCorrelations?: boolean;
}
function getRowDirection(showColumn: boolean) {
return showColumn ? 'column' : 'row';
}
export function SearchBar({ prepend, showTimeComparison = false }: Props) {
export function SearchBar({
prepend,
showTimeComparison = false,
showCorrelations = false,
}: Props) {
const { isMedium, isLarge } = useBreakPoints();
const itemsStyle = { marginBottom: isLarge ? px(unit) : 0 };
return (
<SearchBarFlexGroup gutterSize="m" direction={getRowDirection(isLarge)}>
<EuiFlexItem>

View file

@ -19,6 +19,7 @@ import {
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
PROCESSOR_EVENT,
} from '../../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../../common/processor_event';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
@ -48,6 +49,7 @@ export async function getCorrelationsForFailedTransactions({
const backgroundFilters: ESFilter[] = [
...esFilter,
{ range: rangeFilter(start, end) },
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
];
if (serviceName) {
@ -82,7 +84,14 @@ export async function getCorrelationsForFailedTransactions({
significant_terms: {
size: 10,
field: fieldName,
background_filter: { bool: { filter: backgroundFilters } },
background_filter: {
bool: {
filter: backgroundFilters,
must_not: {
term: { [EVENT_OUTCOME]: EventOutcome.failure },
},
},
},
},
},
};
@ -97,19 +106,12 @@ export async function getCorrelationsForFailedTransactions({
return {};
}
const failedTransactionCount =
response.aggregations?.failed_transactions.doc_count;
const totalTransactionCount = response.hits.total.value;
const avgErrorRate = (failedTransactionCount / totalTransactionCount) * 100;
const sigTermAggs = omit(
response.aggregations?.failed_transactions,
'doc_count'
);
const topSigTerms = processSignificantTermAggs({
sigTermAggs,
thresholdPercentage: avgErrorRate,
});
const topSigTerms = processSignificantTermAggs({ sigTermAggs });
return getErrorRateTimeSeries({ setup, backgroundFilters, topSigTerms });
});
}
@ -125,7 +127,7 @@ export async function getErrorRateTimeSeries({
}) {
return withApmSpan('get_error_rate_timeseries', async () => {
const { start, end, apmEventClient } = setup;
const { intervalString } = getBucketSize({ start, end, numBuckets: 30 });
const { intervalString } = getBucketSize({ start, end, numBuckets: 15 });
if (isEmpty(topSigTerms)) {
return {};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { isEmpty } from 'lodash';
import { isEmpty, dropRightWhile } from 'lodash';
import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations';
import { ESFilter } from '../../../../../../typings/elasticsearch';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
@ -41,8 +41,8 @@ export async function getLatencyDistribution({
return {};
}
const intervalBuckets = 20;
const distributionInterval = roundtoTenth(maxLatency / intervalBuckets);
const intervalBuckets = 15;
const distributionInterval = Math.floor(maxLatency / intervalBuckets);
const distributionAgg = {
// filter out outliers not included in the significant term docs
@ -111,7 +111,14 @@ export async function getLatencyDistribution({
function formatDistribution(distribution: Agg['distribution']) {
const total = distribution.doc_count;
return distribution.dist_filtered_by_latency.buckets.map((bucket) => ({
// remove trailing buckets that are empty and out of bounds of the desired number of buckets
const buckets = dropRightWhile(
distribution.dist_filtered_by_latency.buckets,
(bucket, index) => bucket.doc_count === 0 && index > intervalBuckets - 1
);
return buckets.map((bucket) => ({
x: bucket.key,
y: (bucket.doc_count / total) * 100,
}));
@ -134,7 +141,3 @@ export async function getLatencyDistribution({
};
});
}
function roundtoTenth(v: number) {
return Math.pow(10, Math.round(Math.log10(v)));
}

View file

@ -13,6 +13,7 @@ import {
TRANSACTION_DURATION,
TRANSACTION_NAME,
TRANSACTION_TYPE,
PROCESSOR_EVENT,
} from '../../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../../common/processor_event';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
@ -42,6 +43,7 @@ export async function getCorrelationsForSlowTransactions({
const backgroundFilters: ESFilter[] = [
...esFilter,
{ range: rangeFilter(start, end) },
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
];
if (serviceName) {
@ -70,14 +72,21 @@ export async function getCorrelationsForSlowTransactions({
query: {
bool: {
// foreground filters
filter: [
...backgroundFilters,
{
range: {
[TRANSACTION_DURATION]: { gte: durationForPercentile },
filter: backgroundFilters,
must: {
function_score: {
query: {
range: {
[TRANSACTION_DURATION]: { gte: durationForPercentile },
},
},
script_score: {
script: {
source: `Math.log(2 + doc['${TRANSACTION_DURATION}'].value)`,
},
},
},
],
},
},
},
aggs: fieldNames.reduce((acc, fieldName) => {
@ -87,7 +96,20 @@ export async function getCorrelationsForSlowTransactions({
significant_terms: {
size: 10,
field: fieldName,
background_filter: { bool: { filter: backgroundFilters } },
background_filter: {
bool: {
filter: [
...backgroundFilters,
{
range: {
[TRANSACTION_DURATION]: {
lt: durationForPercentile,
},
},
},
],
},
},
},
},
};
@ -102,7 +124,6 @@ export async function getCorrelationsForSlowTransactions({
const topSigTerms = processSignificantTermAggs({
sigTermAggs: response.aggregations,
thresholdPercentage: 100 - durationPercentile,
});
return getLatencyDistribution({

View file

@ -12,11 +12,12 @@ import {
} from '../../../../../typings/elasticsearch/aggregations';
export interface TopSigTerm {
bgCount: number;
fgCount: number;
fieldName: string;
fieldValue: string | number;
score: number;
impact: number;
fieldCount: number;
valueCount: number;
}
type SigTermAgg = AggregationResultOf<
@ -24,31 +25,52 @@ type SigTermAgg = AggregationResultOf<
{}
>;
function getMaxImpactScore(scores: number[]) {
if (scores.length === 0) {
return 0;
}
const sortedScores = scores.sort((a, b) => b - a);
const maxScore = sortedScores[0];
// calculate median
const halfSize = scores.length / 2;
const medianIndex = Math.floor(halfSize);
const medianScore =
medianIndex < halfSize
? sortedScores[medianIndex]
: (sortedScores[medianIndex - 1] + sortedScores[medianIndex]) / 2;
return Math.max(maxScore, medianScore * 2);
}
export function processSignificantTermAggs({
sigTermAggs,
thresholdPercentage,
}: {
sigTermAggs: Record<string, SigTermAgg>;
thresholdPercentage: number;
}) {
const significantTerms = Object.entries(sigTermAggs).flatMap(
([fieldName, agg]) => {
return agg.buckets.map((bucket) => ({
fieldName,
fieldValue: bucket.key,
bgCount: bucket.bg_count,
fgCount: bucket.doc_count,
fieldCount: agg.doc_count,
valueCount: bucket.doc_count,
score: bucket.score,
}));
}
);
const maxImpactScore = getMaxImpactScore(
significantTerms.map(({ score }) => score)
);
// get top 10 terms ordered by score
const topSigTerms = orderBy(significantTerms, 'score', 'desc')
.filter(({ bgCount, fgCount }) => {
// only include results that are above the threshold
return Math.floor((fgCount / bgCount) * 100) > thresholdPercentage;
})
.map((significantTerm) => ({
...significantTerm,
impact: significantTerm.score / maxImpactScore,
}))
.slice(0, 10);
return topSigTerms;
}

View file

@ -8,29 +8,12 @@
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { UiSettingsParams } from '../../../../src/core/types';
import {
enableCorrelations,
enableServiceOverview,
} from '../common/ui_settings_keys';
import { enableServiceOverview } from '../common/ui_settings_keys';
/**
* uiSettings definitions for APM.
*/
export const uiSettings: Record<string, UiSettingsParams<boolean>> = {
[enableCorrelations]: {
category: ['observability'],
name: i18n.translate('xpack.apm.enableCorrelationsExperimentName', {
defaultMessage: 'APM correlations (Platinum required)',
}),
value: false,
description: i18n.translate(
'xpack.apm.enableCorrelationsExperimentDescription',
{
defaultMessage: 'Enable the experimental correlations feature in APM',
}
),
schema: schema.boolean(),
},
[enableServiceOverview]: {
category: ['observability'],
name: i18n.translate('xpack.apm.enableServiceOverviewExperimentName', {

View file

@ -5016,8 +5016,6 @@
"xpack.apm.customLink.empty": "カスタムリンクが見つかりません。独自のカスタムリンク、たとえば特定のダッシュボードまたは外部リンクへのリンクをセットアップします。",
"xpack.apm.emptyMessage.noDataFoundDescription": "別の時間範囲を試すか検索フィルターをリセットしてください。",
"xpack.apm.emptyMessage.noDataFoundLabel": "データが見つかりません。",
"xpack.apm.enableCorrelationsExperimentDescription": "APM で実験的な重要な用語機能を有効にする",
"xpack.apm.enableCorrelationsExperimentName": "APM 重要な用語",
"xpack.apm.enableServiceOverviewExperimentDescription": "APM でサービスの[概要]タブを有効にします。",
"xpack.apm.enableServiceOverviewExperimentName": "APM サービス概要",
"xpack.apm.error.prompt.body": "詳細はブラウザの開発者コンソールをご確認ください。",

View file

@ -5022,8 +5022,6 @@
"xpack.apm.customLink.empty": "未找到定制链接。设置自己的定制链接,如特定仪表板的链接或外部链接。",
"xpack.apm.emptyMessage.noDataFoundDescription": "尝试其他时间范围或重置搜索筛选。",
"xpack.apm.emptyMessage.noDataFoundLabel": "未找到任何数据。",
"xpack.apm.enableCorrelationsExperimentDescription": "在 APM 中启用实验性重要词功能",
"xpack.apm.enableCorrelationsExperimentName": "APM 重要词",
"xpack.apm.enableServiceOverviewExperimentDescription": "为 APM 中的服务启用“概览”选项卡。",
"xpack.apm.enableServiceOverviewExperimentName": "APM 服务概览",
"xpack.apm.error.prompt.body": "有关详情,请查看您的浏览器开发者控制台。",

View file

@ -55,35 +55,41 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns significant terms', () => {
const sorted = response.body?.significantTerms?.sort();
expectSnapshot(sorted?.map((term) => term.fieldName)).toMatchInline(`
Array [
"user_agent.name",
"url.domain",
"host.ip",
"service.node.name",
"container.id",
"url.domain",
"user_agent.name",
]
`);
Array [
"user_agent.name",
"url.domain",
"host.ip",
"service.node.name",
"container.id",
"url.domain",
"host.ip",
"service.node.name",
"container.id",
"user_agent.name",
]
`);
});
it('returns a distribution per term', () => {
expectSnapshot(response.body?.significantTerms?.map((term) => term.distribution.length))
.toMatchInline(`
Array [
11,
11,
11,
11,
11,
11,
11,
]
`);
Array [
15,
15,
15,
15,
15,
15,
15,
15,
15,
15,
]
`);
});
it('returns overall distribution', () => {
expectSnapshot(response.body?.overall?.distribution.length).toMatchInline(`11`);
expectSnapshot(response.body?.overall?.distribution.length).toMatchInline(`15`);
});
});
}