Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix preview chart in APM error rate and error count rules #159544

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2583478
fix preview chart
benakansara Jun 13, 2023
47ad358
update tests
benakansara Jun 13, 2023
a6fe3bd
revert changes related to latency threshold rule
benakansara Jun 13, 2023
d3db009
Merge branch 'main' into fix/apm-preview-chart-in-error-rules
benakansara Jun 13, 2023
4ea3068
fix y-axis in error rate rule
benakansara Jun 13, 2023
5e91ed6
update tests to use synthtrace
benakansara Jun 13, 2023
41d8586
update tests to use synthtrace
benakansara Jun 13, 2023
f81a86c
updated tests
benakansara Jun 13, 2023
2ac6523
renaming
benakansara Jun 14, 2023
aaa2532
fix types
benakansara Jun 14, 2023
bf39b63
PR feedback
benakansara Jun 14, 2023
4b135c1
Merge branch 'main' into fix/apm-preview-chart-in-error-rules
benakansara Jun 14, 2023
5009664
refactoring
benakansara Jun 14, 2023
d52b7f5
PR feedback
benakansara Jun 14, 2023
aaa7e04
bug fix
benakansara Jun 14, 2023
d23a7f4
refactoring
benakansara Jun 15, 2023
f40f335
filtering series on server
benakansara Jun 15, 2023
3359972
refactoring series filtering on server
benakansara Jun 16, 2023
11fb01a
fix chart domain
benakansara Jun 16, 2023
a6efe41
fix grouping in chart
benakansara Jun 16, 2023
767e41a
Merge branch 'main' into fix/apm-preview-chart-in-error-rules
benakansara Jun 16, 2023
8d22e8a
renaming
benakansara Jun 19, 2023
94e8f96
refactoring as per feedback
benakansara Jun 19, 2023
f608cd6
refactoring as per feedback
benakansara Jun 19, 2023
d6b5475
Merge branch 'main' into fix/apm-preview-chart-in-error-rules
benakansara Jun 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions x-pack/plugins/apm/common/rules/apm_rule_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export enum AggregationType {
P99 = '99th',
}

export const NUMBER_OF_TIME_SERIES_FOR_PREVIEW_CHART = 5;
export const INTERVAL_MULTIPLIER_FOR_LOOKBACK = 5;

export const THRESHOLD_MET_GROUP_ID = 'threshold_met';
export type ThresholdMetActionGroupId = typeof THRESHOLD_MET_GROUP_ID;
const THRESHOLD_MET_GROUP: ActionGroup<ThresholdMetActionGroupId> = {
Expand Down
33 changes: 33 additions & 0 deletions x-pack/plugins/apm/common/rules/get_all_groupby_fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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,
ruleParamsGroupBy: string[] | undefined = []
) => {
let predefinedGroupBy: string[] = [];

switch (ruleType) {
case ApmRuleType.TransactionDuration:
case ApmRuleType.TransactionErrorRate:
predefinedGroupBy = [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE];
break;
case ApmRuleType.ErrorCount:
predefinedGroupBy = [SERVICE_NAME, SERVICE_ENVIRONMENT];
break;
}

return union(predefinedGroupBy, ruleParamsGroupBy);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -72,7 +81,7 @@ export function ErrorCountRuleType(props: Props) {
}
);

const { data } = useFetcher(
const { data, status } = useFetcher(
(callApmApi) => {
const { interval, start, end } = getIntervalAndTimeRange({
windowSize: params.windowSize,
Expand All @@ -90,6 +99,7 @@ export function ErrorCountRuleType(props: Props) {
interval,
start,
end,
groupBy: params.groupBy,
},
},
}
Expand All @@ -102,6 +112,7 @@ export function ErrorCountRuleType(props: Props) {
params.environment,
params.serviceName,
params.errorGroupingKey,
params.groupBy,
]
);

Expand Down Expand Up @@ -162,16 +173,26 @@ 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 hasData = errorCountChartPreview.length > 0;

const chartPreview = isPending(status) ? (
<LoadingState />
) : !hasData ? (
<NoDataState />
) : status === FETCH_STATUS.SUCCESS ? (
<ChartPreview
series={[{ data: data?.errorCountChartPreview ?? [] }]}
series={errorCountChartPreview}
threshold={params.threshold}
yTickFormat={asInteger}
uiSettings={services.uiSettings}
timeSize={params.windowSize}
timeUnit={params.windowUnit}
/>
) : null;
) : (
<ErrorState />
);

const groupAlertsBy = (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -74,9 +83,7 @@ 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,
Expand All @@ -95,6 +102,7 @@ export function TransactionErrorRateRuleType(props: Props) {
interval,
start,
end,
groupBy: params.groupBy,
},
},
}
Expand All @@ -108,6 +116,7 @@ export function TransactionErrorRateRuleType(props: Props) {
params.serviceName,
params.windowSize,
params.windowUnit,
params.groupBy,
]
);

Expand Down Expand Up @@ -171,16 +180,26 @@ 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 hasData = errorRateChartPreview.length > 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={errorRateChartPreview}
yTickFormat={(d: number | null) => asPercent(d, 100)}
threshold={params.threshold}
uiSettings={services.uiSettings}
timeSize={params.windowSize}
timeUnit={params.windowUnit}
/>
) : null;
) : (
<ErrorState />
);

const groupAlertsBy = (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* 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 { EuiLoadingChart } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { niceTimeFormatter } from '@elastic/charts';
import { min as getMin, max as getMax } from 'lodash';
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 useDateFormatter = (xMin?: number, xMax?: number) => {
const dateFormatter = useMemo(() => {
if (typeof xMin === 'number' && typeof xMax === 'number') {
return niceTimeFormatter([xMin, xMax]);
} else {
return (value: number) => `${value}`;
}
}, [xMin, xMax]);
return dateFormatter;
};

export const getDomain = (
series: Array<{ name?: string; data: Coordinate[] }>
) => {
let min: number | null = null;
let max: number | null = null;
const valuesByTimestamp = series.reduce<{ [timestamp: number]: number[] }>(
(acc, serie) => {
serie.data.forEach((point) => {
const valuesForTimestamp = acc[point.x] || [];
acc[point.x] = [...valuesForTimestamp, point.y ?? 0];
});
return acc;
},
{}
);
const pointValues = Object.values(valuesByTimestamp);
pointValues.forEach((results) => {
const maxResult = getMax(results);
const minResult = getMin(results);
if (maxResult && (!max || maxResult > max)) {
max = maxResult;
}
if (minResult && (!min || minResult < min)) {
min = minResult;
}
});
const timestampValues = Object.keys(valuesByTimestamp).map(Number);
const minTimestamp = getMin(timestampValues) || 0;
const maxTimestamp = getMax(timestampValues) || 0;
return {
yMin: min || 0,
yMax: max || 0,
xMin: minTimestamp,
xMax: maxTimestamp,
};
};

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>
);
}
Loading