From 38a647539643700d66179532bec78d62ffd8676c Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 9 Feb 2021 20:30:25 -0600 Subject: [PATCH] Revert "Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)"" (#90889) * Revert "Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)"" This reverts commit 8166becc5555f132636bc1e8662370d1b4bf7b6a. * Fix type error --- .../infra/common/alerting/metrics/types.ts | 43 ++- .../infra/common/infra_ml/anomaly_results.ts | 56 +-- .../common/components/alert_preview.tsx | 5 +- .../common/components/get_alert_preview.ts | 4 +- .../components/metrics_alert_dropdown.tsx | 151 +++++++++ .../inventory/components/alert_dropdown.tsx | 59 ---- .../inventory/components/alert_flyout.tsx | 18 +- .../inventory/components/node_type.tsx | 2 +- .../components/alert_flyout.tsx | 53 +++ .../components/expression.test.tsx | 74 ++++ .../metric_anomaly/components/expression.tsx | 320 ++++++++++++++++++ .../components/influencer_filter.tsx | 193 +++++++++++ .../metric_anomaly/components/node_type.tsx | 117 +++++++ .../components/severity_threshold.tsx | 140 ++++++++ .../metric_anomaly/components/validation.tsx | 35 ++ .../public/alerting/metric_anomaly/index.ts | 46 +++ .../components/alert_dropdown.tsx | 57 ---- .../components/alert_flyout.tsx | 11 +- .../logging/log_analysis_setup/index.ts | 1 - .../subscription_splash_content.tsx | 176 ---------- .../source_configuration_settings.tsx | 4 +- .../subscription_splash_content.tsx | 110 +++--- .../containers/ml/infra_ml_capabilities.tsx | 4 +- .../containers/with_kuery_autocompletion.tsx | 11 +- .../log_entry_categories/page_content.tsx | 2 +- .../logs/log_entry_rate/page_content.tsx | 2 +- .../infra/public/pages/metrics/index.tsx | 8 +- ...lyout.tsx => anomaly_detection_flyout.tsx} | 0 .../ml/anomaly_detection/flyout_home.tsx | 6 +- .../metrics_explorer/components/kuery_bar.tsx | 21 +- x-pack/plugins/infra/public/plugin.ts | 2 + x-pack/plugins/infra/public/types.ts | 3 +- .../metric_anomaly/evaluate_condition.ts | 51 +++ .../metric_anomaly/metric_anomaly_executor.ts | 142 ++++++++ .../preview_metric_anomaly_alert.ts | 120 +++++++ .../register_metric_anomaly_alert_type.ts | 110 ++++++ .../lib/alerting/register_alert_types.ts | 10 +- .../infra/server/lib/infra_ml/common.ts | 17 + .../infra/server/lib/infra_ml/index.ts | 1 + .../lib/infra_ml/metrics_hosts_anomalies.ts | 39 +-- .../lib/infra_ml/metrics_k8s_anomalies.ts | 39 +-- .../server/lib/infra_ml/queries/common.ts | 32 ++ .../queries/metrics_hosts_anomalies.ts | 12 +- .../infra_ml/queries/metrics_k8s_anomalies.ts | 12 +- x-pack/plugins/infra/server/plugin.ts | 2 +- .../infra/server/routes/alerting/preview.ts | 51 ++- .../results/metrics_hosts_anomalies.ts | 2 +- .../infra_ml/results/metrics_k8s_anomalies.ts | 2 +- .../translations/translations/ja-JP.json | 11 - .../translations/translations/zh-CN.json | 11 - 50 files changed, 1918 insertions(+), 480 deletions(-) create mode 100644 x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts delete mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx delete mode 100644 x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx rename x-pack/plugins/infra/public/{pages/metrics/inventory_view/components/ml/anomaly_detection => components}/subscription_splash_content.tsx (58%) rename x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/{anomoly_detection_flyout.tsx => anomaly_detection_flyout.tsx} (100%) create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index a89f82e931fd..7a4edb8f4918 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -4,14 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import * as rt from 'io-ts'; +import { ANOMALY_THRESHOLD } from '../../infra_ml'; import { ItemTypeRT } from '../../inventory_models/types'; // TODO: Have threshold and inventory alerts import these types from this file instead of from their // local directories export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; +export const METRIC_ANOMALY_ALERT_TYPE_ID = 'metrics.alert.anomaly'; export enum Comparator { GT = '>', @@ -34,6 +35,26 @@ export enum Aggregators { P99 = 'p99', } +const metricAnomalyNodeTypeRT = rt.union([rt.literal('hosts'), rt.literal('k8s')]); +const metricAnomalyMetricRT = rt.union([ + rt.literal('memory_usage'), + rt.literal('network_in'), + rt.literal('network_out'), +]); +const metricAnomalyInfluencerFilterRT = rt.type({ + fieldName: rt.string, + fieldValue: rt.string, +}); + +export interface MetricAnomalyParams { + nodeType: rt.TypeOf; + metric: rt.TypeOf; + alertInterval?: string; + sourceId?: string; + threshold: Exclude; + influencerFilter: rt.TypeOf | undefined; +} + // Alert Preview API const baseAlertRequestParamsRT = rt.intersection([ rt.partial({ @@ -51,7 +72,6 @@ const baseAlertRequestParamsRT = rt.intersection([ rt.literal('M'), rt.literal('y'), ]), - criteria: rt.array(rt.any), alertInterval: rt.string, alertThrottle: rt.string, alertOnNoData: rt.boolean, @@ -65,6 +85,7 @@ const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([ }), rt.type({ alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID), + criteria: rt.array(rt.any), }), ]); export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf< @@ -76,15 +97,33 @@ const inventoryAlertPreviewRequestParamsRT = rt.intersection([ rt.type({ nodeType: ItemTypeRT, alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID), + criteria: rt.array(rt.any), }), ]); export type InventoryAlertPreviewRequestParams = rt.TypeOf< typeof inventoryAlertPreviewRequestParamsRT >; +const metricAnomalyAlertPreviewRequestParamsRT = rt.intersection([ + baseAlertRequestParamsRT, + rt.type({ + nodeType: metricAnomalyNodeTypeRT, + metric: metricAnomalyMetricRT, + threshold: rt.number, + alertType: rt.literal(METRIC_ANOMALY_ALERT_TYPE_ID), + }), + rt.partial({ + influencerFilter: metricAnomalyInfluencerFilterRT, + }), +]); +export type MetricAnomalyAlertPreviewRequestParams = rt.TypeOf< + typeof metricAnomalyAlertPreviewRequestParamsRT +>; + export const alertPreviewRequestParamsRT = rt.union([ metricThresholdAlertPreviewRequestParamsRT, inventoryAlertPreviewRequestParamsRT, + metricAnomalyAlertPreviewRequestParamsRT, ]); export type AlertPreviewRequestParams = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts index 589e57a1388b..81e46d85ba22 100644 --- a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts +++ b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts @@ -5,36 +5,44 @@ * 2.0. */ -export const ML_SEVERITY_SCORES = { - warning: 3, - minor: 25, - major: 50, - critical: 75, -}; +export enum ANOMALY_SEVERITY { + CRITICAL = 'critical', + MAJOR = 'major', + MINOR = 'minor', + WARNING = 'warning', + LOW = 'low', + UNKNOWN = 'unknown', +} -export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES; +export enum ANOMALY_THRESHOLD { + CRITICAL = 75, + MAJOR = 50, + MINOR = 25, + WARNING = 3, + LOW = 0, +} -export const ML_SEVERITY_COLORS = { - critical: 'rgb(228, 72, 72)', - major: 'rgb(229, 113, 0)', - minor: 'rgb(255, 221, 0)', - warning: 'rgb(125, 180, 226)', +export const SEVERITY_COLORS = { + CRITICAL: '#fe5050', + MAJOR: '#fba740', + MINOR: '#fdec25', + WARNING: '#8bc8fb', + LOW: '#d2e9f7', + BLANK: '#ffffff', }; -export const getSeverityCategoryForScore = ( - score: number -): MLSeverityScoreCategories | undefined => { - if (score >= ML_SEVERITY_SCORES.critical) { - return 'critical'; - } else if (score >= ML_SEVERITY_SCORES.major) { - return 'major'; - } else if (score >= ML_SEVERITY_SCORES.minor) { - return 'minor'; - } else if (score >= ML_SEVERITY_SCORES.warning) { - return 'warning'; +export const getSeverityCategoryForScore = (score: number): ANOMALY_SEVERITY | undefined => { + if (score >= ANOMALY_THRESHOLD.CRITICAL) { + return ANOMALY_SEVERITY.CRITICAL; + } else if (score >= ANOMALY_THRESHOLD.MAJOR) { + return ANOMALY_SEVERITY.MAJOR; + } else if (score >= ANOMALY_THRESHOLD.MINOR) { + return ANOMALY_SEVERITY.MINOR; + } else if (score >= ANOMALY_THRESHOLD.WARNING) { + return ANOMALY_SEVERITY.WARNING; } else { // Category is too low to include - return undefined; + return ANOMALY_SEVERITY.LOW; } }; diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index fac87e20dfe7..57c6f695453e 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -37,7 +37,7 @@ interface Props { alertInterval: string; alertThrottle: string; alertType: PreviewableAlertTypes; - alertParams: { criteria: any[]; sourceId: string } & Record; + alertParams: { criteria?: any[]; sourceId: string } & Record; validate: (params: any) => ValidationResult; showNoDataResults?: boolean; groupByDisplayName?: string; @@ -109,6 +109,7 @@ export const AlertPreview: React.FC = (props) => { }, [previewLookbackInterval, alertInterval]); const isPreviewDisabled = useMemo(() => { + if (!alertParams.criteria) return false; const validationResult = validate({ criteria: alertParams.criteria } as any); const hasValidationErrors = Object.values(validationResult.errors).some((result) => Object.values(result).some((arr) => Array.isArray(arr) && arr.length) @@ -124,7 +125,7 @@ export const AlertPreview: React.FC = (props) => { }, [previewResult, showNoDataResults]); const hasWarningThreshold = useMemo( - () => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')), + () => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')) ?? false, [alertParams] ); diff --git a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts index a1cee1361a18..2bb98e83cbe7 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts +++ b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts @@ -10,13 +10,15 @@ import { INFRA_ALERT_PREVIEW_PATH, METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + METRIC_ANOMALY_ALERT_TYPE_ID, AlertPreviewRequestParams, AlertPreviewSuccessResponsePayload, } from '../../../../common/alerting/metrics'; export type PreviewableAlertTypes = | typeof METRIC_THRESHOLD_ALERT_TYPE_ID - | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID; + | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID + | typeof METRIC_ANOMALY_ALERT_TYPE_ID; export async function getAlertPreview({ fetch, diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx new file mode 100644 index 000000000000..f1236c4fc2c2 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx @@ -0,0 +1,151 @@ +/* + * 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 React, { useState, useCallback, useMemo } from 'react'; +import { + EuiPopover, + EuiButtonEmpty, + EuiContextMenu, + EuiContextMenuPanelDescriptor, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; +import { PrefilledInventoryAlertFlyout } from '../../inventory/components/alert_flyout'; +import { PrefilledThresholdAlertFlyout } from '../../metric_threshold/components/alert_flyout'; +import { PrefilledAnomalyAlertFlyout } from '../../metric_anomaly/components/alert_flyout'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +type VisibleFlyoutType = 'inventory' | 'threshold' | 'anomaly' | null; + +export const MetricsAlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [visibleFlyoutType, setVisibleFlyoutType] = useState(null); + const { hasInfraMLCapabilities } = useInfraMLCapabilities(); + + const closeFlyout = useCallback(() => setVisibleFlyoutType(null), [setVisibleFlyoutType]); + + const manageAlertsLinkProps = useLinkProps({ + app: 'management', + pathname: '/insightsAndAlerting/triggersActions/alerts', + }); + + const panels: EuiContextMenuPanelDescriptor[] = useMemo( + () => [ + { + id: 0, + title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', { + defaultMessage: 'Alerts', + }), + items: [ + { + name: i18n.translate('xpack.infra.alerting.infrastructureDropdownMenu', { + defaultMessage: 'Infrastructure', + }), + panel: 1, + }, + { + name: i18n.translate('xpack.infra.alerting.metricsDropdownMenu', { + defaultMessage: 'Metrics', + }), + panel: 2, + }, + { + name: i18n.translate('xpack.infra.alerting.manageAlerts', { + defaultMessage: 'Manage alerts', + }), + icon: 'tableOfContents', + onClick: manageAlertsLinkProps.onClick, + }, + ], + }, + { + id: 1, + title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', { + defaultMessage: 'Infrastructure alerts', + }), + items: [ + { + name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', { + defaultMessage: 'Create inventory alert', + }), + onClick: () => setVisibleFlyoutType('inventory'), + }, + ].concat( + hasInfraMLCapabilities + ? { + name: i18n.translate('xpack.infra.alerting.createAnomalyAlertButton', { + defaultMessage: 'Create anomaly alert', + }), + onClick: () => setVisibleFlyoutType('anomaly'), + } + : [] + ), + }, + { + id: 2, + title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', { + defaultMessage: 'Metrics alerts', + }), + items: [ + { + name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', { + defaultMessage: 'Create threshold alert', + }), + onClick: () => setVisibleFlyoutType('threshold'), + }, + ], + }, + ], + [manageAlertsLinkProps, setVisibleFlyoutType, hasInfraMLCapabilities] + ); + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + return ( + <> + + + + } + isOpen={popoverOpen} + closePopover={closePopover} + > + + + + + ); +}; + +interface AlertFlyoutProps { + visibleFlyoutType: VisibleFlyoutType; + onClose(): void; +} + +const AlertFlyout = ({ visibleFlyoutType, onClose }: AlertFlyoutProps) => { + switch (visibleFlyoutType) { + case 'inventory': + return ; + case 'threshold': + return ; + case 'anomaly': + return ; + default: + return null; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx deleted file mode 100644 index a7b6c9fb7104..000000000000 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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, { useState, useCallback } from 'react'; -import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; -import { AlertFlyout } from './alert_flyout'; -import { ManageAlertsContextMenuItem } from './manage_alerts_context_menu_item'; - -export const InventoryAlertDropdown = () => { - const [popoverOpen, setPopoverOpen] = useState(false); - const [flyoutVisible, setFlyoutVisible] = useState(false); - - const { inventoryPrefill } = useAlertPrefillContext(); - const { nodeType, metric, filterQuery } = inventoryPrefill; - - const closePopover = useCallback(() => { - setPopoverOpen(false); - }, [setPopoverOpen]); - - const openPopover = useCallback(() => { - setPopoverOpen(true); - }, [setPopoverOpen]); - - const menuItems = [ - setFlyoutVisible(true)}> - - , - , - ]; - - return ( - <> - - - - } - isOpen={popoverOpen} - closePopover={closePopover} - > - - - - - ); -}; 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 815e1f2be33f..33fe3c7af30c 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 @@ -8,8 +8,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import { InfraWaffleMapOptions } from '../../../lib/lib'; import { InventoryItemType } from '../../../../common/inventory_models/types'; import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; @@ -49,3 +48,18 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: return <>{visible && AddAlertFlyout}; }; + +export const PrefilledInventoryAlertFlyout = ({ onClose }: { onClose(): void }) => { + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric, filterQuery } = inventoryPrefill; + + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx index f02f98c49f01..bd7812acac67 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx @@ -68,7 +68,7 @@ export const NodeTypeExpression = ({ setAggTypePopoverOpen(false)}> { + const { triggersActionsUI } = useContext(TriggerActionsContext); + + const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); + const AddAlertFlyout = useMemo( + () => + triggersActionsUI && + triggersActionsUI.getAddAlertFlyout({ + consumer: 'infrastructure', + onClose: onCloseFlyout, + canChangeTrigger: false, + alertTypeId: METRIC_ANOMALY_ALERT_TYPE_ID, + metadata: { + metric, + nodeType, + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [triggersActionsUI, visible] + ); + + return <>{visible && AddAlertFlyout}; +}; + +export const PrefilledAnomalyAlertFlyout = ({ onClose }: { onClose(): void }) => { + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric } = inventoryPrefill; + + return ; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx new file mode 100644 index 000000000000..ae2c6ed81bad --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 { mountWithIntl, nextTick } from '@kbn/test/jest'; +// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` +import { coreMock as mockCoreMock } from 'src/core/public/mocks'; +import React from 'react'; +import { Expression, AlertContextMeta } from './expression'; +import { act } from 'react-dom/test-utils'; + +jest.mock('../../../containers/source/use_source_via_http', () => ({ + useSourceViaHttp: () => ({ + source: { id: 'default' }, + createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), + }), +})); + +jest.mock('../../../hooks/use_kibana', () => ({ + useKibanaContextForPlugin: () => ({ + services: mockCoreMock.createStart(), + }), +})); + +jest.mock('../../../containers/ml/infra_ml_capabilities', () => ({ + useInfraMLCapabilities: () => ({ + isLoading: false, + hasInfraMLCapabilities: true, + }), +})); + +describe('Expression', () => { + async function setup(currentOptions: AlertContextMeta) { + const alertParams = { + metric: undefined, + nodeType: undefined, + threshold: 50, + }; + const wrapper = mountWithIntl( + Reflect.set(alertParams, key, value)} + setAlertProperty={() => {}} + metadata={currentOptions} + /> + ); + + 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 = { + nodeType: 'pod', + metric: { type: 'tx' }, + }; + const { alertParams } = await setup(currentOptions as AlertContextMeta); + expect(alertParams.nodeType).toBe('k8s'); + expect(alertParams.metric).toBe('network_out'); + }); +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx new file mode 100644 index 000000000000..5938c7119616 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx @@ -0,0 +1,320 @@ +/* + * 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 { pick } from 'lodash'; +import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { EuiFlexGroup, EuiSpacer, EuiText, EuiLoadingContent } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; +import { AlertPreview } from '../../common'; +import { + METRIC_ANOMALY_ALERT_TYPE_ID, + MetricAnomalyParams, +} from '../../../../common/alerting/metrics'; +import { euiStyled, EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { + WhenExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { NodeTypeExpression } from './node_type'; +import { SeverityThresholdExpression } from './severity_threshold'; +import { InfraWaffleMapOptions } from '../../../lib/lib'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; + +import { validateMetricAnomaly } from './validation'; +import { InfluencerFilter } from './influencer_filter'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; + +export interface AlertContextMeta { + metric?: InfraWaffleMapOptions['metric']; + nodeType?: InventoryItemType; +} + +interface Props { + errors: IErrorObject[]; + alertParams: MetricAnomalyParams & { + sourceId: string; + }; + alertInterval: string; + alertThrottle: string; + setAlertParams(key: string, value: any): void; + setAlertProperty(key: string, value: any): void; + metadata: AlertContextMeta; +} + +export const defaultExpression = { + metric: 'memory_usage' as MetricAnomalyParams['metric'], + threshold: ANOMALY_THRESHOLD.MAJOR, + nodeType: 'hosts', + influencerFilter: undefined, +}; + +export const Expression: React.FC = (props) => { + const { hasInfraMLCapabilities, isLoading: isLoadingMLCapabilities } = useInfraMLCapabilities(); + const { http, notifications } = useKibanaContextForPlugin().services; + const { setAlertParams, alertParams, alertInterval, alertThrottle, metadata } = props; + const { source, createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + fetch: http.fetch, + toastWarning: notifications.toasts.addWarning, + }); + + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + createDerivedIndexPattern, + ]); + + const [influencerFieldName, updateInfluencerFieldName] = useState( + alertParams.influencerFilter?.fieldName ?? 'host.name' + ); + + useEffect(() => { + setAlertParams('hasInfraMLCapabilities', hasInfraMLCapabilities); + }, [setAlertParams, hasInfraMLCapabilities]); + + useEffect(() => { + if (alertParams.influencerFilter) { + setAlertParams('influencerFilter', { + ...alertParams.influencerFilter, + fieldName: influencerFieldName, + }); + } + }, [influencerFieldName, alertParams, setAlertParams]); + const updateInfluencerFieldValue = useCallback( + (value: string) => { + if (value) { + setAlertParams('influencerFilter', { + ...alertParams.influencerFilter, + fieldValue: value, + }); + } else { + setAlertParams('influencerFilter', undefined); + } + }, + [setAlertParams, alertParams] + ); + + useEffect(() => { + setAlertParams('alertInterval', alertInterval); + }, [setAlertParams, alertInterval]); + + const updateNodeType = useCallback( + (nt: any) => { + setAlertParams('nodeType', nt); + }, + [setAlertParams] + ); + + const updateMetric = useCallback( + (metric: string) => { + setAlertParams('metric', metric); + }, + [setAlertParams] + ); + + const updateSeverityThreshold = useCallback( + (threshold: any) => { + setAlertParams('threshold', threshold); + }, + [setAlertParams] + ); + + const prefillNodeType = useCallback(() => { + const md = metadata; + if (md && md.nodeType) { + setAlertParams( + 'nodeType', + getMLNodeTypeFromInventoryNodeType(md.nodeType) ?? defaultExpression.nodeType + ); + } else { + setAlertParams('nodeType', defaultExpression.nodeType); + } + }, [metadata, setAlertParams]); + + const prefillMetric = useCallback(() => { + const md = metadata; + if (md && md.metric) { + setAlertParams( + 'metric', + getMLMetricFromInventoryMetric(md.metric.type) ?? defaultExpression.metric + ); + } else { + setAlertParams('metric', defaultExpression.metric); + } + }, [metadata, setAlertParams]); + + useEffect(() => { + if (!alertParams.nodeType) { + prefillNodeType(); + } + + if (!alertParams.threshold) { + setAlertParams('threshold', defaultExpression.threshold); + } + + if (!alertParams.metric) { + prefillMetric(); + } + + if (!alertParams.sourceId) { + setAlertParams('sourceId', source?.id || 'default'); + } + }, [metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + + if (isLoadingMLCapabilities) return ; + if (!hasInfraMLCapabilities) return ; + + return ( + // https://github.com/elastic/kibana/issues/89506 + + +

