diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 4b72b6389b49f..38a6542b9f788 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -99,19 +99,12 @@ interface BaseMetricExpressionParams { } export interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { - aggType: Exclude; + aggType: Exclude; 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< @@ -128,7 +121,6 @@ export interface MetricExpressionCustomMetric { export interface CustomMetricExpressionParams extends BaseMetricExpressionParams { aggType: Aggregators.CUSTOM; - metric: never; customMetrics: MetricExpressionCustomMetric[]; equation?: string; label?: string; 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 new file mode 100644 index 0000000000000..ef30035f220d4 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx @@ -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( + + + + ); + }; + + it('should render rule data correctly', async () => { + const result = renderComponent(); + + expect((await result.findByTestId('metricThresholdAppSection')).children.length).toBe(3); + }); +}); 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 new file mode 100644 index 0000000000000..86d29c627927b --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx @@ -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 ? ( + + {rule.params.criteria.map((criterion) => ( + + + + + + ))} + + ) : null; +} + +// eslint-disable-next-line import/no-default-export +export default AlertDetailsAppSection; 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 8fe00f5a34c73..0ce6a5ac9d13d 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 @@ -43,6 +43,7 @@ interface Props { source: MetricsSourceConfiguration | null; filterQuery?: string; groupBy?: string | string[]; + chartType?: MetricsExplorerChartType; } export const ExpressionChart: React.FC = ({ @@ -51,6 +52,7 @@ export const ExpressionChart: React.FC = ({ source, filterQuery, groupBy, + chartType = MetricsExplorerChartType.bar, }) => { const { loading, data } = useMetricsExplorerChartData( expression, @@ -137,7 +139,7 @@ export const ExpressionChart: React.FC = ({ import('./components/expression')), + ruleParamsExpression: lazy(() => import('./components/expression')), validate: validateMetricThreshold, defaultActionMessage: i18n.translate( 'xpack.infra.metrics.alerting.threshold.defaultActionMessage', @@ -44,5 +44,6 @@ Reason: ), requiresAppContext: false, format: formatReason, + alertDetailsAppSection: lazy(() => import('./components/alert_details_app_section')), }; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/generate_unique_key.test.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/generate_unique_key.test.ts new file mode 100644 index 0000000000000..a9460e700f38b --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/generate_unique_key.test.ts @@ -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); + }); +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/generate_unique_key.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/generate_unique_key.ts new file mode 100644 index 0000000000000..ec83311055a08 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/generate_unique_key.ts @@ -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(','); +}; 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 new file mode 100644 index 0000000000000..d1e79edef1f19 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/mocks/metric_threshold_rule.ts @@ -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 => { + 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, + }; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index aa9336cb6023d..2aecaf4f4febe 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { FilterQuery, MetricExpressionParams } from '../../../common/alerting/metrics'; +import { + CustomMetricExpressionParams, + FilterQuery, + MetricExpressionParams, + NonCountMetricExpressionParams, +} from '../../../common/alerting/metrics'; import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions } from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; @@ -18,10 +23,10 @@ export type MetricExpression = Omit< MetricExpressionParams, 'metric' | 'timeSize' | 'timeUnit' | 'metrics' | 'equation' | 'customMetrics' > & { - metric?: MetricExpressionParams['metric']; - customMetrics?: MetricExpressionParams['customMetrics']; - label?: MetricExpressionParams['label']; - equation?: MetricExpressionParams['equation']; + metric?: NonCountMetricExpressionParams['metric']; + customMetrics?: CustomMetricExpressionParams['customMetrics']; + label?: CustomMetricExpressionParams['label']; + equation?: CustomMetricExpressionParams['equation']; timeSize?: MetricExpressionParams['timeSize']; timeUnit?: MetricExpressionParams['timeUnit']; }; @@ -53,11 +58,6 @@ export interface ExpressionChartRow { export type ExpressionChartSeries = ExpressionChartRow[][]; -export interface ExpressionChartData { - id: string; - series: ExpressionChartSeries; -} - export interface AlertParams { criteria: MetricExpression[]; groupBy?: string | string[]; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index ccb1d316d73d2..6f385ba3d5193 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -50,7 +50,7 @@ export function useMetricsExplorerData( createPromise: () => { setLoading(true); if (!from || !to) { - return Promise.reject(new Error('Unalble to parse timerange')); + return Promise.reject(new Error('Unable to parse timerange')); } if (!fetchFn) { return Promise.reject(new Error('HTTP service is unavailable')); @@ -58,9 +58,6 @@ export function useMetricsExplorerData( if (!source) { return Promise.reject(new Error('Source is unavailable')); } - if (!fetchFn) { - return Promise.reject(new Error('HTTP service is unavailable')); - } return fetchFn('/api/infra/metrics_explorer', { method: 'POST', diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts index 7125f18aa2a8d..03fc6eec0f792 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts @@ -8,6 +8,7 @@ import { ElasticsearchClient } from '@kbn/core/server'; import moment from 'moment'; import type { Logger } from '@kbn/logging'; +import { isCustom } from './metric_expression_params'; import { MetricExpressionParams } from '../../../../../common/alerting/metrics'; import { InfraSource } from '../../../../../common/source_configuration/source_configuration'; import { getIntervalInSeconds } from '../../../../../common/utils/get_interval_in_seconds'; @@ -104,7 +105,7 @@ export const evaluateRule = async { + const { aggType } = metricExpressionParams; + return aggType !== 'count' && aggType !== 'custom'; +}; + +export const isCustom = ( + metricExpressionParams: MetricExpressionParams +): metricExpressionParams is CustomMetricExpressionParams => { + const { aggType } = metricExpressionParams; + return aggType === 'custom'; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts index 966ac767bf458..381465fa198d9 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts @@ -6,18 +6,22 @@ */ import moment from 'moment'; -import { Comparator, MetricExpressionParams } from '../../../../../common/alerting/metrics'; +import { + Aggregators, + Comparator, + MetricExpressionParams, +} from '../../../../../common/alerting/metrics'; import { getElasticsearchMetricQuery } from './metric_query'; describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { - const expressionParams = { + const expressionParams: MetricExpressionParams = { metric: 'system.is.a.good.puppy.dog', - aggType: 'avg', + aggType: Aggregators.AVERAGE, timeUnit: 'm', timeSize: 1, threshold: [1], comparator: Comparator.GT, - } as MetricExpressionParams; + }; const groupBy = 'host.doggoname'; const timeframe = { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 4593ffd8a2028..df52fe3f0e068 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; +import { isCustom, isNotCountOrCustom } from './metric_expression_params'; import { Aggregators, MetricExpressionParams } from '../../../../../common/alerting/metrics'; import { createCustomMetricsAggregations } from '../../../create_custom_metrics_aggregations'; import { @@ -46,7 +47,6 @@ export const createBaseFilters = ( timeframe: { start: number; end: number }, filterQuery?: string ) => { - const { metric } = metricParams; const rangeFilters = [ { range: { @@ -58,15 +58,16 @@ export const createBaseFilters = ( }, ]; - const metricFieldFilters = metric - ? [ - { - exists: { - field: metric, + const metricFieldFilters = + isNotCountOrCustom(metricParams) && metricParams.metric + ? [ + { + exists: { + field: metricParams.metric, + }, }, - }, - ] - : []; + ] + : []; const parsedFilterQuery = getParsedFilterQuery(filterQuery); @@ -84,12 +85,11 @@ export const getElasticsearchMetricQuery = ( afterKey?: Record, fieldsExisted?: Record | null ) => { - const { metric, aggType } = metricParams; - if (aggType === Aggregators.COUNT && metric) { - throw new Error('Cannot aggregate document count with a metric'); - } - if (!['count', 'custom'].includes(aggType) && !metric) { - throw new Error('Can only aggregate without a metric if using the document count aggregator'); + const { aggType } = metricParams; + if (isNotCountOrCustom(metricParams) && !metricParams.metric) { + throw new Error( + 'Can only aggregate without a metric if using the document count or custom aggregator' + ); } // We need to make a timeframe that represents the current timeframe as oppose @@ -100,10 +100,10 @@ export const getElasticsearchMetricQuery = ( aggType === Aggregators.COUNT ? {} : aggType === Aggregators.RATE - ? createRateAggsBuckets(currentTimeframe, 'aggregatedValue', metric) + ? createRateAggsBuckets(currentTimeframe, 'aggregatedValue', metricParams.metric) : aggType === Aggregators.P95 || aggType === Aggregators.P99 - ? createPercentileAggregation(aggType, metric) - : aggType === Aggregators.CUSTOM + ? createPercentileAggregation(aggType, metricParams.metric) + : isCustom(metricParams) ? createCustomMetricsAggregations( 'aggregatedValue', metricParams.customMetrics, @@ -112,7 +112,7 @@ export const getElasticsearchMetricQuery = ( : { aggregatedValue: { [aggType]: { - field: metric, + field: metricParams.metric, }, }, };