* [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:
parent
8ce6ed42a3
commit
d7d2b15cdb
|
@ -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' },
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -4384,9 +4384,6 @@
|
|||
},
|
||||
"apm:enableServiceOverview": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"apm:enableCorrelations": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const enableCorrelations = 'apm:enableCorrelations';
|
||||
export const enableServiceOverview = 'apm:enableServiceOverview';
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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' }
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
191
x-pack/plugins/apm/public/components/app/correlations/index.tsx
Normal file
191
x-pack/plugins/apm/public/components/app/correlations/index.tsx
Normal 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}
|
||||
|
||||
<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.`,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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)}%`}
|
|
@ -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'];
|
||||
}
|
|
@ -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}
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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": "詳細はブラウザの開発者コンソールをご確認ください。",
|
||||
|
|
|
@ -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": "有关详情,请查看您的浏览器开发者控制台。",
|
||||
|
|
|
@ -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`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue