Skip to content

Commit

Permalink
[AO] Adding charts to the alert details page for metric threshold (#1…
Browse files Browse the repository at this point in the history
…52697)

Closes #151412

## 📝 Summary 
This PR adds preview charts to the metric threshold's rule details page.

![image](https://user-images.githubusercontent.com/12370520/223083505-522dbb7c-d65f-4c79-bcd1-9cf04923d964.png)

**Note**
- This is only the first step, so there are limitations that will be
tackled in the follow-up PRs, such as:
  - Allow custom time range for preview chart
  - Adding extra alert annotation on the preview charts
  - Sync chart pointers

## ✅ Acceptance Criteria
- Create a metric threshold rule that fires and check the related alert
details page
  • Loading branch information
maryam-saeidi authored Mar 21, 2023
1 parent 3872fd6 commit 563ee27
Show file tree
Hide file tree
Showing 15 changed files with 365 additions and 52 deletions.
10 changes: 1 addition & 9 deletions x-pack/plugins/infra/common/alerting/metrics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,12 @@ interface BaseMetricExpressionParams {
}

export interface NonCountMetricExpressionParams extends BaseMetricExpressionParams {
aggType: Exclude<Aggregators, Aggregators.COUNT>;
aggType: Exclude<Aggregators, [Aggregators.COUNT, Aggregators.CUSTOM]>;
metric: string;
customMetrics: never;
equation: never;
label: never;
}

export interface CountMetricExpressionParams extends BaseMetricExpressionParams {
aggType: Aggregators.COUNT;
metric: never;
customMetrics: never;
equation: never;
label: never;
}

export type CustomMetricAggTypes = Exclude<
Expand All @@ -128,7 +121,6 @@ export interface MetricExpressionCustomMetric {

export interface CustomMetricExpressionParams extends BaseMetricExpressionParams {
aggType: Aggregators.CUSTOM;
metric: never;
customMetrics: MetricExpressionCustomMetric[];
equation?: string;
label?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { coreMock as mockCoreMock } from '@kbn/core/public/mocks';
import React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render } from '@testing-library/react';
import { buildMetricThresholdRule } from '../mocks/metric_threshold_rule';
import AlertDetailsAppSection from './alert_details_app_section';

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

jest.mock('../../../containers/metrics_source/use_source_via_http', () => ({
useSourceViaHttp: () => ({
source: { id: 'default' },
createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }),
}),
}));

