diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 5d611c75cdb9..d632c3ad61a8 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -127,5 +127,4 @@ export const stackManagementSchema: MakeSchemaFrom = { 'securitySolution:rulesTableRefresh': { type: 'text' }, 'apm:enableSignificantTerms': { type: 'boolean' }, 'apm:enableServiceOverview': { type: 'boolean' }, - 'apm:enableCorrelations': { type: 'boolean' }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index ee602fd51c32..698e7e711529 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -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; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index c129fd006ae1..566d10182b54 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -4384,9 +4384,6 @@ }, "apm:enableServiceOverview": { "type": "boolean" - }, - "apm:enableCorrelations": { - "type": "boolean" } } }, diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts index 83d358068905..427c30605e71 100644 --- a/x-pack/plugins/apm/common/ui_settings_keys.ts +++ b/x-pack/plugins/apm/common/ui_settings_keys.ts @@ -5,5 +5,4 @@ * 2.0. */ -export const enableCorrelations = 'apm:enableCorrelations'; export const enableServiceOverview = 'apm:enableServiceOverview'; diff --git a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx deleted file mode 100644 index c5b2f265fac8..000000000000 --- a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx +++ /dev/null @@ -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 ( - <> - { - setIsFlyoutVisible(true); - }} - > - View correlations - - - - - {isFlyoutVisible && ( - - setIsFlyoutVisible(false)} - > - - -

Correlations

-
-
- - {urlParams.kuery ? ( - <> - - Filtering by - {urlParams.kuery} - - Clear - - - - - ) : null} - - -

- 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. -

-
- - - - - -
-
-
- )} - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx similarity index 51% rename from x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx rename to x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx index f7580cc65c54..c75c1fb6d96a 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx @@ -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 { significantTerms?: T[]; status: FETCH_STATUS; - cardinalityColumnName: string; + percentageColumnName: string; setSelectedSignificantTerm: (term: T | null) => void; + onFilter: () => void; } -export function SignificantTermsTable({ +export function CorrelationsTable({ significantTerms, status, - cardinalityColumnName, + percentageColumnName, setSelectedSignificantTerm, + onFilter, }: Props) { + const trackApmEvent = useUiTracker({ app: 'apm' }); + const trackSelectSignificantTerm = useCallback( + () => + debounce( + () => trackApmEvent({ metric: 'select_significant_term' }), + 1000 + ), + [trackApmEvent] + ); const history = useHistory(); const columns: Array> = [ { width: '100px', - field: 'score', - name: 'Score', + field: 'impact', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.impactLabel', + { defaultMessage: 'Impact' } + ), render: (_: any, term: T) => { - return {Math.round(term.score)}; + return ; }, }, { - 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 ( + + <>{asPercent(term.valueCount, term.fieldCount)} + + ); }, }, { 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({ )}"`, }, }); + 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({ )}"`, }, }); + 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({ return ( { 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' } +); diff --git a/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx b/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx new file mode 100644 index 000000000000..9d7da4c0d308 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx @@ -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 ( + + + + {showThreshold && ( + + + ({ + 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 + ); + }} + /> + + + )} + + { + setFieldNames(defaultFieldNames); + }} + > + {i18n.translate( + 'xpack.apm.correlations.customize.fieldHelpTextReset', + { defaultMessage: 'reset' } + )} + + ), + docsLink: ( + + {i18n.translate( + 'xpack.apm.correlations.customize.fieldHelpTextDocsLink', + { + defaultMessage: + 'Learn more about the default fields.', + } + )} + + ), + }} + /> + } + > + ({ 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 }))} + /> + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx similarity index 61% rename from x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx rename to x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx index 533373d7e877..7386209310c1 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx @@ -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(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 ( <> - -

Error rate over time

+ +

+ {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.', + })} +

+
+
+ + +

+ {i18n.translate('xpack.apm.correlations.error.chart.title', { + defaultMessage: 'Error rate over time', + })} +

@@ -109,26 +121,20 @@ export function ErrorCorrelations() { /> - - - setFieldNames((names) => [...names, { label: term }]) - } - /> - - - - + + +
); @@ -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({ /> diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx new file mode 100644 index 000000000000..eba7c42490e0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -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 ( + <> + { + setIsFlyoutVisible(true); + }} + iconType="visTagCloud" + > + {i18n.translate('xpack.apm.correlations.buttonLabel', { + defaultMessage: 'View correlations', + })} + + + {isFlyoutVisible && ( + + setIsFlyoutVisible(false)} + > + + +

+ {CORRELATIONS_TITLE} +   + +

+
+
+ + + {urlParams.kuery ? ( + <> + + + {i18n.translate( + 'xpack.apm.correlations.filteringByLabel', + { defaultMessage: 'Filtering by' } + )} + + {urlParams.kuery} + + + {i18n.translate( + 'xpack.apm.correlations.clearFiltersLabel', + { defaultMessage: 'Clear' } + )} + + + + + + ) : null} + + + + {tabs.map(({ key, label }) => ( + { + setCurrentTab(key); + }} + > + {label} + + ))} + + + setIsFlyoutVisible(false)} /> + + +
+
+ )} + + ); +} + +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 + ) : ( + + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx similarity index 62% rename from x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx rename to x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 19f6248f56da..c88aaa85bb85 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -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 })); - -export function LatencyCorrelations() { +interface Props { + onClose: () => void; +} + +export function LatencyCorrelations({ onClose }: Props) { const [ selectedSignificantTerm, setSelectedSignificantTerm, ] = useState(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( + `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 ( <> + + +

+ {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.', + })} +

+
+
- -

Latency distribution

+ +

+ {i18n.translate( + 'xpack.apm.correlations.latency.chart.title', + { defaultMessage: 'Latency distribution' } + )} +

- - - - - - setDurationPercentile(e.currentTarget.value) - } - /> - - - - - { - setFieldNames((names) => [...names, { label: term }]); - }} - /> - - - - - - - + + +
@@ -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({ /> `${roundFloat(d)}%`} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts b/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts new file mode 100644 index 000000000000..ff88808c51d1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts @@ -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']; +} diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 23f699b63d20..d2d5c9f6f3a9 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -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} ))} +
+ +
{selectedTab ? selectedTab.render() : null} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 8141ecfebb1f..db0b8283a28c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -57,7 +57,11 @@ export function ServiceOverview({ return ( - + diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index d29dad7a7e3d..cae0ef2de2ad 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -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 ( <> - + - - - - - {transactionName} - + - - - - - - diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 1b8c41344efc..720a3857ef52 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -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 ( <> - + @@ -110,9 +109,6 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { - - - diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 260306abe2c3..2bd3fef8c0e8 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -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 ( diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts index ecebf5b5715a..721e35e2ef60 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts @@ -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 {}; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts index 6f7bd9537aa7..eab09e814c18 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts @@ -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))); -} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts index 0088b6ae7bb7..816061da5cfc 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts @@ -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({ diff --git a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts index 6bfc2ab2890b..1fe50c869f5b 100644 --- a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts +++ b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts @@ -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; - 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; } diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts index a52cdbcc4f07..5952cdb70229 100644 --- a/x-pack/plugins/apm/server/ui_settings.ts +++ b/x-pack/plugins/apm/server/ui_settings.ts @@ -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> = { - [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', { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 697cc18ad41d..d60729aeb055 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "詳細はブラウザの開発者コンソールをご確認ください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7c6226131733..4643e64eb6b1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "有关详情,请查看您的浏览器开发者控制台。", diff --git a/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts index 5a9a756d29f2..1f4b43f82506 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts @@ -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`); }); }); }