From bcca9331b568659a908a73d3f6b6c64bd75e0623 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 20 Aug 2020 13:28:10 -0500 Subject: [PATCH] [Metrics UI] Get custom metrics working in inventory alerts with limited UI (#75073) --- .../formatters}/get_custom_metric_label.ts | 2 +- .../inventory/components/alert_flyout.tsx | 17 +- .../inventory/components/expression.test.tsx | 186 ++++++++++++++++++ .../inventory/components/expression.tsx | 86 ++++++-- .../alerting/inventory/components/metric.tsx | 7 +- .../hooks/use_inventory_alert_prefill.ts | 8 +- .../waffle/metric_control/index.tsx | 2 +- .../metric_control/metrics_context_menu.tsx | 2 +- .../metric_control/metrics_edit_mode.tsx | 2 +- .../hooks/use_waffle_options.test.ts | 19 ++ .../hooks/use_waffle_options.ts | 3 +- .../evaluate_condition.ts | 15 +- .../inventory_metric_threshold_executor.ts | 33 ++-- ...r_inventory_metric_threshold_alert_type.ts | 9 + .../inventory_metric_threshold/types.ts | 2 + 15 files changed, 348 insertions(+), 45 deletions(-) rename x-pack/plugins/infra/{public/pages/metrics/inventory_view/components/waffle/metric_control => common/formatters}/get_custom_metric_label.ts (91%) create mode 100644 x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/get_custom_metric_label.ts b/x-pack/plugins/infra/common/formatters/get_custom_metric_label.ts similarity index 91% rename from x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/get_custom_metric_label.ts rename to x-pack/plugins/infra/common/formatters/get_custom_metric_label.ts index 495cc8197d2e7..3be5986d489d3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/get_custom_metric_label.ts +++ b/x-pack/plugins/infra/common/formatters/get_custom_metric_label.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; +import { SnapshotCustomMetricInput } from '../http_api/snapshot_api'; export const getCustomMetricLabel = (metric: SnapshotCustomMetricInput) => { const METRIC_LABELS = { diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index 804ff9602c81c..834afefd74712 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -12,6 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; import { InfraWaffleMapOptions } from '../../../lib/lib'; import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; interface Props { visible?: boolean; @@ -21,16 +22,24 @@ interface Props { setVisible: React.Dispatch>; } -export const AlertFlyout = (props: Props) => { +export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: Props) => { const { triggersActionsUI } = useContext(TriggerActionsContext); const { services } = useKibana(); + const { inventoryPrefill } = useAlertPrefillContext(); + const { customMetrics } = inventoryPrefill; + return ( <> {triggersActionsUI && ( { }} > ({ + useSourceViaHttp: () => ({ + source: { id: 'default' }, + createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), + }), +})); + +const exampleCustomMetric = { + id: 'this-is-an-id', + field: 'some.system.field', + aggregation: 'rate', + type: 'custom', +} as SnapshotCustomMetricInput; + +describe('Expression', () => { + async function setup(currentOptions: AlertContextMeta) { + const alertParams = { + criteria: [], + nodeType: undefined, + filterQueryText: '', + }; + + const mocks = coreMock.createSetup(); + const startMocks = coreMock.createStart(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + const context: AlertsContextValue = { + http: mocks.http, + toastNotifications: mocks.notifications.toasts, + actionTypeRegistry: actionTypeRegistryMock.create() as any, + alertTypeRegistry: alertTypeRegistryMock.create() as any, + docLinks: startMocks.docLinks, + capabilities: { + ...capabilities, + actions: { + delete: true, + save: true, + show: true, + }, + }, + metadata: currentOptions, + }; + + const wrapper = mountWithIntl( + Reflect.set(alertParams, key, value)} + setAlertProperty={() => {}} + /> + ); + + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await update(); + + return { wrapper, update, alertParams }; + } + + it('should prefill the alert using the context metadata', async () => { + const currentOptions = { + filter: 'foo', + nodeType: 'pod', + customMetrics: [], + options: { metric: { type: 'memory' } }, + }; + const { alertParams } = await setup(currentOptions as AlertContextMeta); + expect(alertParams.nodeType).toBe('pod'); + expect(alertParams.filterQueryText).toBe('foo'); + expect(alertParams.criteria).toEqual([ + { + metric: 'memory', + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', + }, + ]); + }); + describe('using custom metrics', () => { + it('should prefill the alert using the context metadata', async () => { + const currentOptions = { + filter: '', + nodeType: 'tx', + customMetrics: [exampleCustomMetric], + options: { metric: exampleCustomMetric }, + }; + const { alertParams, update } = await setup(currentOptions as AlertContextMeta); + await update(); + expect(alertParams.nodeType).toBe('tx'); + expect(alertParams.filterQueryText).toBe(''); + expect(alertParams.criteria).toEqual([ + { + metric: 'custom', + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', + customMetric: exampleCustomMetric, + }, + ]); + }); + }); +}); + +describe('ExpressionRow', () => { + async function setup(expression: InventoryMetricConditions) { + const wrapper = mountWithIntl( + {}} + addExpression={() => {}} + key={1} + expressionId={1} + setAlertParams={() => {}} + errors={{ + aggField: [], + timeSizeUnit: [], + timeWindowSize: [], + metric: [], + }} + expression={expression} + alertsContextMetadata={{ + customMetrics: [], + }} + /> + ); + + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await update(); + + return { wrapper, update }; + } + const expression = { + metric: 'custom', + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', + customMetric: exampleCustomMetric, + }; + + it('loads custom metrics passed in through the expression, even with an empty context', async () => { + const { wrapper } = await setup(expression as InventoryMetricConditions); + const [valueMatch] = + wrapper.html().match('Rate of some.system.field') ?? + []; + expect(valueMatch).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index 7ca17617871ff..78cabcf354437 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { debounce, pick } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { debounce, pick, uniqBy, isEqual } from 'lodash'; import { Unit } from '@elastic/datemath'; import React, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react'; import { @@ -22,6 +23,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertPreview } from '../../common'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; @@ -49,22 +51,31 @@ import { hostMetricTypes } from '../../../../common/inventory_models/host/toolba import { containerMetricTypes } from '../../../../common/inventory_models/container/toolbar_items'; import { podMetricTypes } from '../../../../common/inventory_models/pod/toolbar_items'; import { findInventoryModel } from '../../../../common/inventory_models'; -import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { + InventoryItemType, + SnapshotMetricType, + SnapshotMetricTypeRT, +} from '../../../../common/inventory_models/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; import { MetricExpression } from './metric'; import { NodeTypeExpression } from './node_type'; import { InfraWaffleMapOptions } from '../../../lib/lib'; import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; +import { + SnapshotCustomMetricInput, + SnapshotCustomMetricInputRT, +} from '../../../../common/http_api/snapshot_api'; import { validateMetricThreshold } from './validation'; const FILTER_TYPING_DEBOUNCE_MS = 500; -interface AlertContextMeta { +export interface AlertContextMeta { options?: Partial; nodeType?: InventoryItemType; filter?: string; + customMetrics?: SnapshotCustomMetricInput[]; } interface Props { @@ -89,6 +100,7 @@ const defaultExpression = { threshold: [], timeSize: 1, timeUnit: 'm', + customMetric: undefined, } as InventoryMetricConditions; export const Expressions: React.FC = (props) => { @@ -204,6 +216,9 @@ export const Expressions: React.FC = (props) => { { ...defaultExpression, metric: md.options.metric!.type, + customMetric: SnapshotCustomMetricInputRT.is(md.options.metric) + ? md.options.metric + : undefined, } as InventoryMetricConditions, ]); } else { @@ -282,6 +297,7 @@ export const Expressions: React.FC = (props) => { setAlertParams={updateParams} errors={errors[idx] || emptyError} expression={e || {}} + alertsContextMetadata={alertsContext.metadata} /> ); })} @@ -389,6 +405,7 @@ interface ExpressionRowProps { addExpression(): void; remove(id: number): void; setAlertParams(id: number, params: Partial): void; + alertsContextMetadata: AlertsContextValue['metadata']; } const StyledExpressionRow = euiStyled(EuiFlexGroup)` @@ -402,14 +419,48 @@ const StyledExpression = euiStyled.div` `; export const ExpressionRow: React.FC = (props) => { - const { setAlertParams, expression, errors, expressionId, remove, canDelete } = props; - const { metric, comparator = Comparator.GT, threshold = [] } = expression; + const { + setAlertParams, + expression, + errors, + expressionId, + remove, + canDelete, + alertsContextMetadata, + } = props; + const { metric, comparator = Comparator.GT, threshold = [], customMetric } = expression; + const [customMetrics, updateCustomMetrics] = useState([]); + + // Create and uniquify a list of custom metrics including: + // - The alert metadata context (which only gives us custom metrics on the inventory page) + // - The custom metric stored in the expression (necessary when editing this alert without having + // access to the metadata context) + // - Whatever custom metrics were previously stored in this list (to preserve the custom metric in the dropdown + // if the user edits the alert and switches away from the custom metric) + useEffect(() => { + const ctxCustomMetrics = alertsContextMetadata?.customMetrics ?? []; + const expressionCustomMetrics = customMetric ? [customMetric] : []; + const newCustomMetrics = uniqBy( + [...customMetrics, ...ctxCustomMetrics, ...expressionCustomMetrics], + (cm: SnapshotCustomMetricInput) => cm.id + ); + if (!isEqual(customMetrics, newCustomMetrics)) updateCustomMetrics(newCustomMetrics); + }, [alertsContextMetadata, customMetric, customMetrics, updateCustomMetrics]); const updateMetric = useCallback( - (m?: SnapshotMetricType) => { - setAlertParams(expressionId, { ...expression, metric: m }); + (m?: SnapshotMetricType | string) => { + const newMetric = SnapshotMetricTypeRT.is(m) ? m : 'custom'; + const newAlertParams = { ...expression, metric: newMetric }; + if (newMetric === 'custom' && customMetrics) { + set( + newAlertParams, + 'customMetric', + customMetrics.find((cm) => cm.id === m) + ); + } + setAlertParams(expressionId, newAlertParams); }, - [expressionId, expression, setAlertParams] + [expressionId, expression, setAlertParams, customMetrics] ); const updateComparator = useCallback( @@ -446,6 +497,7 @@ export const ExpressionRow: React.FC = (props) => { break; case 'host': myMetrics = hostMetricTypes; + break; case 'pod': myMetrics = podMetricTypes; @@ -454,8 +506,17 @@ export const ExpressionRow: React.FC = (props) => { myMetrics = containerMetricTypes; break; } - return myMetrics.map(toMetricOpt); - }, [props.nodeType]); + const baseMetricOpts = myMetrics.map(toMetricOpt); + const customMetricOpts = customMetrics + ? customMetrics.map((m, i) => ({ + text: getCustomMetricLabel(m), + value: m.id, + })) + : []; + return [...baseMetricOpts, ...customMetricOpts]; + }, [props.nodeType, customMetrics]); + + const selectedMetricValue = metric === 'custom' && customMetric ? customMetric.id : metric!; return ( <> @@ -465,8 +526,8 @@ export const ExpressionRow: React.FC = (props) => { v?.value === metric)?.text || '', + value: selectedMetricValue, + text: ofFields.find((v) => v?.value === selectedMetricValue)?.text || '', }} metrics={ ofFields.filter((m) => m !== undefined && m.value !== undefined) as Array<{ @@ -568,4 +629,5 @@ const metricUnit: Record = { s3DownloadBytes: { label: 'bytes' }, sqsOldestMessage: { label: 'seconds' }, rdsLatency: { label: 'ms' }, + custom: { label: '' }, }; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx index ff859a95a3d9d..2e5ccbe1a4276 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx @@ -18,13 +18,12 @@ import { import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; -import { SnapshotMetricType } from '../../../../common/inventory_models/types'; interface Props { - metric?: { value: SnapshotMetricType; text: string }; + metric?: { value: string; text: string }; metrics: Array<{ value: string; text: string }>; errors: IErrorObject; - onChange: (metric?: SnapshotMetricType) => void; + onChange: (metric?: string) => void; popupPosition?: | 'upCenter' | 'upLeft' @@ -104,7 +103,7 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit renderOption={(o: any) => o.label} onChange={(selectedOptions) => { if (selectedOptions.length > 0) { - onChange(selectedOptions[0].value as SnapshotMetricType); + onChange(selectedOptions[0].value); setAggFieldPopoverOpen(false); } else { onChange(); diff --git a/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts index d659057b95ed9..a57f9cafa5e19 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts @@ -5,20 +5,26 @@ */ import { useState } from 'react'; -import { SnapshotMetricInput } from '../../../../common/http_api/snapshot_api'; +import { + SnapshotMetricInput, + SnapshotCustomMetricInput, +} from '../../../../common/http_api/snapshot_api'; import { InventoryItemType } from '../../../../common/inventory_models/types'; export const useInventoryAlertPrefill = () => { const [nodeType, setNodeType] = useState('host'); const [filterQuery, setFilterQuery] = useState(); const [metric, setMetric] = useState({ type: 'cpu' }); + const [customMetrics, setCustomMetrics] = useState([]); return { nodeType, filterQuery, metric, + customMetrics, setNodeType, setFilterQuery, setMetric, + setCustomMetrics, }; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx index aae787c8c0395..96169810e02a8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx @@ -8,13 +8,13 @@ import { EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState, useCallback } from 'react'; import { IFieldType } from 'src/plugins/data/public'; +import { getCustomMetricLabel } from '../../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotMetricInput, SnapshotCustomMetricInput, SnapshotCustomMetricInputRT, } from '../../../../../../../common/http_api/snapshot_api'; import { CustomMetricForm } from './custom_metric_form'; -import { getCustomMetricLabel } from './get_custom_metric_label'; import { MetricsContextMenu } from './metrics_context_menu'; import { ModeSwitcher } from './mode_switcher'; import { MetricsEditMode } from './metrics_edit_mode'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx index 7ab90297ebbb3..71a000d3165b6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx @@ -5,6 +5,7 @@ */ import React, { useCallback } from 'react'; import { EuiContextMenuPanelDescriptor, EuiContextMenu } from '@elastic/eui'; +import { getCustomMetricLabel } from '../../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotMetricInput, SnapshotCustomMetricInput, @@ -14,7 +15,6 @@ import { SnapshotMetricTypeRT, SnapshotMetricType, } from '../../../../../../../common/inventory_models/types'; -import { getCustomMetricLabel } from './get_custom_metric_label'; interface Props { options: Array<{ text: string; value: string }>; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx index 649dcc4282d67..e75885ccbc917 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { getCustomMetricLabel } from '../../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; -import { getCustomMetricLabel } from './get_custom_metric_label'; import { EuiTheme, withTheme, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts index 579073e9500d0..d44d8fca4faba 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts @@ -20,6 +20,7 @@ jest.mock('react-router-dom', () => ({ // reassign them, so we can't make these both part of the same object let PREFILL_NODETYPE: WaffleOptionsState['nodeType'] | undefined; let PREFILL_METRIC: WaffleOptionsState['metric'] | undefined; +let PREFILL_CUSTOM_METRICS: WaffleOptionsState['customMetrics'] | undefined; jest.mock('../../../../alerting/use_alert_prefill', () => ({ useAlertPrefillContext: () => ({ inventoryPrefill: { @@ -29,6 +30,9 @@ jest.mock('../../../../alerting/use_alert_prefill', () => ({ setMetric(metric: WaffleOptionsState['metric']) { PREFILL_METRIC = metric; }, + setCustomMetrics(customMetrics: WaffleOptionsState['customMetrics']) { + PREFILL_CUSTOM_METRICS = customMetrics; + }, }, }), })); @@ -39,6 +43,7 @@ describe('useWaffleOptions', () => { beforeEach(() => { PREFILL_NODETYPE = undefined; PREFILL_METRIC = undefined; + PREFILL_CUSTOM_METRICS = undefined; }); it('should sync the options to the inventory alert preview context', () => { @@ -47,6 +52,15 @@ describe('useWaffleOptions', () => { const newOptions = { nodeType: 'pod', metric: { type: 'memory' }, + customMetrics: [ + { + type: 'custom', + id: + "i don't want to bother to copy and paste an actual uuid so instead i'm going to smash my keyboard skjdghsjodkyjheurvjnsgn", + aggregation: 'avg', + field: 'hey.system.are.you.good', + }, + ], } as WaffleOptionsState; act(() => { result.current.changeNodeType(newOptions.nodeType); @@ -58,5 +72,10 @@ describe('useWaffleOptions', () => { }); rerender(); expect(PREFILL_METRIC).toEqual(newOptions.metric); + act(() => { + result.current.changeCustomMetrics(newOptions.customMetrics); + }); + rerender(); + expect(PREFILL_CUSTOM_METRICS).toEqual(newOptions.customMetrics); }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts index 8059d1ad12a3a..35d069adc939e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts @@ -125,9 +125,10 @@ export const useWaffleOptions = () => { const { inventoryPrefill } = useAlertPrefillContext(); useEffect(() => { - const { setNodeType, setMetric } = inventoryPrefill; + const { setNodeType, setMetric, setCustomMetrics } = inventoryPrefill; setNodeType(state.nodeType); setMetric(state.metric); + setCustomMetrics(state.customMetrics); }, [state, inventoryPrefill]); return { diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 3b795810b39f0..9be6a4b52157c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -5,6 +5,7 @@ */ import { mapValues, last, first } from 'lodash'; import moment from 'moment'; +import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; import { isTooManyBucketsPreviewException, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, @@ -37,7 +38,7 @@ export const evaluateCondition = async ( filterQuery?: string, lookbackSize?: number ): Promise> => { - const { comparator, metric } = condition; + const { comparator, metric, customMetric } = condition; let { threshold } = condition; const timerange = { @@ -55,7 +56,8 @@ export const evaluateCondition = async ( metric, timerange, sourceConfiguration, - filterQuery + filterQuery, + customMetric ); threshold = threshold.map((n) => convertMetricValue(metric, n)); @@ -93,19 +95,24 @@ const getData = async ( metric: SnapshotMetricType, timerange: InfraTimerangeInput, sourceConfiguration: InfraSourceConfiguration, - filterQuery?: string + filterQuery?: string, + customMetric?: SnapshotCustomMetricInput ) => { const snapshot = new InfraSnapshot(); const esClient = ( options: CallWithRequestParams ): Promise> => callCluster('search', options); + const metrics = [ + metric === 'custom' ? (customMetric as SnapshotCustomMetricInput) : { type: metric }, + ]; + const options = { filterQuery: parseFilterQuery(filterQuery), nodeType, groupBy: [], sourceConfiguration, - metrics: [{ type: metric }], + metrics, timerange, includeTimeseries: Boolean(timerange.lookbackSize), }; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 7b816f2f225b5..db1ff26ee1810 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -6,6 +6,7 @@ import { first, get, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; +import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; import { AlertExecutorOptions } from '../../../../../alerts/server'; @@ -77,27 +78,19 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = let reason; if (nextState === AlertStates.ALERT) { reason = results - .map((result) => { - if (!result[item]) return ''; - const resultWithVerboseMetricName = { - ...result[item], - metric: toMetricOpt(result[item].metric)?.text || result[item].metric, - currentValue: formatMetric(result[item].metric, result[item].currentValue), - }; - return buildFiredAlertReason(resultWithVerboseMetricName); - }) + .map((result) => buildReasonWithVerboseMetricName(result[item], buildFiredAlertReason)) .join('\n'); } if (alertOnNoData) { if (nextState === AlertStates.NO_DATA) { reason = results .filter((result) => result[item].isNoData) - .map((result) => buildNoDataAlertReason(result[item])) + .map((result) => buildReasonWithVerboseMetricName(result[item], buildNoDataAlertReason)) .join('\n'); } else if (nextState === AlertStates.ERROR) { reason = results .filter((result) => result[item].isError) - .map((result) => buildErrorAlertReason(result[item].metric)) + .map((result) => buildReasonWithVerboseMetricName(result[item], buildErrorAlertReason)) .join('\n'); } } @@ -121,6 +114,20 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } }; +const buildReasonWithVerboseMetricName = (resultItem: any, buildReason: (r: any) => string) => { + if (!resultItem) return ''; + const resultWithVerboseMetricName = { + ...resultItem, + metric: + toMetricOpt(resultItem.metric)?.text || + (resultItem.metric === 'custom' + ? getCustomMetricLabel(resultItem.customMetric) + : resultItem.metric), + currentValue: formatMetric(resultItem.metric, resultItem.currentValue), + }; + return buildReason(resultWithVerboseMetricName); +}; + const mapToConditionsLookup = ( list: any[], mapFn: (value: any, index: number, array: any[]) => unknown @@ -140,10 +147,6 @@ export const FIRED_ACTIONS = { }; const formatMetric = (metric: SnapshotMetricType, value: number) => { - // if (SnapshotCustomMetricInputRT.is(metric)) { - // const formatter = createFormatterForMetric(metric); - // return formatter(val); - // } const metricFormatter = get(METRIC_FORMATTERS, metric, METRIC_FORMATTERS.count); if (value == null) { return ''; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index f664a59acd165..14d1acf0e4a9f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -27,6 +27,15 @@ const condition = schema.object({ timeUnit: schema.string(), timeSize: schema.number(), metric: schema.string(), + customMetric: schema.maybe( + schema.object({ + type: schema.literal('custom'), + id: schema.string(), + field: schema.string(), + aggregation: schema.string(), + label: schema.maybe(schema.string()), + }) + ), }); export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs) => ({ diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts index 86c77e6d7459a..06f5efaf9eb36 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { Unit } from '@elastic/datemath'; +import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; import { SnapshotMetricType } from '../../../../common/inventory_models/types'; import { Comparator, AlertStates } from '../common/types'; @@ -18,4 +19,5 @@ export interface InventoryMetricConditions { sourceId?: string; threshold: number[]; comparator: Comparator; + customMetric?: SnapshotCustomMetricInput; }