From dfea483ef21cc8c03ab118de3716d3fb7a4fbe23 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Tue, 25 Apr 2023 11:14:47 +0200 Subject: [PATCH] [AO] Add threshold information to the metric threshold alert details page (#155493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #153740, closes #153833, closes #155593 This PR adds threshold information and rule name to the metric threshold alert details page. ![image](https://user-images.githubusercontent.com/12370520/233968325-8a66166b-2534-4b9b-9054-9085270db5f6.png) ## 🧪 How to test - Add xpack.observability.unsafe.alertDetails.metrics.enabled: true to the Kibana config - Create a metric threshold rule with multiple conditions that generates an alert - Go to the alert details page and check threshold information - Click on the rule link; it should send you to the rule page --- .../metrics/metric_value_formatter.test.ts | 27 ++++ .../metrics/metric_value_formatter.ts | 21 ++++ .../alert_details_app_section.test.tsx.snap | 1 + .../alert_details_app_section.test.tsx | 36 +++++- .../components/alert_details_app_section.tsx | 115 +++++++++++++++--- .../components/expression_chart.tsx | 54 ++++---- .../mocks/metric_threshold_rule.ts | 19 ++- .../pages/alert_details/alert_details.tsx | 1 + 8 files changed, 225 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugins/infra/common/alerting/metrics/metric_value_formatter.test.ts create mode 100644 x-pack/plugins/infra/common/alerting/metrics/metric_value_formatter.ts diff --git a/x-pack/plugins/infra/common/alerting/metrics/metric_value_formatter.test.ts b/x-pack/plugins/infra/common/alerting/metrics/metric_value_formatter.test.ts new file mode 100644 index 0000000000000..b6413b37b0380 --- /dev/null +++ b/x-pack/plugins/infra/common/alerting/metrics/metric_value_formatter.test.ts @@ -0,0 +1,27 @@ +/* + * 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 { metricValueFormatter } from './metric_value_formatter'; + +describe('metricValueFormatter', () => { + const testData = [ + { value: null, metric: undefined, result: '[NO DATA]' }, + { value: null, metric: 'system.cpu.user.pct', result: '[NO DATA]' }, + { value: 50, metric: undefined, result: '50' }, + { value: 0.7, metric: 'system.cpu.user.pct', result: '70%' }, + { value: 0.7012345, metric: 'system.cpu.user.pct', result: '70.1%' }, + { value: 208, metric: 'system.cpu.user.ticks', result: '208' }, + { value: 0.8, metric: 'system.cpu.user.ticks', result: '0.8' }, + ]; + + it.each(testData)( + 'metricValueFormatter($value, $metric) = $result', + ({ value, metric, result }) => { + expect(metricValueFormatter(value, metric)).toBe(result); + } + ); +}); diff --git a/x-pack/plugins/infra/common/alerting/metrics/metric_value_formatter.ts b/x-pack/plugins/infra/common/alerting/metrics/metric_value_formatter.ts new file mode 100644 index 0000000000000..2049a3667d0e5 --- /dev/null +++ b/x-pack/plugins/infra/common/alerting/metrics/metric_value_formatter.ts @@ -0,0 +1,21 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { createFormatter } from '../../formatters'; + +export const metricValueFormatter = (value: number | null, metric: string = '') => { + const noDataValue = i18n.translate('xpack.infra.metrics.alerting.noDataFormattedValue', { + defaultMessage: '[NO DATA]', + }); + + const formatter = metric.endsWith('.pct') + ? createFormatter('percent') + : createFormatter('highPrecision'); + + return value == null ? noDataValue : formatter(value); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/__snapshots__/alert_details_app_section.test.tsx.snap b/x-pack/plugins/infra/public/alerting/metric_threshold/components/__snapshots__/alert_details_app_section.test.tsx.snap index 9994945cd3290..5ee10d2d3381e 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/__snapshots__/alert_details_app_section.test.tsx.snap +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/__snapshots__/alert_details_app_section.test.tsx.snap @@ -34,6 +34,7 @@ Array [ "groupBy": Array [ "host.hostname", ], + "hideTitle": true, "source": Object { "id": "default", }, diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx index 20f134633e04b..d73aec96da4d1 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx @@ -6,6 +6,8 @@ */ import React from 'react'; +import { EuiLink } from '@elastic/eui'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { coreMock as mockCoreMock } from '@kbn/core/public/mocks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; @@ -17,6 +19,8 @@ import { import { AlertDetailsAppSection } from './alert_details_app_section'; import { ExpressionChart } from './expression_chart'; +const mockedChartStartContract = chartPluginMock.createStartContract(); + jest.mock('@kbn/observability-alert-details', () => ({ AlertAnnotation: () => {}, AlertActiveTimeRangeAnnotation: () => {}, @@ -32,7 +36,10 @@ jest.mock('./expression_chart', () => ({ jest.mock('../../../hooks/use_kibana', () => ({ useKibanaContextForPlugin: () => ({ - services: mockCoreMock.createStart(), + services: { + ...mockCoreMock.createStart(), + charts: mockedChartStartContract, + }, }), })); @@ -46,6 +53,8 @@ jest.mock('../../../containers/metrics_source/source', () => ({ describe('AlertDetailsAppSection', () => { const queryClient = new QueryClient(); + const mockedSetAlertSummaryFields = jest.fn(); + const ruleLink = 'ruleLink'; const renderComponent = () => { return render( @@ -53,16 +62,39 @@ describe('AlertDetailsAppSection', () => { ); }; - it('should render rule data', async () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render rule and alert data', async () => { const result = renderComponent(); expect((await result.findByTestId('metricThresholdAppSection')).children.length).toBe(3); + expect(result.getByTestId('threshold-2000-2500')).toBeTruthy(); + }); + + it('should render rule link', async () => { + renderComponent(); + + expect(mockedSetAlertSummaryFields).toBeCalledTimes(1); + expect(mockedSetAlertSummaryFields).toBeCalledWith([ + { + label: 'Rule', + value: ( + + Monitoring hosts + + ), + }, + ]); }); it('should render annotations', async () => { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx index 06602c9638865..466d032b5c01f 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx @@ -5,17 +5,31 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useEffect, useMemo } from 'react'; import moment from 'moment'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, useEuiTheme } from '@elastic/eui'; -import { TopAlert } from '@kbn/observability-plugin/public'; -import { ALERT_END, ALERT_START } from '@kbn/rule-data-utils'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import { AlertSummaryField, TopAlert } from '@kbn/observability-plugin/public'; +import { ALERT_END, ALERT_START, ALERT_EVALUATION_VALUES } from '@kbn/rule-data-utils'; import { Rule } from '@kbn/alerting-plugin/common'; import { AlertAnnotation, getPaddedAlertTimeRange, AlertActiveTimeRangeAnnotation, } from '@kbn/observability-alert-details'; +import { metricValueFormatter } from '../../../../common/alerting/metrics/metric_value_formatter'; +import { TIME_LABELS } from '../../common/criterion_preview_chart/criterion_preview_chart'; +import { Threshold } from '../../common/components/threshold'; import { useSourceContext, withSourceProvider } from '../../../containers/metrics_source'; import { generateUniqueKey } from '../lib/generate_unique_key'; import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; @@ -37,12 +51,19 @@ const ALERT_START_ANNOTATION_ID = 'alert_start_annotation'; const ALERT_TIME_RANGE_ANNOTATION_ID = 'alert_time_range_annotation'; interface AppSectionProps { - rule: MetricThresholdRule; alert: MetricThresholdAlert; + rule: MetricThresholdRule; + ruleLink: string; + setAlertSummaryFields: React.Dispatch>; } -export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) { - const { uiSettings } = useKibanaContextForPlugin().services; +export function AlertDetailsAppSection({ + alert, + rule, + ruleLink, + setAlertSummaryFields, +}: AppSectionProps) { + const { uiSettings, charts } = useKibanaContextForPlugin().services; const { source, createDerivedIndexPattern } = useSourceContext(); const { euiTheme } = useEuiTheme(); @@ -50,6 +71,10 @@ export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) { () => createDerivedIndexPattern(), [createDerivedIndexPattern] ); + const chartProps = { + theme: charts.theme.useChartsTheme(), + baseTheme: charts.theme.useChartsBaseTheme(), + }; const timeRange = getPaddedAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]); const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined; const annotations = [ @@ -68,22 +93,76 @@ export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) { key={ALERT_TIME_RANGE_ANNOTATION_ID} />, ]; + useEffect(() => { + setAlertSummaryFields([ + { + label: i18n.translate('xpack.infra.metrics.alertDetailsAppSection.summaryField.rule', { + defaultMessage: 'Rule', + }), + value: ( + + {rule.name} + + ), + }, + ]); + }, [alert, rule, ruleLink, setAlertSummaryFields]); return !!rule.params.criteria ? ( - {rule.params.criteria.map((criterion) => ( + {rule.params.criteria.map((criterion, index) => ( - + +

+ {criterion.aggType.toUpperCase()}{' '} + {'metric' in criterion ? criterion.metric : undefined} +

+
+ + + + + + + + metricValueFormatter(d, 'metric' in criterion ? criterion.metric : undefined) + } + title={i18n.translate( + 'xpack.infra.metrics.alertDetailsAppSection.thresholdTitle', + { + defaultMessage: 'Threshold breached', + } + )} + comparator={criterion.comparator} + /> + + + + +
))} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 8b453579b5e67..41a313adda8fa 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -49,23 +49,25 @@ import { CUSTOM_EQUATION } from '../i18n_strings'; interface Props { expression: MetricExpression; derivedIndexPattern: DataViewBase; - source?: MetricsSourceConfiguration; + annotations?: Array>; + chartType?: MetricsExplorerChartType; filterQuery?: string; groupBy?: string | string[]; - chartType?: MetricsExplorerChartType; + hideTitle?: boolean; + source?: MetricsSourceConfiguration; timeRange?: TimeRange; - annotations?: Array>; } export const ExpressionChart: React.FC = ({ expression, derivedIndexPattern, - source, + annotations, + chartType = MetricsExplorerChartType.bar, filterQuery, groupBy, - chartType = MetricsExplorerChartType.bar, + hideTitle = false, + source, timeRange, - annotations, }) => { const { uiSettings, charts } = useKibanaContextForPlugin().services; @@ -200,25 +202,27 @@ export const ExpressionChart: React.FC = ({ /> -
- {series.id !== 'ALL' ? ( - - - - ) : ( - - - - )} -
+ {!hideTitle && ( +
+ {series.id !== 'ALL' ? ( + + + + ) : ( + + + + )} +
+ )} ); }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/mocks/metric_threshold_rule.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/mocks/metric_threshold_rule.ts index 3f579a56ce7a3..812a8864cffd7 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/mocks/metric_threshold_rule.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/mocks/metric_threshold_rule.ts @@ -130,18 +130,29 @@ export const buildMetricThresholdAlert = ( 'kibana.alert.rule.parameters': { criteria: [ { - aggType: 'avg', - comparator: '>', - threshold: [0.1], - timeSize: 1, + aggType: Aggregators.AVERAGE, + comparator: Comparator.GT, + threshold: [2000], + timeSize: 15, timeUnit: 'm', metric: 'system.cpu.user.pct', }, + { + aggType: Aggregators.MAX, + comparator: Comparator.GT, + threshold: [4], + timeSize: 15, + timeUnit: 'm', + metric: 'system.cpu.user.pct', + warningComparator: Comparator.GT, + warningThreshold: [2.2], + }, ], sourceId: 'default', alertOnNoData: true, alertOnGroupDisappear: true, }, + 'kibana.alert.evaluation.values': [2500, 5], 'kibana.alert.rule.category': 'Metric threshold', 'kibana.alert.rule.consumer': 'alerts', 'kibana.alert.rule.execution.uuid': '62dd07ef-ead9-4b1f-a415-7c83d03925f7', diff --git a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx index d994669fbc6b0..9572c287257de 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -130,6 +130,7 @@ export function AlertDetails() { rule={rule} timeZone={timeZone} setAlertSummaryFields={setSummaryFields} + ruleLink={http.basePath.prepend(paths.observability.ruleDetails(rule.id))} /> )}