diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts index 70c1c7524cfe9..703106628f561 100644 --- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts @@ -27,7 +27,7 @@ export interface SearchServiceParams { start?: string; end?: string; percentileThreshold?: number; - percentileThresholdValue?: number; + analyzeCorrelations?: boolean; } export interface SearchServiceFetchParams extends SearchServiceParams { diff --git a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx index 298206f30d614..2e5887cab9918 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx @@ -42,11 +42,7 @@ type CorrelationsApiResponse = NonNullable< APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> >; -interface Props { - onClose: () => void; -} - -export function ErrorCorrelations({ onClose }: Props) { +export function ErrorCorrelations() { const [ selectedSignificantTerm, setSelectedSignificantTerm, @@ -130,7 +126,9 @@ export function ErrorCorrelations({ onClose }: Props) { ); const trackApmEvent = useUiTracker({ app: 'apm' }); - trackApmEvent({ metric: 'view_errors_correlations' }); + trackApmEvent({ metric: 'view_failed_transactions' }); + + const onFilter = () => {}; return ( <> @@ -175,7 +173,7 @@ export function ErrorCorrelations({ onClose }: Props) { } status={correlationsStatus} setSelectedSignificantTerm={setSelectedSignificantTerm} - onFilter={onClose} + onFilter={onFilter} /> 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 57ba75d945ee5..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ /dev/null @@ -1,258 +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, { useMemo, 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 { MlLatencyCorrelations } from './ml_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/license_prompt'; -import { IUrlParams } from '../../../context/url_params_context/types'; -import { - IStickyProperty, - StickyProperties, -} from '../../shared/sticky_properties'; -import { getEnvironmentLabel } from '../../../../common/environment_filter_values'; -import { - SERVICE_ENVIRONMENT, - SERVICE_NAME, - TRANSACTION_NAME, -} from '../../../../common/elasticsearch_fieldnames'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useApmParams } from '../../../hooks/use_apm_params'; - -const errorRateTab = { - key: 'errorRate', - label: i18n.translate('xpack.apm.correlations.tabs.errorRateLabel', { - defaultMessage: 'Failed transaction rate', - }), - component: ErrorCorrelations, -}; -const latencyCorrelationsTab = { - key: 'latencyCorrelations', - label: i18n.translate('xpack.apm.correlations.tabs.latencyLabel', { - defaultMessage: 'Latency', - }), - component: MlLatencyCorrelations, -}; -const tabs = [latencyCorrelationsTab, errorRateTab]; - -export function Correlations() { - const license = useLicenseContext(); - const hasActivePlatinumLicense = isActivePlatinumLicense(license); - const { urlParams } = useUrlParams(); - const { serviceName } = useApmServiceContext(); - - const { - query: { environment }, - } = useApmParams('/services/:serviceName'); - - const history = useHistory(); - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const [currentTab, setCurrentTab] = useState(latencyCorrelationsTab.key); - const { component: TabContent } = - tabs.find((tab) => tab.key === currentTab) ?? latencyCorrelationsTab; - 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 }); - - const stickyProperties: IStickyProperty[] = useMemo(() => { - const properties: IStickyProperty[] = []; - if (serviceName !== undefined) { - properties.push({ - label: i18n.translate('xpack.apm.correlations.serviceLabel', { - defaultMessage: 'Service', - }), - fieldName: SERVICE_NAME, - val: serviceName, - width: '20%', - }); - } - - properties.push({ - label: i18n.translate('xpack.apm.correlations.environmentLabel', { - defaultMessage: 'Environment', - }), - fieldName: SERVICE_ENVIRONMENT, - val: getEnvironmentLabel(environment), - width: '20%', - }); - - if (urlParams.transactionName) { - properties.push({ - label: i18n.translate('xpack.apm.correlations.transactionLabel', { - defaultMessage: 'Transaction', - }), - fieldName: TRANSACTION_NAME, - val: urlParams.transactionName, - width: '20%', - }); - } - - return properties; - }, [serviceName, environment, urlParams.transactionName]); - - return ( - <> - { - setIsFlyoutVisible(true); - }} - > - {i18n.translate('xpack.apm.correlations.buttonLabel', { - defaultMessage: 'View correlations', - })} - - - {isFlyoutVisible && ( - - setIsFlyoutVisible(false)} - > - - -

- {CORRELATIONS_TITLE} -   - -

-
- {hasActivePlatinumLicense && ( - <> - - - - {urlParams.kuery ? ( - <> - - - - ) : ( - - )} - - {tabs.map(({ key, label }) => ( - { - setCurrentTab(key); - }} - > - {label} - - ))} - - - )} -
- - {hasActivePlatinumLicense ? ( - <> - setIsFlyoutVisible(false)} /> - - ) : ( - - )} - -
-
- )} - - ); -} - -function Filters({ - urlParams, - history, -}: { - urlParams: IUrlParams; - history: ReturnType; -}) { - if (!urlParams.kuery) { - return null; - } - - return ( - - - {i18n.translate('xpack.apm.correlations.filteringByLabel', { - defaultMessage: 'Filtering by', - })} - - {urlParams.kuery} - - - {i18n.translate('xpack.apm.correlations.clearFiltersLabel', { - defaultMessage: 'Clear', - })} - - - - ); -} - -const CORRELATIONS_TITLE = i18n.translate('xpack.apm.correlations.title', { - defaultMessage: 'Correlations', -}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 0871447337780..0a534ba1b945b 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -5,338 +5,426 @@ * 2.0. */ +import React, { useEffect, useMemo, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { - ScaleType, - Chart, - Axis, - BarSeries, - Position, - Settings, -} from '@elastic/charts'; -import React, { useState } from 'react'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + EuiCallOut, + EuiCode, + EuiAccordion, + EuiPanel, + EuiIcon, + EuiBasicTableColumn, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getDurationFormatter } from '../../../../common/utils/formatters'; +import { FormattedMessage } from '@kbn/i18n/react'; 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 { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useTransactionLatencyCorrelationsFetcher } from '../../../hooks/use_transaction_latency_correlations_fetcher'; +import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; import { CorrelationsTable, SelectedSignificantTerm, } from './correlations_table'; -import { 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'; +import { push } from '../../shared/Links/url_helpers'; +import { + enableInspectEsQueries, + useUiTracker, +} from '../../../../../observability/public'; +import { asPreciseDecimal } from '../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { LatencyCorrelationsHelpPopover } from './latency_correlations_help_popover'; import { useApmParams } from '../../../hooks/use_apm_params'; -type OverallLatencyApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'> ->; - -type CorrelationsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'> ->; +const DEFAULT_PERCENTILE_THRESHOLD = 95; +const isErrorMessage = (arg: unknown): arg is Error => { + return arg instanceof Error; +}; -interface Props { - onClose: () => void; +interface MlCorrelationsTerms { + correlation: number; + ksTest: number; + fieldName: string; + fieldValue: string; + duplicatedFields?: string[]; } -export function LatencyCorrelations({ onClose }: Props) { - const [ - selectedSignificantTerm, - setSelectedSignificantTerm, - ] = useState(null); +export function LatencyCorrelations() { + const { + core: { notifications, uiSettings }, + } = useApmPluginContext(); - const { serviceName } = useApmServiceContext(); + const { serviceName, transactionType } = useApmServiceContext(); const { query: { kuery, environment }, } = useApmParams('/services/:serviceName'); const { urlParams } = useUrlParams(); - const { transactionName, transactionType, start, end } = urlParams; - const { defaultFieldNames } = useFieldNames(); - const [fieldNames, setFieldNames] = useLocalStorage( - `apm.correlations.latency.fields:${serviceName}`, - defaultFieldNames - ); - const hasFieldNames = fieldNames.length > 0; + + const { transactionName, start, end } = urlParams; + + const displayLog = uiSettings.get(enableInspectEsQueries); + + const { + ccsWarning, + log, + error, + histograms, + percentileThresholdValue, + isRunning, + progress, + startFetch, + cancelFetch, + overallHistogram, + } = useTransactionLatencyCorrelationsFetcher({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }); + + // start fetching on load + // we want this effect to execute exactly once after the component mounts + useEffect(() => { + startFetch(); + + return () => { + // cancel any running async partial request when unmounting the component + // we want this effect to execute exactly once after the component mounts + cancelFetch(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (isErrorMessage(error)) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.errorTitle', + { + defaultMessage: 'An error occurred fetching correlations', + } + ), + text: error.toString(), + }); + } + }, [error, notifications.toasts]); const [ - durationPercentile, - setDurationPercentile, - ] = useLocalStorage( - `apm.correlations.latency.threshold:${serviceName}`, - 75 - ); + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); - const { data: overallData, status: overallStatus } = useFetcher( - (callApmApi) => { - if (start && end) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/latency/overall_distribution', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - }, - }, - }); - } - }, - [ - environment, - kuery, - serviceName, - start, - end, - transactionName, - transactionType, - ] - ); + let selectedHistogram = histograms.length > 0 ? histograms[0] : undefined; - const maxLatency = overallData?.maxLatency; - const distributionInterval = overallData?.distributionInterval; - const fieldNamesCommaSeparated = fieldNames.join(','); + if (histograms.length > 0 && selectedSignificantTerm !== null) { + selectedHistogram = histograms.find( + (h) => + h.field === selectedSignificantTerm.fieldName && + h.value === selectedSignificantTerm.fieldValue + ); + } + const history = useHistory(); + const trackApmEvent = useUiTracker({ app: 'apm' }); - const { data: correlationsData, status: correlationsStatus } = useFetcher( - (callApmApi) => { - if (start && end && hasFieldNames && maxLatency && distributionInterval) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/latency/slow_transactions', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - durationPercentile: durationPercentile.toString(10), - fieldNames: fieldNamesCommaSeparated, - maxLatency: maxLatency.toString(10), - distributionInterval: distributionInterval.toString(10), + const mlCorrelationColumns: Array< + EuiBasicTableColumn + > = useMemo( + () => [ + { + width: '116px', + field: 'correlation', + name: ( + + <> + {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel', + { + defaultMessage: 'Correlation', + } + )} + + + + ), + render: (correlation: number) => { + return
{asPreciseDecimal(correlation, 2)}
; + }, + }, + { + field: 'fieldName', + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel', + { defaultMessage: 'Field name' } + ), + }, + { + field: 'fieldValue', + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldValueLabel', + { defaultMessage: 'Field value' } + ), + render: (fieldValue: string) => String(fieldValue).slice(0, 50), + }, + { + width: '100px', + actions: [ + { + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel', + { defaultMessage: 'Filter' } + ), + description: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription', + { defaultMessage: 'Filter by value' } + ), + icon: 'plusInCircle', + type: 'icon', + onClick: (term: MlCorrelationsTerms) => { + push(history, { + query: { + kuery: `${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_include_filter' }); }, }, - }); - } - }, - [ - environment, - kuery, - serviceName, - start, - end, - transactionName, - transactionType, - durationPercentile, - fieldNamesCommaSeparated, - hasFieldNames, - maxLatency, - distributionInterval, - ] + { + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeLabel', + { defaultMessage: 'Exclude' } + ), + description: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeDescription', + { defaultMessage: 'Filter out value' } + ), + icon: 'minusInCircle', + type: 'icon', + onClick: (term: MlCorrelationsTerms) => { + push(history, { + query: { + kuery: `not ${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_exclude_filter' }); + }, + }, + ], + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel', + { defaultMessage: 'Filter' } + ), + }, + ], + [history, trackApmEvent] ); - const trackApmEvent = useUiTracker({ app: 'apm' }); - trackApmEvent({ metric: 'view_latency_correlations' }); + const histogramTerms: MlCorrelationsTerms[] = useMemo(() => { + return histograms.map((d) => { + return { + fieldName: d.field, + fieldValue: d.value, + ksTest: d.ksTest, + correlation: d.correlation, + duplicatedFields: d.duplicatedFields, + }; + }); + }, [histograms]); 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.', - })} -

-
-
- - - - -

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

-
- + + +
+ {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.panelTitle', + { + defaultMessage: 'Latency distribution', } - status={overallStatus} - selectedSignificantTerm={selectedSignificantTerm} - /> - - - - - - - - + )} +
+
- - ); -} -function getAxisMaxes(data?: OverallLatencyApiResponse) { - if (!data?.overallDistribution) { - return { xMax: 0, yMax: 0 }; - } - const { overallDistribution } = data; - const xValues = overallDistribution.map((p) => p.x ?? 0); - const yValues = overallDistribution.map((p) => p.y ?? 0); - return { - xMax: Math.max(...xValues), - yMax: Math.max(...yValues), - }; -} + -function getSelectedDistribution( - significantTerms: CorrelationsApiResponse['significantTerms'], - selectedSignificantTerm: SelectedSignificantTerm -) { - if (!significantTerms) { - return []; - } - return ( - significantTerms.find( - ({ fieldName, fieldValue }) => - selectedSignificantTerm.fieldName === fieldName && - selectedSignificantTerm.fieldValue === fieldValue - )?.distribution || [] - ); -} - -function LatencyDistributionChart({ - overallData, - correlationsData, - selectedSignificantTerm, - status, -}: { - overallData?: OverallLatencyApiResponse; - correlationsData?: CorrelationsApiResponse['significantTerms']; - selectedSignificantTerm: SelectedSignificantTerm | null; - status: FETCH_STATUS; -}) { - const theme = useTheme(); - const { xMax, yMax } = getAxisMaxes(overallData); - const durationFormatter = getDurationFormatter(xMax); + - return ( - - - { - const start = durationFormatter(obj.value); - const end = durationFormatter( - obj.value + overallData?.distributionInterval - ); + + + + {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.tableTitle', + { + defaultMessage: 'Correlations', + } + )} + + - return `${start.value} - ${end.formatted}`; - }, - }} - /> - durationFormatter(d).formatted} - /> - `${d}%`} - domain={{ min: 0, max: yMax }} - /> + - + + {!isRunning && ( + + + )} - xScaleType={ScaleType.Linear} - yScaleType={ScaleType.Linear} - xAccessor={'x'} - yAccessors={['y']} - color={theme.eui.euiColorVis1} - data={overallData?.overallDistribution || []} - minBarHeight={5} - tickFormat={(d) => `${roundFloat(d)}%`} - /> - - {correlationsData && selectedSignificantTerm ? ( - + + + )} + + + + + + + + + + + + + + + + +
+ {ccsWarning && ( + <> + + `${roundFloat(d)}%`} + color="warning" + > +

+ {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.ccsWarningCalloutBody', + { + defaultMessage: + 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.14 and later versions.', + } + )} +

+
+ + )} + +
+ {histograms.length > 0 && selectedHistogram !== undefined && ( + + )} + {histograms.length < 1 && progress > 0.99 ? ( + <> + + + + + ) : null} - - +
+ {log.length > 0 && displayLog && ( + + + {log.map((d, i) => { + const splitItem = d.split(': '); + return ( +

+ + {splitItem[0]} {splitItem[1]} + +

+ ); + })} +
+
+ )} + ); } - -function roundFloat(n: number, digits = 2) { - const factor = Math.pow(10, digits); - return Math.round(n * factor) / factor; -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx rename to x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx deleted file mode 100644 index bbd6648ccaf6e..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx +++ /dev/null @@ -1,430 +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, { useEffect, useMemo, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import { - EuiCallOut, - EuiCode, - EuiAccordion, - EuiPanel, - EuiIcon, - EuiBasicTableColumn, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiSpacer, - EuiText, - EuiTitle, - EuiToolTip, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { - CorrelationsChart, - replaceHistogramDotsWithBars, -} from './correlations_chart'; -import { - CorrelationsTable, - SelectedSignificantTerm, -} from './correlations_table'; -import { useCorrelations } from './use_correlations'; -import { push } from '../../shared/Links/url_helpers'; -import { - enableInspectEsQueries, - useUiTracker, -} from '../../../../../observability/public'; -import { asPreciseDecimal } from '../../../../common/utils/formatters'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { LatencyCorrelationsHelpPopover } from './ml_latency_correlations_help_popover'; - -const DEFAULT_PERCENTILE_THRESHOLD = 95; -const isErrorMessage = (arg: unknown): arg is Error => { - return arg instanceof Error; -}; - -interface Props { - onClose: () => void; -} - -interface MlCorrelationsTerms { - correlation: number; - ksTest: number; - fieldName: string; - fieldValue: string; - duplicatedFields?: string[]; -} - -export function MlLatencyCorrelations({ onClose }: Props) { - const { - core: { notifications, uiSettings }, - } = useApmPluginContext(); - - const { serviceName, transactionType } = useApmServiceContext(); - const { urlParams } = useUrlParams(); - - const { environment, kuery, transactionName, start, end } = urlParams; - - const displayLog = uiSettings.get(enableInspectEsQueries); - - const { - ccsWarning, - log, - error, - histograms, - percentileThresholdValue, - isRunning, - progress, - startFetch, - cancelFetch, - overallHistogram: originalOverallHistogram, - } = useCorrelations({ - ...{ - ...{ - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - }, - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - }, - }); - - const overallHistogram = useMemo( - () => replaceHistogramDotsWithBars(originalOverallHistogram), - [originalOverallHistogram] - ); - - // start fetching on load - // we want this effect to execute exactly once after the component mounts - useEffect(() => { - startFetch(); - - return () => { - // cancel any running async partial request when unmounting the component - // we want this effect to execute exactly once after the component mounts - cancelFetch(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (isErrorMessage(error)) { - notifications.toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.errorTitle', - { - defaultMessage: 'An error occurred fetching correlations', - } - ), - text: error.toString(), - }); - } - }, [error, notifications.toasts]); - - const [ - selectedSignificantTerm, - setSelectedSignificantTerm, - ] = useState(null); - - let selectedHistogram = histograms.length > 0 ? histograms[0] : undefined; - - if (histograms.length > 0 && selectedSignificantTerm !== null) { - selectedHistogram = histograms.find( - (h) => - h.field === selectedSignificantTerm.fieldName && - h.value === selectedSignificantTerm.fieldValue - ); - } - const history = useHistory(); - const trackApmEvent = useUiTracker({ app: 'apm' }); - - const mlCorrelationColumns: Array< - EuiBasicTableColumn - > = useMemo( - () => [ - { - width: '116px', - field: 'correlation', - name: ( - - <> - {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel', - { - defaultMessage: 'Correlation', - } - )} - - - - ), - render: (correlation: number) => { - return
{asPreciseDecimal(correlation, 2)}
; - }, - }, - { - field: 'fieldName', - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel', - { defaultMessage: 'Field name' } - ), - }, - { - field: 'fieldValue', - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldValueLabel', - { defaultMessage: 'Field value' } - ), - render: (fieldValue: string) => String(fieldValue).slice(0, 50), - }, - { - width: '100px', - actions: [ - { - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel', - { defaultMessage: 'Filter' } - ), - description: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription', - { defaultMessage: 'Filter by value' } - ), - icon: 'plusInCircle', - type: 'icon', - onClick: (term: MlCorrelationsTerms) => { - push(history, { - query: { - kuery: `${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onClose(); - trackApmEvent({ metric: 'correlations_term_include_filter' }); - }, - }, - { - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeLabel', - { defaultMessage: 'Exclude' } - ), - description: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeDescription', - { defaultMessage: 'Filter out value' } - ), - icon: 'minusInCircle', - type: 'icon', - onClick: (term: MlCorrelationsTerms) => { - push(history, { - query: { - kuery: `not ${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onClose(); - trackApmEvent({ metric: 'correlations_term_exclude_filter' }); - }, - }, - ], - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel', - { defaultMessage: 'Filter' } - ), - }, - ], - [history, onClose, trackApmEvent] - ); - - const histogramTerms: MlCorrelationsTerms[] = useMemo(() => { - return histograms.map((d) => { - return { - fieldName: d.field, - fieldValue: d.value, - ksTest: d.ksTest, - correlation: d.correlation, - duplicatedFields: d.duplicatedFields, - }; - }); - }, [histograms]); - - return ( - <> - - - {!isRunning && ( - - - - )} - {isRunning && ( - - - - )} - - - - - - - - - - - - - - - - - - - {ccsWarning && ( - <> - - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.ccsWarningCalloutBody', - { - defaultMessage: - 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.14 and later versions.', - } - )} -

-
- - )} - - - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.chartTitle', - { - defaultMessage: 'Latency distribution for {name} (Log-Log Plot)', - values: { - name: transactionName ?? serviceName, - }, - } - )} -

-
- - - - - -
- {histograms.length > 0 && selectedHistogram !== undefined && ( - - )} - {histograms.length < 1 && progress > 0.99 ? ( - <> - - - - - - ) : null} -
- {log.length > 0 && displayLog && ( - - - {log.map((d, i) => { - const splitItem = d.split(': '); - return ( -

- - {splitItem[0]} {splitItem[1]} - -

- ); - })} -
-
- )} - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts deleted file mode 100644 index 05cb367a9fde7..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts +++ /dev/null @@ -1,123 +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 { useRef, useState } from 'react'; -import type { Subscription } from 'rxjs'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - isCompleteResponse, - isErrorResponse, -} from '../../../../../../../src/plugins/data/public'; -import type { - HistogramItem, - SearchServiceValue, -} from '../../../../common/search_strategies/correlations/types'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApmPluginStartDeps } from '../../../plugin'; - -interface CorrelationsOptions { - environment?: string; - kuery?: string; - serviceName?: string; - transactionName?: string; - transactionType?: string; - start?: string; - end?: string; -} - -interface RawResponse { - percentileThresholdValue?: number; - took: number; - values: SearchServiceValue[]; - overallHistogram: HistogramItem[]; - log: string[]; - ccsWarning: boolean; -} - -export const useCorrelations = (params: CorrelationsOptions) => { - const { - services: { data }, - } = useKibana(); - - const [error, setError] = useState(); - const [isComplete, setIsComplete] = useState(false); - const [isRunning, setIsRunning] = useState(false); - const [loaded, setLoaded] = useState(0); - const [rawResponse, setRawResponse] = useState(); - const [timeTook, setTimeTook] = useState(); - const [total, setTotal] = useState(100); - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(); - - function setResponse(response: IKibanaSearchResponse) { - // @TODO: optimize rawResponse.overallHistogram if histogram is the same - setIsRunning(response.isRunning || false); - setRawResponse(response.rawResponse); - setLoaded(response.loaded!); - setTotal(response.total!); - setTimeTook(response.rawResponse.took); - } - - const startFetch = () => { - setError(undefined); - setIsComplete(false); - searchSubscription$.current?.unsubscribe(); - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - - const req = { params }; - - // Submit the search request using the `data.search` service. - searchSubscription$.current = data.search - .search>(req, { - strategy: 'apmCorrelationsSearchStrategy', - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (res: IKibanaSearchResponse) => { - setResponse(res); - if (isCompleteResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setIsRunning(false); - setIsComplete(true); - } else if (isErrorResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setError((res as unknown) as Error); - setIsRunning(false); - } - }, - error: (e: Error) => { - setError(e); - setIsRunning(false); - }, - }); - }; - - const cancelFetch = () => { - searchSubscription$.current?.unsubscribe(); - searchSubscription$.current = undefined; - abortCtrl.current.abort(); - setIsRunning(false); - }; - - return { - ccsWarning: rawResponse?.ccsWarning ?? false, - log: rawResponse?.log ?? [], - error, - histograms: rawResponse?.values ?? [], - percentileThresholdValue: - rawResponse?.percentileThresholdValue ?? undefined, - overallHistogram: rawResponse?.overallHistogram, - isComplete, - isRunning, - progress: loaded / total, - timeTook, - startFetch, - cancelFetch, - }; -}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx deleted file mode 100644 index ba007015b25f8..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx +++ /dev/null @@ -1,68 +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 { TooltipInfo } from '@elastic/charts'; -import { EuiIcon, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { TimeFormatter } from '../../../../../common/utils/formatters'; -import { useTheme } from '../../../../hooks/use_theme'; -import { formatYLong, IChartPoint } from './'; - -export function CustomTooltip( - props: TooltipInfo & { - serie?: IChartPoint; - isSamplesEmpty: boolean; - timeFormatter: TimeFormatter; - } -) { - const theme = useTheme(); - const { values, header, serie, isSamplesEmpty, timeFormatter } = props; - const { color, value } = values[0]; - - let headerTitle = `${timeFormatter(header?.value)}`; - if (serie) { - const xFormatted = timeFormatter(serie.x); - const x0Formatted = timeFormatter(serie.x0); - headerTitle = `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; - } - - return ( -
- <> -
{headerTitle}
-
-
-
-
-
-
- {formatYLong(value)} - {value} -
-
-
- - {isSamplesEmpty && ( -
- - - {i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSamplesAvailable', - { defaultMessage: 'No samples available' } - )} - -
- )} -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts deleted file mode 100644 index 5d6d73f36fac1..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts +++ /dev/null @@ -1,56 +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 { getFormattedBuckets } from './index'; - -describe('Distribution', () => { - it('getFormattedBuckets', () => { - const buckets = [ - { key: 0, count: 0, samples: [] }, - { key: 20, count: 0, samples: [] }, - { key: 40, count: 0, samples: [] }, - { - key: 60, - count: 5, - samples: [ - { - transactionId: 'someTransactionId', - traceId: 'someTraceId', - }, - ], - }, - { - key: 80, - count: 100, - samples: [ - { - transactionId: 'anotherTransactionId', - traceId: 'anotherTraceId', - }, - ], - }, - ]; - - expect(getFormattedBuckets(buckets, 20)).toEqual([ - { x: 20, x0: 0, y: 0, style: { cursor: 'default' } }, - { x: 40, x0: 20, y: 0, style: { cursor: 'default' } }, - { x: 60, x0: 40, y: 0, style: { cursor: 'default' } }, - { - x: 80, - x0: 60, - y: 5, - style: { cursor: 'pointer' }, - }, - { - x: 100, - x0: 80, - y: 100, - style: { cursor: 'pointer' }, - }, - ]); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx deleted file mode 100644 index 4ff094c025451..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Axis, - Chart, - HistogramBarSeries, - Position, - ProjectionClickListener, - RectAnnotation, - ScaleType, - Settings, - SettingsSpec, - TooltipInfo, - XYChartSeriesIdentifier, -} from '@elastic/charts'; -import { EuiIconTip, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import d3 from 'd3'; -import { isEmpty, keyBy } from 'lodash'; -import React from 'react'; -import { ValuesType } from 'utility-types'; -import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { useTheme } from '../../../../hooks/use_theme'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { unit } from '../../../../utils/style'; -import { ChartContainer } from '../../../shared/charts/chart_container'; -import { EmptyMessage } from '../../../shared/EmptyMessage'; -import { CustomTooltip } from './custom_tooltip'; - -type TransactionDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; - -type DistributionBucket = TransactionDistributionAPIResponse['buckets'][0]; - -export interface IChartPoint { - x0: number; - x: number; - y: number; - style: { - cursor: string; - }; -} - -export function getFormattedBuckets( - buckets?: DistributionBucket[], - bucketSize?: number -) { - if (!buckets || !bucketSize) { - return []; - } - - return buckets.map( - ({ samples, count, key }): IChartPoint => { - return { - x0: key, - x: key + bucketSize, - y: count, - style: { - cursor: isEmpty(samples) ? 'default' : 'pointer', - }, - }; - } - ); -} - -const formatYShort = (t: number) => { - return i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel', - { - defaultMessage: '{transCount} trans.', - values: { transCount: t }, - } - ); -}; - -export const formatYLong = (t: number) => { - return i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel', - { - defaultMessage: - '{transCount, plural, =0 {transactions} one {transaction} other {transactions}}', - values: { - transCount: t, - }, - } - ); -}; - -interface Props { - distribution?: TransactionDistributionAPIResponse; - fetchStatus: FETCH_STATUS; - bucketIndex: number; - onBucketClick: ( - bucket: ValuesType - ) => void; -} - -export function TransactionDistribution({ - distribution, - fetchStatus, - bucketIndex, - onBucketClick, -}: Props) { - const theme = useTheme(); - - // no data in response - if ( - (!distribution || distribution.noHits) && - fetchStatus !== FETCH_STATUS.LOADING - ) { - return ( - - ); - } - - const buckets = getFormattedBuckets( - distribution?.buckets, - distribution?.bucketSize - ); - - const xMin = d3.min(buckets, (d) => d.x0) || 0; - const xMax = d3.max(buckets, (d) => d.x0) || 0; - const timeFormatter = getDurationFormatter(xMax); - - const distributionMap = keyBy(distribution?.buckets, 'key'); - const bucketsMap = keyBy(buckets, 'x0'); - - const tooltip: SettingsSpec['tooltip'] = { - stickTo: 'top', - customTooltip: (props: TooltipInfo) => { - const datum = props.header?.datum as IChartPoint; - const selectedDistribution = distributionMap[datum?.x0]; - const serie = bucketsMap[datum?.x0]; - return ( - - ); - }, - }; - - const onBarClick: ProjectionClickListener = ({ x }) => { - const clickedBucket = distribution?.buckets.find((bucket) => { - return bucket.key === x; - }); - if (clickedBucket) { - onBucketClick(clickedBucket); - } - }; - - const selectedBucket = buckets[bucketIndex]; - - return ( -
- -
- {i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle', - { - defaultMessage: 'Latency distribution', - } - )}{' '} - -
-
- - - - {selectedBucket && ( - - )} - timeFormatter(time).formatted} - /> - formatYShort(value)} - /> - value} - minBarHeight={2} - id="transactionDurationDistribution" - name={(series: XYChartSeriesIdentifier) => { - const bucketCount = series.splitAccessors.get( - series.yAccessor - ) as number; - return formatYLong(bucketCount); - }} - splitSeriesAccessors={['y']} - xScaleType={ScaleType.Linear} - yScaleType={ScaleType.Linear} - xAccessor="x0" - yAccessors={['y']} - data={buckets} - color={theme.eui.euiColorVis1} - /> - - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx new file mode 100644 index 0000000000000..c21d292c05c85 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { BrushEndListener, XYBrushArea } from '@elastic/charts'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useTransactionDistributionFetcher } from '../../../../hooks/use_transaction_distribution_fetcher'; +import { TransactionDistributionChart } from '../../../shared/charts/transaction_distribution_chart'; +import { useUiTracker } from '../../../../../../observability/public'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { useApmParams } from '../../../../hooks/use_apm_params'; + +const DEFAULT_PERCENTILE_THRESHOLD = 95; +const isErrorMessage = (arg: unknown): arg is Error => { + return arg instanceof Error; +}; + +interface Props { + markerCurrentTransaction?: number; + onChartSelection: BrushEndListener; + onClearSelection: () => void; + selection?: [number, number]; +} + +export function TransactionDistribution({ + markerCurrentTransaction, + onChartSelection, + onClearSelection, + selection, +}: Props) { + const { + core: { notifications }, + } = useApmPluginContext(); + + const { serviceName, transactionType } = useApmServiceContext(); + + const { + query: { kuery, environment }, + } = useApmParams('/services/:serviceName'); + + const { urlParams } = useUrlParams(); + + const { transactionName, start, end } = urlParams; + + const clearSelectionButtonLabel = i18n.translate( + 'xpack.apm.transactionDetails.clearSelectionButtonLabel', + { + defaultMessage: 'Clear selection', + } + ); + + const { + error, + percentileThresholdValue, + startFetch, + cancelFetch, + transactionDistribution, + } = useTransactionDistributionFetcher({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }); + + // start fetching on load + // we want this effect to execute exactly once after the component mounts + useEffect(() => { + startFetch(); + + return () => { + // cancel any running async partial request when unmounting the component + // we want this effect to execute exactly once after the component mounts + cancelFetch(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (isErrorMessage(error)) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.transactionDetails.distribution.errorTitle', + { + defaultMessage: 'An error occurred fetching the distribution', + } + ), + text: error.toString(), + }); + } + }, [error, notifications.toasts]); + + const trackApmEvent = useUiTracker({ app: 'apm' }); + + const onTrackedChartSelection: BrushEndListener = ( + brushArea: XYBrushArea + ) => { + onChartSelection(brushArea); + trackApmEvent({ metric: 'transaction_distribution_chart_selection' }); + }; + + const onTrackedClearSelection = () => { + onClearSelection(); + trackApmEvent({ metric: 'transaction_distribution_chart_clear_selection' }); + }; + + return ( + <> + + + +
+ {i18n.translate( + 'xpack.apm.transactionDetails.distribution.panelTitle', + { + defaultMessage: 'Latency distribution', + } + )} +
+
+
+ {selection && ( + + + + + {i18n.translate( + 'xpack.apm.transactionDetails.distribution.selectionText', + { + defaultMessage: `Selection: {selectionFrom} - {selectionTo}ms`, + values: { + selectionFrom: Math.round(selection[0] / 1000), + selectionTo: Math.round(selection[1] / 1000), + }, + } + )} + + + + + {clearSelectionButtonLabel} + + + + + )} +
+ + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx new file mode 100644 index 0000000000000..e727aa4dfc5fd --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx @@ -0,0 +1,94 @@ +/* + * 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 from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiBetaBadge } from '@elastic/eui'; + +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/license_prompt'; + +import { ErrorCorrelations } from '../correlations/error_correlations'; + +import type { TabContentProps } from './types'; + +function FailedTransactionsCorrelationsTab({}: TabContentProps) { + const license = useLicenseContext(); + + const hasActivePlatinumLicense = isActivePlatinumLicense(license); + + const metric = { + app: 'apm' as const, + metric: hasActivePlatinumLicense + ? 'failed_transactions_tab_view' + : 'failed_transactions_license_prompt', + metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT, + }; + useTrackMetric(metric); + useTrackMetric({ ...metric, delay: 15000 }); + + return hasActivePlatinumLicense ? ( + + ) : ( + + ); +} + +export const failedTransactionsCorrelationsTab = { + dataTestSubj: 'apmFailedTransactionsCorrelationsTabButton', + key: 'failedTransactionsCorrelations', + label: ( + <> + {i18n.translate( + 'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel', + { + defaultMessage: 'Failed transaction correlations', + } + )}{' '} + + + ), + component: FailedTransactionsCorrelationsTab, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 143e82649facd..0c6f03047dc7d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -5,55 +5,23 @@ * 2.0. */ -import { EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { flatten, isEmpty } from 'lodash'; +import { EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; -import { useHistory } from 'react-router-dom'; import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; -import { HeightRetainer } from '../../shared/HeightRetainer'; -import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; -import { TransactionDistribution } from './Distribution'; -import { useWaterfallFetcher } from './use_waterfall_fetcher'; -import { WaterfallWithSummary } from './waterfall_with_summary'; -interface Sample { - traceId: string; - transactionId: string; -} +import { TransactionDetailsTabs } from './transaction_details_tabs'; export function TransactionDetails() { - const { urlParams } = useUrlParams(); - const history = useHistory(); - - const { - waterfall, - exceedsMax, - status: waterfallStatus, - } = useWaterfallFetcher(); - const { path, query } = useApmParams( '/services/:serviceName/transactions/view' ); - - const apmRouter = useApmRouter(); - const { transactionName } = query; - const { - distributionData, - distributionStatus, - } = useTransactionDistributionFetcher({ - transactionName, - environment: query.environment, - kuery: query.kuery, - }); + const apmRouter = useApmRouter(); useBreadcrumb({ title: transactionName, @@ -63,36 +31,6 @@ export function TransactionDetails() { }), }); - const selectedSample = flatten( - distributionData.buckets.map((bucket) => bucket.samples) - ).find( - (sample) => - sample.transactionId === urlParams.transactionId && - sample.traceId === urlParams.traceId - ); - - const bucketWithSample = - selectedSample && - distributionData.buckets.find((bucket) => - bucket.samples.includes(selectedSample) - ); - - const traceSamples = bucketWithSample?.samples ?? []; - const bucketIndex = bucketWithSample - ? distributionData.buckets.indexOf(bucketWithSample) - : -1; - - const selectSampleFromBucketClick = (sample: Sample) => { - history.push({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - transactionId: sample.transactionId, - traceId: sample.traceId, - }), - }); - }; - return ( <> @@ -110,32 +48,9 @@ export function TransactionDetails() { /> - - - - { - if (!isEmpty(bucket.samples)) { - selectSampleFromBucketClick(bucket.samples[0]); - } - }} - /> - - - + - - - + ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx new file mode 100644 index 0000000000000..c396b6317c311 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx @@ -0,0 +1,60 @@ +/* + * 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 from 'react'; + +import { i18n } from '@kbn/i18n'; + +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/license_prompt'; + +import { LatencyCorrelations } from '../correlations/latency_correlations'; + +import type { TabContentProps } from './types'; + +function LatencyCorrelationsTab({}: TabContentProps) { + const license = useLicenseContext(); + + const hasActivePlatinumLicense = isActivePlatinumLicense(license); + + const metric = { + app: 'apm' as const, + metric: hasActivePlatinumLicense + ? 'correlations_tab_view' + : 'correlations_license_prompt', + metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT, + }; + useTrackMetric(metric); + useTrackMetric({ ...metric, delay: 15000 }); + + return hasActivePlatinumLicense ? ( + + ) : ( + + ); +} + +export const latencyCorrelationsTab = { + dataTestSubj: 'apmLatencyCorrelationsTabButton', + key: 'latencyCorrelations', + label: i18n.translate('xpack.apm.transactionDetails.tabs.latencyLabel', { + defaultMessage: 'Latency correlations', + }), + component: LatencyCorrelationsTab, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx new file mode 100644 index 0000000000000..0421fcd055d6c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx @@ -0,0 +1,72 @@ +/* + * 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 from 'react'; + +import { EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; + +import { TransactionDistribution } from './distribution'; +import { useWaterfallFetcher } from './use_waterfall_fetcher'; +import type { TabContentProps } from './types'; +import { WaterfallWithSummary } from './waterfall_with_summary'; + +function TraceSamplesTab({ + selectSampleFromChartSelection, + clearChartSelection, + sampleRangeFrom, + sampleRangeTo, + traceSamples, +}: TabContentProps) { + const { urlParams } = useUrlParams(); + + const { + waterfall, + exceedsMax, + status: waterfallStatus, + } = useWaterfallFetcher(); + + return ( + <> + + + + + + + ); +} + +export const traceSamplesTab = { + dataTestSubj: 'apmTraceSamplesTabButton', + key: 'traceSamples', + label: i18n.translate('xpack.apm.transactionDetails.tabs.traceSamplesLabel', { + defaultMessage: 'Trace samples', + }), + component: TraceSamplesTab, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx new file mode 100644 index 0000000000000..8cdfd44c7581a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; + +import { omit } from 'lodash'; +import { useHistory } from 'react-router-dom'; + +import { XYBrushArea } from '@elastic/charts'; +import { EuiPanel, EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; + +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useTransactionTraceSamplesFetcher } from '../../../hooks/use_transaction_trace_samples_fetcher'; + +import { maybe } from '../../../../common/utils/maybe'; +import { HeightRetainer } from '../../shared/HeightRetainer'; +import { fromQuery, push, toQuery } from '../../shared/Links/url_helpers'; + +import { failedTransactionsCorrelationsTab } from './failed_transactions_correlations_tab'; +import { latencyCorrelationsTab } from './latency_correlations_tab'; +import { traceSamplesTab } from './trace_samples_tab'; + +const tabs = [ + traceSamplesTab, + latencyCorrelationsTab, + failedTransactionsCorrelationsTab, +]; + +export function TransactionDetailsTabs() { + const { query } = useApmParams('/services/:serviceName/transactions/view'); + + const { urlParams } = useUrlParams(); + const history = useHistory(); + + const [currentTab, setCurrentTab] = useState(traceSamplesTab.key); + const { component: TabContent } = + tabs.find((tab) => tab.key === currentTab) ?? traceSamplesTab; + + const { environment, kuery, transactionName } = query; + const { traceSamplesData } = useTransactionTraceSamplesFetcher({ + transactionName, + kuery, + environment, + }); + + const selectSampleFromChartSelection = (selection: XYBrushArea) => { + if (selection !== undefined) { + const { x } = selection; + if (Array.isArray(x)) { + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + sampleRangeFrom: Math.round(x[0]), + sampleRangeTo: Math.round(x[1]), + }), + }); + } + } + }; + + const { sampleRangeFrom, sampleRangeTo, transactionId, traceId } = urlParams; + const { traceSamples } = traceSamplesData; + + const clearChartSelection = () => { + // enforces a reset of the current sample to be highlighted in the chart + // and selected in waterfall section below, otherwise we end up with + // stale data for the selected sample + push(history, { + query: { + sampleRangeFrom: '', + sampleRangeTo: '', + traceId: '', + transactionId: '', + }, + }); + }; + + useEffect(() => { + const selectedSample = traceSamples.find( + (sample) => + sample.transactionId === transactionId && sample.traceId === traceId + ); + + if (!selectedSample) { + // selected sample was not found. select a new one: + const preferredSample = maybe(traceSamples[0]); + + history.replace({ + ...history.location, + search: fromQuery({ + ...omit(toQuery(history.location.search), [ + 'traceId', + 'transactionId', + ]), + ...preferredSample, + }), + }); + } + }, [history, traceSamples, transactionId, traceId]); + + return ( + <> + + {tabs.map(({ dataTestSubj, key, label }) => ( + { + setCurrentTab(key); + }} + > + {label} + + ))} + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/types.ts b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts new file mode 100644 index 0000000000000..5396d5a8a538d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts @@ -0,0 +1,18 @@ +/* + * 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 { XYBrushArea } from '@elastic/charts'; + +import type { TraceSample } from '../../../hooks/use_transaction_trace_samples_fetcher'; + +export interface TabContentProps { + selectSampleFromChartSelection: (selection: XYBrushArea) => void; + clearChartSelection: () => void; + sampleRangeFrom?: number; + sampleRangeTo?: number; + traceSamples: TraceSample[]; +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx index 64c4e7dcb42b9..19199cda9495e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx @@ -10,34 +10,29 @@ import { EuiFlexGroup, EuiFlexItem, EuiPagination, - EuiPanel, EuiSpacer, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import type { IUrlParams } from '../../../../context/url_params_context/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; import { TransactionActionMenu } from '../../../shared/transaction_action_menu/TransactionActionMenu'; +import type { TraceSample } from '../../../../hooks/use_transaction_trace_samples_fetcher'; import { MaybeViewTraceLink } from './MaybeViewTraceLink'; import { TransactionTabs } from './TransactionTabs'; import { IWaterfall } from './waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; import { useApmParams } from '../../../../hooks/use_apm_params'; -type DistributionApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; - -type DistributionBucket = DistributionApiResponse['buckets'][0]; - interface Props { urlParams: IUrlParams; waterfall: IWaterfall; exceedsMax: boolean; isLoading: boolean; - traceSamples: DistributionBucket['samples']; + traceSamples: TraceSample[]; } export function WaterfallWithSummary({ @@ -88,13 +83,13 @@ export function WaterfallWithSummary({ /> ); - return {content}; + return content; } const entryTransaction = entryWaterfallTransaction.doc; return ( - + <> @@ -142,6 +137,6 @@ export function WaterfallWithSummary({ waterfall={waterfall} exceedsMax={exceedsMax} /> - + ); } 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 e53ca324eac0a..137e0ea7b2d29 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 @@ -18,6 +18,7 @@ import { AggregatedTransactionsCallout } from '../../shared/aggregated_transacti import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { TransactionsTable } from '../../shared/transactions_table'; + import { useRedirect } from './useRedirect'; function getRedirectLocation({ diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index d332048338cc0..b01c45437f430 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -26,7 +26,6 @@ import { useApmServiceContext } from '../../../../context/apm_service/use_apm_se import { useBreadcrumb } from '../../../../context/breadcrumbs/use_breadcrumb'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { useApmRouter } from '../../../../hooks/use_apm_router'; -import { Correlations } from '../../../app/correlations'; import { SearchBar } from '../../../shared/search_bar'; import { ServiceIcons } from '../../../shared/service_icons'; import { ApmMainTemplate } from '../apm_main_template'; @@ -108,10 +107,6 @@ function TemplateWithContext({ - - - - ), }} diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index 9acc04f18f187..b0cadd50b3d61 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -65,6 +65,8 @@ export function createHref( } export type APMQueryParams = { + sampleRangeFrom?: number; + sampleRangeTo?: number; transactionId?: string; transactionName?: string; transactionType?: string; diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx similarity index 61% rename from x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx rename to x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index e3ff631ae1a6f..51250818a2269 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -5,38 +5,42 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { AnnotationDomainType, - Chart, - CurveType, - Settings, - Axis, - ScaleType, - Position, AreaSeries, - RecursivePartial, + Axis, AxisStyle, - PartialTheme, + BrushEndListener, + Chart, + CurveType, LineAnnotation, LineAnnotationDatum, + PartialTheme, + Position, + RectAnnotation, + RecursivePartial, + ScaleType, + Settings, } from '@elastic/charts'; import euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiPaletteColorBlind } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; import { getDurationUnitKey, getUnitLabelAndConvertedValue, -} from '../../../../common/utils/formatters'; +} from '../../../../../common/utils/formatters'; -import { HistogramItem } from '../../../../common/search_strategies/correlations/types'; +import { HistogramItem } from '../../../../../common/search_strategies/correlations/types'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useTheme } from '../../../hooks/use_theme'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../hooks/use_theme'; -import { ChartContainer } from '../../shared/charts/chart_container'; +import { ChartContainer } from '../chart_container'; const { euiColorMediumShade } = euiVars; const axisColor = euiColorMediumShade; @@ -79,25 +83,28 @@ interface CorrelationsChartProps { field?: string; value?: string; histogram?: HistogramItem[]; + markerCurrentTransaction?: number; markerValue: number; markerPercentile: number; overallHistogram?: HistogramItem[]; + onChartSelection?: BrushEndListener; + selection?: [number, number]; } -const annotationsStyle = { +const getAnnotationsStyle = (color = 'gray') => ({ line: { strokeWidth: 1, - stroke: 'gray', + stroke: color, opacity: 0.8, }, details: { fontSize: 8, fontFamily: 'Arial', fontStyle: 'normal', - fill: 'gray', + fill: color, padding: 0, }, -}; +}); const CHART_PLACEHOLDER_VALUE = 0.0001; @@ -123,21 +130,29 @@ export const replaceHistogramDotsWithBars = ( } }; -export function CorrelationsChart({ +export function TransactionDistributionChart({ field, value, histogram: originalHistogram, + markerCurrentTransaction, markerValue, markerPercentile, overallHistogram, + onChartSelection, + selection, }: CorrelationsChartProps) { const euiTheme = useTheme(); + const patchedOverallHistogram = useMemo( + () => replaceHistogramDotsWithBars(overallHistogram), + [overallHistogram] + ); + const annotationsDataValues: LineAnnotationDatum[] = [ { dataValue: markerValue, details: i18n.translate( - 'xpack.apm.correlations.latency.chart.percentileMarkerLabel', + 'xpack.apm.transactionDistribution.chart.percentileMarkerLabel', { defaultMessage: '{markerPercentile}th percentile', values: { @@ -159,6 +174,21 @@ export function CorrelationsChart({ const histogram = replaceHistogramDotsWithBars(originalHistogram); + const selectionAnnotation = + selection !== undefined + ? [ + { + coordinates: { + x0: selection[0], + x1: selection[1], + y0: 0, + y1: 100000, + }, + details: 'selection', + }, + ] + : undefined; + return (
0} + hasData={ + Array.isArray(patchedOverallHistogram) && + patchedOverallHistogram.length > 0 + } status={ - Array.isArray(overallHistogram) + Array.isArray(patchedOverallHistogram) ? FETCH_STATUS.SUCCESS : FETCH_STATUS.LOADING } @@ -179,12 +212,51 @@ export function CorrelationsChart({ theme={chartTheme} showLegend legendPosition={Position.Bottom} + onBrushEnd={onChartSelection} /> + {selectionAnnotation !== undefined && ( + + )} + {typeof markerCurrentTransaction === 'number' && ( + + )} @@ -208,7 +280,7 @@ export function CorrelationsChart({ id="y-axis" domain={yAxisDomain} title={i18n.translate( - 'xpack.apm.correlations.latency.chart.numberOfTransactionsLabel', + 'xpack.apm.transactionDistribution.chart.numberOfTransactionsLabel', { defaultMessage: '# transactions' } )} position={Position.Left} @@ -216,12 +288,12 @@ export function CorrelationsChart({ /> ; - -const INITIAL_DATA = { - buckets: [] as APIResponse['buckets'], - noHits: true, - bucketSize: 0, -}; +interface RawResponse { + percentileThresholdValue?: number; + took: number; + values: SearchServiceValue[]; + overallHistogram: HistogramItem[]; + log: string[]; + ccsWarning: boolean; +} -export function useTransactionDistributionFetcher({ - transactionName, - kuery, - environment, -}: { - transactionName: string; - kuery: string; - environment: string; -}) { - const { serviceName, transactionType } = useApmServiceContext(); +interface TransactionDistributionFetcherState { + error?: Error; + isComplete: boolean; + isRunning: boolean; + loaded: number; + ccsWarning: RawResponse['ccsWarning']; + log: RawResponse['log']; + transactionDistribution?: RawResponse['overallHistogram']; + percentileThresholdValue?: RawResponse['percentileThresholdValue']; + timeTook?: number; + total: number; +} +export function useTransactionDistributionFetcher( + params: Omit +) { const { - urlParams: { start, end, transactionId, traceId }, - } = useUrlParams(); + services: { data }, + } = useKibana(); - const history = useHistory(); - const { data = INITIAL_DATA, status, error } = useFetcher( - async (callApmApi) => { - if (serviceName && start && end && transactionType && transactionName) { - const response = await callApmApi({ - endpoint: - 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', - params: { - path: { - serviceName, - }, - query: { - environment, - kuery, - start, - end, - transactionType, - transactionName, - transactionId, - traceId, - }, - }, - }); + const [ + fetchState, + setFetchState, + ] = useState({ + isComplete: false, + isRunning: false, + loaded: 0, + ccsWarning: false, + log: [], + total: 100, + }); - const selectedSample = - transactionId && traceId - ? flatten(response.buckets.map((bucket) => bucket.samples)).find( - (sample) => - sample.transactionId === transactionId && - sample.traceId === traceId - ) - : undefined; + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); - if (!selectedSample) { - // selected sample was not found. select a new one: - // sorted by total number of requests, but only pick - // from buckets that have samples - const bucketsSortedByCount = response.buckets - .filter((bucket) => !isEmpty(bucket.samples)) - .sort((bucket) => bucket.count); + function setResponse(response: IKibanaSearchResponse) { + setFetchState((prevState) => ({ + ...prevState, + isRunning: response.isRunning || false, + ccsWarning: response.rawResponse?.ccsWarning ?? false, + histograms: response.rawResponse?.values ?? [], + log: response.rawResponse?.log ?? [], + loaded: response.loaded!, + total: response.total!, + timeTook: response.rawResponse.took, + // only set percentileThresholdValue and overallHistogram once it's repopulated on a refresh, + // otherwise the consuming chart would flicker with an empty state on reload. + ...(response.rawResponse?.percentileThresholdValue !== undefined && + response.rawResponse?.overallHistogram !== undefined + ? { + transactionDistribution: response.rawResponse?.overallHistogram, + percentileThresholdValue: + response.rawResponse?.percentileThresholdValue, + } + : {}), + })); + } - const preferredSample = maybe(bucketsSortedByCount[0]?.samples[0]); + const startFetch = () => { + setFetchState((prevState) => ({ + ...prevState, + error: undefined, + isComplete: false, + })); + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); - history.replace({ - ...history.location, - search: fromQuery({ - ...omit(toQuery(history.location.search), [ - 'traceId', - 'transactionId', - ]), - ...preferredSample, - }), - }); - } + const searchServiceParams: SearchServiceParams = { + ...params, + analyzeCorrelations: false, + }; + const req = { params: searchServiceParams }; - return response; - } - }, - // the histogram should not be refetched if the transactionId or traceId changes - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - environment, - kuery, - serviceName, - start, - end, - transactionType, - transactionName, - ] - ); + // Submit the search request using the `data.search` service. + searchSubscription$.current = data.search + .search>(req, { + strategy: 'apmCorrelationsSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (res: IKibanaSearchResponse) => { + setResponse(res); + if (isCompleteResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + isRunnning: false, + isComplete: true, + })); + } else if (isErrorResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + error: (res as unknown) as Error, + setIsRunning: false, + })); + } + }, + error: (error: Error) => { + setFetchState((prevState) => ({ + ...prevState, + error, + setIsRunning: false, + })); + }, + }); + }; + + const cancelFetch = () => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + setFetchState((prevState) => ({ + ...prevState, + setIsRunning: false, + })); + }; return { - distributionData: data, - distributionStatus: status, - distributionError: error, + ...fetchState, + progress: fetchState.loaded / fetchState.total, + startFetch, + cancelFetch, }; } diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts new file mode 100644 index 0000000000000..538792bbf23a8 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef, useState } from 'react'; +import type { Subscription } from 'rxjs'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + isCompleteResponse, + isErrorResponse, +} from '../../../../../src/plugins/data/public'; +import type { + HistogramItem, + SearchServiceParams, + SearchServiceValue, +} from '../../common/search_strategies/correlations/types'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../plugin'; + +interface RawResponse { + percentileThresholdValue?: number; + took: number; + values: SearchServiceValue[]; + overallHistogram: HistogramItem[]; + log: string[]; + ccsWarning: boolean; +} + +interface TransactionLatencyCorrelationsFetcherState { + error?: Error; + isComplete: boolean; + isRunning: boolean; + loaded: number; + ccsWarning: RawResponse['ccsWarning']; + histograms: RawResponse['values']; + log: RawResponse['log']; + overallHistogram?: RawResponse['overallHistogram']; + percentileThresholdValue?: RawResponse['percentileThresholdValue']; + timeTook?: number; + total: number; +} + +export const useTransactionLatencyCorrelationsFetcher = ( + params: Omit +) => { + const { + services: { data }, + } = useKibana(); + + const [ + fetchState, + setFetchState, + ] = useState({ + isComplete: false, + isRunning: false, + loaded: 0, + ccsWarning: false, + histograms: [], + log: [], + total: 100, + }); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + + function setResponse(response: IKibanaSearchResponse) { + setFetchState((prevState) => ({ + ...prevState, + isRunning: response.isRunning || false, + ccsWarning: response.rawResponse?.ccsWarning ?? false, + histograms: response.rawResponse?.values ?? [], + log: response.rawResponse?.log ?? [], + loaded: response.loaded!, + total: response.total!, + timeTook: response.rawResponse.took, + // only set percentileThresholdValue and overallHistogram once it's repopulated on a refresh, + // otherwise the consuming chart would flicker with an empty state on reload. + ...(response.rawResponse?.percentileThresholdValue !== undefined && + response.rawResponse?.overallHistogram !== undefined + ? { + overallHistogram: response.rawResponse?.overallHistogram, + percentileThresholdValue: + response.rawResponse?.percentileThresholdValue, + } + : {}), + })); + } + + const startFetch = () => { + setFetchState((prevState) => ({ + ...prevState, + error: undefined, + isComplete: false, + })); + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + const searchServiceParams: SearchServiceParams = { + ...params, + analyzeCorrelations: true, + }; + const req = { params: searchServiceParams }; + + // Submit the search request using the `data.search` service. + searchSubscription$.current = data.search + .search>(req, { + strategy: 'apmCorrelationsSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (res: IKibanaSearchResponse) => { + setResponse(res); + if (isCompleteResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + isRunnning: false, + isComplete: true, + })); + } else if (isErrorResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + error: (res as unknown) as Error, + setIsRunning: false, + })); + } + }, + error: (error: Error) => { + setFetchState((prevState) => ({ + ...prevState, + error, + setIsRunning: false, + })); + }, + }); + }; + + const cancelFetch = () => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + setFetchState((prevState) => ({ + ...prevState, + setIsRunning: false, + })); + }; + + return { + ...fetchState, + progress: fetchState.loaded / fetchState.total, + startFetch, + cancelFetch, + }; +}; diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts new file mode 100644 index 0000000000000..673c1086033b5 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts @@ -0,0 +1,101 @@ +/* + * 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 { useFetcher } from './use_fetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; + +export interface TraceSample { + traceId: string; + transactionId: string; +} + +const INITIAL_DATA = { + noHits: true, + traceSamples: [] as TraceSample[], +}; + +export function useTransactionTraceSamplesFetcher({ + transactionName, + kuery, + environment, +}: { + transactionName: string; + kuery: string; + environment: string; +}) { + const { serviceName, transactionType } = useApmServiceContext(); + + const { + urlParams: { + start, + end, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + }, + } = useUrlParams(); + + const { data = INITIAL_DATA, status, error } = useFetcher( + async (callApmApi) => { + if (serviceName && start && end && transactionType && transactionName) { + const response = await callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/traces/samples', + params: { + path: { + serviceName, + }, + query: { + environment, + kuery, + start, + end, + transactionType, + transactionName, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + }, + }, + }); + + if (response.noHits) { + return response; + } + + const { traceSamples } = response; + + return { + noHits: false, + traceSamples, + }; + } + }, + // the samples should not be refetched if the transactionId or traceId changes + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + environment, + kuery, + serviceName, + start, + end, + transactionType, + transactionName, + sampleRangeFrom, + sampleRangeTo, + ] + ); + + return { + traceSamplesData: data, + traceSamplesStatus: status, + traceSamplesError: error, + }; +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts index ae42a0c94fe9c..e9986bd9f0cf5 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts @@ -49,14 +49,14 @@ export const asyncSearchServiceProvider = ( // 95th percentile to be displayed as a marker in the log log chart const { totalDocs, - percentiles: percentileThreshold, + percentiles: percentilesResponseThresholds, } = await fetchTransactionDurationPercentiles( esClient, params, params.percentileThreshold ? [params.percentileThreshold] : undefined ); const percentileThresholdValue = - percentileThreshold[`${params.percentileThreshold}.0`]; + percentilesResponseThresholds[`${params.percentileThreshold}.0`]; state.setPercentileThresholdValue(percentileThresholdValue); addLogMessage( @@ -107,11 +107,31 @@ export const asyncSearchServiceProvider = ( return; } + // finish early if correlation analysis is not required. + if (params.analyzeCorrelations === false) { + addLogMessage( + `Finish service since correlation analysis wasn't requested.` + ); + state.setProgress({ + loadedHistogramStepsize: 1, + loadedOverallHistogram: 1, + loadedFieldCanditates: 1, + loadedFieldValuePairs: 1, + loadedHistograms: 1, + }); + state.setIsRunning(false); + return; + } + // Create an array of ranges [2, 4, 6, ..., 98] - const percents = Array.from(range(2, 100, 2)); + const percentileAggregationPercents = range(2, 100, 2); const { percentiles: percentilesRecords, - } = await fetchTransactionDurationPercentiles(esClient, params, percents); + } = await fetchTransactionDurationPercentiles( + esClient, + params, + percentileAggregationPercents + ); const percentiles = Object.values(percentilesRecords); addLogMessage(`Loaded percentiles.`); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts index 4b10ceb035e15..3be3438b2d18f 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts @@ -49,7 +49,6 @@ describe('correlations', () => { end: '2021', environment: 'dev', kuery: '', - percentileThresholdValue: 75, includeFrozen: false, }, }); @@ -85,13 +84,6 @@ describe('correlations', () => { 'transaction.name': 'actualTransactionName', }, }, - { - range: { - 'transaction.duration.us': { - gte: 75, - }, - }, - }, ], }, }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts index f28556f7a90b5..8bd9f3d4e582c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts @@ -10,28 +10,11 @@ import { getOrElse } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { rangeRt } from '../../../../routes/default_api_types'; import { getCorrelationsFilters } from '../../../correlations/get_filters'; import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; -const getPercentileThresholdValueQuery = ( - percentileThresholdValue: number | undefined -): estypes.QueryDslQueryContainer[] => { - return percentileThresholdValue - ? [ - { - range: { - [TRANSACTION_DURATION]: { - gte: percentileThresholdValue, - }, - }, - }, - ] - : []; -}; - export const getTermsQuery = ( fieldName: string | undefined, fieldValue: string | undefined @@ -55,7 +38,6 @@ export const getQueryWithParams = ({ serviceName, start, end, - percentileThresholdValue, transactionType, transactionName, } = params; @@ -82,7 +64,6 @@ export const getQueryWithParams = ({ filter: [ ...filters, ...getTermsQuery(fieldName, fieldValue), - ...getPercentileThresholdValueQuery(percentileThresholdValue), ] as estypes.QueryDslQueryContainer[], }, }; diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index baa9b3ae230fe..44125d557dcc8 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -335,7 +335,7 @@ Object { } `; -exports[`transaction queries fetches transaction distribution 1`] = ` +exports[`transaction queries fetches transaction trace samples 1`] = ` Object { "apm": Object { "events": Array [ @@ -343,13 +343,6 @@ Object { ], }, "body": Object { - "aggs": Object { - "stats": Object { - "max": Object { - "field": "transaction.duration.us", - }, - }, - }, "query": Object { "bool": Object { "filter": Array [ @@ -377,10 +370,27 @@ Object { }, }, }, + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "trace.id": "qux", + }, + }, + Object { + "term": Object { + "transaction.id": "quz", + }, + }, ], }, }, - "size": 0, + "size": 500, }, } `; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts deleted file mode 100644 index e868f7de049f9..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ /dev/null @@ -1,217 +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 { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; -import { withApmSpan } from '../../../../utils/with_apm_span'; -import { - SERVICE_NAME, - TRACE_ID, - TRANSACTION_DURATION, - TRANSACTION_ID, - TRANSACTION_NAME, - TRANSACTION_SAMPLED, - TRANSACTION_TYPE, -} from '../../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../../common/processor_event'; -import { joinByKey } from '../../../../../common/utils/join_by_key'; -import { rangeQuery, kqlQuery } from '../../../../../../observability/server'; -import { environmentQuery } from '../../../../../common/utils/environment_query'; -import { - getDocumentTypeFilterForAggregatedTransactions, - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../../../helpers/aggregated_transactions'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; - -function getHistogramAggOptions({ - bucketSize, - field, - distributionMax, -}: { - bucketSize: number; - field: string; - distributionMax: number; -}) { - return { - field, - interval: bucketSize, - min_doc_count: 0, - extended_bounds: { - min: 0, - max: distributionMax, - }, - }; -} - -export async function getBuckets({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - distributionMax, - bucketSize, - setup, - searchAggregatedTransactions, -}: { - environment: string; - kuery: string; - serviceName: string; - transactionName: string; - transactionType: string; - transactionId: string; - traceId: string; - distributionMax: number; - bucketSize: number; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - return withApmSpan( - 'get_latency_distribution_buckets_with_samples', - async () => { - const { start, end, apmEventClient } = setup; - - const commonFilters = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [TRANSACTION_NAME]: transactionName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ] as QueryDslQueryContainer[]; - - async function getSamplesForDistributionBuckets() { - const response = await apmEventClient.search( - 'get_samples_for_latency_distribution_buckets', - { - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - query: { - bool: { - filter: [ - ...commonFilters, - { term: { [TRANSACTION_SAMPLED]: true } }, - ], - should: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [TRANSACTION_ID]: transactionId } }, - ] as QueryDslQueryContainer[], - }, - }, - aggs: { - distribution: { - histogram: getHistogramAggOptions({ - bucketSize, - field: TRANSACTION_DURATION, - distributionMax, - }), - aggs: { - samples: { - top_hits: { - _source: [TRANSACTION_ID, TRACE_ID], - size: 10, - sort: { - _score: 'desc' as const, - }, - }, - }, - }, - }, - }, - }, - } - ); - - return ( - response.aggregations?.distribution.buckets.map((bucket) => { - const samples = bucket.samples.hits.hits; - return { - key: bucket.key, - samples: samples.map(({ _source: sample }) => ({ - traceId: sample.trace.id, - transactionId: sample.transaction.id, - })), - }; - }) ?? [] - ); - } - - async function getDistributionBuckets() { - const response = await apmEventClient.search( - 'get_latency_distribution_buckets', - { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - query: { - bool: { - filter: [ - ...commonFilters, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - }, - aggs: { - distribution: { - histogram: getHistogramAggOptions({ - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - bucketSize, - distributionMax, - }), - }, - }, - }, - } - ); - - return ( - response.aggregations?.distribution.buckets.map((bucket) => { - return { - key: bucket.key, - count: bucket.doc_count, - }; - }) ?? [] - ); - } - - const [ - samplesForDistributionBuckets, - distributionBuckets, - ] = await Promise.all([ - getSamplesForDistributionBuckets(), - getDistributionBuckets(), - ]); - - const buckets = joinByKey( - [...samplesForDistributionBuckets, ...distributionBuckets], - 'key' - ).map((bucket) => ({ - ...bucket, - samples: bucket.samples ?? [], - count: bucket.count ?? 0, - })); - - return { - noHits: buckets.length === 0, - bucketSize, - buckets, - }; - } - ); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts deleted file mode 100644 index 9c056bc506e92..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ /dev/null @@ -1,79 +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 { - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../../helpers/aggregated_transactions'; -import { rangeQuery, kqlQuery } from '../../../../../observability/server'; -import { environmentQuery } from '../../../../common/utils/environment_query'; - -export async function getDistributionMax({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - setup, - searchAggregatedTransactions, -}: { - environment: string; - kuery: string; - serviceName: string; - transactionName: string; - transactionType: string; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - const { start, end, apmEventClient } = setup; - - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [TRANSACTION_NAME]: transactionName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], - }, - }, - aggs: { - stats: { - max: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }; - - const resp = await apmEventClient.search( - 'get_latency_distribution_max', - params - ); - return resp.aggregations?.stats.value ?? null; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts deleted file mode 100644 index ef72f2434fde2..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts +++ /dev/null @@ -1,80 +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 { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { getBuckets } from './get_buckets'; -import { getDistributionMax } from './get_distribution_max'; -import { roundToNearestFiveOrTen } from '../../helpers/round_to_nearest_five_or_ten'; -import { MINIMUM_BUCKET_SIZE, BUCKET_TARGET_COUNT } from '../constants'; -import { withApmSpan } from '../../../utils/with_apm_span'; - -function getBucketSize(max: number) { - const bucketSize = max / BUCKET_TARGET_COUNT; - return roundToNearestFiveOrTen( - bucketSize > MINIMUM_BUCKET_SIZE ? bucketSize : MINIMUM_BUCKET_SIZE - ); -} - -export async function getTransactionDistribution({ - kuery, - environment, - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - setup, - searchAggregatedTransactions, -}: { - environment: string; - kuery: string; - serviceName: string; - transactionName: string; - transactionType: string; - transactionId: string; - traceId: string; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - return withApmSpan('get_transaction_latency_distribution', async () => { - const distributionMax = await getDistributionMax({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - setup, - searchAggregatedTransactions, - }); - - if (distributionMax == null) { - return { noHits: true, buckets: [], bucketSize: 0 }; - } - - const bucketSize = getBucketSize(distributionMax); - - const { buckets, noHits } = await getBuckets({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - distributionMax, - bucketSize, - setup, - searchAggregatedTransactions, - }); - - return { - noHits, - buckets, - bucketSize, - }; - }); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts index b1d942a261387..b6b727d2273a1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts @@ -11,7 +11,7 @@ import { SearchParamsMock, } from '../../utils/test_helpers'; import { getTransactionBreakdown } from './breakdown'; -import { getTransactionDistribution } from './distribution'; +import { getTransactionTraceSamples } from './trace_samples'; import { getTransaction } from './get_transaction'; describe('transaction queries', () => { @@ -50,16 +50,15 @@ describe('transaction queries', () => { expect(mock.params).toMatchSnapshot(); }); - it('fetches transaction distribution', async () => { + it('fetches transaction trace samples', async () => { mock = await inspectSearchParams((setup) => - getTransactionDistribution({ + getTransactionTraceSamples({ serviceName: 'foo', transactionName: 'bar', transactionType: 'baz', traceId: 'qux', transactionId: 'quz', setup, - searchAggregatedTransactions: false, environment: ENVIRONMENT_ALL.value, kuery: '', }) diff --git a/x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.ts b/x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.ts new file mode 100644 index 0000000000000..98ef9ecaf346f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.ts @@ -0,0 +1,107 @@ +/* + * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import { withApmSpan } from '../../../../utils/with_apm_span'; +import { + SERVICE_NAME, + TRACE_ID, + TRANSACTION_ID, + TRANSACTION_NAME, + TRANSACTION_SAMPLED, + TRANSACTION_TYPE, +} from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { rangeQuery, kqlQuery } from '../../../../../../observability/server'; +import { environmentQuery } from '../../../../../common/utils/environment_query'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; + +const TRACE_SAMPLES_SIZE = 500; + +export async function getTraceSamples({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + setup, +}: { + environment: string; + kuery: string; + serviceName: string; + transactionName: string; + transactionType: string; + transactionId: string; + traceId: string; + sampleRangeFrom?: number; + sampleRangeTo?: number; + setup: Setup & SetupTimeRange; +}) { + return withApmSpan('get_trace_samples', async () => { + const { start, end, apmEventClient } = setup; + + const commonFilters = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + { term: { [TRANSACTION_NAME]: transactionName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ] as QueryDslQueryContainer[]; + + if (sampleRangeFrom !== undefined && sampleRangeTo !== undefined) { + commonFilters.push({ + range: { + 'transaction.duration.us': { + gte: sampleRangeFrom, + lte: sampleRangeTo, + }, + }, + }); + } + + async function getTraceSamplesHits() { + const response = await apmEventClient.search('get_trace_samples_hits', { + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + ...commonFilters, + { term: { [TRANSACTION_SAMPLED]: true } }, + ], + should: [ + { term: { [TRACE_ID]: traceId } }, + { term: { [TRANSACTION_ID]: transactionId } }, + ] as QueryDslQueryContainer[], + }, + }, + size: TRACE_SAMPLES_SIZE, + }, + }); + + return response.hits.hits; + } + + const samplesForDistributionHits = await getTraceSamplesHits(); + + const traceSamples = samplesForDistributionHits.map((hit) => ({ + transactionId: hit._source.transaction.id, + traceId: hit._source.trace.id, + })); + + return { + noHits: samplesForDistributionHits.length === 0, + traceSamples, + }; + }); +} diff --git a/x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts b/x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts new file mode 100644 index 0000000000000..95548cd2afadf --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts @@ -0,0 +1,49 @@ +/* + * 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 { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getTraceSamples } from './get_trace_samples'; +import { withApmSpan } from '../../../utils/with_apm_span'; + +export async function getTransactionTraceSamples({ + kuery, + environment, + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + setup, +}: { + environment: string; + kuery: string; + serviceName: string; + transactionName: string; + transactionType: string; + transactionId: string; + traceId: string; + sampleRangeFrom?: number; + sampleRangeTo?: number; + setup: Setup & SetupTimeRange; +}) { + return withApmSpan('get_transaction_trace_samples', async () => { + return await getTraceSamples({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + setup, + }); + }); +} diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index f211e722958c5..c267487cd36b7 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -16,7 +16,7 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; import { getServiceTransactionGroupDetailedStatisticsPeriods } from '../lib/services/get_service_transaction_group_detailed_statistics'; import { getTransactionBreakdown } from '../lib/transactions/breakdown'; -import { getTransactionDistribution } from '../lib/transactions/distribution'; +import { getTransactionTraceSamples } from '../lib/transactions/trace_samples'; import { getAnomalySeries } from '../lib/transactions/get_anomaly_data'; import { getLatencyPeriods } from '../lib/transactions/get_latency_charts'; import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate'; @@ -204,9 +204,8 @@ const transactionLatencyChartsRoute = createApmServerRoute({ }, }); -const transactionChartsDistributionRoute = createApmServerRoute({ - endpoint: - 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', +const transactionTraceSamplesRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transactions/traces/samples', params: t.type({ path: t.type({ serviceName: t.string, @@ -219,6 +218,8 @@ const transactionChartsDistributionRoute = createApmServerRoute({ t.partial({ transactionId: t.string, traceId: t.string, + sampleRangeFrom: toNumberRt, + sampleRangeTo: toNumberRt, }), environmentRt, kueryRt, @@ -237,14 +238,11 @@ const transactionChartsDistributionRoute = createApmServerRoute({ transactionName, transactionId = '', traceId = '', + sampleRangeFrom, + sampleRangeTo, } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions({ - ...setup, - kuery, - }); - - return getTransactionDistribution({ + return getTransactionTraceSamples({ environment, kuery, serviceName, @@ -252,8 +250,9 @@ const transactionChartsDistributionRoute = createApmServerRoute({ transactionName, transactionId, traceId, + sampleRangeFrom, + sampleRangeTo, setup, - searchAggregatedTransactions, }); }, }); @@ -347,6 +346,6 @@ export const transactionRouteRepository = createApmServerRouteRepository() .add(transactionGroupsMainStatisticsRoute) .add(transactionGroupsDetailedStatisticsRoute) .add(transactionLatencyChartsRoute) - .add(transactionChartsDistributionRoute) + .add(transactionTraceSamplesRoute) .add(transactionChartsBreakdownRoute) .add(transactionChartsErrorRateRoute); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6160f22675cb9..cc52bbbc92b4f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5511,10 +5511,6 @@ "xpack.apm.chart.memorySeries.systemAverageLabel": "平均", "xpack.apm.chart.memorySeries.systemMaxLabel": "最高", "xpack.apm.clearFilters": "フィルターを消去", - "xpack.apm.correlations.betaDescription": "相関関係がGAではありません。不具合が発生したら報告してください。", - "xpack.apm.correlations.betaLabel": "ベータ", - "xpack.apm.correlations.buttonLabel": "相関関係を表示", - "xpack.apm.correlations.clearFiltersLabel": "クリア", "xpack.apm.correlations.correlationsTable.actionsLabel": "フィルター", "xpack.apm.correlations.correlationsTable.excludeDescription": "値を除外", "xpack.apm.correlations.correlationsTable.excludeLabel": "除外", @@ -5534,21 +5530,12 @@ "xpack.apm.correlations.customize.fieldPlaceholder": "オプションを選択または作成", "xpack.apm.correlations.customize.thresholdLabel": "しきい値", "xpack.apm.correlations.customize.thresholdPercentile": "{percentile}パーセンタイル", - "xpack.apm.correlations.environmentLabel": "環境", "xpack.apm.correlations.error.chart.overallErrorRateLabel": "全体のエラー率", "xpack.apm.correlations.error.chart.selectedTermErrorRateLabel": "{fieldName}:{fieldValue}", "xpack.apm.correlations.error.chart.title": "経時的なエラー率", "xpack.apm.correlations.error.description": "一部のトランザクションが失敗してエラーが返される理由。相関関係は、データの特定のコホートで想定される原因を検出するのに役立ちます。ホスト、バージョン、または他のカスタムフィールドのいずれか。", "xpack.apm.correlations.error.percentageColumnName": "失敗したトランザクションの%", - "xpack.apm.correlations.filteringByLabel": "フィルタリング条件", - "xpack.apm.correlations.latency.chart.numberOfTransactionsLabel": "# トランザクション", - "xpack.apm.correlations.latency.chart.overallLatencyDistributionLabel": "全体のレイテンシ分布", - "xpack.apm.correlations.latency.chart.selectedTermLatencyDistributionLabel": "{fieldName}:{fieldValue}", - "xpack.apm.correlations.latency.chart.title": "レイテンシ分布", - "xpack.apm.correlations.latency.description": "サービスが低速になっている原因。相関関係は、データの特定のコホートにあるパフォーマンス低下を特定するのに役立ちます。ホスト、バージョン、または他のカスタムフィールドのいずれか。", - "xpack.apm.correlations.latency.percentageColumnName": "低速なトランザクションの%", "xpack.apm.correlations.latencyCorrelations.cancelButtonTitle": "キャンセル", - "xpack.apm.correlations.latencyCorrelations.chartTitle": "{name}の遅延分布", "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "フィルター", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "サービスの遅延に対するフィールドの影響。0~1の範囲。", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel": "相関関係", @@ -5563,12 +5550,6 @@ "xpack.apm.correlations.latencyCorrelations.progressAriaLabel": "進捗", "xpack.apm.correlations.latencyCorrelations.progressTitle": "進捗状況: {progress}%", "xpack.apm.correlations.latencyCorrelations.refreshButtonTitle": "更新", - "xpack.apm.correlations.licenseCheckText": "相関関係を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。使用すると、パフォーマンスの低下に関連しているフィールドを検出できます。", - "xpack.apm.correlations.serviceLabel": "サービス", - "xpack.apm.correlations.tabs.errorRateLabel": "エラー率", - "xpack.apm.correlations.tabs.latencyLabel": "レイテンシ", - "xpack.apm.correlations.title": "相関関係", - "xpack.apm.correlations.transactionLabel": "トランザクション", "xpack.apm.csm.breakdownFilter.browser": "ブラウザー", "xpack.apm.csm.breakdownFilter.device": "デバイス", "xpack.apm.csm.breakdownFilter.location": "場所", @@ -6061,7 +6042,6 @@ "xpack.apm.transactionCardinalityWarning.body": "一意のトランザクション名の数が構成された値{bucketSize}を超えています。エージェントを再構成し、類似したトランザクションをグループ化するか、{codeBlock}の値を増やしてください。", "xpack.apm.transactionCardinalityWarning.docsLink": "詳細はドキュメントをご覧ください", "xpack.apm.transactionCardinalityWarning.title": "このビューには、報告されたトランザクションのサブセットが表示されます。", - "xpack.apm.transactionDetails.notFoundLabel": "トランザクションが見つかりませんでした。", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "トレースの親が見つかりませんでした", "xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "{parentType, select, transaction {トランザクション} trace {トレース} }の割合が100%を超えています。これは、この{childType, select, span {スパン} transaction {トランザクション} }がルートトランザクションよりも時間がかかるためです。", "xpack.apm.transactionDetails.requestMethodLabel": "リクエストメソッド", @@ -6084,11 +6064,6 @@ "xpack.apm.transactionDetails.traceNotFound": "選択されたトレースが見つかりません", "xpack.apm.transactionDetails.traceSampleTitle": "トレースのサンプル", "xpack.apm.transactionDetails.transactionLabel": "トランザクション", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSamplesAvailable": "サンプルがありません", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel": "{transCount} 件のトランザクション", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle": "レイテンシ分布", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingDescription": "各バケットはサンプルトランザクションを示します。利用可能なサンプルがない場合、おそらくエージェントの構成で設定されたサンプリング制限が原因です。", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingLabel": "サンプリング", "xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "このトランザクションを報告した APM エージェントが、構成に基づき {dropped} 個以上のスパンをドロップしました。", "xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "ドロップされたスパンの詳細。", "xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "トランザクションの詳細", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0a65035a585e5..1f88ab9472619 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5536,10 +5536,6 @@ "xpack.apm.chart.memorySeries.systemAverageLabel": "平均值", "xpack.apm.chart.memorySeries.systemMaxLabel": "最大值", "xpack.apm.clearFilters": "清除筛选", - "xpack.apm.correlations.betaDescription": "相关性不是 GA 版。请通过报告错误来帮助我们。", - "xpack.apm.correlations.betaLabel": "公测版", - "xpack.apm.correlations.buttonLabel": "查看相关性", - "xpack.apm.correlations.clearFiltersLabel": "清除", "xpack.apm.correlations.correlationsTable.actionsLabel": "筛选", "xpack.apm.correlations.correlationsTable.excludeDescription": "筛除值", "xpack.apm.correlations.correlationsTable.excludeLabel": "排除", @@ -5559,21 +5555,12 @@ "xpack.apm.correlations.customize.fieldPlaceholder": "选择或创建选项", "xpack.apm.correlations.customize.thresholdLabel": "阈值", "xpack.apm.correlations.customize.thresholdPercentile": "第 {percentile} 个百分位数", - "xpack.apm.correlations.environmentLabel": "环境", "xpack.apm.correlations.error.chart.overallErrorRateLabel": "总错误率", "xpack.apm.correlations.error.chart.selectedTermErrorRateLabel": "{fieldName}:{fieldValue}", "xpack.apm.correlations.error.chart.title": "时移错误率", "xpack.apm.correlations.error.description": "为什么某些事务失败并返回错误?相关性将有助于在您数据的特定群组中发现可能的原因。按主机、版本或其他定制字段。", "xpack.apm.correlations.error.percentageColumnName": "失败事务 %", - "xpack.apm.correlations.filteringByLabel": "筛选依据", - "xpack.apm.correlations.latency.chart.numberOfTransactionsLabel": "事务数", - "xpack.apm.correlations.latency.chart.overallLatencyDistributionLabel": "总体延迟分布", - "xpack.apm.correlations.latency.chart.selectedTermLatencyDistributionLabel": "{fieldName}:{fieldValue}", - "xpack.apm.correlations.latency.chart.title": "延迟分布", - "xpack.apm.correlations.latency.description": "什么在拖慢我的服务?相关性将有助于在您数据的特定群组中发现较慢的性能。按主机、版本或其他定制字段。", - "xpack.apm.correlations.latency.percentageColumnName": "缓慢事务 %", "xpack.apm.correlations.latencyCorrelations.cancelButtonTitle": "取消", - "xpack.apm.correlations.latencyCorrelations.chartTitle": "{name} 的延迟分布", "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "筛选", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "字段对服务延迟的影响,范围从 0 到 1。", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel": "相关性", @@ -5588,12 +5575,6 @@ "xpack.apm.correlations.latencyCorrelations.progressAriaLabel": "进度", "xpack.apm.correlations.latencyCorrelations.progressTitle": "进度:{progress}%", "xpack.apm.correlations.latencyCorrelations.refreshButtonTitle": "刷新", - "xpack.apm.correlations.licenseCheckText": "要使用相关性,必须订阅 Elastic 白金级许可证。使用相关性,将能够发现哪些字段与性能差相关。", - "xpack.apm.correlations.serviceLabel": "服务", - "xpack.apm.correlations.tabs.errorRateLabel": "错误率", - "xpack.apm.correlations.tabs.latencyLabel": "延迟", - "xpack.apm.correlations.title": "相关性", - "xpack.apm.correlations.transactionLabel": "事务", "xpack.apm.csm.breakdownFilter.browser": "浏览器", "xpack.apm.csm.breakdownFilter.device": "设备", "xpack.apm.csm.breakdownFilter.location": "位置", @@ -6094,7 +6075,6 @@ "xpack.apm.transactionCardinalityWarning.title": "此视图显示已报告事务的子集。", "xpack.apm.transactionDetails.errorCount": "{errorCount, number} 个 {errorCount, plural, other {错误}}", "xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {查看 1 个相关错误} other {查看 # 个相关错误}}", - "xpack.apm.transactionDetails.notFoundLabel": "未找到任何事务。", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "找不到上级追溯", "xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "{parentType, select, transaction {事务} trace {追溯} }的百分比超过 100%,因为此{childType, select, span {跨度} transaction {事务} }比根事务花费更长的时间。", "xpack.apm.transactionDetails.requestMethodLabel": "请求方法", @@ -6117,12 +6097,6 @@ "xpack.apm.transactionDetails.traceNotFound": "找不到所选跟踪", "xpack.apm.transactionDetails.traceSampleTitle": "跟踪样例", "xpack.apm.transactionDetails.transactionLabel": "事务", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSamplesAvailable": "没有可用样本", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel": "{transCount, plural, other {事务}}", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel": "{transCount} 个事务", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle": "延迟分布", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingDescription": "每个存储桶将显示一个样例事务。如果没有可用的样例,很可能是在代理配置设置了采样限制。", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingLabel": "采样", "xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "报告此事务的 APM 代理基于其配置丢弃了 {dropped} 个跨度。", "xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "详细了解丢弃的跨度。", "xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "事务详情", diff --git a/x-pack/test/apm_api_integration/tests/feature_controls.ts b/x-pack/test/apm_api_integration/tests/feature_controls.ts index 589fba8561ae6..11c16fd87483c 100644 --- a/x-pack/test/apm_api_integration/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.ts @@ -124,7 +124,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }, { req: { - url: `/api/apm/services/foo/transactions/charts/distribution?start=${start}&end=${end}&transactionType=bar&transactionName=baz&environment=ENVIRONMENT_ALL&kuery=`, + url: `/api/apm/services/foo/transactions/traces/samples?start=${start}&end=${end}&transactionType=bar&transactionName=baz&environment=ENVIRONMENT_ALL&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 12b21ad17bf2f..0c1f695d4395b 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -158,8 +158,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./transactions/breakdown')); }); - describe('transactions/distribution', function () { - loadTestFile(require.resolve('./transactions/distribution')); + describe('transactions/trace_samples', function () { + loadTestFile(require.resolve('./transactions/trace_samples')); }); describe('transactions/error_rate', function () { diff --git a/x-pack/test/apm_api_integration/tests/transactions/distribution.ts b/x-pack/test/apm_api_integration/tests/transactions/distribution.ts deleted file mode 100644 index 3c322a727d1f0..0000000000000 --- a/x-pack/test/apm_api_integration/tests/transactions/distribution.ts +++ /dev/null @@ -1,95 +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 expect from '@kbn/expect'; -import qs from 'querystring'; -import { isEmpty } from 'lodash'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - const url = `/api/apm/services/opbeans-java/transactions/charts/distribution?${qs.stringify({ - start: metadata.start, - end: metadata.end, - transactionName: 'APIRestController#stats', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - kuery: '', - })}`; - - registry.when( - 'Transaction groups distribution when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - - expect(response.body.noHits).to.be(true); - expect(response.body.buckets.length).to.be(0); - }); - } - ); - - registry.when( - 'Transaction groups distribution when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - let response: any; - before(async () => { - response = await supertest.get(url); - }); - - it('returns the correct metadata', () => { - expect(response.status).to.be(200); - expect(response.body.noHits).to.be(false); - expect(response.body.buckets.length).to.be.greaterThan(0); - }); - - it('returns groups with some hits', () => { - expect(response.body.buckets.some((bucket: any) => bucket.count > 0)).to.be(true); - }); - - it('returns groups with some samples', () => { - expect(response.body.buckets.some((bucket: any) => !isEmpty(bucket.samples))).to.be(true); - }); - - it('returns the correct number of buckets', () => { - expectSnapshot(response.body.buckets.length).toMatchInline(`26`); - }); - - it('returns the correct bucket size', () => { - expectSnapshot(response.body.bucketSize).toMatchInline(`1000`); - }); - - it('returns the correct buckets', () => { - const bucketWithSamples = response.body.buckets.find( - (bucket: any) => !isEmpty(bucket.samples) - ); - - expectSnapshot(bucketWithSamples.count).toMatchInline(`1`); - - expectSnapshot(bucketWithSamples.samples.sort((sample: any) => sample.traceId)) - .toMatchInline(` - Array [ - Object { - "traceId": "6d85d8f1bc4bbbfdb19cdba59d2fc164", - "transactionId": "d0a16f0f52f25d6b", - }, - ] - `); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts b/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts new file mode 100644 index 0000000000000..73b1bbfd781d0 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts @@ -0,0 +1,133 @@ +/* + * 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 expect from '@kbn/expect'; +import qs from 'querystring'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + const url = `/api/apm/services/opbeans-java/transactions/traces/samples?${qs.stringify({ + environment: 'ENVIRONMENT_ALL', + kuery: '', + start: metadata.start, + end: metadata.end, + transactionName: 'APIRestController#stats', + transactionType: 'request', + })}`; + + registry.when( + 'Transaction trace samples response structure when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + + expect(response.body.noHits).to.be(true); + expect(response.body.traceSamples.length).to.be(0); + }); + } + ); + + registry.when( + 'Transaction trace samples response structure when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + let response: any; + before(async () => { + response = await supertest.get(url); + }); + + it('returns the correct metadata', () => { + expect(response.status).to.be(200); + expect(response.body.noHits).to.be(false); + expect(response.body.traceSamples.length).to.be.greaterThan(0); + }); + + it('returns the correct number of samples', () => { + expectSnapshot(response.body.traceSamples.length).toMatchInline(`15`); + }); + + it('returns the correct samples', () => { + const { traceSamples } = response.body; + + expectSnapshot(traceSamples.sort((sample: any) => sample.traceId)).toMatchInline(` + Array [ + Object { + "traceId": "5267685738bf75b68b16bf3426ba858c", + "transactionId": "5223f43bc3154c5a", + }, + Object { + "traceId": "9a84d15e5a0e32098d569948474e8e2f", + "transactionId": "b85db78a9824107b", + }, + Object { + "traceId": "e123f0466fa092f345d047399db65aa2", + "transactionId": "c0af16286229d811", + }, + Object { + "traceId": "4943691f87b7eb97d442d1ef33ca65c7", + "transactionId": "f6f4677d731e57c5", + }, + Object { + "traceId": "66bd97c457f5675665397ac9201cc050", + "transactionId": "592b60cc9ddabb15", + }, + Object { + "traceId": "10d882b7118870015815a27c37892375", + "transactionId": "0cf9db0b1e321239", + }, + Object { + "traceId": "6d85d8f1bc4bbbfdb19cdba59d2fc164", + "transactionId": "d0a16f0f52f25d6b", + }, + Object { + "traceId": "0996b09e42ad4dbfaaa6a069326c6e66", + "transactionId": "5721364b179716d0", + }, + Object { + "traceId": "d9415d102c0634e1e8fa53ceef07be70", + "transactionId": "fab91c68c9b1c42b", + }, + Object { + "traceId": "ca7a2072e7974ae84b5096706c6b6255", + "transactionId": "92ab7f2ef11685dd", + }, + Object { + "traceId": "d250e2a1bad40f78653d8858db65326b", + "transactionId": "6fcd12599c1b57fa", + }, + Object { + "traceId": "2ca82e99453c58584c4b8de9a8ba4ec3", + "transactionId": "8fa2ca73976ce1e7", + }, + Object { + "traceId": "45b3d1a86003938687a55e49bf3610b8", + "transactionId": "a707456bda99ee98", + }, + Object { + "traceId": "7483bd52150d1c93a858c60bfdd0c138", + "transactionId": "e20e701ff93bdb55", + }, + Object { + "traceId": "a21ea39b41349a4614a86321d965c957", + "transactionId": "338bd7908cbf7f2d", + }, + ] + `); + }); + } + ); +} diff --git a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts index 616402098acec..c2b24e87266af 100644 --- a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts +++ b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts @@ -17,7 +17,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); - const testData = { serviceName: 'opbeans-go' }; + const testData = { + latencyCorrelationsTab: 'Latency correlations', + logLogChartTitle: 'Latency distribution', + serviceName: 'opbeans-go', + transactionsTab: 'Transactions', + transaction: 'GET /api/stats', + }; describe('latency correlations', () => { describe('space with no features disabled', () => { @@ -90,23 +96,48 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const apmMainTemplateHeaderServiceName = await testSubjects.getVisibleTextAll( 'apmMainTemplateHeaderServiceName' ); - expect(apmMainTemplateHeaderServiceName).to.contain('opbeans-go'); + expect(apmMainTemplateHeaderServiceName).to.contain(testData.serviceName); }); }); - it('shows the correlations flyout', async function () { - await testSubjects.click('apmViewCorrelationsButton'); + it('navigates to the transactions tab', async function () { + await find.clickByDisplayedLinkText(testData.transactionsTab); await retry.try(async () => { - await testSubjects.existOrFail('apmCorrelationsFlyout', { - timeout: 10000, - }); + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + + expect(apmMainContainerTextItems).to.contain(testData.transaction); + }); + }); + + it(`navigates to the 'GET /api/stats' transactions`, async function () { + await find.clickByDisplayedLinkText(testData.transaction); + + await retry.try(async () => { + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + + expect(apmMainContainerTextItems).to.contain(testData.transaction); + expect(apmMainContainerTextItems).to.contain(testData.latencyCorrelationsTab); - const apmCorrelationsFlyoutHeader = await testSubjects.getVisibleText( - 'apmCorrelationsFlyoutHeader' + // The default tab 'Trace samples' should show the log log chart without the correlations analysis part. + // First assert that the log log chart and its header are present + const apmCorrelationsLatencyCorrelationsChartTitle = await testSubjects.getVisibleText( + 'apmCorrelationsLatencyCorrelationsChartTitle' ); + expect(apmCorrelationsLatencyCorrelationsChartTitle).to.be(testData.logLogChartTitle); + await testSubjects.existOrFail('apmCorrelationsChart'); + // Then assert that the correlation analysis part is not present + await testSubjects.missingOrFail('apmCorrelationsLatencyCorrelationsTablePanelTitle'); + }); + }); - expect(apmCorrelationsFlyoutHeader).to.contain('Correlations BETA'); + it('shows the correlations tab', async function () { + await testSubjects.click('apmLatencyCorrelationsTabButton'); + + await retry.try(async () => { + await testSubjects.existOrFail('apmCorrelationsTabContent'); }); }); @@ -122,12 +153,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const apmCorrelationsLatencyCorrelationsChartTitle = await testSubjects.getVisibleText( 'apmCorrelationsLatencyCorrelationsChartTitle' ); - expect(apmCorrelationsLatencyCorrelationsChartTitle).to.be( - `Latency distribution for ${testData.serviceName} (Log-Log Plot)` - ); - await testSubjects.existOrFail('apmCorrelationsChart', { - timeout: 10000, - }); + expect(apmCorrelationsLatencyCorrelationsChartTitle).to.be(testData.logLogChartTitle); + await testSubjects.existOrFail('apmCorrelationsChart'); + await testSubjects.existOrFail('apmCorrelationsLatencyCorrelationsTablePanelTitle'); // Assert that results for the given service didn't find any correlations const apmCorrelationsTable = await testSubjects.getVisibleText('apmCorrelationsTable');