mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Fix preview chart in APM error rate and error count rules (#159544)
Resolves the following for APM Failed transaction rate and APM Error count rules: - https://github.com/elastic/kibana/issues/152218 - https://github.com/elastic/kibana/issues/155489 Improvements in the preview chart: - Showing data grouped by selected group by fields - Showing 5 groups from each bucket - Showing last x units (minutes, hours, etc.) data - Added loading indicator - Showing tooltip - Always showing legend - Max Y is slightly higher than maximum of highest y value or threshold - Adjusting chart to always show the threshold line - Fixed - When removing some fields like transaction type, transaction name, chart showed no data - Hiding a particular group (by clicking on legend item), readjusted max Y value, but same was not reset to original when enabling the group back
This commit is contained in:
parent
cfa46e473a
commit
6dbf5483cf
18 changed files with 1318 additions and 383 deletions
37
x-pack/plugins/apm/common/rules/get_all_groupby_fields.ts
Normal file
37
x-pack/plugins/apm/common/rules/get_all_groupby_fields.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { union } from 'lodash';
|
||||
import { ApmRuleType } from './apm_rule_types';
|
||||
import {
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../es_fields/apm';
|
||||
|
||||
export const getAllGroupByFields = (
|
||||
ruleType: string,
|
||||
ruleParamsGroupByFields: string[] | undefined = []
|
||||
) => {
|
||||
let predefinedGroupByFields: string[] = [];
|
||||
|
||||
switch (ruleType) {
|
||||
case ApmRuleType.TransactionDuration:
|
||||
case ApmRuleType.TransactionErrorRate:
|
||||
predefinedGroupByFields = [
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT,
|
||||
TRANSACTION_TYPE,
|
||||
];
|
||||
break;
|
||||
case ApmRuleType.ErrorCount:
|
||||
predefinedGroupByFields = [SERVICE_NAME, SERVICE_ENVIRONMENT];
|
||||
break;
|
||||
}
|
||||
|
||||
return union(predefinedGroupByFields, ruleParamsGroupByFields);
|
||||
};
|
|
@ -18,7 +18,11 @@ import { EuiFormRow } from '@elastic/eui';
|
|||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
|
||||
import { asInteger } from '../../../../../common/utils/formatters';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import {
|
||||
FETCH_STATUS,
|
||||
isPending,
|
||||
useFetcher,
|
||||
} from '../../../../hooks/use_fetcher';
|
||||
import { createCallApmApi } from '../../../../services/rest/create_call_apm_api';
|
||||
import { ChartPreview } from '../../ui_components/chart_preview';
|
||||
import {
|
||||
|
@ -36,6 +40,11 @@ import {
|
|||
TRANSACTION_NAME,
|
||||
ERROR_GROUP_ID,
|
||||
} from '../../../../../common/es_fields/apm';
|
||||
import {
|
||||
ErrorState,
|
||||
LoadingState,
|
||||
NoDataState,
|
||||
} from '../../ui_components/chart_preview/chart_preview_helper';
|
||||
|
||||
export interface RuleParams {
|
||||
windowSize?: number;
|
||||
|
@ -72,13 +81,13 @@ export function ErrorCountRuleType(props: Props) {
|
|||
}
|
||||
);
|
||||
|
||||
const { data } = useFetcher(
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
const { interval, start, end } = getIntervalAndTimeRange({
|
||||
windowSize: params.windowSize,
|
||||
windowUnit: params.windowUnit,
|
||||
});
|
||||
if (interval && start && end) {
|
||||
if (params.windowSize && start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
{
|
||||
|
@ -90,6 +99,7 @@ export function ErrorCountRuleType(props: Props) {
|
|||
interval,
|
||||
start,
|
||||
end,
|
||||
groupBy: params.groupBy,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -102,6 +112,7 @@ export function ErrorCountRuleType(props: Props) {
|
|||
params.environment,
|
||||
params.serviceName,
|
||||
params.errorGroupingKey,
|
||||
params.groupBy,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -162,16 +173,28 @@ export function ErrorCountRuleType(props: Props) {
|
|||
/>,
|
||||
];
|
||||
|
||||
// hide preview chart until https://github.com/elastic/kibana/pull/156625 gets merged
|
||||
const showChartPreview = false;
|
||||
const chartPreview = showChartPreview ? (
|
||||
const errorCountChartPreview = data?.errorCountChartPreview;
|
||||
const series = errorCountChartPreview?.series ?? [];
|
||||
const hasData = series.length > 0;
|
||||
const totalGroups = errorCountChartPreview?.totalGroups ?? 0;
|
||||
|
||||
const chartPreview = isPending(status) ? (
|
||||
<LoadingState />
|
||||
) : !hasData ? (
|
||||
<NoDataState />
|
||||
) : status === FETCH_STATUS.SUCCESS ? (
|
||||
<ChartPreview
|
||||
series={[{ data: data?.errorCountChartPreview ?? [] }]}
|
||||
series={series}
|
||||
threshold={params.threshold}
|
||||
yTickFormat={asInteger}
|
||||
uiSettings={services.uiSettings}
|
||||
timeSize={params.windowSize}
|
||||
timeUnit={params.windowUnit}
|
||||
totalGroups={totalGroups}
|
||||
/>
|
||||
) : null;
|
||||
) : (
|
||||
<ErrorState />
|
||||
);
|
||||
|
||||
const groupAlertsBy = (
|
||||
<>
|
||||
|
|
|
@ -155,6 +155,7 @@ export function TransactionDurationRuleType(props: Props) {
|
|||
threshold={thresholdMs}
|
||||
yTickFormat={yTickFormat}
|
||||
uiSettings={services.uiSettings}
|
||||
totalGroups={0} // TODO: will be updated in https://github.com/elastic/kibana/pull/158439
|
||||
/>
|
||||
) : null;
|
||||
|
||||
|
|
|
@ -18,7 +18,11 @@ import { EuiFormRow } from '@elastic/eui';
|
|||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
|
||||
import { asPercent } from '../../../../../common/utils/formatters';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import {
|
||||
FETCH_STATUS,
|
||||
isPending,
|
||||
useFetcher,
|
||||
} from '../../../../hooks/use_fetcher';
|
||||
import { createCallApmApi } from '../../../../services/rest/create_call_apm_api';
|
||||
import { ChartPreview } from '../../ui_components/chart_preview';
|
||||
import {
|
||||
|
@ -37,6 +41,11 @@ import {
|
|||
TRANSACTION_TYPE,
|
||||
TRANSACTION_NAME,
|
||||
} from '../../../../../common/es_fields/apm';
|
||||
import {
|
||||
ErrorState,
|
||||
LoadingState,
|
||||
NoDataState,
|
||||
} from '../../ui_components/chart_preview/chart_preview_helper';
|
||||
|
||||
export interface RuleParams {
|
||||
windowSize?: number;
|
||||
|
@ -74,15 +83,13 @@ export function TransactionErrorRateRuleType(props: Props) {
|
|||
}
|
||||
);
|
||||
|
||||
const thresholdAsPercent = (params.threshold ?? 0) / 100;
|
||||
|
||||
const { data } = useFetcher(
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
const { interval, start, end } = getIntervalAndTimeRange({
|
||||
windowSize: params.windowSize,
|
||||
windowUnit: params.windowUnit,
|
||||
});
|
||||
if (interval && start && end) {
|
||||
if (params.windowSize && start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
{
|
||||
|
@ -95,6 +102,7 @@ export function TransactionErrorRateRuleType(props: Props) {
|
|||
interval,
|
||||
start,
|
||||
end,
|
||||
groupBy: params.groupBy,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -108,6 +116,7 @@ export function TransactionErrorRateRuleType(props: Props) {
|
|||
params.serviceName,
|
||||
params.windowSize,
|
||||
params.windowUnit,
|
||||
params.groupBy,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -171,16 +180,28 @@ export function TransactionErrorRateRuleType(props: Props) {
|
|||
/>,
|
||||
];
|
||||
|
||||
// hide preview chart until https://github.com/elastic/kibana/pull/156625 gets merged
|
||||
const showChartPreview = false;
|
||||
const chartPreview = showChartPreview ? (
|
||||
const errorRateChartPreview = data?.errorRateChartPreview;
|
||||
const series = errorRateChartPreview?.series ?? [];
|
||||
const hasData = series.length > 0;
|
||||
const totalGroups = errorRateChartPreview?.totalGroups ?? 0;
|
||||
|
||||
const chartPreview = isPending(status) ? (
|
||||
<LoadingState />
|
||||
) : !hasData ? (
|
||||
<NoDataState />
|
||||
) : status === FETCH_STATUS.SUCCESS ? (
|
||||
<ChartPreview
|
||||
series={[{ data: data?.errorRateChartPreview ?? [] }]}
|
||||
yTickFormat={(d: number | null) => asPercent(d, 1)}
|
||||
threshold={thresholdAsPercent}
|
||||
series={series}
|
||||
yTickFormat={(d: number | null) => asPercent(d, 100)}
|
||||
threshold={params.threshold}
|
||||
uiSettings={services.uiSettings}
|
||||
timeSize={params.windowSize}
|
||||
timeUnit={params.windowUnit}
|
||||
totalGroups={totalGroups}
|
||||
/>
|
||||
) : null;
|
||||
) : (
|
||||
<ErrorState />
|
||||
);
|
||||
|
||||
const groupAlertsBy = (
|
||||
<>
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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 { EuiLoadingChart } from '@elastic/eui';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Coordinate } from '../../../../../typings/timeseries';
|
||||
|
||||
export const TIME_LABELS = {
|
||||
s: i18n.translate('xpack.apm.alerts.timeLabels.seconds', {
|
||||
defaultMessage: 'seconds',
|
||||
}),
|
||||
m: i18n.translate('xpack.apm.alerts.timeLabels.minutes', {
|
||||
defaultMessage: 'minutes',
|
||||
}),
|
||||
h: i18n.translate('xpack.apm.alerts.timeLabels.hours', {
|
||||
defaultMessage: 'hours',
|
||||
}),
|
||||
d: i18n.translate('xpack.apm.alerts.timeLabels.days', {
|
||||
defaultMessage: 'days',
|
||||
}),
|
||||
};
|
||||
|
||||
export const getDomain = (
|
||||
series: Array<{ name?: string; data: Coordinate[] }>
|
||||
) => {
|
||||
const xValues = series.flatMap((item) => item.data.map((d) => d.x));
|
||||
const yValues = series.flatMap((item) => item.data.map((d) => d.y || 0));
|
||||
return {
|
||||
xMax: Math.max(...xValues),
|
||||
xMin: Math.min(...xValues),
|
||||
yMax: Math.max(...yValues),
|
||||
yMin: Math.min(...yValues),
|
||||
};
|
||||
};
|
||||
|
||||
const EmptyContainer: React.FC = ({ children }) => (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 150,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export function NoDataState() {
|
||||
return (
|
||||
<EmptyContainer>
|
||||
<EuiText color="subdued" data-test-subj="noChartData">
|
||||
<FormattedMessage
|
||||
id="xpack.apm.alerts.charts.noDataMessage"
|
||||
defaultMessage="No chart data available"
|
||||
/>
|
||||
</EuiText>
|
||||
</EmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingState() {
|
||||
return (
|
||||
<EmptyContainer>
|
||||
<EuiText color="subdued" data-test-subj="loadingData">
|
||||
<EuiLoadingChart size="m" />
|
||||
</EuiText>
|
||||
</EmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorState() {
|
||||
return (
|
||||
<EmptyContainer>
|
||||
<EuiText color="subdued" data-test-subj="chartErrorState">
|
||||
<FormattedMessage
|
||||
id="xpack.apm.alerts.charts.errorMessage"
|
||||
defaultMessage="Uh oh, something went wrong"
|
||||
/>
|
||||
</EuiText>
|
||||
</EmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface PreviewChartLabel {
|
||||
lookback: number;
|
||||
timeLabel: string;
|
||||
displayedGroups: number;
|
||||
totalGroups: number;
|
||||
}
|
||||
|
||||
export function TimeLabelForData({
|
||||
lookback,
|
||||
timeLabel,
|
||||
displayedGroups,
|
||||
totalGroups,
|
||||
}: PreviewChartLabel) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.apm.alerts.timeLabelForData"
|
||||
defaultMessage="Last {lookback} {timeLabel} of data, showing {displayedGroups}/{totalGroups} groups"
|
||||
values={{
|
||||
lookback,
|
||||
timeLabel,
|
||||
displayedGroups,
|
||||
totalGroups,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -18,19 +18,32 @@ import {
|
|||
ScaleType,
|
||||
Settings,
|
||||
TickFormatter,
|
||||
TooltipProps,
|
||||
} from '@elastic/charts';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { IUiSettingsClient } from '@kbn/core/public';
|
||||
import { Coordinate } from '../../../../../typings/timeseries';
|
||||
import { TimeUnitChar } from '@kbn/observability-plugin/common';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
import moment from 'moment';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { getTimeZone } from '../../../shared/charts/helper/timezone';
|
||||
import {
|
||||
TimeLabelForData,
|
||||
TIME_LABELS,
|
||||
getDomain,
|
||||
} from './chart_preview_helper';
|
||||
import { ALERT_PREVIEW_BUCKET_SIZE } from '../../utils/helper';
|
||||
import { Coordinate } from '../../../../../typings/timeseries';
|
||||
|
||||
interface ChartPreviewProps {
|
||||
yTickFormat?: TickFormatter;
|
||||
threshold: number;
|
||||
uiSettings?: IUiSettingsClient;
|
||||
series: Array<{ name?: string; data: Coordinate[] }>;
|
||||
timeSize?: number;
|
||||
timeUnit?: TimeUnitChar;
|
||||
totalGroups: number;
|
||||
}
|
||||
|
||||
export function ChartPreview({
|
||||
|
@ -38,21 +51,12 @@ export function ChartPreview({
|
|||
threshold,
|
||||
uiSettings,
|
||||
series,
|
||||
timeSize = 5,
|
||||
timeUnit = 'm',
|
||||
totalGroups,
|
||||
}: ChartPreviewProps) {
|
||||
const [yMax, setYMax] = useState(threshold);
|
||||
|
||||
const theme = useTheme();
|
||||
const thresholdOpacity = 0.3;
|
||||
const timestamps = series.flatMap(({ data }) => data.map(({ x }) => x));
|
||||
const xMin = Math.min(...timestamps);
|
||||
const xMax = Math.max(...timestamps);
|
||||
const xFormatter = niceTimeFormatter([xMin, xMax]);
|
||||
|
||||
function updateYMax() {
|
||||
// Make the maximum Y value either the actual max or 20% more than the threshold
|
||||
const values = series.flatMap(({ data }) => data.map((d) => d.y ?? 0));
|
||||
setYMax(Math.max(...values, threshold * 1.2));
|
||||
}
|
||||
|
||||
const style = {
|
||||
fill: theme.eui.euiColorVis2,
|
||||
|
@ -64,36 +68,69 @@ export function ChartPreview({
|
|||
opacity: thresholdOpacity,
|
||||
};
|
||||
|
||||
const DEFAULT_DATE_FORMAT = 'Y-MM-DD HH:mm:ss';
|
||||
|
||||
const tooltipProps: TooltipProps = {
|
||||
headerFormatter: ({ value }) => {
|
||||
const dateFormat =
|
||||
(uiSettings && uiSettings.get(UI_SETTINGS.DATE_FORMAT)) ||
|
||||
DEFAULT_DATE_FORMAT;
|
||||
return moment(value).format(dateFormat);
|
||||
},
|
||||
};
|
||||
|
||||
const barSeries = useMemo(() => {
|
||||
return series.flatMap((serie) =>
|
||||
serie.data.map((point) => ({ ...point, groupBy: serie.name }))
|
||||
);
|
||||
}, [series]);
|
||||
|
||||
const timeZone = getTimeZone(uiSettings);
|
||||
|
||||
const legendSize =
|
||||
series.length > 1 ? Math.ceil(series.length / 2) * 30 : series.length * 35;
|
||||
|
||||
const chartSize = 150;
|
||||
|
||||
const { yMin, yMax, xMin, xMax } = getDomain(series);
|
||||
const chartDomain = {
|
||||
max: Math.max(yMax, threshold) * 1.1, // Add 10% headroom.
|
||||
min: Math.min(yMin, threshold) * 0.9, // Add 10% headroom.
|
||||
};
|
||||
|
||||
const dateFormatter = useMemo(
|
||||
() => niceTimeFormatter([xMin, xMax]),
|
||||
[xMin, xMax]
|
||||
);
|
||||
|
||||
const lookback = timeSize * ALERT_PREVIEW_BUCKET_SIZE;
|
||||
const timeLabel = TIME_LABELS[timeUnit as keyof typeof TIME_LABELS];
|
||||
|
||||
const rectDataValues: RectAnnotationDatum[] = [
|
||||
{
|
||||
coordinates: {
|
||||
x0: null,
|
||||
x1: null,
|
||||
x0: xMin,
|
||||
x1: xMax,
|
||||
y0: threshold,
|
||||
y1: null,
|
||||
y1: chartDomain.max,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const timeZone = getTimeZone(uiSettings);
|
||||
const legendSize = Math.ceil(series.length / 2) * 30;
|
||||
const chartSize = 150;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<Chart
|
||||
size={{
|
||||
height: series.length > 1 ? chartSize + legendSize : chartSize,
|
||||
height: chartSize + legendSize,
|
||||
}}
|
||||
data-test-subj="ChartPreview"
|
||||
>
|
||||
<Settings
|
||||
tooltip="none"
|
||||
showLegend={series.length > 1}
|
||||
tooltip={tooltipProps}
|
||||
showLegend={true}
|
||||
legendPosition={'bottom'}
|
||||
legendSize={legendSize}
|
||||
onLegendItemClick={updateYMax}
|
||||
/>
|
||||
<LineAnnotation
|
||||
dataValues={[{ dataValue: threshold }]}
|
||||
|
@ -111,30 +148,44 @@ export function ChartPreview({
|
|||
<Axis
|
||||
id="chart_preview_x_axis"
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks
|
||||
tickFormat={xFormatter}
|
||||
showOverlappingTicks={true}
|
||||
tickFormat={dateFormatter}
|
||||
/>
|
||||
<Axis
|
||||
id="chart_preview_y_axis"
|
||||
position={Position.Left}
|
||||
tickFormat={yTickFormat}
|
||||
ticks={5}
|
||||
domain={{ max: yMax, min: 0 }}
|
||||
domain={chartDomain}
|
||||
/>
|
||||
<BarSeries
|
||||
id="apm-chart-preview"
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor="x"
|
||||
yAccessors={['y']}
|
||||
splitSeriesAccessors={['groupBy']}
|
||||
data={barSeries}
|
||||
barSeriesStyle={{
|
||||
rectBorder: {
|
||||
strokeWidth: 1,
|
||||
visible: true,
|
||||
},
|
||||
rect: {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
{series.map(({ name, data }, index) => (
|
||||
<BarSeries
|
||||
key={index}
|
||||
timeZone={timeZone}
|
||||
data={data}
|
||||
id={`chart_preview_bar_series_${name || index}`}
|
||||
name={name}
|
||||
xAccessor="x"
|
||||
xScaleType={ScaleType.Time}
|
||||
yAccessors={['y']}
|
||||
yScaleType={ScaleType.Linear}
|
||||
/>
|
||||
))}
|
||||
</Chart>
|
||||
{series.length > 0 && (
|
||||
<TimeLabelForData
|
||||
lookback={lookback}
|
||||
timeLabel={timeLabel}
|
||||
displayedGroups={series.length}
|
||||
totalGroups={totalGroups}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export interface AlertMetadata {
|
|||
end?: string;
|
||||
}
|
||||
|
||||
const BUCKET_SIZE = 20;
|
||||
export const ALERT_PREVIEW_BUCKET_SIZE = 5;
|
||||
|
||||
export function getIntervalAndTimeRange({
|
||||
windowSize,
|
||||
|
@ -28,7 +28,8 @@ export function getIntervalAndTimeRange({
|
|||
const end = Date.now();
|
||||
const start =
|
||||
end -
|
||||
moment.duration(windowSize, windowUnit).asMilliseconds() * BUCKET_SIZE;
|
||||
moment.duration(windowSize, windowUnit).asMilliseconds() *
|
||||
ALERT_PREVIEW_BUCKET_SIZE;
|
||||
|
||||
return {
|
||||
interval: `${windowSize}${windowUnit}`,
|
||||
|
|
|
@ -6,15 +6,13 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { Coordinate } from '../../../typings/timeseries';
|
||||
import {
|
||||
getTransactionDurationChartPreview,
|
||||
TransactionDurationChartPreviewResponse,
|
||||
} from './rule_types/transaction_duration/get_transaction_duration_chart_preview';
|
||||
import { getTransactionErrorCountChartPreview } from './rule_types/error_count/get_error_count_chart_preview';
|
||||
import {
|
||||
getTransactionErrorRateChartPreview,
|
||||
TransactionErrorRateChartPreviewResponse,
|
||||
} from './rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview';
|
||||
import { getTransactionErrorRateChartPreview } from './rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { environmentRt, rangeRt } from '../default_api_types';
|
||||
import { AggregationType } from '../../../common/rules/apm_rule_types';
|
||||
|
@ -37,8 +35,21 @@ const alertParamsRt = t.intersection([
|
|||
t.type({
|
||||
interval: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
groupBy: t.array(t.string),
|
||||
}),
|
||||
]);
|
||||
|
||||
export interface PreviewChartResponseItem {
|
||||
name: string;
|
||||
data: Coordinate[];
|
||||
}
|
||||
|
||||
export interface PreviewChartResponse {
|
||||
series: PreviewChartResponseItem[];
|
||||
totalGroups: number;
|
||||
}
|
||||
|
||||
export type AlertParams = t.TypeOf<typeof alertParamsRt>;
|
||||
|
||||
const transactionErrorRateChartPreview = createApmServerRoute({
|
||||
|
@ -48,7 +59,7 @@ const transactionErrorRateChartPreview = createApmServerRoute({
|
|||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
errorRateChartPreview: TransactionErrorRateChartPreviewResponse;
|
||||
errorRateChartPreview: PreviewChartResponse;
|
||||
}> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params, config } = resources;
|
||||
|
@ -70,7 +81,9 @@ const transactionErrorCountChartPreview = createApmServerRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{ errorCountChartPreview: Array<{ x: number; y: number }> }> => {
|
||||
): Promise<{
|
||||
errorCountChartPreview: PreviewChartResponse;
|
||||
}> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params } = resources;
|
||||
|
||||
|
@ -112,7 +125,6 @@ const transactionDurationChartPreview = createApmServerRoute({
|
|||
|
||||
export const alertsChartPreviewRouteRepository = {
|
||||
...transactionErrorRateChartPreview,
|
||||
...transactionDurationChartPreview,
|
||||
...transactionErrorCountChartPreview,
|
||||
...transactionDurationChartPreview,
|
||||
};
|
||||
|
|
|
@ -9,16 +9,19 @@ import { rangeQuery, termQuery } from '@kbn/observability-plugin/server';
|
|||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
ERROR_GROUP_ID,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
} from '../../../../../common/es_fields/apm';
|
||||
import { AlertParams } from '../../route';
|
||||
import { AlertParams, PreviewChartResponse } from '../../route';
|
||||
import { environmentQuery } from '../../../../../common/utils/environment_query';
|
||||
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
|
||||
export type TransactionErrorCountChartPreviewResponse = Array<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
import { getGroupByTerms } from '../utils/get_groupby_terms';
|
||||
import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby_fields';
|
||||
import { ApmRuleType } from '../../../../../common/rules/apm_rule_types';
|
||||
import {
|
||||
BarSeriesDataMap,
|
||||
getFilteredBarSeries,
|
||||
} from '../utils/get_filtered_series_for_preview_chart';
|
||||
|
||||
export async function getTransactionErrorCountChartPreview({
|
||||
apmEventClient,
|
||||
|
@ -26,17 +29,34 @@ export async function getTransactionErrorCountChartPreview({
|
|||
}: {
|
||||
apmEventClient: APMEventClient;
|
||||
alertParams: AlertParams;
|
||||
}): Promise<TransactionErrorCountChartPreviewResponse> {
|
||||
const { serviceName, environment, errorGroupingKey, interval, start, end } =
|
||||
alertParams;
|
||||
}): Promise<PreviewChartResponse> {
|
||||
const {
|
||||
serviceName,
|
||||
environment,
|
||||
errorGroupingKey,
|
||||
interval,
|
||||
start,
|
||||
end,
|
||||
groupBy: groupByFields,
|
||||
} = alertParams;
|
||||
|
||||
const allGroupByFields = getAllGroupByFields(
|
||||
ApmRuleType.ErrorCount,
|
||||
groupByFields
|
||||
);
|
||||
|
||||
const query = {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(ERROR_GROUP_ID, errorGroupingKey),
|
||||
...termQuery(SERVICE_NAME, serviceName, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...termQuery(ERROR_GROUP_ID, errorGroupingKey, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.error } },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -51,6 +71,15 @@ export async function getTransactionErrorCountChartPreview({
|
|||
max: end,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
series: {
|
||||
multi_terms: {
|
||||
terms: getGroupByTerms(allGroupByFields),
|
||||
size: 1000,
|
||||
order: { _count: 'desc' as const },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -65,13 +94,37 @@ export async function getTransactionErrorCountChartPreview({
|
|||
);
|
||||
|
||||
if (!resp.aggregations) {
|
||||
return [];
|
||||
return { series: [], totalGroups: 0 };
|
||||
}
|
||||
|
||||
return resp.aggregations.timeseries.buckets.map((bucket) => {
|
||||
return {
|
||||
x: bucket.key,
|
||||
y: bucket.doc_count,
|
||||
};
|
||||
});
|
||||
const seriesDataMap = resp.aggregations.timeseries.buckets.reduce(
|
||||
(acc, bucket) => {
|
||||
const x = bucket.key;
|
||||
bucket.series.buckets.forEach((seriesBucket) => {
|
||||
const bucketKey = seriesBucket.key.join('_');
|
||||
const y = seriesBucket.doc_count;
|
||||
|
||||
if (acc[bucketKey]) {
|
||||
acc[bucketKey].push({ x, y });
|
||||
} else {
|
||||
acc[bucketKey] = [{ x, y }];
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as BarSeriesDataMap
|
||||
);
|
||||
|
||||
const series = Object.keys(seriesDataMap).map((key) => ({
|
||||
name: key,
|
||||
data: seriesDataMap[key],
|
||||
}));
|
||||
|
||||
const filteredSeries = getFilteredBarSeries(series);
|
||||
|
||||
return {
|
||||
series: filteredSeries,
|
||||
totalGroups: series.length,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ import {
|
|||
} from '../get_service_group_fields';
|
||||
import { getGroupByTerms } from '../utils/get_groupby_terms';
|
||||
import { getGroupByActionVariables } from '../utils/get_groupby_action_variables';
|
||||
import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby_fields';
|
||||
|
||||
const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.ErrorCount];
|
||||
|
||||
|
@ -96,10 +97,9 @@ export function registerErrorCountRuleType({
|
|||
spaceId,
|
||||
startedAt,
|
||||
}) => {
|
||||
const predefinedGroupby = [SERVICE_NAME, SERVICE_ENVIRONMENT];
|
||||
|
||||
const allGroupbyFields = Array.from(
|
||||
new Set([...predefinedGroupby, ...(ruleParams.groupBy ?? [])])
|
||||
const allGroupByFields = getAllGroupByFields(
|
||||
ApmRuleType.ErrorCount,
|
||||
ruleParams.groupBy
|
||||
);
|
||||
|
||||
const config = await firstValueFrom(config$);
|
||||
|
@ -145,7 +145,7 @@ export function registerErrorCountRuleType({
|
|||
aggs: {
|
||||
error_counts: {
|
||||
multi_terms: {
|
||||
terms: getGroupByTerms(allGroupbyFields),
|
||||
terms: getGroupByTerms(allGroupByFields),
|
||||
size: 1000,
|
||||
order: { _count: 'desc' as const },
|
||||
},
|
||||
|
@ -164,7 +164,7 @@ export function registerErrorCountRuleType({
|
|||
response.aggregations?.error_counts.buckets.map((bucket) => {
|
||||
const groupByFields = bucket.key.reduce(
|
||||
(obj, bucketKey, bucketIndex) => {
|
||||
obj[allGroupbyFields[bucketIndex]] = bucketKey;
|
||||
obj[allGroupByFields[bucketIndex]] = bucketKey;
|
||||
return obj;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
|
|
|
@ -6,30 +6,29 @@
|
|||
*/
|
||||
|
||||
import { rangeQuery, termQuery } from '@kbn/observability-plugin/server';
|
||||
import { ApmRuleType } from '../../../../../common/rules/apm_rule_types';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
TRANSACTION_NAME,
|
||||
EVENT_OUTCOME,
|
||||
} from '../../../../../common/es_fields/apm';
|
||||
import { environmentQuery } from '../../../../../common/utils/environment_query';
|
||||
import { AlertParams } from '../../route';
|
||||
import { AlertParams, PreviewChartResponse } from '../../route';
|
||||
import {
|
||||
getSearchTransactionsEvents,
|
||||
getDocumentTypeFilterForTransactions,
|
||||
getProcessorEventForTransactions,
|
||||
} from '../../../../lib/helpers/transactions';
|
||||
import {
|
||||
calculateFailedTransactionRate,
|
||||
getOutcomeAggregation,
|
||||
} from '../../../../lib/helpers/transaction_error_rate';
|
||||
import { APMConfig } from '../../../..';
|
||||
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { ApmDocumentType } from '../../../../../common/document_type';
|
||||
|
||||
export type TransactionErrorRateChartPreviewResponse = Array<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
import { EventOutcome } from '../../../../../common/event_outcome';
|
||||
import { getGroupByTerms } from '../utils/get_groupby_terms';
|
||||
import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby_fields';
|
||||
import {
|
||||
BarSeriesDataMap,
|
||||
getFilteredBarSeries,
|
||||
} from '../utils/get_filtered_series_for_preview_chart';
|
||||
|
||||
export async function getTransactionErrorRateChartPreview({
|
||||
config,
|
||||
|
@ -39,7 +38,7 @@ export async function getTransactionErrorRateChartPreview({
|
|||
config: APMConfig;
|
||||
apmEventClient: APMEventClient;
|
||||
alertParams: AlertParams;
|
||||
}): Promise<TransactionErrorRateChartPreviewResponse> {
|
||||
}): Promise<PreviewChartResponse> {
|
||||
const {
|
||||
serviceName,
|
||||
environment,
|
||||
|
@ -48,16 +47,20 @@ export async function getTransactionErrorRateChartPreview({
|
|||
start,
|
||||
end,
|
||||
transactionName,
|
||||
groupBy: groupByFields,
|
||||
} = alertParams;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchTransactionsEvents({
|
||||
config,
|
||||
apmEventClient,
|
||||
kuery: '',
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const allGroupByFields = getAllGroupByFields(
|
||||
ApmRuleType.TransactionErrorRate,
|
||||
groupByFields
|
||||
);
|
||||
|
||||
const params = {
|
||||
apm: {
|
||||
events: [getProcessorEventForTransactions(searchAggregatedTransactions)],
|
||||
|
@ -68,14 +71,25 @@ export async function getTransactionErrorRateChartPreview({
|
|||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(TRANSACTION_TYPE, transactionType),
|
||||
...termQuery(TRANSACTION_NAME, transactionName),
|
||||
...termQuery(SERVICE_NAME, serviceName, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...termQuery(TRANSACTION_TYPE, transactionType, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...termQuery(TRANSACTION_NAME, transactionName, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...getDocumentTypeFilterForTransactions(
|
||||
searchAggregatedTransactions
|
||||
),
|
||||
{
|
||||
terms: {
|
||||
[EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -89,11 +103,22 @@ export async function getTransactionErrorRateChartPreview({
|
|||
max: end,
|
||||
},
|
||||
},
|
||||
aggs: getOutcomeAggregation(
|
||||
searchAggregatedTransactions
|
||||
? ApmDocumentType.TransactionMetric
|
||||
: ApmDocumentType.TransactionEvent
|
||||
),
|
||||
aggs: {
|
||||
series: {
|
||||
multi_terms: {
|
||||
terms: [...getGroupByTerms(allGroupByFields)],
|
||||
size: 1000,
|
||||
order: { _count: 'desc' as const },
|
||||
},
|
||||
aggs: {
|
||||
outcomes: {
|
||||
terms: {
|
||||
field: EVENT_OUTCOME,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -105,13 +130,54 @@ export async function getTransactionErrorRateChartPreview({
|
|||
);
|
||||
|
||||
if (!resp.aggregations) {
|
||||
return [];
|
||||
return { series: [], totalGroups: 0 };
|
||||
}
|
||||
|
||||
return resp.aggregations.timeseries.buckets.map((bucket) => {
|
||||
return {
|
||||
x: bucket.key,
|
||||
y: calculateFailedTransactionRate(bucket),
|
||||
};
|
||||
});
|
||||
const seriesDataMap = resp.aggregations.timeseries.buckets.reduce(
|
||||
(acc, bucket) => {
|
||||
const x = bucket.key;
|
||||
bucket.series.buckets.forEach((seriesBucket) => {
|
||||
const bucketKey = seriesBucket.key.join('_');
|
||||
const y = calculateErrorRate(seriesBucket.outcomes.buckets);
|
||||
|
||||
if (acc[bucketKey]) {
|
||||
acc[bucketKey].push({ x, y });
|
||||
} else {
|
||||
acc[bucketKey] = [{ x, y }];
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as BarSeriesDataMap
|
||||
);
|
||||
|
||||
const series = Object.keys(seriesDataMap).map((key) => ({
|
||||
name: key,
|
||||
data: seriesDataMap[key],
|
||||
}));
|
||||
|
||||
const filteredSeries = getFilteredBarSeries(series);
|
||||
|
||||
return {
|
||||
series: filteredSeries,
|
||||
totalGroups: series.length,
|
||||
};
|
||||
}
|
||||
|
||||
const calculateErrorRate = (
|
||||
buckets: Array<{
|
||||
doc_count: number;
|
||||
key: string | number;
|
||||
}>
|
||||
) => {
|
||||
const failed =
|
||||
buckets.find((outcomeBucket) => outcomeBucket.key === EventOutcome.failure)
|
||||
?.doc_count ?? 0;
|
||||
|
||||
const succesful =
|
||||
buckets.find((outcomeBucket) => outcomeBucket.key === EventOutcome.success)
|
||||
?.doc_count ?? 0;
|
||||
|
||||
return (failed / (failed + succesful)) * 100;
|
||||
};
|
||||
|
|
|
@ -59,6 +59,7 @@ import {
|
|||
} from '../get_service_group_fields';
|
||||
import { getGroupByTerms } from '../utils/get_groupby_terms';
|
||||
import { getGroupByActionVariables } from '../utils/get_groupby_action_variables';
|
||||
import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby_fields';
|
||||
|
||||
const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionErrorRate];
|
||||
|
||||
|
@ -106,14 +107,9 @@ export function registerTransactionErrorRateRuleType({
|
|||
params: ruleParams,
|
||||
startedAt,
|
||||
}) => {
|
||||
const predefinedGroupby = [
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT,
|
||||
TRANSACTION_TYPE,
|
||||
];
|
||||
|
||||
const allGroupbyFields = Array.from(
|
||||
new Set([...predefinedGroupby, ...(ruleParams.groupBy ?? [])])
|
||||
const allGroupByFields = getAllGroupByFields(
|
||||
ApmRuleType.TransactionErrorRate,
|
||||
ruleParams.groupBy
|
||||
);
|
||||
|
||||
const config = await firstValueFrom(config$);
|
||||
|
@ -183,7 +179,7 @@ export function registerTransactionErrorRateRuleType({
|
|||
aggs: {
|
||||
series: {
|
||||
multi_terms: {
|
||||
terms: [...getGroupByTerms(allGroupbyFields)],
|
||||
terms: [...getGroupByTerms(allGroupByFields)],
|
||||
size: 1000,
|
||||
order: { _count: 'desc' as const },
|
||||
},
|
||||
|
@ -214,7 +210,7 @@ export function registerTransactionErrorRateRuleType({
|
|||
for (const bucket of response.aggregations.series.buckets) {
|
||||
const groupByFields = bucket.key.reduce(
|
||||
(obj, bucketKey, bucketIndex) => {
|
||||
obj[allGroupbyFields[bucketIndex]] = bucketKey;
|
||||
obj[allGroupByFields[bucketIndex]] = bucketKey;
|
||||
return obj;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { Coordinate } from '../../../../../typings/timeseries';
|
||||
|
||||
export type BarSeriesDataMap = Record<string, Coordinate[]>;
|
||||
type BarSeriesData = Array<{ name: string; data: Coordinate[] }>;
|
||||
|
||||
const NUM_SERIES = 5;
|
||||
|
||||
export const getFilteredBarSeries = (barSeries: BarSeriesData) => {
|
||||
const sortedSeries = barSeries.sort((a, b) => {
|
||||
const aMax = Math.max(...a.data.map((point) => point.y as number));
|
||||
const bMax = Math.max(...b.data.map((point) => point.y as number));
|
||||
return bMax - aMax;
|
||||
});
|
||||
|
||||
return sortedSeries.slice(0, NUM_SERIES);
|
||||
};
|
|
@ -1,250 +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 archives from '../../common/fixtures/es_archiver/archives_metadata';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const { end } = archives[archiveName];
|
||||
const start = new Date(Date.parse(end) - 600000).toISOString();
|
||||
|
||||
const getOptions = () => ({
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceName: 'opbeans-java',
|
||||
transactionType: 'request' as string | undefined,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => {
|
||||
it('transaction_error_rate (without data)', async () => {
|
||||
const options = getOptions();
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorRateChartPreview).to.eql([]);
|
||||
});
|
||||
|
||||
it('error_count (without data)', async () => {
|
||||
const options = getOptions();
|
||||
options.params.query.transactionType = undefined;
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorCountChartPreview).to.eql([]);
|
||||
});
|
||||
|
||||
it('transaction_duration (without data)', async () => {
|
||||
const options = getOptions();
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.latencyChartPreview).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(`with data loaded`, { config: 'basic', archives: [archiveName] }, () => {
|
||||
it('transaction_error_rate (with data)', async () => {
|
||||
const options = getOptions();
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.some(
|
||||
(item: { x: number; y: number | null }) => item.x && item.y
|
||||
)
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('transaction_error_rate with transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceName: 'opbeans-java',
|
||||
transactionName: 'APIRestController#product',
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorRateChartPreview[0]).to.eql({
|
||||
x: 1627974600000,
|
||||
y: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('transaction_error_rate with nonexistent transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceName: 'opbeans-java',
|
||||
transactionName: 'foo',
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.every(
|
||||
(item: { x: number; y: number | null }) => item.y === null
|
||||
)
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('error_count (with data)', async () => {
|
||||
const options = getOptions();
|
||||
options.params.query.transactionType = undefined;
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorCountChartPreview.some(
|
||||
(item: { x: number; y: number | null }) => item.x && item.y
|
||||
)
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('error_count with error grouping key', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceName: 'opbeans-java',
|
||||
errorGroupingKey: 'd16d39e7fa133b8943cea035430a7b4e',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorCountChartPreview).to.eql([
|
||||
{ x: 1627974600000, y: 4 },
|
||||
{ x: 1627974900000, y: 2 },
|
||||
{ x: 1627975200000, y: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('transaction_duration (with data)', async () => {
|
||||
const options = getOptions();
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.latencyChartPreview.some(
|
||||
(item: { name: string; data: Array<{ x: number; y: number | null }> }) =>
|
||||
item.data.some((coordinate) => coordinate.x && coordinate.y)
|
||||
)
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('transaction_duration with transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceName: 'opbeans-java',
|
||||
transactionName: 'DispatcherServlet#doGet',
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.latencyChartPreview[0].data[0]).to.eql({
|
||||
x: 1627974600000,
|
||||
y: 18485.85714285714,
|
||||
});
|
||||
});
|
||||
|
||||
it('transaction_duration with nonexistent transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceName: 'opbeans-java',
|
||||
transactionType: 'request',
|
||||
transactionName: 'foo',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.latencyChartPreview).to.eql([]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
|
||||
export const config = {
|
||||
appleTransaction: {
|
||||
name: 'GET /apple',
|
||||
successRate: 75,
|
||||
failureRate: 25,
|
||||
},
|
||||
bananaTransaction: {
|
||||
name: 'GET /banana',
|
||||
successRate: 50,
|
||||
failureRate: 50,
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateErrorData({
|
||||
synthtraceEsClient,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
synthtraceEsClient: ApmSynthtraceEsClient;
|
||||
serviceName: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const serviceInstance = apm
|
||||
.service({ name: serviceName, environment: 'production', agentName: 'go' })
|
||||
.instance('instance-a');
|
||||
|
||||
const interval = '1m';
|
||||
|
||||
const { bananaTransaction, appleTransaction } = config;
|
||||
|
||||
const documents = [appleTransaction, bananaTransaction].flatMap((transaction, index) => {
|
||||
return [
|
||||
timerange(start, end)
|
||||
.interval(interval)
|
||||
.rate(transaction.successRate)
|
||||
.generator((timestamp) =>
|
||||
serviceInstance
|
||||
.transaction({ transactionName: transaction.name })
|
||||
.timestamp(timestamp)
|
||||
.duration(10)
|
||||
.success()
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval(interval)
|
||||
.rate(transaction.failureRate)
|
||||
.generator((timestamp) =>
|
||||
serviceInstance
|
||||
.transaction({ transactionName: transaction.name })
|
||||
.errors(
|
||||
serviceInstance
|
||||
.error({ message: `Error ${index}`, type: transaction.name })
|
||||
.timestamp(timestamp)
|
||||
)
|
||||
.duration(10)
|
||||
.timestamp(timestamp)
|
||||
.failure()
|
||||
),
|
||||
];
|
||||
});
|
||||
|
||||
await synthtraceEsClient.index(documents);
|
||||
}
|
|
@ -0,0 +1,285 @@
|
|||
/*
|
||||
* 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_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
ERROR_GROUP_ID,
|
||||
} from '@kbn/apm-plugin/common/es_fields/apm';
|
||||
import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { generateErrorData } from './generate_data';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
const getOptions = () => ({
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
serviceName: 'synth-go',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => {
|
||||
it('error_count (without data)', async () => {
|
||||
const options = getOptions();
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorCountChartPreview.series).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(`with data loaded`, { config: 'basic', archives: [] }, () => {
|
||||
describe('error_count', () => {
|
||||
before(async () => {
|
||||
await generateErrorData({ serviceName: 'synth-go', start, end, synthtraceEsClient });
|
||||
await generateErrorData({ serviceName: 'synth-java', start, end, synthtraceEsClient });
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('with data', async () => {
|
||||
const options = getOptions();
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorCountChartPreview.series.some((item: PreviewChartResponseItem) =>
|
||||
item.data.some((coordinate) => coordinate.x && coordinate.y)
|
||||
)
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('with error grouping key', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
serviceName: 'synth-go',
|
||||
errorGroupingKey: '98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([{ name: 'synth-go_production', y: 250 }]);
|
||||
});
|
||||
|
||||
it('with no group by parameter', async () => {
|
||||
const options = getOptions();
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorCountChartPreview.series.length).to.equal(1);
|
||||
expect(
|
||||
response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([{ name: 'synth-go_production', y: 375 }]);
|
||||
});
|
||||
|
||||
it('with default group by fields', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
...getOptions().params.query,
|
||||
groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorCountChartPreview.series.length).to.equal(1);
|
||||
expect(
|
||||
response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([{ name: 'synth-go_production', y: 375 }]);
|
||||
});
|
||||
|
||||
it('with group by on error grouping key', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
...getOptions().params.query,
|
||||
groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorCountChartPreview.series.length).to.equal(2);
|
||||
expect(
|
||||
response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([
|
||||
{
|
||||
name: 'synth-go_production_98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03',
|
||||
y: 250,
|
||||
},
|
||||
{
|
||||
name: 'synth-go_production_cf676a2665c3c548caaab78db6d23af63aed81bff4360a5b9873c07443aee78c',
|
||||
y: 125,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('with group by on error grouping key and filter on error grouping key', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
...getOptions().params.query,
|
||||
errorGroupingKey: 'cf676a2665c3c548caaab78db6d23af63aed81bff4360a5b9873c07443aee78c',
|
||||
groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorCountChartPreview.series.length).to.equal(1);
|
||||
expect(
|
||||
response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([
|
||||
{
|
||||
name: 'synth-go_production_cf676a2665c3c548caaab78db6d23af63aed81bff4360a5b9873c07443aee78c',
|
||||
y: 125,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('with empty service name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
serviceName: '',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([
|
||||
{ name: 'synth-go_production', y: 375 },
|
||||
{ name: 'synth-java_production', y: 375 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('with empty service name and group by on error grouping key', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
serviceName: '',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([
|
||||
{
|
||||
name: 'synth-go_production_98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03',
|
||||
y: 250,
|
||||
},
|
||||
{
|
||||
name: 'synth-java_production_98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03',
|
||||
y: 250,
|
||||
},
|
||||
{
|
||||
name: 'synth-go_production_cf676a2665c3c548caaab78db6d23af63aed81bff4360a5b9873c07443aee78c',
|
||||
y: 125,
|
||||
},
|
||||
{
|
||||
name: 'synth-java_production_cf676a2665c3c548caaab78db6d23af63aed81bff4360a5b9873c07443aee78c',
|
||||
y: 125,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,309 @@
|
|||
/*
|
||||
* 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_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '@kbn/apm-plugin/common/es_fields/apm';
|
||||
import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { generateErrorData } from './generate_data';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
const getOptions = () => ({
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
serviceName: 'synth-go',
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => {
|
||||
it('transaction_error_rate without data', async () => {
|
||||
const options = getOptions();
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorRateChartPreview.series).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(`with data loaded`, { config: 'basic', archives: [] }, () => {
|
||||
describe('transaction_error_rate', () => {
|
||||
before(async () => {
|
||||
await generateErrorData({ serviceName: 'synth-go', start, end, synthtraceEsClient });
|
||||
await generateErrorData({ serviceName: 'synth-java', start, end, synthtraceEsClient });
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('with data', async () => {
|
||||
const options = getOptions();
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.series.some((item: PreviewChartResponseItem) =>
|
||||
item.data.some((coordinate) => coordinate.x && coordinate.y)
|
||||
)
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('with transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
serviceName: 'synth-go',
|
||||
transactionName: 'GET /banana',
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([{ name: 'synth-go_production_request', y: 50 }]);
|
||||
});
|
||||
|
||||
it('with nonexistent transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
serviceName: 'synth-go',
|
||||
transactionName: 'foo',
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorRateChartPreview.series).to.eql([]);
|
||||
});
|
||||
|
||||
it('with no group by parameter', async () => {
|
||||
const options = getOptions();
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorRateChartPreview.series.length).to.equal(1);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]);
|
||||
});
|
||||
|
||||
it('with default group by fields', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
...getOptions().params.query,
|
||||
groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorRateChartPreview.series.length).to.equal(1);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]);
|
||||
});
|
||||
|
||||
it('with group by on transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
...getOptions().params.query,
|
||||
groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorRateChartPreview.series.length).to.equal(2);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([
|
||||
{
|
||||
name: 'synth-go_production_request_GET /banana',
|
||||
y: 50,
|
||||
},
|
||||
{
|
||||
name: 'synth-go_production_request_GET /apple',
|
||||
y: 25,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('with group by on transaction name and filter on transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
...getOptions().params.query,
|
||||
transactionName: 'GET /apple',
|
||||
groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorRateChartPreview.series.length).to.equal(1);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 25 }]);
|
||||
});
|
||||
|
||||
it('with empty service name, transaction name and transaction type', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
serviceName: '',
|
||||
transactionName: '',
|
||||
transactionType: '',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([
|
||||
{ name: 'synth-go_production_request', y: 37.5 },
|
||||
{ name: 'synth-java_production_request', y: 37.5 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('with empty service name, transaction name, transaction type and group by on transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
serviceName: '',
|
||||
transactionName: '',
|
||||
transactionType: '',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({
|
||||
name: item.name,
|
||||
y: item.data[0].y,
|
||||
}))
|
||||
).to.eql([
|
||||
{
|
||||
name: 'synth-go_production_request_GET /banana',
|
||||
y: 50,
|
||||
},
|
||||
{
|
||||
name: 'synth-java_production_request_GET /banana',
|
||||
y: 50,
|
||||
},
|
||||
{
|
||||
name: 'synth-go_production_request_GET /apple',
|
||||
y: 25,
|
||||
},
|
||||
{
|
||||
name: 'synth-java_production_request_GET /apple',
|
||||
y: 25,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 archives from '../../common/fixtures/es_archiver/archives_metadata';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const { end } = archives[archiveName];
|
||||
const start = new Date(Date.parse(end) - 600000).toISOString();
|
||||
|
||||
const getOptions = () => ({
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceName: 'opbeans-java',
|
||||
transactionType: 'request' as string | undefined,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => {
|
||||
it('transaction_duration (without data)', async () => {
|
||||
const options = getOptions();
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview',
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.latencyChartPreview).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(`with data loaded`, { config: 'basic', archives: [archiveName] }, () => {
|
||||
it('transaction_duration (with data)', async () => {
|
||||
const options = getOptions();
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(
|
||||
response.body.latencyChartPreview.some(
|
||||
(item: { name: string; data: Array<{ x: number; y: number | null }> }) =>
|
||||
item.data.some((coordinate) => coordinate.x && coordinate.y)
|
||||
)
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('transaction_duration with transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceName: 'opbeans-java',
|
||||
transactionName: 'DispatcherServlet#doGet',
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.latencyChartPreview[0].data[0]).to.eql({
|
||||
x: 1627974600000,
|
||||
y: 18485.85714285714,
|
||||
});
|
||||
});
|
||||
|
||||
it('transaction_duration with nonexistent transaction name', async () => {
|
||||
const options = {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceName: 'opbeans-java',
|
||||
transactionType: 'request',
|
||||
transactionName: 'foo',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
interval: '5m',
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await apmApiClient.readUser({
|
||||
...options,
|
||||
endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview',
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.latencyChartPreview).to.eql([]);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue