[APM] Add throughput, error rate charts to backend detail page (#107379)

This commit is contained in:
Dario Gieselaar 2021-08-03 12:36:20 +02:00 committed by GitHub
parent 66b951c07c
commit cee5bc6f41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 586 additions and 16 deletions

View file

@ -46,11 +46,11 @@ export function BackendDetailDependenciesTable() {
path: {
backendName,
},
query: { start, end, environment, numBuckets: 20, offset },
query: { start, end, environment, numBuckets: 20, offset, kuery },
},
});
},
[start, end, environment, offset, backendName]
[start, end, environment, offset, backendName, kuery]
);
const dependencies =

View file

@ -0,0 +1,100 @@
/*
* 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 } from 'react';
import { i18n } from '@kbn/i18n';
import { asPercent } from '../../../../common/utils/formatters';
import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useComparison } from '../../../hooks/use_comparison';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { Coordinate, TimeSeries } from '../../../../typings/timeseries';
import { TimeseriesChart } from '../../shared/charts/timeseries_chart';
import { useTheme } from '../../../hooks/use_theme';
function yLabelFormat(y?: number | null) {
return asPercent(y || 0, 1);
}
export function BackendErrorRateChart({ height }: { height: number }) {
const { backendName } = useApmBackendContext();
const theme = useTheme();
const { start, end } = useTimeRange();
const {
urlParams: { kuery, environment },
} = useUrlParams();
const { offset, comparisonChartTheme } = useComparison();
const { data, status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
return;
}
return callApmApi({
endpoint: 'GET /api/apm/backends/{backendName}/charts/error_rate',
params: {
path: {
backendName,
},
query: {
start,
end,
offset,
kuery,
environment,
},
},
});
},
[backendName, start, end, offset, kuery, environment]
);
const timeseries = useMemo(() => {
const specs: Array<TimeSeries<Coordinate>> = [];
if (data?.currentTimeseries) {
specs.push({
data: data.currentTimeseries,
type: 'linemark',
color: theme.eui.euiColorVis7,
title: i18n.translate('xpack.apm.backendErrorRateChart.chartTitle', {
defaultMessage: 'Error rate',
}),
});
}
if (data?.comparisonTimeseries) {
specs.push({
data: data.comparisonTimeseries,
type: 'area',
color: theme.eui.euiColorMediumShade,
title: i18n.translate(
'xpack.apm.backendErrorRateChart.previousPeriodLabel',
{ defaultMessage: 'Previous period' }
),
});
}
return specs;
}, [data, theme.eui.euiColorVis7, theme.eui.euiColorMediumShade]);
return (
<TimeseriesChart
height={height}
fetchStatus={status}
id="errorRateChart"
customTheme={comparisonChartTheme}
timeseries={timeseries}
yLabelFormat={yLabelFormat}
/>
);
}

View file

@ -65,7 +65,7 @@ export function BackendLatencyChart({ height }: { height: number }) {
specs.push({
data: data.currentTimeseries,
type: 'linemark',
color: theme.eui.euiColorVis0,
color: theme.eui.euiColorVis1,
title: i18n.translate('xpack.apm.backendLatencyChart.chartTitle', {
defaultMessage: 'Latency',
}),
@ -85,7 +85,7 @@ export function BackendLatencyChart({ height }: { height: number }) {
}
return specs;
}, [data, theme.eui.euiColorVis0, theme.eui.euiColorMediumShade]);
}, [data, theme.eui.euiColorVis1, theme.eui.euiColorMediumShade]);
const maxY = getMaxY(timeseries);
const latencyFormatter = getDurationFormatter(maxY);

View file

@ -0,0 +1,96 @@
/*
* 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 } from 'react';
import { i18n } from '@kbn/i18n';
import { asTransactionRate } from '../../../../common/utils/formatters';
import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useComparison } from '../../../hooks/use_comparison';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { Coordinate, TimeSeries } from '../../../../typings/timeseries';
import { TimeseriesChart } from '../../shared/charts/timeseries_chart';
import { useTheme } from '../../../hooks/use_theme';
export function BackendThroughputChart({ height }: { height: number }) {
const { backendName } = useApmBackendContext();
const theme = useTheme();
const { start, end } = useTimeRange();
const {
urlParams: { kuery, environment },
} = useUrlParams();
const { offset, comparisonChartTheme } = useComparison();
const { data, status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
return;
}
return callApmApi({
endpoint: 'GET /api/apm/backends/{backendName}/charts/throughput',
params: {
path: {
backendName,
},
query: {
start,
end,
offset,
kuery,
environment,
},
},
});
},
[backendName, start, end, offset, kuery, environment]
);
const timeseries = useMemo(() => {
const specs: Array<TimeSeries<Coordinate>> = [];
if (data?.currentTimeseries) {
specs.push({
data: data.currentTimeseries,
type: 'linemark',
color: theme.eui.euiColorVis0,
title: i18n.translate('xpack.apm.backendThroughputChart.chartTitle', {
defaultMessage: 'Throughput',
}),
});
}
if (data?.comparisonTimeseries) {
specs.push({
data: data.comparisonTimeseries,
type: 'area',
color: theme.eui.euiColorMediumShade,
title: i18n.translate(
'xpack.apm.backendThroughputChart.previousPeriodLabel',
{ defaultMessage: 'Previous period' }
),
});
}
return specs;
}, [data, theme.eui.euiColorVis0, theme.eui.euiColorMediumShade]);
return (
<TimeseriesChart
height={height}
fetchStatus={status}
id="throughputChart"
customTheme={comparisonChartTheme}
timeseries={timeseries}
yLabelFormat={asTransactionRate}
/>
);
}

View file

@ -9,16 +9,20 @@ import { EuiPanel } from '@elastic/eui';
import { EuiFlexGroup } from '@elastic/eui';
import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ApmBackendContextProvider } from '../../../context/apm_backend/apm_backend_context';
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { ApmMainTemplate } from '../../routing/templates/apm_main_template';
import { SearchBar } from '../../shared/search_bar';
import { BackendLatencyChart } from './backend_latency_chart';
import { BackendInventoryTitle } from '../../routing/home';
import { BackendDetailDependenciesTable } from './backend_detail_dependencies_table';
import { BackendThroughputChart } from './backend_throughput_chart';
import { BackendErrorRateChart } from './backend_error_rate_chart';
import { BackendDetailTemplate } from '../../routing/templates/backend_detail_template';
export function BackendDetailOverview() {
const {
@ -43,21 +47,55 @@ export function BackendDetailOverview() {
]);
return (
<ApmMainTemplate pageTitle={backendName}>
<ApmBackendContextProvider>
<ApmBackendContextProvider>
<BackendDetailTemplate title={backendName}>
<SearchBar showTimeComparison />
<ChartPointerEventContextProvider>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.backendDetailLatencyChartTitle',
{ defaultMessage: 'Latency' }
)}
</h2>
</EuiTitle>
<BackendLatencyChart height={200} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.backendDetailThroughputChartTitle',
{ defaultMessage: 'Throughput' }
)}
</h2>
</EuiTitle>
<BackendThroughputChart height={200} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.backendDetailErrorRateChartTitle',
{ defaultMessage: 'Error rate' }
)}
</h2>
</EuiTitle>
<BackendErrorRateChart height={200} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</ChartPointerEventContextProvider>
<EuiSpacer size="m" />
<BackendDetailDependenciesTable />
</ApmBackendContextProvider>
</ApmMainTemplate>
</BackendDetailTemplate>
</ApmBackendContextProvider>
);
}

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import React from 'react';
import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context';
import { ApmMainTemplate } from './apm_main_template';
import { SpanIcon } from '../../shared/span_icon';
interface Props {
title: string;
children: React.ReactNode;
}
export function BackendDetailTemplate({ title, children }: Props) {
const {
backendName,
metadata: { data },
} = useApmBackendContext();
const metadata = data?.metadata;
return (
<ApmMainTemplate
pageHeader={{
pageTitle: (
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h1>{backendName}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SpanIcon
type={metadata?.spanType}
subtype={metadata?.spanSubtype}
/>
</EuiFlexItem>
</EuiFlexGroup>
),
}}
>
{children}
</ApmMainTemplate>
);
}

View file

@ -0,0 +1,98 @@
/*
* 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 { EventOutcome } from '../../../common/event_outcome';
import {
EVENT_OUTCOME,
SPAN_DESTINATION_SERVICE_RESOURCE,
} from '../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../common/utils/environment_query';
import { kqlQuery, rangeQuery } from '../../../../observability/server';
import { ProcessorEvent } from '../../../common/processor_event';
import { Setup } from '../helpers/setup_request';
import { getMetricsDateHistogramParams } from '../helpers/metrics';
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
export async function getErrorRateChartsForBackend({
backendName,
setup,
start,
end,
environment,
kuery,
offset,
}: {
backendName: string;
setup: Setup;
start: number;
end: number;
environment?: string;
kuery?: string;
offset?: string;
}) {
const { apmEventClient } = setup;
const { offsetInMs, startWithOffset, endWithOffset } = getOffsetInMs({
start,
end,
offset,
});
const response = await apmEventClient.search('get_error_rate_for_backend', {
apm: {
events: [ProcessorEvent.metric],
},
body: {
size: 0,
query: {
bool: {
filter: [
...environmentQuery(environment),
...kqlQuery(kuery),
...rangeQuery(startWithOffset, endWithOffset),
{ term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } },
{
terms: {
[EVENT_OUTCOME]: [EventOutcome.success, EventOutcome.failure],
},
},
],
},
},
aggs: {
timeseries: {
date_histogram: getMetricsDateHistogramParams({
start: startWithOffset,
end: endWithOffset,
metricsInterval: 60,
}),
aggs: {
failures: {
filter: {
term: {
[EVENT_OUTCOME]: EventOutcome.failure,
},
},
},
},
},
},
},
});
return (
response.aggregations?.timeseries.buckets.map((bucket) => {
const totalCount = bucket.doc_count;
const failureCount = bucket.failures.doc_count;
return {
x: bucket.key + offsetInMs,
y: failureCount / totalCount,
};
}) ?? []
);
}

View file

@ -51,7 +51,7 @@ export async function getMetadataForBackend({
const sample = maybe(sampleResponse.hits.hits[0])?._source;
return {
'span.type': sample?.span.type,
'span.subtype': sample?.span.subtype,
spanType: sample?.span.type,
spanSubtype: sample?.span.subtype,
};
}

View file

@ -0,0 +1,82 @@
/*
* 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 { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../common/utils/environment_query';
import { kqlQuery, rangeQuery } from '../../../../observability/server';
import { ProcessorEvent } from '../../../common/processor_event';
import { Setup } from '../helpers/setup_request';
import { getMetricsDateHistogramParams } from '../helpers/metrics';
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
export async function getThroughputChartsForBackend({
backendName,
setup,
start,
end,
environment,
kuery,
offset,
}: {
backendName: string;
setup: Setup;
start: number;
end: number;
environment?: string;
kuery?: string;
offset?: string;
}) {
const { apmEventClient } = setup;
const { offsetInMs, startWithOffset, endWithOffset } = getOffsetInMs({
start,
end,
offset,
});
const response = await apmEventClient.search('get_throughput_for_backend', {
apm: {
events: [ProcessorEvent.metric],
},
body: {
size: 0,
query: {
bool: {
filter: [
...environmentQuery(environment),
...kqlQuery(kuery),
...rangeQuery(startWithOffset, endWithOffset),
{ term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } },
],
},
},
aggs: {
timeseries: {
date_histogram: getMetricsDateHistogramParams({
start: startWithOffset,
end: endWithOffset,
metricsInterval: 60,
}),
aggs: {
throughput: {
rate: {},
},
},
},
},
},
});
return (
response.aggregations?.timeseries.buckets.map((bucket) => {
return {
x: bucket.key + offsetInMs,
y: bucket.throughput.value,
};
}) ?? []
);
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { kqlQuery } from '../../../../observability/server';
import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../common/utils/environment_query';
import { getConnectionStats } from '../connections/get_connection_stats';
@ -17,6 +18,7 @@ export async function getUpstreamServicesForBackend({
end,
backendName,
numBuckets,
kuery,
environment,
offset,
}: {
@ -25,6 +27,7 @@ export async function getUpstreamServicesForBackend({
end: number;
backendName: string;
numBuckets: number;
kuery?: string;
environment?: string;
offset?: string;
}) {
@ -35,6 +38,7 @@ export async function getUpstreamServicesForBackend({
filter: [
{ term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } },
...environmentQuery(environment),
...kqlQuery(kuery),
],
collapseBy: 'upstream',
numBuckets,

View file

@ -15,6 +15,8 @@ import { getMetadataForBackend } from '../lib/backends/get_metadata_for_backend'
import { getLatencyChartsForBackend } from '../lib/backends/get_latency_charts_for_backend';
import { getTopBackends } from '../lib/backends/get_top_backends';
import { getUpstreamServicesForBackend } from '../lib/backends/get_upstream_services_for_backend';
import { getThroughputChartsForBackend } from '../lib/backends/get_throughput_charts_for_backend';
import { getErrorRateChartsForBackend } from '../lib/backends/get_error_rate_charts_for_backend';
const topBackendsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/backends/top_backends',
@ -68,7 +70,7 @@ const upstreamServicesForBackendRoute = createApmServerRoute({
query: t.intersection([rangeRt, t.type({ numBuckets: toNumberRt })]),
}),
t.partial({
query: t.intersection([environmentRt, offsetRt]),
query: t.intersection([environmentRt, offsetRt, kueryRt]),
}),
]),
options: {
@ -80,10 +82,18 @@ const upstreamServicesForBackendRoute = createApmServerRoute({
const { start, end } = setup;
const {
path: { backendName },
query: { environment, offset, numBuckets },
query: { environment, offset, numBuckets, kuery },
} = resources.params;
const opts = { backendName, setup, start, end, numBuckets, environment };
const opts = {
backendName,
setup,
start,
end,
numBuckets,
environment,
kuery,
};
const [currentServices, previousServices] = await Promise.all([
getUpstreamServicesForBackend(opts),
@ -182,8 +192,100 @@ const backendLatencyChartsRoute = createApmServerRoute({
},
});
const backendThroughputChartsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/backends/{backendName}/charts/throughput',
params: t.type({
path: t.type({
backendName: t.string,
}),
query: t.intersection([rangeRt, kueryRt, environmentRt, offsetRt]),
}),
options: {
tags: ['access:apm'],
},
handler: async (resources) => {
const setup = await setupRequest(resources);
const { params } = resources;
const { backendName } = params.path;
const { kuery, environment, offset } = params.query;
const { start, end } = setup;
const [currentTimeseries, comparisonTimeseries] = await Promise.all([
getThroughputChartsForBackend({
backendName,
setup,
start,
end,
kuery,
environment,
}),
offset
? getThroughputChartsForBackend({
backendName,
setup,
start,
end,
kuery,
environment,
offset,
})
: null,
]);
return { currentTimeseries, comparisonTimeseries };
},
});
const backendErrorRateChartsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/backends/{backendName}/charts/error_rate',
params: t.type({
path: t.type({
backendName: t.string,
}),
query: t.intersection([rangeRt, kueryRt, environmentRt, offsetRt]),
}),
options: {
tags: ['access:apm'],
},
handler: async (resources) => {
const setup = await setupRequest(resources);
const { params } = resources;
const { backendName } = params.path;
const { kuery, environment, offset } = params.query;
const { start, end } = setup;
const [currentTimeseries, comparisonTimeseries] = await Promise.all([
getErrorRateChartsForBackend({
backendName,
setup,
start,
end,
kuery,
environment,
}),
offset
? getErrorRateChartsForBackend({
backendName,
setup,
start,
end,
kuery,
environment,
offset,
})
: null,
]);
return { currentTimeseries, comparisonTimeseries };
},
});
export const backendsRouteRepository = createApmServerRouteRepository()
.add(topBackendsRoute)
.add(upstreamServicesForBackendRoute)
.add(backendMetadataRoute)
.add(backendLatencyChartsRoute);
.add(backendLatencyChartsRoute)
.add(backendThroughputChartsRoute)
.add(backendErrorRateChartsRoute);