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

[AO] Add threshold information to the metric threshold alert details page #155493

Merged
merged 8 commits into from
Apr 25, 2023
Original file line number Diff line number Diff line change
@@ -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);
}
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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]',
});

let formatter;
maryam-saeidi marked this conversation as resolved.
Show resolved Hide resolved
if (metric.endsWith('.pct')) {
formatter = createFormatter('percent');
} else {
formatter = createFormatter('highPrecision');
}

return value !== null && value !== undefined ? formatter(value) : noDataValue;
maryam-saeidi marked this conversation as resolved.
Show resolved Hide resolved
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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: () => {},
Expand All @@ -32,7 +36,10 @@ jest.mock('./expression_chart', () => ({

jest.mock('../../../hooks/use_kibana', () => ({
useKibanaContextForPlugin: () => ({
services: mockCoreMock.createStart(),
services: {
...mockCoreMock.createStart(),
charts: mockedChartStartContract,
},
}),
}));

Expand All @@ -46,23 +53,48 @@ jest.mock('../../../containers/metrics_source/source', () => ({

describe('AlertDetailsAppSection', () => {
const queryClient = new QueryClient();
const mockedSetAlertSummaryFields = jest.fn();
const ruleLink = 'ruleLink';
const renderComponent = () => {
return render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<AlertDetailsAppSection
alert={buildMetricThresholdAlert()}
rule={buildMetricThresholdRule()}
ruleLink={ruleLink}
setAlertSummaryFields={mockedSetAlertSummaryFields}
/>
</QueryClientProvider>
</IntlProvider>
);
};

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: (
<EuiLink data-test-subj="alertDetailsAppSectionRuleLink" href={ruleLink}>
Monitoring hosts
</EuiLink>
),
},
]);
});

it('should render annotations', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,19 +51,30 @@ 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<React.SetStateAction<AlertSummaryField[] | undefined>>;
}

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();

const derivedIndexPattern = useMemo(
() => 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 = [
Expand All @@ -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: (
<EuiLink data-test-subj="alertDetailsAppSectionRuleLink" href={ruleLink}>
{rule.name}
</EuiLink>
),
},
]);
}, [alert, rule, ruleLink, setAlertSummaryFields]);

return !!rule.params.criteria ? (
<EuiFlexGroup direction="column" data-test-subj="metricThresholdAppSection">
{rule.params.criteria.map((criterion) => (
{rule.params.criteria.map((criterion, index) => (
<EuiFlexItem key={generateUniqueKey(criterion)}>
<EuiPanel hasBorder hasShadow={false}>
<ExpressionChart
expression={criterion}
derivedIndexPattern={derivedIndexPattern}
source={source}
filterQuery={rule.params.filterQueryText}
groupBy={rule.params.groupBy}
chartType={MetricsExplorerChartType.line}
timeRange={timeRange}
annotations={annotations}
/>
<EuiTitle size="xs">
<h4>
{criterion.aggType.toUpperCase()}{' '}
{'metric' in criterion ? criterion.metric : undefined}
</h4>
</EuiTitle>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.infra.metrics.alertDetailsAppSection.criterion.subtitle"
defaultMessage="Last {lookback} {timeLabel}"
values={{
lookback: criterion.timeSize,
timeLabel: TIME_LABELS[criterion.timeUnit as keyof typeof TIME_LABELS],
}}
/>
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexItem style={{ minHeight: 150, minWidth: 160 }} grow={1}>
<Threshold
chartProps={chartProps}
id={`threshold-${generateUniqueKey(criterion)}`}
threshold={criterion.threshold[0]}
value={alert.fields[ALERT_EVALUATION_VALUES]![index]}
valueFormatter={(d) =>
metricValueFormatter(d, 'metric' in criterion ? criterion.metric : undefined)
}
title={i18n.translate(
'xpack.infra.metrics.alertDetailsAppSection.thresholdTitle',
{
defaultMessage: 'Threshold breached',
}
)}
comparator={criterion.comparator}
/>
</EuiFlexItem>
<EuiFlexItem grow={5}>
<ExpressionChart
annotations={annotations}
chartType={MetricsExplorerChartType.line}
derivedIndexPattern={derivedIndexPattern}
expression={criterion}
filterQuery={rule.params.filterQueryText}
groupBy={rule.params.groupBy}
hideTitle
source={source}
timeRange={timeRange}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,25 @@ import { CUSTOM_EQUATION } from '../i18n_strings';
interface Props {
expression: MetricExpression;
derivedIndexPattern: DataViewBase;
source?: MetricsSourceConfiguration;
annotations?: Array<ReactElement<typeof RectAnnotation | typeof LineAnnotation>>;
chartType?: MetricsExplorerChartType;
filterQuery?: string;
groupBy?: string | string[];
chartType?: MetricsExplorerChartType;
hideTitle?: boolean;
source?: MetricsSourceConfiguration;
timeRange?: TimeRange;
annotations?: Array<ReactElement<typeof RectAnnotation | typeof LineAnnotation>>;
}

export const ExpressionChart: React.FC<Props> = ({
expression,
derivedIndexPattern,
source,
annotations,
chartType = MetricsExplorerChartType.bar,
filterQuery,
groupBy,
chartType = MetricsExplorerChartType.bar,
hideTitle = false,
source,
timeRange,
annotations,
}) => {
const { uiSettings } = useKibanaContextForPlugin().services;

Expand Down Expand Up @@ -187,25 +189,27 @@ export const ExpressionChart: React.FC<Props> = ({
<Settings tooltip={tooltipProps} theme={getChartTheme(isDarkMode)} />
</Chart>
</ChartContainer>
<div style={{ textAlign: 'center' }}>
{series.id !== 'ALL' ? (
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.metrics.alerts.dataTimeRangeLabelWithGrouping"
defaultMessage="Last {lookback} {timeLabel} of data for {id}"
values={{ id: series.id, timeLabel, lookback: timeSize! * 20 }}
/>
</EuiText>
) : (
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.metrics.alerts.dataTimeRangeLabel"
defaultMessage="Last {lookback} {timeLabel}"
values={{ timeLabel, lookback: timeSize! * 20 }}
/>
</EuiText>
)}
</div>
{!hideTitle && (
<div style={{ textAlign: 'center' }}>
{series.id !== 'ALL' ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe extracting this would make it easier to read?

<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.metrics.alerts.dataTimeRangeLabelWithGrouping"
defaultMessage="Last {lookback} {timeLabel} of data for {id}"
values={{ id: series.id, timeLabel, lookback: timeSize! * 20 }}
/>
</EuiText>
) : (
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.metrics.alerts.dataTimeRangeLabel"
defaultMessage="Last {lookback} {timeLabel}"
values={{ timeLabel, lookback: timeSize! * 20 }}
/>
</EuiText>
)}
</div>
)}
</>
);
};
Loading