From d321f85ee92c8f25e89c99101e13c4246df77718 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 1 Feb 2021 19:54:57 -0500 Subject: [PATCH] [APM] Correlations GA (#86477) --- ...tTermsTable.tsx => correlations_table.tsx} | 57 +++- ...orrelations.tsx => error_correlations.tsx} | 55 +++- .../components/app/Correlations/index.tsx | 95 +++++- ...relations.tsx => latency_correlations.tsx} | 74 +++-- .../Correlations/throughput_correlations.tsx | 288 ++++++++++++++++++ .../components/app/trace_overview/index.tsx | 2 +- .../app/transaction_details/index.tsx | 2 +- .../app/transaction_overview/index.tsx | 2 +- 8 files changed, 514 insertions(+), 61 deletions(-) rename x-pack/plugins/apm/public/components/app/Correlations/{SignificantTermsTable.tsx => correlations_table.tsx} (69%) rename x-pack/plugins/apm/public/components/app/Correlations/{ErrorCorrelations.tsx => error_correlations.tsx} (77%) rename x-pack/plugins/apm/public/components/app/Correlations/{LatencyCorrelations.tsx => latency_correlations.tsx} (75%) create mode 100644 x-pack/plugins/apm/public/components/app/Correlations/throughput_correlations.tsx diff --git a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx b/x-pack/plugins/apm/public/components/app/Correlations/correlations_table.tsx similarity index 69% rename from x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx rename to x-pack/plugins/apm/public/components/app/Correlations/correlations_table.tsx index f7580cc65c543..57d949d82c0fe 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/correlations_table.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { EuiIcon, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useHistory } from 'react-router-dom'; import { EuiBasicTable } from '@elastic/eui'; import { EuiBasicTableColumn } from '@elastic/eui'; @@ -31,7 +32,7 @@ interface Props { setSelectedSignificantTerm: (term: T | null) => void; } -export function SignificantTermsTable({ +export function CorrelationsTable({ significantTerms, status, cardinalityColumnName, @@ -42,7 +43,10 @@ export function SignificantTermsTable({ { width: '100px', field: 'score', - name: 'Score', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.scoreLabel', + { defaultMessage: 'Score' } + ), render: (_: any, term: T) => { return {Math.round(term.score)}; }, @@ -57,19 +61,31 @@ export function SignificantTermsTable({ }, { field: 'fieldName', - name: 'Field name', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.fieldNameLabel', + { defaultMessage: 'Field name' } + ), }, { field: 'fieldValue', - name: 'Field value', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.fieldValueLabel', + { defaultMessage: 'Field value' } + ), render: (_: any, term: T) => String(term.fieldValue).slice(0, 50), }, { width: '100px', actions: [ { - name: 'Focus', - description: 'Focus on this term', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.filterLabel', + { defaultMessage: 'Filter' } + ), + description: i18n.translate( + 'xpack.apm.correlations.correlationsTable.filterDescription', + { defaultMessage: 'Filter by value' } + ), icon: 'magnifyWithPlus', type: 'icon', onClick: (term: T) => { @@ -83,8 +99,14 @@ export function SignificantTermsTable({ }, }, { - name: 'Exclude', - description: 'Exclude this term', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.excludeLabel', + { defaultMessage: 'Exclude' } + ), + description: i18n.translate( + 'xpack.apm.correlations.correlationsTable.excludeDescription', + { defaultMessage: 'Filter out value' } + ), icon: 'magnifyWithMinus', type: 'icon', onClick: (term: T) => { @@ -98,7 +120,10 @@ export function SignificantTermsTable({ }, }, ], - name: 'Actions', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.actionsLabel', + { defaultMessage: 'Actions' } + ), render: (_: any, term: T) => { return ( <> @@ -134,7 +159,9 @@ export function SignificantTermsTable({ return ( { @@ -146,3 +173,13 @@ export function SignificantTermsTable({ /> ); } + +const loadingText = i18n.translate( + 'xpack.apm.correlations.correlationsTable.loadingText', + { defaultMessage: 'Loading' } +); + +const noDataText = i18n.translate( + 'xpack.apm.correlations.correlationsTable.noDataText', + { defaultMessage: 'No data' } +); diff --git a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/error_correlations.tsx similarity index 77% rename from x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx rename to x-pack/plugins/apm/public/components/app/Correlations/error_correlations.tsx index 533373d7e8778..4ec2564d61cde 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/error_correlations.tsx @@ -24,12 +24,14 @@ import { EuiComboBox, EuiAccordion, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { px } from '../../../style/variables'; -import { SignificantTermsTable } from './SignificantTermsTable'; +import { CorrelationsTable } from './correlations_table'; import { ChartContainer } from '../../shared/charts/chart_container'; +import { useTheme } from '../../../hooks/use_theme'; type CorrelationsApiResponse = NonNullable< APIReturnType<'GET /api/apm/correlations/failed_transactions'> @@ -98,7 +100,11 @@ export function ErrorCorrelations() { -

Error rate over time

+

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

@@ -109,10 +115,30 @@ export function ErrorCorrelations() { /> - + + + + @@ -121,14 +147,6 @@ export function ErrorCorrelations() { /> - - -
); @@ -143,6 +161,7 @@ function ErrorTimeseriesChart({ selectedSignificantTerm: SignificantTerm | null; status: FETCH_STATUS; }) { + const theme = useTheme(); const dateFormatter = timeFormatter('HH:mm:ss'); return ( @@ -164,7 +183,10 @@ function ErrorTimeseriesChart({ /> diff --git a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx index c5b2f265fac8c..85e4ab84aec08 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx @@ -17,24 +17,55 @@ import { EuiLink, EuiCallOut, EuiButton, + EuiTab, + EuiTabs, + EuiSpacer, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useHistory } from 'react-router-dom'; -import { EuiSpacer } from '@elastic/eui'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { enableCorrelations } from '../../../../common/ui_settings_keys'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { LatencyCorrelations } from './LatencyCorrelations'; -import { ErrorCorrelations } from './ErrorCorrelations'; +import { LatencyCorrelations } from './latency_correlations'; +import { ErrorCorrelations } from './error_correlations'; +import { ThroughputCorrelations } from './throughput_correlations'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { createHref } from '../../shared/Links/url_helpers'; import { useLicenseContext } from '../../../context/license/use_license_context'; +const latencyTab = { + key: 'latency', + label: i18n.translate('xpack.apm.correlations.tabs.latencyLabel', { + defaultMessage: 'Latency', + }), + component: LatencyCorrelations, +}; +const throughputTab = { + key: 'throughput', + label: i18n.translate('xpack.apm.correlations.tabs.throughputLabel', { + defaultMessage: 'Throughput', + }), + component: ThroughputCorrelations, +}; +const errorRateTab = { + key: 'errorRate', + label: i18n.translate('xpack.apm.correlations.tabs.errorRateLabel', { + defaultMessage: 'Error rate', + }), + component: ErrorCorrelations, +}; +const tabs = [latencyTab, throughputTab, errorRateTab]; + export function Correlations() { const { uiSettings } = useApmPluginContext().core; const { urlParams } = useUrlParams(); const license = useLicenseContext(); const history = useHistory(); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [currentTab, setCurrentTab] = useState(latencyTab.key); + const { component: TabContent } = + tabs.find((tab) => tab.key === currentTab) ?? latencyTab; + if ( !uiSettings.get(enableCorrelations) || !isActivePlatinumLicense(license) @@ -48,8 +79,11 @@ export function Correlations() { onClick={() => { setIsFlyoutVisible(true); }} + iconType="visTagCloud" > - View correlations + {i18n.translate('xpack.apm.correlations.buttonLabel', { + defaultMessage: 'Explore correlations', + })} @@ -63,19 +97,33 @@ export function Correlations() { > -

Correlations

+

+ {i18n.translate('xpack.apm.correlations.title', { + defaultMessage: 'Correlations', + })} +

{urlParams.kuery ? ( <> - Filtering by + + {i18n.translate( + 'xpack.apm.correlations.filteringByLabel', + { defaultMessage: 'Filtering by' } + )} + {urlParams.kuery} - Clear + + {i18n.translate( + 'xpack.apm.correlations.clearFiltersLabel', + { defaultMessage: 'Clear' } + )} + @@ -84,21 +132,40 @@ export function Correlations() {

- Correlations is an experimental feature and in active - development. Bugs and surprises are to be expected but let us - know your feedback so we can improve it. + {i18n.translate( + 'xpack.apm.correlations.experimentalWarning.text', + { + defaultMessage: + 'Correlations is an experimental feature and in active development. Bugs and surprises are to be expected but let us know your feedback so we can improve it.', + } + )}

- - - + + {tabs.map(({ key, label }) => ( + { + setCurrentTab(key); + }} + > + {label} + + ))} + + +
diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/latency_correlations.tsx similarity index 75% rename from x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx rename to x-pack/plugins/apm/public/components/app/Correlations/latency_correlations.tsx index 19f6248f56da6..d798ee66ea82f 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/latency_correlations.tsx @@ -24,12 +24,14 @@ import { EuiFormRow, EuiFieldNumber, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { getDurationFormatter } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { SignificantTermsTable } from './SignificantTermsTable'; +import { CorrelationsTable } from './correlations_table'; import { ChartContainer } from '../../shared/charts/chart_container'; +import { useTheme } from '../../../hooks/use_theme'; type CorrelationsApiResponse = NonNullable< APIReturnType<'GET /api/apm/correlations/slow_transactions'> @@ -56,7 +58,6 @@ export function LatencyCorrelations() { selectedSignificantTerm, setSelectedSignificantTerm, ] = useState(null); - const [fieldNames, setFieldNames] = useState(initialFieldNames); const [durationPercentile, setDurationPercentile] = useState('50'); const { serviceName } = useParams<{ serviceName?: string }>(); @@ -102,7 +103,12 @@ export function LatencyCorrelations() { -

Latency distribution

+

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

- + + + + - + @@ -128,12 +156,21 @@ export function LatencyCorrelations() { { @@ -145,14 +182,6 @@ export function LatencyCorrelations() { - - -
); @@ -181,6 +210,7 @@ function LatencyDistributionChart({ selectedSignificantTerm: SignificantTerm | null; status: FETCH_STATUS; }) { + const theme = useTheme(); const xMax = Math.max( ...(data?.overall?.distribution.map((p) => p.x ?? 0) ?? []) ); @@ -218,7 +248,10 @@ function LatencyDistributionChart({ /> `${roundFloat(d)}%`} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/throughput_correlations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/throughput_correlations.tsx new file mode 100644 index 0000000000000..b6dfbbd6fbb7c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Correlations/throughput_correlations.tsx @@ -0,0 +1,288 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ScaleType, + Chart, + Axis, + BarSeries, + Position, + Settings, +} from '@elastic/charts'; +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiComboBox, + EuiAccordion, + EuiFormRow, + EuiFieldNumber, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { getDurationFormatter } from '../../../../common/utils/formatters'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { + APIReturnType, + callApmApi, +} from '../../../services/rest/createCallApmApi'; +import { CorrelationsTable } from './correlations_table'; +import { ChartContainer } from '../../shared/charts/chart_container'; +import { useTheme } from '../../../hooks/use_theme'; + +type CorrelationsApiResponse = NonNullable< + APIReturnType<'GET /api/apm/correlations/slow_transactions'> +>; + +type SignificantTerm = NonNullable< + CorrelationsApiResponse['significantTerms'] +>[0]; + +const initialFieldNames = [ + 'user.username', + 'user.id', + 'host.ip', + 'user_agent.name', + 'kubernetes.pod.uuid', + 'kubernetes.pod.name', + 'url.domain', + 'container.id', + 'service.node.name', +].map((label) => ({ label })); + +export function ThroughputCorrelations() { + const [ + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); + + const [fieldNames, setFieldNames] = useState(initialFieldNames); + const [durationPercentile, setDurationPercentile] = useState('50'); + const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams, uiFilters } = useUrlParams(); + const { transactionName, transactionType, start, end } = urlParams; + + const { data, status } = useFetcher(() => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/correlations/slow_transactions', + params: { + query: { + serviceName, + transactionName, + transactionType, + start, + end, + uiFilters: JSON.stringify(uiFilters), + durationPercentile, + fieldNames: fieldNames.map((field) => field.label).join(','), + }, + }, + }); + } + }, [ + serviceName, + start, + end, + transactionName, + transactionType, + uiFilters, + durationPercentile, + fieldNames, + ]); + + return ( + <> + + + + + +

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

+
+ +
+
+
+ + + + + + + + + + setDurationPercentile(e.currentTarget.value) + } + /> + + + + + { + setFieldNames((names) => [...names, { label: term }]); + }} + /> + + + + + +
+ + ); +} + +function getDistributionYMax(data?: CorrelationsApiResponse) { + if (!data?.overall) { + return 0; + } + + const yValues = [ + ...data.overall.distribution.map((p) => p.y ?? 0), + ...data.significantTerms.flatMap((term) => + term.distribution.map((p) => p.y ?? 0) + ), + ]; + return Math.max(...yValues); +} + +function ThroughputDistributionChart({ + data, + selectedSignificantTerm, + status, +}: { + data?: CorrelationsApiResponse; + selectedSignificantTerm: SignificantTerm | null; + status: FETCH_STATUS; +}) { + const theme = useTheme(); + const xMax = Math.max( + ...(data?.overall?.distribution.map((p) => p.x ?? 0) ?? []) + ); + const durationFormatter = getDurationFormatter(xMax); + const yMax = getDistributionYMax(data); + + return ( + + + { + const start = durationFormatter(obj.value); + const end = durationFormatter( + obj.value + data?.distributionInterval + ); + + return `${start.value} - ${end.formatted}`; + }, + }} + /> + durationFormatter(d).formatted} + /> + `${d}%`} + domain={{ min: 0, max: yMax }} + /> + + `${roundFloat(d)}%`} + /> + + {selectedSignificantTerm !== null ? ( + `${roundFloat(d)}%`} + /> + ) : null} + + + ); +} + +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/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index d29dad7a7e3de..954df99e2b4ef 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -12,8 +12,8 @@ import { useUrlParams } from '../../../context/url_params_context/use_url_params import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { SearchBar } from '../../shared/search_bar'; -import { Correlations } from '../Correlations'; import { TraceList } from './TraceList'; +import { Correlations } from '../correlations'; type TracesAPIResponse = APIReturnType<'GET /api/apm/traces'>; const DEFAULT_RESPONSE: TracesAPIResponse = { 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 d5f5eed311de8..b421837b80437 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 @@ -26,8 +26,8 @@ import { ApmHeader } from '../../shared/ApmHeader'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { HeightRetainer } from '../../shared/HeightRetainer'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; +import { Correlations } from '../correlations'; import { SearchBar } from '../../shared/search_bar'; -import { Correlations } from '../Correlations'; import { TransactionDistribution } from './Distribution'; import { useWaterfallFetcher } from './use_waterfall_fetcher'; import { WaterfallWithSummmary } from './WaterfallWithSummmary'; 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 1f8b431d072b7..6ab1f0ada4e73 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 @@ -30,7 +30,7 @@ import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { SearchBar } from '../../shared/search_bar'; import { TransactionTypeSelect } from '../../shared/transaction_type_select'; -import { Correlations } from '../Correlations'; +import { Correlations } from '../correlations'; import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; import { UserExperienceCallout } from './user_experience_callout';