[AO] Add alert time range annotation for metric threshold (#154440)

Resolves #153851

## Summary

This PR adds alert time range annotation for the metric threshold rule
details page.


![image](https://user-images.githubusercontent.com/12370520/230080136-434bb586-c00e-4f36-aa11-525a38f8a650.png)
**Note**
I changed the rule criteria to stop the alert, hence the weird graph!

## 🧪 How to test
1. Add `xpack.observability.unsafe.alertDetails.metrics.enabled: true`
to the Kibana config
2. Generate a metric threshold alert
3. Go to the related alert details page and check the annotation
This commit is contained in:
Maryam Saeidi 2023-04-06 00:33:53 +02:00 committed by GitHub
parent 625966da23
commit 7dba0145ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 86 additions and 17 deletions

View file

@ -6,4 +6,5 @@
*/
export { AlertAnnotation } from './src/components/alert_annotation';
export { getAlertTimeRange } from './src/helpers/get_alert_time_range';
export { AlertActiveTimeRangeAnnotation } from './src/components/alert_active_time_range_annotation';
export { getPaddedAlertTimeRange } from './src/helpers/get_padded_alert_time_range';

View file

@ -0,0 +1,43 @@
/*
* 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 { RectAnnotation } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
interface Props {
alertStart: number;
alertEnd?: number;
color: string;
id: string;
}
const RECT_ANNOTATION_TITLE = i18n.translate(
'observabilityAlertDetails.alertActiveTimeRangeAnnotation.detailsTooltip',
{
defaultMessage: 'Active',
}
);
export function AlertActiveTimeRangeAnnotation({ alertStart, alertEnd, color, id }: Props) {
return (
<RectAnnotation
id={id}
dataValues={[
{
coordinates: {
y0: 0,
x0: alertStart,
x1: alertEnd,
},
details: RECT_ANNOTATION_TITLE,
},
]}
style={{ fill: color, opacity: 0.1 }}
/>
);
}

View file

@ -18,9 +18,12 @@ interface Props {
id: string;
}
const ANNOTATION_TITLE = i18n.translate('observabilityAlertDetails.alertAnnotation.title', {
defaultMessage: 'Alert started',
});
const ANNOTATION_TITLE = i18n.translate(
'observabilityAlertDetails.alertAnnotation.detailsTooltip',
{
defaultMessage: 'Alert started',
}
);
export function AlertAnnotation({ alertStart, color, dateFormat, id }: Props) {
return (

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { getAlertTimeRange } from './get_alert_time_range';
import { getPaddedAlertTimeRange } from './get_padded_alert_time_range';
describe('getAlertTimeRange', () => {
describe('getPaddedAlertTimeRange', () => {
const mockedDate = '2023-03-28T09:22:32.660Z';
const mockDate = jest
.spyOn(global.Date, 'now')
@ -31,7 +31,7 @@ describe('getAlertTimeRange', () => {
];
it.each(testData)('%s', (_, start, end, output) => {
expect(getAlertTimeRange(start, end)).toEqual(output);
expect(getPaddedAlertTimeRange(start, end)).toEqual(output);
});
describe('active alert', () => {
@ -43,7 +43,7 @@ describe('getAlertTimeRange', () => {
from: '2023-03-28T03:45:02.660Z',
to: mockedDate,
};
expect(getAlertTimeRange(start)).toEqual(output);
expect(getPaddedAlertTimeRange(start)).toEqual(output);
});
it('with end time than 10 minutes before now', () => {
@ -55,7 +55,7 @@ describe('getAlertTimeRange', () => {
from: '2023-03-28T04:47:32.660Z',
to: mockedDate,
};
expect(getAlertTimeRange(start, end)).toEqual(output);
expect(getPaddedAlertTimeRange(start, end)).toEqual(output);
});
});
});

View file

@ -13,7 +13,7 @@ export interface TimeRange {
interval?: string;
}
export const getAlertTimeRange = (alertStart: string, alertEnd?: string): TimeRange => {
export const getPaddedAlertTimeRange = (alertStart: string, alertEnd?: string): TimeRange => {
const alertDuration = moment.duration(moment(alertEnd).diff(moment(alertStart)));
const now = moment().toISOString();
const durationMs =

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertDetailsAppSection should render annotation 1`] = `
exports[`AlertDetailsAppSection should render annotations 1`] = `
Array [
Object {
"annotations": Array [
@ -8,7 +8,12 @@ Array [
alertStart={1678716383695}
color="#BD271E"
dateFormat="YYYY-MM-DD HH:mm"
id="annotation_alert_start"
id="alert_start_annotation"
/>,
<AlertActiveTimeRangeAnnotation
alertStart={1678716383695}
color="#BD271E"
id="alert_time_range_annotation"
/>,
],
"chartType": "line",

View file

@ -19,7 +19,11 @@ import { ExpressionChart } from './expression_chart';
jest.mock('@kbn/observability-alert-details', () => ({
AlertAnnotation: () => {},
getAlertTimeRange: () => ({ from: '2023-03-28T10:43:13.802Z', to: '2023-03-29T13:14:09.581Z' }),
AlertActiveTimeRangeAnnotation: () => {},
getPaddedAlertTimeRange: () => ({
from: '2023-03-28T10:43:13.802Z',
to: '2023-03-29T13:14:09.581Z',
}),
}));
jest.mock('./expression_chart', () => ({
@ -61,7 +65,7 @@ describe('AlertDetailsAppSection', () => {
expect((await result.findByTestId('metricThresholdAppSection')).children.length).toBe(3);
});
it('should render annotation', async () => {
it('should render annotations', async () => {
const mockedExpressionChart = jest.fn(() => <div data-test-subj="ExpressionChart" />);
(ExpressionChart as jest.Mock).mockImplementation(mockedExpressionChart);
renderComponent();

View file

@ -6,11 +6,16 @@
*/
import React, { 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 { Rule } from '@kbn/alerting-plugin/common';
import { AlertAnnotation, getAlertTimeRange } from '@kbn/observability-alert-details';
import {
AlertAnnotation,
getPaddedAlertTimeRange,
AlertActiveTimeRangeAnnotation,
} from '@kbn/observability-alert-details';
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';
@ -28,7 +33,8 @@ export type MetricThresholdRule = Rule<
export type MetricThresholdAlert = TopAlert;
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm';
const ALERT_START_ANNOTATION_ID = 'annotation_alert_start';
const ALERT_START_ANNOTATION_ID = 'alert_start_annotation';
const ALERT_TIME_RANGE_ANNOTATION_ID = 'alert_time_range_annotation';
interface AppSectionProps {
rule: MetricThresholdRule;
@ -44,7 +50,8 @@ export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) {
() => createDerivedIndexPattern(),
[createDerivedIndexPattern]
);
const timeRange = getAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]);
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 = [
<AlertAnnotation
key={ALERT_START_ANNOTATION_ID}
@ -53,6 +60,12 @@ export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) {
dateFormat={uiSettings.get('dateFormat') || DEFAULT_DATE_FORMAT}
id={ALERT_START_ANNOTATION_ID}
/>,
<AlertActiveTimeRangeAnnotation
alertStart={alert.start}
alertEnd={alertEnd}
color={euiTheme.colors.danger}
id={ALERT_TIME_RANGE_ANNOTATION_ID}
/>,
];
return !!rule.params.criteria ? (