+ +

+
+ + + + + + + + + + + + + + + + + + + +
+ ); +}; + +// required for dynamic import +// eslint-disable-next-line import/no-default-export +export default Expression; + +const StyledExpressionRow = euiStyled(EuiFlexGroup)` + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +`; + +const StyledExpression = euiStyled.div` + padding: 0 4px; +`; + +const getDisplayNameForType = (type: InventoryItemType) => { + const inventoryModel = findInventoryModel(type); + return inventoryModel.displayName; +}; + +export const nodeTypes: { [key: string]: any } = { + hosts: { + text: getDisplayNameForType('host'), + value: 'hosts', + }, + k8s: { + text: getDisplayNameForType('pod'), + value: 'k8s', + }, +}; + +const getMLMetricFromInventoryMetric = (metric: SnapshotMetricType) => { + switch (metric) { + case 'memory': + return 'memory_usage'; + case 'tx': + return 'network_out'; + case 'rx': + return 'network_in'; + default: + return null; + } +}; + +const getMLNodeTypeFromInventoryNodeType = (nodeType: InventoryItemType) => { + switch (nodeType) { + case 'host': + return 'hosts'; + case 'pod': + return 'k8s'; + default: + return null; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx new file mode 100644 index 000000000000..34a917a77dcf --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx @@ -0,0 +1,193 @@ +/* + * 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 { debounce } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import { first } from 'lodash'; +import { EuiFlexGroup, EuiFormRow, EuiCheckbox, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { + MetricsExplorerKueryBar, + CurryLoadSuggestionsType, +} from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; + +interface Props { + fieldName: string; + fieldValue: string; + nodeType: MetricAnomalyParams['nodeType']; + onChangeFieldName: (v: string) => void; + onChangeFieldValue: (v: string) => void; + derivedIndexPattern: Parameters[0]['derivedIndexPattern']; +} + +const FILTER_TYPING_DEBOUNCE_MS = 500; + +export const InfluencerFilter = ({ + fieldName, + fieldValue, + nodeType, + onChangeFieldName, + onChangeFieldValue, + derivedIndexPattern, +}: Props) => { + const fieldNameOptions = useMemo(() => (nodeType === 'k8s' ? k8sFieldNames : hostFieldNames), [ + nodeType, + ]); + + // If initial props contain a fieldValue, assume it was passed in from loaded alertParams, + // and enable the UI element + const [isEnabled, updateIsEnabled] = useState(fieldValue ? true : false); + const [storedFieldValue, updateStoredFieldValue] = useState(fieldValue); + + useEffect( + () => + nodeType === 'k8s' + ? onChangeFieldName(first(k8sFieldNames)!.value) + : onChangeFieldName(first(hostFieldNames)!.value), + [nodeType, onChangeFieldName] + ); + + const onSelectFieldName = useCallback((e) => onChangeFieldName(e.target.value), [ + onChangeFieldName, + ]); + const onUpdateFieldValue = useCallback( + (value) => { + updateStoredFieldValue(value); + onChangeFieldValue(value); + }, + [onChangeFieldValue] + ); + + const toggleEnabled = useCallback(() => { + const nextState = !isEnabled; + updateIsEnabled(nextState); + if (!nextState) { + onChangeFieldValue(''); + } else { + onChangeFieldValue(storedFieldValue); + } + }, [isEnabled, updateIsEnabled, onChangeFieldValue, storedFieldValue]); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnUpdateFieldValue = useCallback( + debounce(onUpdateFieldValue, FILTER_TYPING_DEBOUNCE_MS), + [onUpdateFieldValue] + ); + + const affixFieldNameToQuery: CurryLoadSuggestionsType = (fn) => ( + expression, + cursorPosition, + maxSuggestions + ) => { + // Add the field name to the front of the passed-in query + const prefix = `${fieldName}:`; + // Trim whitespace to prevent AND/OR suggestions + const modifiedExpression = `${prefix}${expression}`.trim(); + // Move the cursor position forward by the length of the field name + const modifiedPosition = cursorPosition + prefix.length; + return fn(modifiedExpression, modifiedPosition, maxSuggestions, (suggestions) => + suggestions + .map((s) => ({ + ...s, + // Remove quotes from suggestions + text: s.text.replace(/\"/g, '').trim(), + // Offset the returned suggestions' cursor positions so that they can be autocompleted accurately + start: s.start - prefix.length, + end: s.end - prefix.length, + })) + // Removing quotes can lead to an already-selected suggestion still coming up in the autocomplete list, + // so filter these out + .filter((s) => !expression.startsWith(s.text)) + ); + }; + + return ( + + } + helpText={ + isEnabled ? ( + <> + {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpText', { + defaultMessage: + 'Limit the scope of your alert trigger to anomalies influenced by certain node(s).', + })} +
+ {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpTextExample', { + defaultMessage: 'For example: "my-node-1" or "my-node-*"', + })} + + ) : null + } + fullWidth + display="rowCompressed" + > + {isEnabled ? ( + + + + + + + + + ) : ( + <> + )} +
+ ); +}; + +const hostFieldNames = [ + { + value: 'host.name', + text: 'host.name', + }, +]; + +const k8sFieldNames = [ + { + value: 'kubernetes.pod.uid', + text: 'kubernetes.pod.uid', + }, + { + value: 'kubernetes.node.name', + text: 'kubernetes.node.name', + }, + { + value: 'kubernetes.namespace', + text: 'kubernetes.namespace', + }, +]; + +const filterByNodeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.filterByNodeLabel', { + defaultMessage: 'Filter by node', +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx new file mode 100644 index 000000000000..6ddcf8fd5cb6 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx @@ -0,0 +1,117 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; + +type Node = MetricAnomalyParams['nodeType']; + +interface WhenExpressionProps { + value: Node; + options: { [key: string]: { text: string; value: Node } }; + onChange: (value: Node) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const NodeTypeExpression = ({ + value, + options, + onChange, + popupPosition, +}: WhenExpressionProps) => { + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + + return ( + { + setAggTypePopoverOpen(true); + }} + /> + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + ownFocus + anchorPosition={popupPosition ?? 'downLeft'} + > +
+ setAggTypePopoverOpen(false)}> + + + { + onChange(e.target.value as Node); + setAggTypePopoverOpen(false); + }} + options={Object.values(options).map((o) => o)} + /> +
+
+ ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx new file mode 100644 index 000000000000..2dc561ff172b --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx @@ -0,0 +1,140 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; + +interface WhenExpressionProps { + value: Exclude; + onChange: (value: ANOMALY_THRESHOLD) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +const options = { + [ANOMALY_THRESHOLD.CRITICAL]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.criticalLabel', { + defaultMessage: 'Critical', + }), + value: ANOMALY_THRESHOLD.CRITICAL, + }, + [ANOMALY_THRESHOLD.MAJOR]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.majorLabel', { + defaultMessage: 'Major', + }), + value: ANOMALY_THRESHOLD.MAJOR, + }, + [ANOMALY_THRESHOLD.MINOR]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.minorLabel', { + defaultMessage: 'Minor', + }), + value: ANOMALY_THRESHOLD.MINOR, + }, + [ANOMALY_THRESHOLD.WARNING]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.warningLabel', { + defaultMessage: 'Warning', + }), + value: ANOMALY_THRESHOLD.WARNING, + }, +}; + +export const SeverityThresholdExpression = ({ + value, + onChange, + popupPosition, +}: WhenExpressionProps) => { + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + + return ( + { + setAggTypePopoverOpen(true); + }} + /> + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + ownFocus + anchorPosition={popupPosition ?? 'downLeft'} + > +
+ setAggTypePopoverOpen(false)}> + + + { + onChange(Number(e.target.value) as ANOMALY_THRESHOLD); + setAggTypePopoverOpen(false); + }} + options={Object.values(options).map((o) => o)} + /> +
+
+ ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx new file mode 100644 index 000000000000..8e254fb2b67a --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx @@ -0,0 +1,35 @@ +/* + * 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'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; + +export function validateMetricAnomaly({ + hasInfraMLCapabilities, +}: { + hasInfraMLCapabilities: boolean; +}): ValidationResult { + const validationResult = { errors: {} }; + const errors: { + hasInfraMLCapabilities: string[]; + } = { + hasInfraMLCapabilities: [], + }; + + validationResult.errors = errors; + + if (!hasInfraMLCapabilities) { + errors.hasInfraMLCapabilities.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.mlCapabilitiesRequired', { + defaultMessage: 'Cannot create an anomaly alert when machine learning is disabled.', + }) + ); + } + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts new file mode 100644 index 000000000000..31fed514bdac --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts @@ -0,0 +1,46 @@ +/* + * 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 React from 'react'; +import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../common/alerting/metrics'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { AlertTypeParams } from '../../../../alerts/common'; +import { validateMetricAnomaly } from './components/validation'; + +interface MetricAnomalyAlertTypeParams extends AlertTypeParams { + hasInfraMLCapabilities: boolean; +} + +export function createMetricAnomalyAlertType(): AlertTypeModel { + return { + id: METRIC_ANOMALY_ALERT_TYPE_ID, + description: i18n.translate('xpack.infra.metrics.anomaly.alertFlyout.alertDescription', { + defaultMessage: 'Alert when the anomaly score exceeds a defined threshold.', + }), + iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/metric-anomaly-alert.html`; + }, + alertParamsExpression: React.lazy(() => import('./components/expression')), + validate: validateMetricAnomaly, + defaultActionMessage: i18n.translate( + 'xpack.infra.metrics.alerting.anomaly.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} is in a state of \\{\\{context.alertState\\}\\} + +\\{\\{context.metric\\}\\} was \\{\\{context.summary\\}\\} than normal at \\{\\{context.timestamp\\}\\} + +Typical value: \\{\\{context.typical\\}\\} +Actual value: \\{\\{context.actual\\}\\} +`, + } + ), + requiresAppContext: false, + }; +} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx deleted file mode 100644 index 3bbe81122582..000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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, { useState, useCallback } from 'react'; -import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useAlertPrefillContext } from '../../use_alert_prefill'; -import { AlertFlyout } from './alert_flyout'; -import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item'; - -export const MetricsAlertDropdown = () => { - const [popoverOpen, setPopoverOpen] = useState(false); - const [flyoutVisible, setFlyoutVisible] = useState(false); - - const { metricThresholdPrefill } = useAlertPrefillContext(); - const { groupBy, filterQuery, metrics } = metricThresholdPrefill; - - const closePopover = useCallback(() => { - setPopoverOpen(false); - }, [setPopoverOpen]); - - const openPopover = useCallback(() => { - setPopoverOpen(true); - }, [setPopoverOpen]); - - const menuItems = [ - setFlyoutVisible(true)}> - - , - , - ]; - - return ( - <> - - - - } - isOpen={popoverOpen} - closePopover={closePopover} - > - - - - - ); -}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index 929654ecb469..e7e4ade5257f 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -7,10 +7,10 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; interface Props { visible?: boolean; @@ -42,3 +42,10 @@ export const AlertFlyout = (props: Props) => { return <>{visible && AddAlertFlyout}; }; + +export const PrefilledThresholdAlertFlyout = ({ onClose }: { onClose(): void }) => { + const { metricThresholdPrefill } = useAlertPrefillContext(); + const { groupBy, filterQuery, metrics } = metricThresholdPrefill; + + return ; +}; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts index 1bcc9e7157a5..db5a996c604f 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts @@ -14,4 +14,3 @@ export * from './missing_results_privileges_prompt'; export * from './missing_setup_privileges_prompt'; export * from './ml_unavailable_prompt'; export * from './setup_status_unknown_prompt'; -export * from './subscription_splash_content'; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx deleted file mode 100644 index c91c1d82afe9..000000000000 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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, { useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, - EuiText, - EuiButton, - EuiButtonEmpty, - EuiImage, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { HttpStart } from 'src/core/public'; -import { LoadingPage } from '../../loading_page'; - -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { useTrialStatus } from '../../../hooks/use_trial_status'; - -export const SubscriptionSplashContent: React.FC = () => { - const { services } = useKibana<{ http: HttpStart }>(); - const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus(); - - useEffect(() => { - checkTrialAvailability(); - }, [checkTrialAvailability]); - - if (loadState === 'pending') { - return ( - - ); - } - - const canStartTrial = isTrialAvailable && loadState === 'resolved'; - - let title; - let description; - let cta; - - if (canStartTrial) { - title = ( - - ); - - description = ( - - ); - - cta = ( - - - - ); - } else { - title = ( - - ); - - description = ( - - ); - - cta = ( - - - - ); - } - - return ( - - - - - - -

{title}

-
- - -

{description}

-
- -
{cta}
-
- - - -
- - -

- -

-
- - - -
-
-
-
- ); -}; - -const SubscriptionPage = euiStyled(EuiPage)` - height: 100% -`; - -const SubscriptionPageContent = euiStyled(EuiPageContent)` - max-width: 768px !important; -`; - -const SubscriptionPageFooter = euiStyled.div` - background: ${(props) => props.theme.eui.euiColorLightestShade}; - margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) => - props.theme.eui.paddingSizes.l}; - padding: ${(props) => props.theme.eui.paddingSizes.l}; -`; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index 4b609a881bd1..e63f43470497 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -75,7 +75,7 @@ export const SourceConfigurationSettings = ({ source, ]); - const { hasInfraMLCapabilites } = useInfraMLCapabilitiesContext(); + const { hasInfraMLCapabilities } = useInfraMLCapabilitiesContext(); if ((isLoading || isUninitialized) && !source) { return ; @@ -128,7 +128,7 @@ export const SourceConfigurationSettings = ({ /> - {hasInfraMLCapabilites && ( + {hasInfraMLCapabilities && ( <> { const { services } = useKibana<{ http: HttpStart }>(); @@ -102,58 +102,60 @@ export const SubscriptionSplashContent: React.FC = () => { } return ( - - - - - - -

{title}

+ + + + + + + +

{title}

+
+ + +

{description}

+
+ +
{cta}
+
+ + + +
+ + +

+ +

- - -

{description}

-
- -
{cta}
-
- - - -
- - -

+ -

-
- - - -
-
-
-
+ + + + + + ); }; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx index 72dc4da01d86..661ce8f8a253 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx @@ -52,11 +52,11 @@ export const useInfraMLCapabilities = () => { const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob; const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs; - const hasInfraMLCapabilites = + const hasInfraMLCapabilities = mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace; return { - hasInfraMLCapabilites, + hasInfraMLCapabilities, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities, isLoading, diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index 379ac9774c24..1a759950f640 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -56,7 +56,8 @@ class WithKueryAutocompletionComponent extends React.Component< private loadSuggestions = async ( expression: string, cursorPosition: number, - maxSuggestions?: number + maxSuggestions?: number, + transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[] ) => { const { indexPattern } = this.props; const language = 'kuery'; @@ -86,6 +87,10 @@ class WithKueryAutocompletionComponent extends React.Component< boolFilter: [], })) || []; + const transformedSuggestions = transformSuggestions + ? transformSuggestions(suggestions) + : suggestions; + this.setState((state) => state.currentRequest && state.currentRequest.expression !== expression && @@ -94,7 +99,9 @@ class WithKueryAutocompletionComponent extends React.Component< : { ...state, currentRequest: null, - suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions, + suggestions: maxSuggestions + ? transformedSuggestions.slice(0, maxSuggestions) + : transformedSuggestions, } ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index f0fdd79bcd93..628df397998e 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect } from 'react'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, - SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index 4d06d23ef93e..5fd00527b8b7 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import React, { memo, useEffect, useCallback } from 'react'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, - SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 52c2a70f2d35..8fd32bda7fbc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -35,12 +35,11 @@ import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; -import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown'; -import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; +import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown'; import { SavedView } from '../../containers/saved_view/saved_view'; import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities'; -import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout'; +import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout'; import { HeaderMenuPortal } from '../../../../observability/public'; import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; @@ -83,8 +82,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { - - + { jobSummaries: k8sJobSummaries, } = useMetricK8sModuleContext(); const { - hasInfraMLCapabilites, + hasInfraMLCapabilities, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities, } = useInfraMLCapabilitiesContext(); @@ -69,7 +69,7 @@ export const FlyoutHome = (props: Props) => { } }, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]); - if (!hasInfraMLCapabilites) { + if (!hasInfraMLCapabilities) { return ; } else if (!hasInfraMLReadCapabilities) { return ; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx index 44391568741f..e22c6fa66118 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx @@ -10,7 +10,19 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../../../../components/autocomplete_field'; -import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { + esKuery, + IIndexPattern, + QuerySuggestion, +} from '../../../../../../../../src/plugins/data/public'; + +type LoadSuggestionsFn = ( + e: string, + p: number, + m?: number, + transform?: (s: QuerySuggestion[]) => QuerySuggestion[] +) => void; +export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn; interface Props { derivedIndexPattern: IIndexPattern; @@ -18,6 +30,7 @@ interface Props { onChange?: (query: string) => void; value?: string | null; placeholder?: string; + curryLoadSuggestions?: CurryLoadSuggestionsType; } function validateQuery(query: string) { @@ -35,6 +48,7 @@ export const MetricsExplorerKueryBar = ({ onChange, value, placeholder, + curryLoadSuggestions = defaultCurryLoadSuggestions, }: Props) => { const [draftQuery, setDraftQuery] = useState(value || ''); const [isValid, setValidation] = useState(true); @@ -73,7 +87,7 @@ export const MetricsExplorerKueryBar = ({ aria-label={placeholder} isLoadingSuggestions={isLoadingSuggestions} isValid={isValid} - loadSuggestions={loadSuggestions} + loadSuggestions={curryLoadSuggestions(loadSuggestions)} onChange={handleChange} onSubmit={onSubmit} placeholder={placeholder || defaultPlaceholder} @@ -84,3 +98,6 @@ export const MetricsExplorerKueryBar = ({ ); }; + +const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = (loadSuggestions) => (...args) => + loadSuggestions(...args); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 8e7d165f8a53..d4bb83e8668b 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -10,6 +10,7 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { createMetricThresholdAlertType } from './alerting/metric_threshold'; import { createInventoryMetricAlertType } from './alerting/inventory'; +import { createMetricAnomalyAlertType } from './alerting/metric_anomaly'; import { getAlertType as getLogsAlertType } from './alerting/log_threshold'; import { registerFeatures } from './register_feature'; import { @@ -35,6 +36,7 @@ export class Plugin implements InfraClientPluginClass { pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType()); pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType()); pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType()); + pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricAnomalyAlertType()); if (pluginsSetup.observability) { pluginsSetup.observability.dashboard.register({ diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index b18b6e8a6eba..4d70676d25e4 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -23,7 +23,7 @@ import type { ObservabilityPluginStart, } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; -import { MlPluginStart } from '../../ml/public'; +import { MlPluginStart, MlPluginSetup } from '../../ml/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; // Our own setup and start contract values @@ -36,6 +36,7 @@ export interface InfraClientSetupDeps { observability: ObservabilityPluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionSetup; + ml: MlPluginSetup; embeddable: EmbeddableSetup; } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts new file mode 100644 index 000000000000..b7ef8ec7d231 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.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 { MetricAnomalyParams } from '../../../../common/alerting/metrics'; +import { getMetricsHostsAnomalies, getMetricK8sAnomalies } from '../../infra_ml'; +import { MlSystem, MlAnomalyDetectors } from '../../../types'; + +type ConditionParams = Omit & { + spaceId: string; + startTime: number; + endTime: number; + mlSystem: MlSystem; + mlAnomalyDetectors: MlAnomalyDetectors; +}; + +export const evaluateCondition = async ({ + nodeType, + spaceId, + sourceId, + mlSystem, + mlAnomalyDetectors, + startTime, + endTime, + metric, + threshold, + influencerFilter, +}: ConditionParams) => { + const getAnomalies = nodeType === 'k8s' ? getMetricK8sAnomalies : getMetricsHostsAnomalies; + + const result = await getAnomalies( + { + spaceId, + mlSystem, + mlAnomalyDetectors, + }, + sourceId ?? 'default', + threshold, + startTime, + endTime, + metric, + { field: 'anomalyScore', direction: 'desc' }, + { pageSize: 100 }, + influencerFilter + ); + + return result; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts new file mode 100644 index 000000000000..ec95aac7268a --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts @@ -0,0 +1,142 @@ +/* + * 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 { first } from 'lodash'; +import moment from 'moment'; +import { stateToAlertMessage } from '../common/messages'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; +import { MappedAnomalyHit } from '../../infra_ml'; +import { AlertStates } from '../common/types'; +import { + ActionGroup, + AlertInstanceContext, + AlertInstanceState, +} from '../../../../../alerts/common'; +import { AlertExecutorOptions } from '../../../../../alerts/server'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { MetricAnomalyAllowedActionGroups } from './register_metric_anomaly_alert_type'; +import { MlPluginSetup } from '../../../../../ml/server'; +import { KibanaRequest } from '../../../../../../../src/core/server'; +import { InfraBackendLibs } from '../../infra_types'; +import { evaluateCondition } from './evaluate_condition'; + +export const createMetricAnomalyExecutor = (libs: InfraBackendLibs, ml?: MlPluginSetup) => async ({ + services, + params, + startedAt, +}: AlertExecutorOptions< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + MetricAnomalyAllowedActionGroups +>) => { + if (!ml) { + return; + } + const request = {} as KibanaRequest; + const mlSystem = ml.mlSystemProvider(request, services.savedObjectsClient); + const mlAnomalyDetectors = ml.anomalyDetectorsProvider(request, services.savedObjectsClient); + + const { + metric, + alertInterval, + influencerFilter, + sourceId, + nodeType, + threshold, + } = params as MetricAnomalyParams; + + const alertInstance = services.alertInstanceFactory(`${nodeType}-${metric}`); + + const bucketInterval = getIntervalInSeconds('15m') * 1000; + const alertIntervalInMs = getIntervalInSeconds(alertInterval ?? '1m') * 1000; + + const endTime = startedAt.getTime(); + // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour + const previousBucketStartTime = endTime - (endTime % bucketInterval); + + // If the alert interval is less than 15m, make sure that it actually queries an anomaly bucket + const startTime = Math.min(endTime - alertIntervalInMs, previousBucketStartTime); + + const { data } = await evaluateCondition({ + sourceId: sourceId ?? 'default', + spaceId: 'default', + mlSystem, + mlAnomalyDetectors, + startTime, + endTime, + metric, + threshold, + nodeType, + influencerFilter, + }); + + const shouldAlertFire = data.length > 0; + + if (shouldAlertFire) { + const { startTime: anomalyStartTime, anomalyScore, actual, typical, influencers } = first( + data as MappedAnomalyHit[] + )!; + + alertInstance.scheduleActions(FIRED_ACTIONS_ID, { + alertState: stateToAlertMessage[AlertStates.ALERT], + timestamp: moment(anomalyStartTime).toISOString(), + anomalyScore, + actual, + typical, + metric: metricNameMap[metric], + summary: generateSummaryMessage(actual, typical), + influencers: influencers.join(', '), + }); + } +}; + +export const FIRED_ACTIONS_ID = 'metrics.anomaly.fired'; +export const FIRED_ACTIONS: ActionGroup = { + id: FIRED_ACTIONS_ID, + name: i18n.translate('xpack.infra.metrics.alerting.anomaly.fired', { + defaultMessage: 'Fired', + }), +}; + +const generateSummaryMessage = (actual: number, typical: number) => { + const differential = (Math.max(actual, typical) / Math.min(actual, typical)) + .toFixed(1) + .replace('.0', ''); + if (actual > typical) { + return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryHigher', { + defaultMessage: '{differential}x higher', + values: { + differential, + }, + }); + } else { + return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryLower', { + defaultMessage: '{differential}x lower', + values: { + differential, + }, + }); + } +}; + +const metricNameMap = { + memory_usage: i18n.translate('xpack.infra.metrics.alerting.anomaly.memoryUsage', { + defaultMessage: 'Memory usage', + }), + network_in: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkIn', { + defaultMessage: 'Network in', + }), + network_out: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkOut', { + defaultMessage: 'Network out', + }), +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts new file mode 100644 index 000000000000..98992701e3bb --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts @@ -0,0 +1,120 @@ +/* + * 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 { Unit } from '@elastic/datemath'; +import { countBy } from 'lodash'; +import { MappedAnomalyHit } from '../../infra_ml'; +import { MlSystem, MlAnomalyDetectors } from '../../../types'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; +import { + TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, + isTooManyBucketsPreviewException, +} from '../../../../common/alerting/metrics'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { evaluateCondition } from './evaluate_condition'; + +interface PreviewMetricAnomalyAlertParams { + mlSystem: MlSystem; + mlAnomalyDetectors: MlAnomalyDetectors; + spaceId: string; + params: MetricAnomalyParams; + sourceId: string; + lookback: Unit; + alertInterval: string; + alertThrottle: string; + alertOnNoData: boolean; +} + +export const previewMetricAnomalyAlert = async ({ + mlSystem, + mlAnomalyDetectors, + spaceId, + params, + sourceId, + lookback, + alertInterval, + alertThrottle, +}: PreviewMetricAnomalyAlertParams) => { + const { metric, threshold, influencerFilter, nodeType } = params as MetricAnomalyParams; + + const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); + const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle); + const executionsPerThrottle = Math.floor(throttleIntervalInSeconds / alertIntervalInSeconds); + + const lookbackInterval = `1${lookback}`; + const lookbackIntervalInSeconds = getIntervalInSeconds(lookbackInterval); + const endTime = Date.now(); + const startTime = endTime - lookbackIntervalInSeconds * 1000; + + const numberOfExecutions = Math.floor(lookbackIntervalInSeconds / alertIntervalInSeconds); + const bucketIntervalInSeconds = getIntervalInSeconds('15m'); + const bucketsPerExecution = Math.max( + 1, + Math.floor(alertIntervalInSeconds / bucketIntervalInSeconds) + ); + + try { + let anomalies: MappedAnomalyHit[] = []; + const { data } = await evaluateCondition({ + nodeType, + spaceId, + sourceId, + mlSystem, + mlAnomalyDetectors, + startTime, + endTime, + metric, + threshold, + influencerFilter, + }); + anomalies = [...anomalies, ...data]; + + const anomaliesByTime = countBy(anomalies, ({ startTime: anomStartTime }) => anomStartTime); + + let numberOfTimesFired = 0; + let numberOfNotifications = 0; + let throttleTracker = 0; + const notifyWithThrottle = () => { + if (throttleTracker === 0) numberOfNotifications++; + throttleTracker++; + }; + // Mock each alert evaluation + for (let i = 0; i < numberOfExecutions; i++) { + const executionTime = startTime + alertIntervalInSeconds * 1000 * i; + // Get an array of bucket times this mock alert evaluation will be looking at + // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour, + // so this is an array of how many of those times occurred between this evaluation + // and the previous one + const bucketsLookedAt = Array.from(Array(bucketsPerExecution), (_, idx) => { + const previousBucketStartTime = + executionTime - + (executionTime % (bucketIntervalInSeconds * 1000)) - + idx * bucketIntervalInSeconds * 1000; + return previousBucketStartTime; + }); + const anomaliesDetectedInBuckets = bucketsLookedAt.some((bucketTime) => + Reflect.has(anomaliesByTime, bucketTime) + ); + + if (anomaliesDetectedInBuckets) { + numberOfTimesFired++; + notifyWithThrottle(); + } else if (throttleTracker > 0) { + throttleTracker++; + } + if (throttleTracker === executionsPerThrottle) { + throttleTracker = 0; + } + } + + return { fired: numberOfTimesFired, notifications: numberOfNotifications }; + } catch (e) { + if (!isTooManyBucketsPreviewException(e)) throw e; + const { maxBuckets } = e; + throw new Error(`${TOO_MANY_BUCKETS_PREVIEW_EXCEPTION}:${maxBuckets}`); + } +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts new file mode 100644 index 000000000000..8ac62c125515 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts @@ -0,0 +1,110 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { MlPluginSetup } from '../../../../../ml/server'; +import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server'; +import { + createMetricAnomalyExecutor, + FIRED_ACTIONS, + FIRED_ACTIONS_ID, +} from './metric_anomaly_executor'; +import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; +import { InfraBackendLibs } from '../../infra_types'; +import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils'; +import { alertStateActionVariableDescription } from '../common/messages'; +import { RecoveredActionGroupId } from '../../../../../alerts/common'; + +export type MetricAnomalyAllowedActionGroups = typeof FIRED_ACTIONS_ID; + +export const registerMetricAnomalyAlertType = ( + libs: InfraBackendLibs, + ml?: MlPluginSetup +): AlertType< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + MetricAnomalyAllowedActionGroups, + RecoveredActionGroupId +> => ({ + id: METRIC_ANOMALY_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.metrics.anomaly.alertName', { + defaultMessage: 'Infrastructure anomaly', + }), + validate: { + params: schema.object( + { + nodeType: oneOfLiterals(['hosts', 'k8s']), + alertInterval: schema.string(), + metric: oneOfLiterals(['memory_usage', 'network_in', 'network_out']), + threshold: schema.number(), + filterQuery: schema.maybe( + schema.string({ validate: validateIsStringElasticsearchJSONFilter }) + ), + sourceId: schema.string(), + }, + { unknowns: 'allow' } + ), + }, + defaultActionGroupId: FIRED_ACTIONS_ID, + actionGroups: [FIRED_ACTIONS], + producer: 'infrastructure', + minimumLicenseRequired: 'basic', + executor: createMetricAnomalyExecutor(libs, ml), + actionVariables: { + context: [ + { name: 'alertState', description: alertStateActionVariableDescription }, + { + name: 'metric', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyMetricDescription', { + defaultMessage: 'The metric name in the specified condition.', + }), + }, + { + name: 'timestamp', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyTimestampDescription', { + defaultMessage: 'A timestamp of when the anomaly was detected.', + }), + }, + { + name: 'anomalyScore', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyScoreDescription', { + defaultMessage: 'The exact severity score of the detected anomaly.', + }), + }, + { + name: 'actual', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyActualDescription', { + defaultMessage: 'The actual value of the monitored metric at the time of the anomaly.', + }), + }, + { + name: 'typical', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyTypicalDescription', { + defaultMessage: 'The typical value of the monitored metric at the time of the anomaly.', + }), + }, + { + name: 'summary', + description: i18n.translate('xpack.infra.metrics.alerting.anomalySummaryDescription', { + defaultMessage: 'A description of the anomaly, e.g. "2x higher."', + }), + }, + { + name: 'influencers', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyInfluencersDescription', { + defaultMessage: 'A list of node names that influenced the anomaly.', + }), + }, + ], + }, +}); diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index 0b4df6805759..11fbe269b854 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -8,13 +8,21 @@ import { PluginSetupContract } from '../../../../alerts/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type'; +import { registerMetricAnomalyAlertType } from './metric_anomaly/register_metric_anomaly_alert_type'; + import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type'; import { InfraBackendLibs } from '../infra_types'; +import { MlPluginSetup } from '../../../../ml/server'; -const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => { +const registerAlertTypes = ( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs, + ml?: MlPluginSetup +) => { if (alertingPlugin) { alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs)); + alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml)); const registerFns = [registerLogThresholdAlertType]; registerFns.forEach((fn) => { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/common.ts index 0182cb0e4099..686f27d714cc 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/common.ts @@ -17,6 +17,23 @@ import { import { decodeOrThrow } from '../../../common/runtime_types'; import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing'; +export interface MappedAnomalyHit { + id: string; + anomalyScore: number; + typical: number; + actual: number; + jobId: string; + startTime: number; + duration: number; + influencers: string[]; + categoryId?: string; +} + +export interface InfluencerFilter { + fieldName: string; + fieldValue: string; +} + export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) { const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES'); const { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/index.ts b/x-pack/plugins/infra/server/lib/infra_ml/index.ts index d346b71d76aa..82093b1a359d 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/index.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/index.ts @@ -8,3 +8,4 @@ export * from './errors'; export * from './metrics_hosts_anomalies'; export * from './metrics_k8s_anomalies'; +export { MappedAnomalyHit } from './common'; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts index 7873fd8e43a7..f6e11f529419 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts @@ -5,11 +5,10 @@ * 2.0. */ -import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob } from './common'; -import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml'; +import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; +import { getJobId, metricsHostsJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -19,18 +18,6 @@ import { createMetricsHostsAnomaliesQuery, } from './queries/metrics_hosts_anomalies'; -interface MappedAnomalyHit { - id: string; - anomalyScore: number; - typical: number; - actual: number; - jobId: string; - startTime: number; - duration: number; - influencers: string[]; - categoryId?: string; -} - async function getCompatibleAnomaliesJobIds( spaceId: string, sourceId: string, @@ -74,14 +61,15 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricsHostsAnomalies( - context: InfraPluginRequestHandlerContext & { infra: Required }, + context: Required, sourceId: string, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter ) { const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies'); @@ -89,10 +77,10 @@ export async function getMetricsHostsAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.infra.spaceId, + context.spaceId, sourceId, metric, - context.infra.mlAnomalyDetectors + context.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -108,13 +96,14 @@ export async function getMetricsHostsAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricsHostsAnomalies( - context.infra.mlSystem, + context.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination + pagination, + influencerFilter ); const data = anomalies.map((anomaly) => { @@ -164,12 +153,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricsHostsAnomalies( mlSystem: MlSystem, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter ) { // We'll request 1 extra entry on top of our pageSize to determine if there are // more entries to be fetched. This avoids scenarios where the client side can't @@ -188,6 +178,7 @@ async function fetchMetricsHostsAnomalies( endTime, sort, pagination: expandedPagination, + influencerFilter, }), jobIds ) diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts index 0c87b2f0f8b5..34039e9107f0 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts @@ -5,11 +5,10 @@ * 2.0. */ -import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob } from './common'; -import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml'; +import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; +import { getJobId, metricsK8SJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -19,18 +18,6 @@ import { createMetricsK8sAnomaliesQuery, } from './queries/metrics_k8s_anomalies'; -interface MappedAnomalyHit { - id: string; - anomalyScore: number; - typical: number; - actual: number; - jobId: string; - startTime: number; - influencers: string[]; - duration: number; - categoryId?: string; -} - async function getCompatibleAnomaliesJobIds( spaceId: string, sourceId: string, @@ -74,14 +61,15 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricK8sAnomalies( - context: InfraPluginRequestHandlerContext & { infra: Required }, + context: Required, sourceId: string, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter ) { const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies'); @@ -89,10 +77,10 @@ export async function getMetricK8sAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.infra.spaceId, + context.spaceId, sourceId, metric, - context.infra.mlAnomalyDetectors + context.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -107,13 +95,14 @@ export async function getMetricK8sAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricK8sAnomalies( - context.infra.mlSystem, + context.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination + pagination, + influencerFilter ); const data = anomalies.map((anomaly) => { @@ -160,12 +149,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricK8sAnomalies( mlSystem: MlSystem, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter | undefined ) { // We'll request 1 extra entry on top of our pageSize to determine if there are // more entries to be fetched. This avoids scenarios where the client side can't @@ -184,6 +174,7 @@ async function fetchMetricK8sAnomalies( endTime, sort, pagination: expandedPagination, + influencerFilter, }), jobIds ) diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts index b3676fc54aea..6f996a672a44 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts @@ -77,3 +77,35 @@ export const createDatasetsFilters = (datasets?: string[]) => }, ] : []; + +export const createInfluencerFilter = ({ + fieldName, + fieldValue, +}: { + fieldName: string; + fieldValue: string; +}) => [ + { + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + match: { + 'influencers.influencer_field_name': fieldName, + }, + }, + { + query_string: { + fields: ['influencers.influencer_field_values'], + query: fieldValue, + minimum_should_match: 1, + }, + }, + ], + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts index 45587cd258e5..7808851508a7 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -13,7 +14,9 @@ import { createResultTypeFilters, defaultRequestParameters, createAnomalyScoreFilter, + createInfluencerFilter, } from './common'; +import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; // TODO: Reassess validity of this against ML docs @@ -32,13 +35,15 @@ export const createMetricsHostsAnomaliesQuery = ({ endTime, sort, pagination, + influencerFilter, }: { jobIds: string[]; - anomalyThreshold: number; + anomalyThreshold: ANOMALY_THRESHOLD; startTime: number; endTime: number; sort: Sort; pagination: Pagination; + influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -50,6 +55,10 @@ export const createMetricsHostsAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; + const influencerQuery = influencerFilter + ? { must: createInfluencerFilter(influencerFilter) } + : {}; + const sourceFields = [ 'job_id', 'record_score', @@ -77,6 +86,7 @@ export const createMetricsHostsAnomaliesQuery = ({ query: { bool: { filter: filters, + ...influencerQuery, }, }, search_after: queryCursor, diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts index 56a4b99e7236..54eea067177e 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -13,7 +14,9 @@ import { createResultTypeFilters, defaultRequestParameters, createAnomalyScoreFilter, + createInfluencerFilter, } from './common'; +import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; // TODO: Reassess validity of this against ML docs @@ -32,13 +35,15 @@ export const createMetricsK8sAnomaliesQuery = ({ endTime, sort, pagination, + influencerFilter, }: { jobIds: string[]; - anomalyThreshold: number; + anomalyThreshold: ANOMALY_THRESHOLD; startTime: number; endTime: number; sort: Sort; pagination: Pagination; + influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -50,6 +55,10 @@ export const createMetricsK8sAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; + const influencerQuery = influencerFilter + ? { must: createInfluencerFilter(influencerFilter) } + : {}; + const sourceFields = [ 'job_id', 'record_score', @@ -76,6 +85,7 @@ export const createMetricsK8sAnomaliesQuery = ({ query: { bool: { filter: filters, + ...influencerQuery, }, }, search_after: queryCursor, diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 99555fa56acd..0ac49e05b36b 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -137,7 +137,7 @@ export class InfraServerPlugin implements Plugin { ]); initInfraServer(this.libs); - registerAlertTypes(plugins.alerts, this.libs); + registerAlertTypes(plugins.alerts, this.libs, plugins.ml); core.http.registerRouteHandlerContext( 'infra', diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index cc2cf4092520..3da560135eaf 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -9,17 +9,21 @@ import { PreviewResult } from '../../lib/alerting/common/types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + METRIC_ANOMALY_ALERT_TYPE_ID, INFRA_ALERT_PREVIEW_PATH, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, alertPreviewRequestParamsRT, alertPreviewSuccessResponsePayloadRT, MetricThresholdAlertPreviewRequestParams, InventoryAlertPreviewRequestParams, + MetricAnomalyAlertPreviewRequestParams, } from '../../../common/alerting/metrics'; import { createValidationFunction } from '../../../common/runtime_types'; import { previewInventoryMetricThresholdAlert } from '../../lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert'; import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert'; +import { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/preview_metric_anomaly_alert'; import { InfraBackendLibs } from '../../lib/infra_types'; +import { assertHasInfraMlPlugins } from '../../utils/request_context'; export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => { const { callWithRequest } = framework; @@ -33,8 +37,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { - criteria, - filterQuery, lookback, sourceId, alertType, @@ -55,7 +57,11 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) try { switch (alertType) { case METRIC_THRESHOLD_ALERT_TYPE_ID: { - const { groupBy } = request.body as MetricThresholdAlertPreviewRequestParams; + const { + groupBy, + criteria, + filterQuery, + } = request.body as MetricThresholdAlertPreviewRequestParams; const previewResult = await previewMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, groupBy }, @@ -72,7 +78,11 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }); } case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: { - const { nodeType } = request.body as InventoryAlertPreviewRequestParams; + const { + nodeType, + criteria, + filterQuery, + } = request.body as InventoryAlertPreviewRequestParams; const previewResult = await previewInventoryMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, nodeType }, @@ -89,6 +99,39 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) body: alertPreviewSuccessResponsePayloadRT.encode(payload), }); } + case METRIC_ANOMALY_ALERT_TYPE_ID: { + assertHasInfraMlPlugins(requestContext); + const { + nodeType, + metric, + threshold, + influencerFilter, + } = request.body as MetricAnomalyAlertPreviewRequestParams; + const { mlAnomalyDetectors, mlSystem, spaceId } = requestContext.infra; + + const previewResult = await previewMetricAnomalyAlert({ + mlAnomalyDetectors, + mlSystem, + spaceId, + params: { nodeType, metric, threshold, influencerFilter }, + lookback, + sourceId: source.id, + alertInterval, + alertThrottle, + alertOnNoData, + }); + + return response.ok({ + body: alertPreviewSuccessResponsePayloadRT.encode({ + numberOfGroups: 1, + resultTotals: { + ...previewResult, + error: 0, + noData: 0, + }, + }), + }); + } default: throw new Error('Unknown alert type'); } diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts index 8ec0b83994e1..6e227cfc12d1 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts @@ -53,7 +53,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { hasMoreEntries, timing, } = await getMetricsHostsAnomalies( - requestContext, + requestContext.infra, sourceId, anomalyThreshold, startTime, diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts index d41fa0ffafec..1c2c4947a02e 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts @@ -52,7 +52,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => { hasMoreEntries, timing, } = await getMetricK8sAnomalies( - requestContext, + requestContext.infra, sourceId, anomalyThreshold, startTime, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6e9d0329eaff..018d2d572eea 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9676,7 +9676,6 @@ "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", "xpack.infra.alerting.alertsButton": "アラート", - "xpack.infra.alerting.createAlertButton": "アラートの作成", "xpack.infra.alerting.logs.alertsButton": "アラート", "xpack.infra.alerting.logs.createAlertButton": "アラートの作成", "xpack.infra.alerting.logs.manageAlerts": "アラートを管理", @@ -9970,16 +9969,6 @@ "xpack.infra.logs.jumpToTailText": "最も新しいエントリーに移動", "xpack.infra.logs.lastUpdate": "前回の更新 {timestamp}", "xpack.infra.logs.loadingNewEntriesText": "新しいエントリーを読み込み中", - "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "ドキュメンテーションを表示", - "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "詳細について", - "xpack.infra.logs.logAnalysis.splash.loadingMessage": "ライセンスを確認しています...", - "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "プレースホルダー画像", - "xpack.infra.logs.logAnalysis.splash.startTrialCta": "トライアルを開始", - "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。", - "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "異常検知を利用するには、無料の試用版を開始してください", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "サブスクリプションのアップグレード", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "機械学習機能を使用するには、プラチナサブスクリプションが必要です。", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "異常検知を利用するには、プラチナサブスクリプションにアップグレードしてください", "xpack.infra.logs.logEntryActionsDetailsButton": "詳細を表示", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index eeda70910447..5a9695b8ddc3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9702,7 +9702,6 @@ "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", "xpack.infra.alerting.alertsButton": "告警", - "xpack.infra.alerting.createAlertButton": "创建告警", "xpack.infra.alerting.logs.alertsButton": "告警", "xpack.infra.alerting.logs.createAlertButton": "创建告警", "xpack.infra.alerting.logs.manageAlerts": "管理告警", @@ -9997,16 +9996,6 @@ "xpack.infra.logs.jumpToTailText": "跳到最近的条目", "xpack.infra.logs.lastUpdate": "上次更新时间 {timestamp}", "xpack.infra.logs.loadingNewEntriesText": "正在加载新条目", - "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "阅读文档", - "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "希望了解详情?", - "xpack.infra.logs.logAnalysis.splash.loadingMessage": "正在检查许可证......", - "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "占位符图像", - "xpack.infra.logs.logAnalysis.splash.startTrialCta": "开始试用", - "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。", - "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "要访问异常检测,请启动免费试用版", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "升级订阅", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "必须具有白金级订阅,才能使用 Machine Learning 功能。", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "要访问异常检测,请升级到白金级订阅", "xpack.infra.logs.logEntryActionsDetailsButton": "查看详情", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。",