describe('AlertDetailsAppSection', () => {
const renderComponent = () => {
return render(
<IntlProvider locale="en">
<AlertDetailsAppSection rule={buildMetricThresholdRule()} />
</IntlProvider>
);
};

it('should render rule data correctly', async () => {
const result = renderComponent();

expect((await result.findByTestId('metricThresholdAppSection')).children.length).toBe(3);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import React, { useMemo } from 'react';
import { Rule } from '@kbn/alerting-plugin/common';
import { MetricThresholdRuleTypeParams } from '..';
import { generateUniqueKey } from '../lib/generate_unique_key';
import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { ExpressionChart } from './expression_chart';
import { useSourceViaHttp } from '../../../containers/metrics_source/use_source_via_http';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';

// TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690
interface AppSectionProps {
rule: Rule<
MetricThresholdRuleTypeParams & {
filterQueryText?: string;
groupBy?: string | string[];
}
>;
}

export function AlertDetailsAppSection({ rule }: AppSectionProps) {
const { http, notifications } = useKibanaContextForPlugin().services;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
fetch: http.fetch,
toastWarning: notifications.toasts.addWarning,
});
const derivedIndexPattern = useMemo(
() => createDerivedIndexPattern(),
[createDerivedIndexPattern]
);

return !!rule.params.criteria ? (
<EuiFlexGroup direction="column" data-test-subj="metricThresholdAppSection">
{rule.params.criteria.map((criterion) => (
<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}
/>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGroup>
) : null;
}

// eslint-disable-next-line import/no-default-export
export default AlertDetailsAppSection;
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface Props {
source: MetricsSourceConfiguration | null;
filterQuery?: string;
groupBy?: string | string[];
chartType?: MetricsExplorerChartType;
}

export const ExpressionChart: React.FC<Props> = ({
Expand All @@ -51,6 +52,7 @@ export const ExpressionChart: React.FC<Props> = ({
source,
filterQuery,
groupBy,
chartType = MetricsExplorerChartType.bar,
}) => {
const { loading, data } = useMetricsExplorerChartData(
expression,
Expand Down Expand Up @@ -137,7 +139,7 @@ export const ExpressionChart: React.FC<Props> = ({
<ChartContainer>
<Chart>
<MetricExplorerSeriesChart
type={MetricsExplorerChartType.bar}
type={chartType}
metric={metric}
id="0"
series={series}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function validateMetricThreshold({
);
}

if (!c.metric && c.aggType !== 'count' && c.aggType !== 'custom') {
if (c.aggType !== 'count' && c.aggType !== 'custom' && !c.metric) {
errors[id].metric.push(
i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', {
defaultMessage: 'Metric is required.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { i18n } from '@kbn/i18n';
import React from 'react';
import { lazy } from 'react';
import { RuleTypeParams } from '@kbn/alerting-plugin/common';
import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public';
import {
Expand All @@ -16,7 +16,7 @@ import {
import { validateMetricThreshold } from './components/validation';
import { formatReason } from './rule_data_formatters';

interface MetricThresholdRuleTypeParams extends RuleTypeParams {
export interface MetricThresholdRuleTypeParams extends RuleTypeParams {
criteria: MetricExpressionParams[];
}

Expand All @@ -30,7 +30,7 @@ export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel<Metr
documentationUrl(docLinks) {
return `${docLinks.links.observability.metricsThreshold}`;
},
ruleParamsExpression: React.lazy(() => import('./components/expression')),
ruleParamsExpression: lazy(() => import('./components/expression')),
validate: validateMetricThreshold,
defaultActionMessage: i18n.translate(
'xpack.infra.metrics.alerting.threshold.defaultActionMessage',
Expand All @@ -44,5 +44,6 @@ Reason:
),
requiresAppContext: false,
format: formatReason,
alertDetailsAppSection: lazy(() => import('./components/alert_details_app_section')),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 { Aggregators, Comparator } from '../../../../common/alerting/metrics';
import { MetricExpression } from '../types';
import { generateUniqueKey } from './generate_unique_key';

describe('generateUniqueKey', () => {
const mockedCriteria: Array<[MetricExpression, string]> = [
[
{
aggType: Aggregators.COUNT,
comparator: Comparator.LT,
threshold: [2000, 5000],
timeSize: 15,
timeUnit: 'm',
},
'count<2000,5000',
],
[
{
aggType: Aggregators.CUSTOM,
comparator: Comparator.GT_OR_EQ,
threshold: [30],
timeSize: 15,
timeUnit: 'm',
},
'custom>=30',
],
[
{
aggType: Aggregators.AVERAGE,
comparator: Comparator.LT_OR_EQ,
threshold: [500],
timeSize: 15,
timeUnit: 'm',
metric: 'metric',
},
'avg(metric)<=500',
],
];
it.each(mockedCriteria)('unique key of %p is %s', (input, output) => {
const uniqueKey = generateUniqueKey(input);

expect(uniqueKey).toBe(output);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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 { MetricExpression } from '../types';

export const generateUniqueKey = (criterion: MetricExpression) => {
const metric = criterion.metric ? `(${criterion.metric})` : '';

return criterion.aggType + metric + criterion.comparator + criterion.threshold.join(',');
};
Original file line number Diff line number Diff line change
@@ -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 { Rule } from '@kbn/alerting-plugin/common';
import { v4 as uuidv4 } from 'uuid';
import { Aggregators, Comparator } from '../../../../common/alerting/metrics';
import { MetricThresholdRuleTypeParams } from '..';

export const buildMetricThresholdRule = (
rule: Partial<Rule<MetricThresholdRuleTypeParams>> = {}
): Rule<MetricThresholdRuleTypeParams> => {
return {
alertTypeId: 'metrics.alert.threshold',
createdBy: 'admin',
updatedBy: 'admin',
createdAt: new Date('2023-02-20T15:25:32.125Z'),
updatedAt: new Date('2023-03-02T16:24:41.177Z'),
apiKey: 'apiKey',
apiKeyOwner: 'admin',
notifyWhen: null,
muteAll: false,
mutedInstanceIds: [],
snoozeSchedule: [],
executionStatus: {
lastExecutionDate: new Date('2023-03-10T12:58:07.823Z'),
lastDuration: 3882,
status: 'ok',
},
actions: [],
scheduledTaskId: 'cfd9c4f0-b132-11ed-88f2-77e0607bce49',
isSnoozedUntil: null,
lastRun: {
outcomeMsg: null,
outcomeOrder: 0,
alertsCount: {
new: 0,
ignored: 0,
recovered: 0,
active: 0,
},
warning: null,
outcome: 'succeeded',
},
nextRun: new Date('2023-03-10T12:59:07.592Z'),
id: uuidv4(),
consumer: 'alerts',
tags: [],
name: 'Monitoring hosts',
enabled: true,
throttle: null,
running: false,
schedule: {
interval: '1m',
},
params: {
criteria: [
{
aggType: Aggregators.COUNT,
comparator: Comparator.GT,
threshold: [2000],
timeSize: 15,
timeUnit: 'm',
},
{
aggType: Aggregators.MAX,
comparator: Comparator.GT,
threshold: [4],
timeSize: 15,
timeUnit: 'm',
metric: 'system.cpu.user.pct',
warningComparator: Comparator.GT,
warningThreshold: [2.2],
},
{
aggType: Aggregators.MIN,
comparator: Comparator.GT,
threshold: [0.8],
timeSize: 15,
timeUnit: 'm',
metric: 'system.memory.used.pct',
},
],
filterQuery:
'{"bool":{"filter":[{"bool":{"should":[{"term":{"host.hostname":{"value":"Maryams-MacBook-Pro.local"}}}],"minimum_should_match":1}},{"bool":{"should":[{"term":{"service.type":{"value":"system"}}}],"minimum_should_match":1}}]}}',
groupBy: ['host.hostname'],
},
monitoring: {
run: {
history: [
{
duration: 4433,
success: true,
timestamp: 1678375661786,
},
],
calculated_metrics: {
success_ratio: 1,
p99: 7745,
p50: 4909.5,
p95: 6319,
},
last_run: {
timestamp: '2023-03-10T12:58:07.823Z',
metrics: {
total_search_duration_ms: null,
total_indexing_duration_ms: null,
total_alerts_detected: null,
total_alerts_created: null,
gap_duration_s: null,
duration: 3882,
},
},
},
},
revision: 1,
...rule,
};
};
Loading

0 comments on commit 563ee27

Please sign in to comment.