From 7627d35443a548037ec5cceb92763be9005e2590 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Tue, 9 Feb 2021 15:34:42 -0800 Subject: [PATCH 1/9] Getting rid of the x-pack tsconfig refs (#90537) --- src/dev/typescript/build_refs.ts | 2 -- tsconfig.refs.json | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/dev/typescript/build_refs.ts b/src/dev/typescript/build_refs.ts index ff6a81843972c2..77d6eb2abc6126 100644 --- a/src/dev/typescript/build_refs.ts +++ b/src/dev/typescript/build_refs.ts @@ -7,12 +7,10 @@ */ import execa from 'execa'; -import Path from 'path'; import { run, ToolingLog } from '@kbn/dev-utils'; export async function buildAllRefs(log: ToolingLog) { await buildRefs(log, 'tsconfig.refs.json'); - await buildRefs(log, Path.join('x-pack', 'tsconfig.refs.json')); } async function buildRefs(log: ToolingLog, projectPath: string) { diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 17b1fc5dc1fe91..86fdfad2f524d6 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -55,5 +55,61 @@ { "path": "./src/plugins/visualizations/tsconfig.json" }, { "path": "./src/plugins/visualize/tsconfig.json" }, { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, + { "path": "./x-pack/plugins/actions/tsconfig.json" }, + { "path": "./x-pack/plugins/alerts/tsconfig.json" }, + { "path": "./x-pack/plugins/beats_management/tsconfig.json" }, + { "path": "./x-pack/plugins/canvas/tsconfig.json" }, + { "path": "./x-pack/plugins/cloud/tsconfig.json" }, + { "path": "./x-pack/plugins/code/tsconfig.json" }, + { "path": "./x-pack/plugins/console_extensions/tsconfig.json" }, + { "path": "./x-pack/plugins/dashboard_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, + { "path": "./x-pack/plugins/discover_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/encrypted_saved_objects/tsconfig.json" }, + { "path": "./x-pack/plugins/enterprise_search/tsconfig.json" }, + { "path": "./x-pack/plugins/event_log/tsconfig.json" }, + { "path": "./x-pack/plugins/features/tsconfig.json" }, + { "path": "./x-pack/plugins/file_upload/tsconfig.json" }, + { "path": "./x-pack/plugins/fleet/tsconfig.json" }, + { "path": "./x-pack/plugins/global_search_bar/tsconfig.json" }, + { "path": "./x-pack/plugins/global_search_providers/tsconfig.json" }, + { "path": "./x-pack/plugins/global_search/tsconfig.json" }, + { "path": "./x-pack/plugins/graph/tsconfig.json" }, + { "path": "./x-pack/plugins/grokdebugger/tsconfig.json" }, + { "path": "./x-pack/plugins/infra/tsconfig.json" }, + { "path": "./x-pack/plugins/ingest_pipelines/tsconfig.json" }, + { "path": "./x-pack/plugins/lens/tsconfig.json" }, + { "path": "./x-pack/plugins/license_management/tsconfig.json" }, + { "path": "./x-pack/plugins/licensing/tsconfig.json" }, + { "path": "./x-pack/plugins/maps_file_upload/tsconfig.json" }, + { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, + { "path": "./x-pack/plugins/maps/tsconfig.json" }, + { "path": "./x-pack/plugins/ml/tsconfig.json" }, + { "path": "./x-pack/plugins/observability/tsconfig.json" }, + { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, + { "path": "./x-pack/plugins/reporting/tsconfig.json" }, + { "path": "./x-pack/plugins/saved_objects_tagging/tsconfig.json" }, + { "path": "./x-pack/plugins/searchprofiler/tsconfig.json" }, + { "path": "./x-pack/plugins/security/tsconfig.json" }, + { "path": "./x-pack/plugins/snapshot_restore/tsconfig.json" }, + { "path": "./x-pack/plugins/spaces/tsconfig.json" }, + { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, + { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, + { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "./x-pack/plugins/transform/tsconfig.json" }, + { "path": "./x-pack/plugins/translations/tsconfig.json" }, + { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, + { "path": "./x-pack/plugins/ui_actions_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/upgrade_assistant/tsconfig.json" }, + { "path": "./x-pack/plugins/runtime_fields/tsconfig.json" }, + { "path": "./x-pack/plugins/index_management/tsconfig.json" }, + { "path": "./x-pack/plugins/watcher/tsconfig.json" }, + { "path": "./x-pack/plugins/rollup/tsconfig.json"}, + { "path": "./x-pack/plugins/remote_clusters/tsconfig.json"}, + { "path": "./x-pack/plugins/cross_cluster_replication/tsconfig.json"}, + { "path": "./x-pack/plugins/index_lifecycle_management/tsconfig.json"}, + { "path": "./x-pack/plugins/uptime/tsconfig.json" }, ] } From 0d94968df1b7e9d3fc3d24abcc7797ba70983090 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 9 Feb 2021 17:40:18 -0600 Subject: [PATCH 2/9] [Metrics UI] Add Metrics Anomaly Alert Type (#89244) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../infra/common/alerting/metrics/types.ts | 43 ++- .../infra/common/infra_ml/anomaly_results.ts | 56 +-- .../common/components/alert_preview.tsx | 3 +- .../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, 1917 insertions(+), 479 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 a89f82e931fd43..7a4edb8f49189d 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 589e57a1388b55..81e46d85ba220d 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 fac87e20dfe7de..0135e185e846af 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) 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 a1cee1361a18f6..2bb98e83cbe70b 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 00000000000000..f1236c4fc2c2bb --- /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 a7b6c9fb7104ce..00000000000000 --- 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 815e1f2be33f27..33fe3c7af30c78 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 f02f98c49f01af..bd7812acac678c 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 00000000000000..ae2c6ed81badb4 --- /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 00000000000000..5938c7119616f0 --- /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 00000000000000..34a917a77dcf53 --- /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 00000000000000..6ddcf8fd5cb659 --- /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 00000000000000..2dc561ff172b9a --- /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 00000000000000..8e254fb2b67a8b --- /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 00000000000000..31fed514bdacc1 --- /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 3bbe8112258258..00000000000000 --- 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 929654ecb4693b..e7e4ade5257fc1 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 1bcc9e7157a514..db5a996c604fcd 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 c91c1d82afe9b0..00000000000000 --- 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 4b609a881bd18f..e63f43470497d2 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 72dc4da01d8678..661ce8f8a253ce 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 379ac9774c242a..1a759950f640d2 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 f0fdd79bcd93df..628df397998eec 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 4d06d23ef93ef7..5fd00527b8b704 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 52c2a70f2d3591..8fd32bda7fbc8f 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 44391568741f35..e22c6fa6611812 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 8e7d165f8a5356..d4bb83e8668ba5 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 b18b6e8a6eba6e..4d70676d25e40f 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 00000000000000..b7ef8ec7d23125 --- /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 00000000000000..ec95aac7268ad9 --- /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 00000000000000..98992701e3bb4e --- /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 00000000000000..8ac62c125515af --- /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 0b4df6805759ee..11fbe269b854d5 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 0182cb0e4099ac..686f27d714cc16 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 d346b71d76aa8e..82093b1a359d0a 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 7873fd8e43a7bb..f6e11f5294191b 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 0c87b2f0f8b536..34039e9107f007 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 b3676fc54aeaa5..6f996a672a44ad 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 45587cd258e5d7..7808851508a7c3 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 56a4b99e7236c6..54eea067177edf 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 99555fa56acd59..0ac49e05b36b92 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 cc2cf4092520a1..3da560135eaf48 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 8ec0b83994e1a8..6e227cfc12d113 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 d41fa0ffafecc5..1c2c4947a02ea3 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 6e9d0329eaff83..018d2d572eea06 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 eeda7091044797..5a9695b8ddc3de 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 应用中分析此类别。", From 82f03282325367c075495baf9811694ced22ebe5 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 9 Feb 2021 18:59:47 -0500 Subject: [PATCH 3/9] Prefix with / (#90836) --- .../server/es_client/monitoring_endpoint_disable_watches.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts b/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts index 454379d17848e3..8e9e8d916e4965 100644 --- a/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts +++ b/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts @@ -13,7 +13,7 @@ export function monitoringEndpointDisableWatches(Client: any, _config: any, comp params: {}, urls: [ { - fmt: '_monitoring/migrate/alerts', + fmt: '/_monitoring/migrate/alerts', }, ], method: 'POST', From 4bab95229d91bcc416cd2f719e1301d7f7522496 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 9 Feb 2021 16:09:02 -0800 Subject: [PATCH 4/9] [dev-utils/ci-stats] support disabling ship errors (#90851) Co-authored-by: spalger --- .ci/Jenkinsfile_security_cypress | 3 ++- .ci/es-snapshots/Jenkinsfile_verify_es | 5 ++++- .../src/ci_stats_reporter/ship_ci_stats_cli.ts | 12 ++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.ci/Jenkinsfile_security_cypress b/.ci/Jenkinsfile_security_cypress index d7f702a56563fa..f7b02cd1c4ab1e 100644 --- a/.ci/Jenkinsfile_security_cypress +++ b/.ci/Jenkinsfile_security_cypress @@ -10,7 +10,8 @@ kibanaPipeline(timeoutMinutes: 180) { ) { catchError { withEnv([ - 'CI_PARALLEL_PROCESS_NUMBER=1' + 'CI_PARALLEL_PROCESS_NUMBER=1', + 'IGNORE_SHIP_CI_STATS_ERROR=true', ]) { def job = 'xpack-securityCypress' diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index b40cd91a45c572..736a71b73d14d4 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -26,7 +26,10 @@ kibanaPipeline(timeoutMinutes: 150) { message: "[${SNAPSHOT_VERSION}] ES Snapshot Verification Failure", ) { retryable.enable(2) - withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { + withEnv([ + "ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}", + 'IGNORE_SHIP_CI_STATS_ERROR=true', + ]) { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts index 1ee78518bb8018..4d07b54b8cf03e 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts @@ -22,10 +22,18 @@ export function shipCiStatsCli() { throw createFlagError('expected --metrics to be a string'); } + const maybeFail = (message: string) => { + const error = createFailError(message); + if (process.env.IGNORE_SHIP_CI_STATS_ERROR === 'true') { + error.exitCode = 0; + } + return error; + }; + const reporter = CiStatsReporter.fromEnv(log); if (!reporter.isEnabled()) { - throw createFailError('unable to initilize the CI Stats reporter'); + throw maybeFail('unable to initilize the CI Stats reporter'); } for (const path of metricPaths) { @@ -35,7 +43,7 @@ export function shipCiStatsCli() { if (await reporter.metrics(JSON.parse(json))) { log.success('shipped metrics from', path); } else { - throw createFailError('failed to ship metrics'); + throw maybeFail('failed to ship metrics'); } } }, From 8166becc5555f132636bc1e8662370d1b4bf7b6a Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 9 Feb 2021 17:15:12 -0700 Subject: [PATCH 5/9] Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)" This reverts commit 0d94968df1b7e9d3fc3d24abcc7797ba70983090. --- .../infra/common/alerting/metrics/types.ts | 43 +-- .../infra/common/infra_ml/anomaly_results.ts | 56 ++- .../common/components/alert_preview.tsx | 3 +- .../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 +- .../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 => anomoly_detection_flyout.tsx} | 0 .../ml/anomaly_detection/flyout_home.tsx | 6 +- .../subscription_splash_content.tsx | 110 +++--- .../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, 479 insertions(+), 1917 deletions(-) delete mode 100644 x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx create mode 100644 x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx create 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/{anomaly_detection_flyout.tsx => anomoly_detection_flyout.tsx} (100%) rename x-pack/plugins/infra/public/{components => pages/metrics/inventory_view/components/ml/anomaly_detection}/subscription_splash_content.tsx (58%) delete mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts delete mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts delete mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts delete 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 7a4edb8f49189d..a89f82e931fd43 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -4,15 +4,14 @@ * 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 = '>', @@ -35,26 +34,6 @@ 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({ @@ -72,6 +51,7 @@ const baseAlertRequestParamsRT = rt.intersection([ rt.literal('M'), rt.literal('y'), ]), + criteria: rt.array(rt.any), alertInterval: rt.string, alertThrottle: rt.string, alertOnNoData: rt.boolean, @@ -85,7 +65,6 @@ const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([ }), rt.type({ alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID), - criteria: rt.array(rt.any), }), ]); export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf< @@ -97,33 +76,15 @@ 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 81e46d85ba220d..589e57a1388b55 100644 --- a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts +++ b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts @@ -5,44 +5,36 @@ * 2.0. */ -export enum ANOMALY_SEVERITY { - CRITICAL = 'critical', - MAJOR = 'major', - MINOR = 'minor', - WARNING = 'warning', - LOW = 'low', - UNKNOWN = 'unknown', -} +export const ML_SEVERITY_SCORES = { + warning: 3, + minor: 25, + major: 50, + critical: 75, +}; -export enum ANOMALY_THRESHOLD { - CRITICAL = 75, - MAJOR = 50, - MINOR = 25, - WARNING = 3, - LOW = 0, -} +export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES; -export const SEVERITY_COLORS = { - CRITICAL: '#fe5050', - MAJOR: '#fba740', - MINOR: '#fdec25', - WARNING: '#8bc8fb', - LOW: '#d2e9f7', - BLANK: '#ffffff', +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 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; +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'; } else { // Category is too low to include - return ANOMALY_SEVERITY.LOW; + return undefined; } }; 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 0135e185e846af..fac87e20dfe7de 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,7 +109,6 @@ 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) 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 2bb98e83cbe70b..a1cee1361a18f6 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,15 +10,13 @@ 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_ANOMALY_ALERT_TYPE_ID; + | typeof METRIC_INVENTORY_THRESHOLD_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 deleted file mode 100644 index f1236c4fc2c2bb..00000000000000 --- a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx +++ /dev/null @@ -1,151 +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 { 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 new file mode 100644 index 00000000000000..a7b6c9fb7104ce --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx @@ -0,0 +1,59 @@ +/* + * 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 33fe3c7af30c78..815e1f2be33f27 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,7 +8,8 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; +// 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 { InfraWaffleMapOptions } from '../../../lib/lib'; import { InventoryItemType } from '../../../../common/inventory_models/types'; import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; @@ -48,18 +49,3 @@ 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 bd7812acac678c..f02f98c49f01af 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 deleted file mode 100644 index ae2c6ed81badb4..00000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx +++ /dev/null @@ -1,74 +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 { 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 deleted file mode 100644 index 5938c7119616f0..00000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx +++ /dev/null @@ -1,320 +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 { 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 deleted file mode 100644 index 34a917a77dcf53..00000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx +++ /dev/null @@ -1,193 +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 { 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 deleted file mode 100644 index 6ddcf8fd5cb659..00000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx +++ /dev/null @@ -1,117 +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 } 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 deleted file mode 100644 index 2dc561ff172b9a..00000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx +++ /dev/null @@ -1,140 +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 } 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 deleted file mode 100644 index 8e254fb2b67a8b..00000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx +++ /dev/null @@ -1,35 +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 { 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 deleted file mode 100644 index 31fed514bdacc1..00000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts +++ /dev/null @@ -1,46 +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 { 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 new file mode 100644 index 00000000000000..3bbe8112258258 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -0,0 +1,57 @@ +/* + * 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 e7e4ade5257fc1..929654ecb4693b 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'; -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; 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,10 +42,3 @@ 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 db5a996c604fcd..1bcc9e7157a514 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,3 +14,4 @@ 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 new file mode 100644 index 00000000000000..c91c1d82afe9b0 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx @@ -0,0 +1,176 @@ +/* + * 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 e63f43470497d2..4b609a881bd18f 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 { hasInfraMLCapabilities } = useInfraMLCapabilitiesContext(); + const { hasInfraMLCapabilites } = useInfraMLCapabilitiesContext(); if ((isLoading || isUninitialized) && !source) { return ; @@ -128,7 +128,7 @@ export const SourceConfigurationSettings = ({ />
- {hasInfraMLCapabilities && ( + {hasInfraMLCapabilites && ( <> { const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob; const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs; - const hasInfraMLCapabilities = + const hasInfraMLCapabilites = mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace; return { - hasInfraMLCapabilities, + hasInfraMLCapabilites, 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 1a759950f640d2..379ac9774c242a 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -56,8 +56,7 @@ class WithKueryAutocompletionComponent extends React.Component< private loadSuggestions = async ( expression: string, cursorPosition: number, - maxSuggestions?: number, - transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[] + maxSuggestions?: number ) => { const { indexPattern } = this.props; const language = 'kuery'; @@ -87,10 +86,6 @@ class WithKueryAutocompletionComponent extends React.Component< boolFilter: [], })) || []; - const transformedSuggestions = transformSuggestions - ? transformSuggestions(suggestions) - : suggestions; - this.setState((state) => state.currentRequest && state.currentRequest.expression !== expression && @@ -99,9 +94,7 @@ class WithKueryAutocompletionComponent extends React.Component< : { ...state, currentRequest: null, - suggestions: maxSuggestions - ? transformedSuggestions.slice(0, maxSuggestions) - : transformedSuggestions, + suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions, } ); }; 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 628df397998eec..f0fdd79bcd93df 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 5fd00527b8b704..4d06d23ef93ef7 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 8fd32bda7fbc8f..52c2a70f2d3591 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -35,11 +35,12 @@ 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 { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown'; +import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown'; +import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/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/anomaly_detection_flyout'; +import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout'; import { HeaderMenuPortal } from '../../../../observability/public'; import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; @@ -82,7 +83,8 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { - + + { jobSummaries: k8sJobSummaries, } = useMetricK8sModuleContext(); const { - hasInfraMLCapabilities, + hasInfraMLCapabilites, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities, } = useInfraMLCapabilitiesContext(); @@ -69,7 +69,7 @@ export const FlyoutHome = (props: Props) => { } }, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]); - if (!hasInfraMLCapabilities) { + if (!hasInfraMLCapabilites) { return ; } else if (!hasInfraMLReadCapabilities) { return ; diff --git a/x-pack/plugins/infra/public/components/subscription_splash_content.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx similarity index 58% rename from x-pack/plugins/infra/public/components/subscription_splash_content.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx index a6477dfc7d172b..e05759ab57dd56 100644 --- a/x-pack/plugins/infra/public/components/subscription_splash_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx @@ -22,11 +22,11 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { euiStyled, EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; -import { HttpStart } from '../../../../../src/core/public'; -import { useTrialStatus } from '../hooks/use_trial_status'; -import { LoadingPage } from '../components/loading_page'; +import { LoadingPage } from '../../../../../../components/loading_page'; +import { useTrialStatus } from '../../../../../../hooks/use_trial_status'; +import { useKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; +import { HttpStart } from '../../../../../../../../../../src/core/public'; export const SubscriptionSplashContent: React.FC = () => { const { services } = useKibana<{ http: HttpStart }>(); @@ -102,60 +102,58 @@ export const SubscriptionSplashContent: React.FC = () => { } return ( - - - - - - - -

{title}

-
- - -

{description}

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

- -

+ + + + + + +

{title}

- + + +

{description}

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

- - - - - - +

+
+ + + +
+
+
+
); }; 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 e22c6fa6611812..44391568741f35 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,19 +10,7 @@ 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, - 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; +import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; interface Props { derivedIndexPattern: IIndexPattern; @@ -30,7 +18,6 @@ interface Props { onChange?: (query: string) => void; value?: string | null; placeholder?: string; - curryLoadSuggestions?: CurryLoadSuggestionsType; } function validateQuery(query: string) { @@ -48,7 +35,6 @@ export const MetricsExplorerKueryBar = ({ onChange, value, placeholder, - curryLoadSuggestions = defaultCurryLoadSuggestions, }: Props) => { const [draftQuery, setDraftQuery] = useState(value || ''); const [isValid, setValidation] = useState(true); @@ -87,7 +73,7 @@ export const MetricsExplorerKueryBar = ({ aria-label={placeholder} isLoadingSuggestions={isLoadingSuggestions} isValid={isValid} - loadSuggestions={curryLoadSuggestions(loadSuggestions)} + loadSuggestions={loadSuggestions} onChange={handleChange} onSubmit={onSubmit} placeholder={placeholder || defaultPlaceholder} @@ -98,6 +84,3 @@ 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 d4bb83e8668ba5..8e7d165f8a5356 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -10,7 +10,6 @@ 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 { @@ -36,7 +35,6 @@ 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 4d70676d25e40f..b18b6e8a6eba6e 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, MlPluginSetup } from '../../ml/public'; +import { MlPluginStart } from '../../ml/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; // Our own setup and start contract values @@ -36,7 +36,6 @@ 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 deleted file mode 100644 index b7ef8ec7d23125..00000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts +++ /dev/null @@ -1,51 +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 { 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 deleted file mode 100644 index ec95aac7268ad9..00000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts +++ /dev/null @@ -1,142 +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 { 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 deleted file mode 100644 index 98992701e3bb4e..00000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts +++ /dev/null @@ -1,120 +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 { 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 deleted file mode 100644 index 8ac62c125515af..00000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts +++ /dev/null @@ -1,110 +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 { 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 11fbe269b854d5..0b4df6805759ee 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,21 +8,13 @@ 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, - ml?: MlPluginSetup -) => { +const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => { 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 686f27d714cc16..0182cb0e4099ac 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/common.ts @@ -17,23 +17,6 @@ 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 82093b1a359d0a..d346b71d76aa8e 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/index.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/index.ts @@ -8,4 +8,3 @@ 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 f6e11f5294191b..7873fd8e43a7bb 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,10 +5,11 @@ * 2.0. */ +import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; -import { getJobId, metricsHostsJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; +import { fetchMlJob } from './common'; +import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -18,6 +19,18 @@ 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, @@ -61,15 +74,14 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricsHostsAnomalies( - context: Required, + context: InfraPluginRequestHandlerContext & { infra: Required }, sourceId: string, - anomalyThreshold: ANOMALY_THRESHOLD, + anomalyThreshold: number, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination, - influencerFilter?: InfluencerFilter + pagination: Pagination ) { const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies'); @@ -77,10 +89,10 @@ export async function getMetricsHostsAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.spaceId, + context.infra.spaceId, sourceId, metric, - context.mlAnomalyDetectors + context.infra.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -96,14 +108,13 @@ export async function getMetricsHostsAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricsHostsAnomalies( - context.mlSystem, + context.infra.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination, - influencerFilter + pagination ); const data = anomalies.map((anomaly) => { @@ -153,13 +164,12 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricsHostsAnomalies( mlSystem: MlSystem, - anomalyThreshold: ANOMALY_THRESHOLD, + anomalyThreshold: number, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination, - influencerFilter?: InfluencerFilter + pagination: Pagination ) { // 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 @@ -178,7 +188,6 @@ 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 34039e9107f007..0c87b2f0f8b536 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,10 +5,11 @@ * 2.0. */ +import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; -import { getJobId, metricsK8SJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; +import { fetchMlJob } from './common'; +import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -18,6 +19,18 @@ 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, @@ -61,15 +74,14 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricK8sAnomalies( - context: Required, + context: InfraPluginRequestHandlerContext & { infra: Required }, sourceId: string, - anomalyThreshold: ANOMALY_THRESHOLD, + anomalyThreshold: number, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination, - influencerFilter?: InfluencerFilter + pagination: Pagination ) { const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies'); @@ -77,10 +89,10 @@ export async function getMetricK8sAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.spaceId, + context.infra.spaceId, sourceId, metric, - context.mlAnomalyDetectors + context.infra.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -95,14 +107,13 @@ export async function getMetricK8sAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricK8sAnomalies( - context.mlSystem, + context.infra.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination, - influencerFilter + pagination ); const data = anomalies.map((anomaly) => { @@ -149,13 +160,12 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricK8sAnomalies( mlSystem: MlSystem, - anomalyThreshold: ANOMALY_THRESHOLD, + anomalyThreshold: number, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination, - influencerFilter?: InfluencerFilter | undefined + pagination: Pagination ) { // 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 @@ -174,7 +184,6 @@ 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 6f996a672a44ad..b3676fc54aeaa5 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,35 +77,3 @@ 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 7808851508a7c3..45587cd258e5d7 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,7 +6,6 @@ */ import * as rt from 'io-ts'; -import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -14,9 +13,7 @@ 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 @@ -35,15 +32,13 @@ export const createMetricsHostsAnomaliesQuery = ({ endTime, sort, pagination, - influencerFilter, }: { jobIds: string[]; - anomalyThreshold: ANOMALY_THRESHOLD; + anomalyThreshold: number; startTime: number; endTime: number; sort: Sort; pagination: Pagination; - influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -55,10 +50,6 @@ export const createMetricsHostsAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; - const influencerQuery = influencerFilter - ? { must: createInfluencerFilter(influencerFilter) } - : {}; - const sourceFields = [ 'job_id', 'record_score', @@ -86,7 +77,6 @@ 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 54eea067177edf..56a4b99e7236c6 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,7 +6,6 @@ */ import * as rt from 'io-ts'; -import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -14,9 +13,7 @@ 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 @@ -35,15 +32,13 @@ export const createMetricsK8sAnomaliesQuery = ({ endTime, sort, pagination, - influencerFilter, }: { jobIds: string[]; - anomalyThreshold: ANOMALY_THRESHOLD; + anomalyThreshold: number; startTime: number; endTime: number; sort: Sort; pagination: Pagination; - influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -55,10 +50,6 @@ export const createMetricsK8sAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; - const influencerQuery = influencerFilter - ? { must: createInfluencerFilter(influencerFilter) } - : {}; - const sourceFields = [ 'job_id', 'record_score', @@ -85,7 +76,6 @@ 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 0ac49e05b36b92..99555fa56acd59 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, plugins.ml); + registerAlertTypes(plugins.alerts, this.libs); 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 3da560135eaf48..cc2cf4092520a1 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -9,21 +9,17 @@ 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; @@ -37,6 +33,8 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { + criteria, + filterQuery, lookback, sourceId, alertType, @@ -57,11 +55,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) try { switch (alertType) { case METRIC_THRESHOLD_ALERT_TYPE_ID: { - const { - groupBy, - criteria, - filterQuery, - } = request.body as MetricThresholdAlertPreviewRequestParams; + const { groupBy } = request.body as MetricThresholdAlertPreviewRequestParams; const previewResult = await previewMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, groupBy }, @@ -78,11 +72,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }); } case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: { - const { - nodeType, - criteria, - filterQuery, - } = request.body as InventoryAlertPreviewRequestParams; + const { nodeType } = request.body as InventoryAlertPreviewRequestParams; const previewResult = await previewInventoryMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, nodeType }, @@ -99,39 +89,6 @@ 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 6e227cfc12d113..8ec0b83994e1a8 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.infra, + requestContext, 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 1c2c4947a02ea3..d41fa0ffafecc5 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.infra, + requestContext, sourceId, anomalyThreshold, startTime, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 018d2d572eea06..6e9d0329eaff83 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9676,6 +9676,7 @@ "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": "アラートを管理", @@ -9969,6 +9970,16 @@ "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 5a9695b8ddc3de..eeda7091044797 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9702,6 +9702,7 @@ "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": "管理告警", @@ -9996,6 +9997,16 @@ "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 应用中分析此类别。", From 1ba7d7af9a36a48d7d956ae58d5acd0967b6ff0f Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 9 Feb 2021 17:31:57 -0700 Subject: [PATCH 6/9] [Maps] remove maps_file_upload plugin and fold public folder into file_upload plugin (#90292) * get geojson working with api/file_upload/import * remove maps_file_upload server code * remove common folder * remove maps_file_upload plugin * fix tsconfig paths * rename file_upload plugin in maps tsconfig * fix file path * node scripts/build_plugin_list_docs * fix webpack compile errors * telemetry schema cleanup, i18n cleanup, limits cleanup * remove mapsFileUpload from limits.yml * remove index pattern link test case * update telemetry/v2/clusters/_stats for new file_upload path Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 4 - packages/kbn-optimizer/limits.yml | 2 +- x-pack/.i18nrc.json | 2 +- x-pack/plugins/file_upload/kibana.json | 4 +- .../public/components/index_settings.js | 0 .../public/components/json_import_progress.js | 4 +- .../components/json_index_file_picker.js | 2 +- .../components/json_upload_and_parse.js | 0 .../public/get_file_upload_component.ts | 0 .../public/index.ts | 2 + .../public/kibana_services.js | 0 .../public/plugin.ts | 0 .../public/util/file_parser.js | 0 .../public/util/file_parser.test.js | 0 .../util/geo_json_clean_and_validate.js | 0 .../util/geo_json_clean_and_validate.test.js | 0 .../public/util/geo_processing.js | 37 ++-- .../public/util/geo_processing.test.js | 3 +- .../public/util/http_service.js | 0 .../public/util/indexing_service.js | 8 +- .../public/util/indexing_service.test.js | 0 .../public/util/size_limited_chunking.js | 2 +- .../public/util/size_limited_chunking.test.js | 0 x-pack/plugins/file_upload/tsconfig.json | 1 + x-pack/plugins/maps/kibana.json | 2 +- .../layers/file_upload_wizard/wizard.tsx | 2 +- x-pack/plugins/maps/public/kibana_services.ts | 2 +- x-pack/plugins/maps/public/plugin.ts | 4 +- x-pack/plugins/maps/tsconfig.json | 2 +- x-pack/plugins/maps_file_upload/README.md | 3 - .../common/constants/file_import.ts | 21 --- .../plugins/maps_file_upload/jest.config.js | 12 -- x-pack/plugins/maps_file_upload/kibana.json | 8 - x-pack/plugins/maps_file_upload/mappings.ts | 16 -- .../maps_file_upload/server/client/errors.js | 12 -- .../plugins/maps_file_upload/server/index.js | 12 -- .../server/kibana_server_services.js | 12 -- .../server/models/import_data/import_data.js | 161 ------------------ .../server/models/import_data/index.js | 8 - .../plugins/maps_file_upload/server/plugin.js | 27 --- .../server/routes/file_upload.js | 131 -------------- .../server/routes/file_upload.test.js | 77 --------- .../telemetry/file_upload_usage_collector.ts | 29 ---- .../server/telemetry/index.ts | 9 - .../server/telemetry/mappings.ts | 22 --- .../server/telemetry/telemetry.test.ts | 48 ------ .../server/telemetry/telemetry.ts | 62 ------- x-pack/plugins/maps_file_upload/tsconfig.json | 15 -- .../file_error_callouts.tsx | 2 +- .../file_based/components/utils/utils.ts | 2 +- .../schema/xpack_plugins.json | 7 - .../apis/telemetry/telemetry_local.js | 2 +- .../import_geojson/file_indexing_panel.js | 15 -- x-pack/tsconfig.json | 2 - x-pack/tsconfig.refs.json | 1 - 55 files changed, 36 insertions(+), 763 deletions(-) rename x-pack/plugins/{maps_file_upload => file_upload}/public/components/index_settings.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/components/json_import_progress.js (96%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/components/json_index_file_picker.js (99%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/components/json_upload_and_parse.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/get_file_upload_component.ts (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/index.ts (94%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/kibana_services.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/plugin.ts (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/file_parser.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/file_parser.test.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/geo_json_clean_and_validate.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/geo_json_clean_and_validate.test.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/geo_processing.js (76%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/geo_processing.test.js (97%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/http_service.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/indexing_service.js (96%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/indexing_service.test.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/size_limited_chunking.js (95%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/size_limited_chunking.test.js (100%) delete mode 100644 x-pack/plugins/maps_file_upload/README.md delete mode 100644 x-pack/plugins/maps_file_upload/common/constants/file_import.ts delete mode 100644 x-pack/plugins/maps_file_upload/jest.config.js delete mode 100644 x-pack/plugins/maps_file_upload/kibana.json delete mode 100644 x-pack/plugins/maps_file_upload/mappings.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/client/errors.js delete mode 100644 x-pack/plugins/maps_file_upload/server/index.js delete mode 100644 x-pack/plugins/maps_file_upload/server/kibana_server_services.js delete mode 100644 x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js delete mode 100644 x-pack/plugins/maps_file_upload/server/models/import_data/index.js delete mode 100644 x-pack/plugins/maps_file_upload/server/plugin.js delete mode 100644 x-pack/plugins/maps_file_upload/server/routes/file_upload.js delete mode 100644 x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/index.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts delete mode 100644 x-pack/plugins/maps_file_upload/tsconfig.json diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 6587d5dc422b47..263addc98ee621 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -443,10 +443,6 @@ using the CURL scripts in the scripts folder. |Visualize geo data from Elasticsearch or 3rd party geo-services. -|{kib-repo}blob/{branch}/x-pack/plugins/maps_file_upload/README.md[mapsFileUpload] -|Deprecated - plugin targeted for removal and will get merged into file_upload plugin - - |{kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] |This plugin provides access to the detailed tile map services from Elastic. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 490c2ccc19d7d1..a1e40c06f6fa17 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -104,4 +104,4 @@ pageLoadAssetSize: presentationUtil: 28545 spacesOss: 18817 osquery: 107090 - mapsFileUpload: 23775 + fileUpload: 25664 diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index f95c4286b3f266..c09198b3874a18 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -20,7 +20,7 @@ "xpack.endpoint": "plugins/endpoint", "xpack.enterpriseSearch": "plugins/enterprise_search", "xpack.features": "plugins/features", - "xpack.fileUpload": "plugins/maps_file_upload", + "xpack.fileUpload": "plugins/file_upload", "xpack.globalSearch": ["plugins/global_search"], "xpack.globalSearchBar": ["plugins/global_search_bar"], "xpack.graph": ["plugins/graph"], diff --git a/x-pack/plugins/file_upload/kibana.json b/x-pack/plugins/file_upload/kibana.json index 7ca024174ec6a1..7676a01d0b0f96 100644 --- a/x-pack/plugins/file_upload/kibana.json +++ b/x-pack/plugins/file_upload/kibana.json @@ -3,6 +3,6 @@ "version": "8.0.0", "kibanaVersion": "kibana", "server": true, - "ui": false, - "requiredPlugins": ["usageCollection"] + "ui": true, + "requiredPlugins": ["data", "usageCollection"] } diff --git a/x-pack/plugins/maps_file_upload/public/components/index_settings.js b/x-pack/plugins/file_upload/public/components/index_settings.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/components/index_settings.js rename to x-pack/plugins/file_upload/public/components/index_settings.js diff --git a/x-pack/plugins/maps_file_upload/public/components/json_import_progress.js b/x-pack/plugins/file_upload/public/components/json_import_progress.js similarity index 96% rename from x-pack/plugins/maps_file_upload/public/components/json_import_progress.js rename to x-pack/plugins/file_upload/public/components/json_import_progress.js index 535142bc3500e1..1f9293e77d33c7 100644 --- a/x-pack/plugins/maps_file_upload/public/components/json_import_progress.js +++ b/x-pack/plugins/file_upload/public/components/json_import_progress.js @@ -118,9 +118,7 @@ export class JsonImportProgress extends Component { {i18n.translate('xpack.fileUpload.jsonImport.indexMgmtLink', { defaultMessage: 'Index Management', diff --git a/x-pack/plugins/maps_file_upload/public/components/json_index_file_picker.js b/x-pack/plugins/file_upload/public/components/json_index_file_picker.js similarity index 99% rename from x-pack/plugins/maps_file_upload/public/components/json_index_file_picker.js rename to x-pack/plugins/file_upload/public/components/json_index_file_picker.js index 8721b5b60f0396..a92412ae9d697c 100644 --- a/x-pack/plugins/maps_file_upload/public/components/json_index_file_picker.js +++ b/x-pack/plugins/file_upload/public/components/json_index_file_picker.js @@ -10,8 +10,8 @@ import { EuiFilePicker, EuiFormRow, EuiProgress } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { parseFile } from '../util/file_parser'; -import { MAX_FILE_SIZE } from '../../common/constants/file_import'; +const MAX_FILE_SIZE = 52428800; const ACCEPTABLE_FILETYPES = ['json', 'geojson']; const acceptedFileTypeString = ACCEPTABLE_FILETYPES.map((type) => `.${type}`).join(','); const acceptedFileTypeStringMessage = ACCEPTABLE_FILETYPES.map((type) => `.${type}`).join(', '); diff --git a/x-pack/plugins/maps_file_upload/public/components/json_upload_and_parse.js b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/components/json_upload_and_parse.js rename to x-pack/plugins/file_upload/public/components/json_upload_and_parse.js diff --git a/x-pack/plugins/maps_file_upload/public/get_file_upload_component.ts b/x-pack/plugins/file_upload/public/get_file_upload_component.ts similarity index 100% rename from x-pack/plugins/maps_file_upload/public/get_file_upload_component.ts rename to x-pack/plugins/file_upload/public/get_file_upload_component.ts diff --git a/x-pack/plugins/maps_file_upload/public/index.ts b/x-pack/plugins/file_upload/public/index.ts similarity index 94% rename from x-pack/plugins/maps_file_upload/public/index.ts rename to x-pack/plugins/file_upload/public/index.ts index 95553685cbbddc..efabc984e0220d 100644 --- a/x-pack/plugins/maps_file_upload/public/index.ts +++ b/x-pack/plugins/file_upload/public/index.ts @@ -11,5 +11,7 @@ export function plugin() { return new FileUploadPlugin(); } +export * from '../common'; + export { StartContract } from './plugin'; export { FileUploadComponentProps } from './get_file_upload_component'; diff --git a/x-pack/plugins/maps_file_upload/public/kibana_services.js b/x-pack/plugins/file_upload/public/kibana_services.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/kibana_services.js rename to x-pack/plugins/file_upload/public/kibana_services.js diff --git a/x-pack/plugins/maps_file_upload/public/plugin.ts b/x-pack/plugins/file_upload/public/plugin.ts similarity index 100% rename from x-pack/plugins/maps_file_upload/public/plugin.ts rename to x-pack/plugins/file_upload/public/plugin.ts diff --git a/x-pack/plugins/maps_file_upload/public/util/file_parser.js b/x-pack/plugins/file_upload/public/util/file_parser.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/file_parser.js rename to x-pack/plugins/file_upload/public/util/file_parser.js diff --git a/x-pack/plugins/maps_file_upload/public/util/file_parser.test.js b/x-pack/plugins/file_upload/public/util/file_parser.test.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/file_parser.test.js rename to x-pack/plugins/file_upload/public/util/file_parser.test.js diff --git a/x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.js b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.js rename to x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.js diff --git a/x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.test.js b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.test.js rename to x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js diff --git a/x-pack/plugins/maps_file_upload/public/util/geo_processing.js b/x-pack/plugins/file_upload/public/util/geo_processing.js similarity index 76% rename from x-pack/plugins/maps_file_upload/public/util/geo_processing.js rename to x-pack/plugins/file_upload/public/util/geo_processing.js index d6f9651496aca9..c90c55c2b49ac3 100644 --- a/x-pack/plugins/maps_file_upload/public/util/geo_processing.js +++ b/x-pack/plugins/file_upload/public/util/geo_processing.js @@ -6,26 +6,12 @@ */ import _ from 'lodash'; -import { ES_GEO_FIELD_TYPE } from '../../common/constants/file_import'; -const DEFAULT_SETTINGS = { - number_of_shards: 1, +export const ES_GEO_FIELD_TYPE = { + GEO_POINT: 'geo_point', + GEO_SHAPE: 'geo_shape', }; -const DEFAULT_GEO_SHAPE_MAPPINGS = { - coordinates: { - type: ES_GEO_FIELD_TYPE.GEO_SHAPE, - }, -}; - -const DEFAULT_GEO_POINT_MAPPINGS = { - coordinates: { - type: ES_GEO_FIELD_TYPE.GEO_POINT, - }, -}; - -const DEFAULT_INGEST_PIPELINE = {}; - export function getGeoIndexTypesForFeatures(featureTypes) { const hasNoFeatureType = !featureTypes || !featureTypes.length; if (hasNoFeatureType) { @@ -77,11 +63,16 @@ export function geoJsonToEs(parsedGeojson, datatype) { export function getGeoJsonIndexingDetails(parsedGeojson, dataType) { return { data: geoJsonToEs(parsedGeojson, dataType), - ingestPipeline: DEFAULT_INGEST_PIPELINE, - mappings: - dataType === ES_GEO_FIELD_TYPE.GEO_POINT - ? DEFAULT_GEO_POINT_MAPPINGS - : DEFAULT_GEO_SHAPE_MAPPINGS, - settings: DEFAULT_SETTINGS, + ingestPipeline: {}, + mappings: { + properties: { + coordinates: { + type: dataType, + }, + }, + }, + settings: { + number_of_shards: 1, + }, }; } diff --git a/x-pack/plugins/maps_file_upload/public/util/geo_processing.test.js b/x-pack/plugins/file_upload/public/util/geo_processing.test.js similarity index 97% rename from x-pack/plugins/maps_file_upload/public/util/geo_processing.test.js rename to x-pack/plugins/file_upload/public/util/geo_processing.test.js index 75da5bae015aff..37b665c0a3e162 100644 --- a/x-pack/plugins/maps_file_upload/public/util/geo_processing.test.js +++ b/x-pack/plugins/file_upload/public/util/geo_processing.test.js @@ -5,8 +5,7 @@ * 2.0. */ -import { geoJsonToEs } from './geo_processing'; -import { ES_GEO_FIELD_TYPE } from '../../common/constants/file_import'; +import { ES_GEO_FIELD_TYPE, geoJsonToEs } from './geo_processing'; describe('geo_processing', () => { describe('getGeoJsonToEs', () => { diff --git a/x-pack/plugins/maps_file_upload/public/util/http_service.js b/x-pack/plugins/file_upload/public/util/http_service.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/http_service.js rename to x-pack/plugins/file_upload/public/util/http_service.js diff --git a/x-pack/plugins/maps_file_upload/public/util/indexing_service.js b/x-pack/plugins/file_upload/public/util/indexing_service.js similarity index 96% rename from x-pack/plugins/maps_file_upload/public/util/indexing_service.js rename to x-pack/plugins/file_upload/public/util/indexing_service.js index c29e9685162bc8..253681dad6a7da 100644 --- a/x-pack/plugins/maps_file_upload/public/util/indexing_service.js +++ b/x-pack/plugins/file_upload/public/util/indexing_service.js @@ -11,8 +11,6 @@ import { getGeoJsonIndexingDetails } from './geo_processing'; import { sizeLimitedChunking } from './size_limited_chunking'; import { i18n } from '@kbn/i18n'; -const fileType = 'json'; - export async function indexData(parsedFile, transformDetails, indexName, dataType, appName) { if (!parsedFile) { throw i18n.translate('xpack.fileUpload.indexingService.noFileImported', { @@ -117,10 +115,10 @@ function transformDataByFormatForIndexing(transform, parsedFile, dataType) { async function writeToIndex(indexingDetails) { const query = indexingDetails.id ? { id: indexingDetails.id } : null; - const { appName, index, data, settings, mappings, ingestPipeline } = indexingDetails; + const { index, data, settings, mappings, ingestPipeline } = indexingDetails; return await httpService({ - url: `/api/maps/fileupload/import`, + url: `/api/file_upload/import`, method: 'POST', ...(query ? { query } : {}), data: { @@ -129,8 +127,6 @@ async function writeToIndex(indexingDetails) { settings, mappings, ingestPipeline, - fileType, - ...(appName ? { app: appName } : {}), }, }); } diff --git a/x-pack/plugins/maps_file_upload/public/util/indexing_service.test.js b/x-pack/plugins/file_upload/public/util/indexing_service.test.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/indexing_service.test.js rename to x-pack/plugins/file_upload/public/util/indexing_service.test.js diff --git a/x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.js b/x-pack/plugins/file_upload/public/util/size_limited_chunking.js similarity index 95% rename from x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.js rename to x-pack/plugins/file_upload/public/util/size_limited_chunking.js index e42e11d0f27f0b..09d4e8ca8e3a2e 100644 --- a/x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.js +++ b/x-pack/plugins/file_upload/public/util/size_limited_chunking.js @@ -5,7 +5,7 @@ * 2.0. */ -import { MAX_BYTES } from '../../common/constants/file_import'; +const MAX_BYTES = 31457280; // MAX_BYTES is a good guideline for splitting up posts, but this logic // occasionally sizes chunks so closely to the limit, that the remaining content diff --git a/x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.test.js b/x-pack/plugins/file_upload/public/util/size_limited_chunking.test.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.test.js rename to x-pack/plugins/file_upload/public/util/size_limited_chunking.test.js diff --git a/x-pack/plugins/file_upload/tsconfig.json b/x-pack/plugins/file_upload/tsconfig.json index f985a4599d5fed..bebb08e6dd5e33 100644 --- a/x-pack/plugins/file_upload/tsconfig.json +++ b/x-pack/plugins/file_upload/tsconfig.json @@ -10,6 +10,7 @@ "include": ["common/**/*", "public/**/*", "server/**/*"], "references": [ { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" } ] } diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 3966af9e287421..1d4f76db797517 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -11,7 +11,7 @@ "features", "inspector", "data", - "mapsFileUpload", + "fileUpload", "uiActions", "navigation", "visualizations", diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 71be12c4304a6e..44a22f1529f189 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -19,7 +19,7 @@ import { GeoJsonFileSource } from '../../sources/geojson_file_source'; import { VectorLayer } from '../../layers/vector_layer'; import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; -import { FileUploadComponentProps } from '../../../../../maps_file_upload/public'; +import { FileUploadComponentProps } from '../../../../../file_upload/public'; export const INDEX_SETUP_STEP_ID = 'INDEX_SETUP_STEP_ID'; export const INDEXING_STEP_ID = 'INDEXING_STEP_ID'; diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 4a7bccb31380db..1fbca669b0d8ef 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -27,7 +27,7 @@ export const getIndexPatternService = () => pluginsStart.data.indexPatterns; export const getAutocompleteService = () => pluginsStart.data.autocomplete; export const getInspector = () => pluginsStart.inspector; export const getFileUploadComponent = async () => { - return await pluginsStart.mapsFileUpload.getFileUploadComponent(); + return await pluginsStart.fileUpload.getFileUploadComponent(); }; export const getUiSettings = () => coreStart.uiSettings; export const getIsDarkMode = () => getUiSettings().get('theme:darkMode', false); diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 4c668e0a2276b2..12cff9edf55ff6 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -54,7 +54,7 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; -import { StartContract as FileUploadStartContract } from '../../maps_file_upload/public'; +import { StartContract as FileUploadStartContract } from '../../file_upload/public'; import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { @@ -80,7 +80,7 @@ export interface MapsPluginStartDependencies { charts: ChartsPluginStart; data: DataPublicPluginStart; embeddable: EmbeddableStart; - mapsFileUpload: FileUploadStartContract; + fileUpload: FileUploadStartContract; inspector: InspectorStartContract; licensing: LicensingPluginStart; navigation: NavigationPublicPluginStart; diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index b70459c690c072..4a8bfe2ebae66e 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -19,7 +19,7 @@ { "path": "../../../src/plugins/maps_legacy/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, - { "path": "../maps_file_upload/tsconfig.json" }, + { "path": "../file_upload/tsconfig.json" }, { "path": "../saved_objects_tagging/tsconfig.json" }, ] } diff --git a/x-pack/plugins/maps_file_upload/README.md b/x-pack/plugins/maps_file_upload/README.md deleted file mode 100644 index 1e3343664afb83..00000000000000 --- a/x-pack/plugins/maps_file_upload/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Maps File upload - -Deprecated - plugin targeted for removal and will get merged into file_upload plugin diff --git a/x-pack/plugins/maps_file_upload/common/constants/file_import.ts b/x-pack/plugins/maps_file_upload/common/constants/file_import.ts deleted file mode 100644 index 9e4763c2c8113b..00000000000000 --- a/x-pack/plugins/maps_file_upload/common/constants/file_import.ts +++ /dev/null @@ -1,21 +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. - */ - -export const MAX_BYTES = 31457280; - -export const MAX_FILE_SIZE = 52428800; - -// Value to use in the Elasticsearch index mapping metadata to identify the -// index as having been created by the File Upload Plugin. -export const INDEX_META_DATA_CREATED_BY = 'file-upload-plugin'; - -export const ES_GEO_FIELD_TYPE = { - GEO_POINT: 'geo_point', - GEO_SHAPE: 'geo_shape', -}; - -export const DEFAULT_KBN_VERSION = 'kbnVersion'; diff --git a/x-pack/plugins/maps_file_upload/jest.config.js b/x-pack/plugins/maps_file_upload/jest.config.js deleted file mode 100644 index e7b45a559df10d..00000000000000 --- a/x-pack/plugins/maps_file_upload/jest.config.js +++ /dev/null @@ -1,12 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/plugins/maps_file_upload'], -}; diff --git a/x-pack/plugins/maps_file_upload/kibana.json b/x-pack/plugins/maps_file_upload/kibana.json deleted file mode 100644 index f544c56cba517e..00000000000000 --- a/x-pack/plugins/maps_file_upload/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "mapsFileUpload", - "version": "8.0.0", - "kibanaVersion": "kibana", - "server": true, - "ui": true, - "requiredPlugins": ["data", "usageCollection"] -} diff --git a/x-pack/plugins/maps_file_upload/mappings.ts b/x-pack/plugins/maps_file_upload/mappings.ts deleted file mode 100644 index b8b263409f8140..00000000000000 --- a/x-pack/plugins/maps_file_upload/mappings.ts +++ /dev/null @@ -1,16 +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. - */ - -export const mappings = { - 'file-upload-telemetry': { - properties: { - filesUploadedTotalCount: { - type: 'long', - }, - }, - }, -}; diff --git a/x-pack/plugins/maps_file_upload/server/client/errors.js b/x-pack/plugins/maps_file_upload/server/client/errors.js deleted file mode 100644 index 8f8516158b3034..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/client/errors.js +++ /dev/null @@ -1,12 +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 { boomify } from '@hapi/boom'; - -export function wrapError(error) { - return boomify(error, { statusCode: error.status }); -} diff --git a/x-pack/plugins/maps_file_upload/server/index.js b/x-pack/plugins/maps_file_upload/server/index.js deleted file mode 100644 index 4bf4e931c7eaaa..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/index.js +++ /dev/null @@ -1,12 +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 { FileUploadPlugin } from './plugin'; - -export * from './plugin'; - -export const plugin = () => new FileUploadPlugin(); diff --git a/x-pack/plugins/maps_file_upload/server/kibana_server_services.js b/x-pack/plugins/maps_file_upload/server/kibana_server_services.js deleted file mode 100644 index 8a1278f433ab9d..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/kibana_server_services.js +++ /dev/null @@ -1,12 +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. - */ - -let internalRepository; -export const setInternalRepository = (createInternalRepository) => { - internalRepository = createInternalRepository(); -}; -export const getInternalRepository = () => internalRepository; diff --git a/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js b/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js deleted file mode 100644 index 7ba491a8ea49eb..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js +++ /dev/null @@ -1,161 +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 { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_import'; -import uuid from 'uuid'; - -export function importDataProvider(callWithRequest) { - async function importData(id, index, settings, mappings, ingestPipeline, data) { - let createdIndex; - let createdPipelineId; - const docCount = data.length; - - try { - const { id: pipelineId, pipeline } = ingestPipeline; - - if (!id) { - // first chunk of data, create the index and id to return - id = uuid.v1(); - - await createIndex(index, settings, mappings); - createdIndex = index; - - // create the pipeline if one has been supplied - if (pipelineId !== undefined) { - const success = await createPipeline(pipelineId, pipeline); - if (success.acknowledged !== true) { - throw success; - } - } - createdPipelineId = pipelineId; - } else { - createdIndex = index; - createdPipelineId = pipelineId; - } - - let failures = []; - if (data.length) { - const resp = await indexData(index, createdPipelineId, data); - if (resp.success === false) { - if (resp.ingestError) { - // all docs failed, abort - throw resp; - } else { - // some docs failed. - // still report success but with a list of failures - failures = resp.failures || []; - } - } - } - - return { - success: true, - id, - index: createdIndex, - pipelineId: createdPipelineId, - docCount, - failures, - }; - } catch (error) { - return { - success: false, - id, - index: createdIndex, - pipelineId: createdPipelineId, - error: error.error !== undefined ? error.error : error, - docCount, - ingestError: error.ingestError, - failures: error.failures || [], - }; - } - } - - async function createIndex(index, settings, mappings) { - const body = { - mappings: { - _meta: { - created_by: INDEX_META_DATA_CREATED_BY, - }, - properties: mappings, - }, - }; - - if (settings && Object.keys(settings).length) { - body.settings = settings; - } - - await callWithRequest('indices.create', { index, body }); - } - - async function indexData(index, pipelineId, data) { - try { - const body = []; - for (let i = 0; i < data.length; i++) { - body.push({ index: {} }); - body.push(data[i]); - } - - const settings = { index, body }; - if (pipelineId !== undefined) { - settings.pipeline = pipelineId; - } - - const resp = await callWithRequest('bulk', settings); - if (resp.errors) { - throw resp; - } else { - return { - success: true, - docs: data.length, - failures: [], - }; - } - } catch (error) { - let failures = []; - let ingestError = false; - if (error.errors !== undefined && Array.isArray(error.items)) { - // an expected error where some or all of the bulk request - // docs have failed to be ingested. - failures = getFailures(error.items, data); - } else { - // some other error has happened. - ingestError = true; - } - - return { - success: false, - error, - docCount: data.length, - failures, - ingestError, - }; - } - } - - async function createPipeline(id, pipeline) { - return await callWithRequest('ingest.putPipeline', { id, body: pipeline }); - } - - function getFailures(items, data) { - const failures = []; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item.index && item.index.error) { - failures.push({ - item: i, - reason: item.index.error.reason, - doc: data[i], - }); - } - } - return failures; - } - - return { - importData, - }; -} diff --git a/x-pack/plugins/maps_file_upload/server/models/import_data/index.js b/x-pack/plugins/maps_file_upload/server/models/import_data/index.js deleted file mode 100644 index c1ba4b84975e5a..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/models/import_data/index.js +++ /dev/null @@ -1,8 +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. - */ - -export { importDataProvider } from './import_data'; diff --git a/x-pack/plugins/maps_file_upload/server/plugin.js b/x-pack/plugins/maps_file_upload/server/plugin.js deleted file mode 100644 index 1072da863acc7f..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/plugin.js +++ /dev/null @@ -1,27 +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 { initRoutes } from './routes/file_upload'; -import { setInternalRepository } from './kibana_server_services'; -import { registerFileUploadUsageCollector, fileUploadTelemetryMappingsType } from './telemetry'; - -export class FileUploadPlugin { - constructor() { - this.router = null; - } - - setup(core, plugins) { - core.savedObjects.registerType(fileUploadTelemetryMappingsType); - this.router = core.http.createRouter(); - registerFileUploadUsageCollector(plugins.usageCollection); - } - - start(core) { - initRoutes(this.router, core.savedObjects.getSavedObjectsRepository); - setInternalRepository(core.savedObjects.createInternalRepository); - } -} diff --git a/x-pack/plugins/maps_file_upload/server/routes/file_upload.js b/x-pack/plugins/maps_file_upload/server/routes/file_upload.js deleted file mode 100644 index 517fab13363a2a..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/routes/file_upload.js +++ /dev/null @@ -1,131 +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 { importDataProvider } from '../models/import_data'; -import { updateTelemetry } from '../telemetry/telemetry'; -import { MAX_BYTES } from '../../common/constants/file_import'; -import { schema } from '@kbn/config-schema'; - -export const IMPORT_ROUTE = '/api/maps/fileupload/import'; - -export const querySchema = schema.maybe( - schema.object({ - id: schema.nullable(schema.string()), - }) -); - -export const bodySchema = schema.object( - { - app: schema.maybe(schema.string()), - index: schema.string(), - fileType: schema.string(), - ingestPipeline: schema.maybe( - schema.object( - {}, - { - defaultValue: {}, - unknowns: 'allow', - } - ) - ), - }, - { unknowns: 'allow' } -); - -const options = { - body: { - maxBytes: MAX_BYTES, - accepts: ['application/json'], - }, - tags: ['access:fileUpload:import'], -}; - -export const idConditionalValidation = (body, boolHasId) => - schema - .object( - { - data: boolHasId - ? schema.arrayOf(schema.object({}, { unknowns: 'allow' }), { minSize: 1 }) - : schema.any(), - settings: boolHasId - ? schema.any() - : schema.object( - {}, - { - defaultValue: { - number_of_shards: 1, - }, - unknowns: 'allow', - } - ), - mappings: boolHasId - ? schema.any() - : schema.object( - {}, - { - defaultValue: {}, - unknowns: 'allow', - } - ), - }, - { unknowns: 'allow' } - ) - .validate(body); - -const finishValidationAndProcessReq = () => { - return async (con, req, { ok, badRequest }) => { - const { - query: { id }, - body, - } = req; - const boolHasId = !!id; - - let resp; - try { - const validIdReqData = idConditionalValidation(body, boolHasId); - const callWithRequest = con.core.elasticsearch.legacy.client.callAsCurrentUser; - const { importData: importDataFunc } = importDataProvider(callWithRequest); - - const { index, settings, mappings, ingestPipeline, data } = validIdReqData; - const processedReq = await importDataFunc( - id, - index, - settings, - mappings, - ingestPipeline, - data - ); - - if (processedReq.success) { - resp = ok({ body: processedReq }); - // If no id's been established then this is a new index, update telemetry - if (!boolHasId) { - await updateTelemetry(); - } - } else { - resp = badRequest(`Error processing request 1: ${processedReq.error.message}`, ['body']); - } - } catch (e) { - resp = badRequest(`Error processing request 2: : ${e.message}`, ['body']); - } - return resp; - }; -}; - -export const initRoutes = (router) => { - router.post( - { - path: `${IMPORT_ROUTE}{id?}`, - validate: { - query: querySchema, - body: bodySchema, - }, - options, - }, - finishValidationAndProcessReq() - ); -}; diff --git a/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js b/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js deleted file mode 100644 index e893e103aad722..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js +++ /dev/null @@ -1,77 +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 { querySchema, bodySchema, idConditionalValidation } from './file_upload'; - -const queryWithId = { - id: '123', -}; - -const bodyWithoutQueryId = { - index: 'islandofone', - data: [], - settings: { number_of_shards: 1 }, - mappings: { coordinates: { type: 'geo_point' } }, - ingestPipeline: {}, - fileType: 'json', - app: 'Maps', -}; - -const bodyWithQueryId = { - index: 'islandofone2', - data: [{ coordinates: [], name: 'islandofone2' }], - settings: {}, - mappings: {}, - ingestPipeline: {}, - fileType: 'json', -}; - -describe('route validation', () => { - it(`validates query with id`, async () => { - const validationResult = querySchema.validate(queryWithId); - expect(validationResult.id).toBe(queryWithId.id); - }); - - it(`validates query without id`, async () => { - const validationResult = querySchema.validate({}); - expect(validationResult.id).toBeNull(); - }); - - it(`throws when query contains content other than an id`, async () => { - expect(() => querySchema.validate({ notAnId: 123 })).toThrowError( - `[notAnId]: definition for this key is missing` - ); - }); - - it(`validates body with valid fields`, async () => { - const validationResult = bodySchema.validate(bodyWithoutQueryId); - expect(validationResult).toEqual(bodyWithoutQueryId); - }); - - it(`throws if an expected field is missing`, async () => { - /* eslint-disable no-unused-vars */ - const { index, ...bodyWithoutIndexField } = bodyWithoutQueryId; - expect(() => bodySchema.validate(bodyWithoutIndexField)).toThrowError( - `[index]: expected value of type [string] but got [undefined]` - ); - }); - - it(`validates conditional fields when id has been provided in query`, async () => { - const validationResult = idConditionalValidation(bodyWithQueryId, true); - expect(validationResult).toEqual(bodyWithQueryId); - }); - - it(`validates conditional fields when no id has been provided in query`, async () => { - const validationResultWhenIdPresent = idConditionalValidation(bodyWithoutQueryId, false); - expect(validationResultWhenIdPresent).toEqual(bodyWithoutQueryId); - // Conditions for no id are more strict since this query sets up the index, - // expect it to throw if expected fields aren't present - expect(() => idConditionalValidation(bodyWithoutQueryId, true)).toThrowError( - `[data]: array size is [0], but cannot be smaller than [1]` - ); - }); -}); diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts deleted file mode 100644 index bf786aa830448e..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts +++ /dev/null @@ -1,29 +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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { getTelemetry, initTelemetry, Telemetry } from './telemetry'; - -export function registerFileUploadUsageCollector(usageCollection: UsageCollectionSetup): void { - const fileUploadUsageCollector = usageCollection.makeUsageCollector({ - type: 'fileUploadTelemetry', - isReady: () => true, - fetch: async () => { - const fileUploadUsage = await getTelemetry(); - if (!fileUploadUsage) { - return initTelemetry(); - } - - return fileUploadUsage; - }, - schema: { - filesUploadedTotalCount: { type: 'long' }, - }, - }); - - usageCollection.registerCollector(fileUploadUsageCollector); -} diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/index.ts b/x-pack/plugins/maps_file_upload/server/telemetry/index.ts deleted file mode 100644 index 83cd64c3f0e6f9..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/index.ts +++ /dev/null @@ -1,9 +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. - */ - -export { registerFileUploadUsageCollector } from './file_upload_usage_collector'; -export { fileUploadTelemetryMappingsType } from './mappings'; diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts b/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts deleted file mode 100644 index ee79e2f6c6d476..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts +++ /dev/null @@ -1,22 +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 { SavedObjectsType } from 'src/core/server'; -import { TELEMETRY_DOC_ID } from './telemetry'; - -export const fileUploadTelemetryMappingsType: SavedObjectsType = { - name: TELEMETRY_DOC_ID, - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - filesUploadedTotalCount: { - type: 'long', - }, - }, - }, -}; diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts b/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts deleted file mode 100644 index 2ca01b03aa6331..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts +++ /dev/null @@ -1,48 +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 { getTelemetry, updateTelemetry } from './telemetry'; - -const internalRepository = () => ({ - get: jest.fn(() => null), - create: jest.fn(() => ({ attributes: 'test' })), - update: jest.fn(() => ({ attributes: 'test' })), -}); - -function mockInit(getVal: any = { attributes: {} }): any { - return { - ...internalRepository(), - get: jest.fn(() => getVal), - }; -} - -describe('file upload plugin telemetry', () => { - describe('getTelemetry', () => { - it('should get existing telemetry', async () => { - const internalRepo = mockInit(); - await getTelemetry(internalRepo); - expect(internalRepo.update.mock.calls.length).toBe(0); - expect(internalRepo.get.mock.calls.length).toBe(1); - expect(internalRepo.create.mock.calls.length).toBe(0); - }); - }); - - describe('updateTelemetry', () => { - it('should update existing telemetry', async () => { - const internalRepo = mockInit({ - attributes: { - filesUploadedTotalCount: 2, - }, - }); - - await updateTelemetry(internalRepo); - expect(internalRepo.update.mock.calls.length).toBe(1); - expect(internalRepo.get.mock.calls.length).toBe(1); - expect(internalRepo.create.mock.calls.length).toBe(0); - }); - }); -}); diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts b/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts deleted file mode 100644 index 0e53c2570e01bc..00000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts +++ /dev/null @@ -1,62 +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 _ from 'lodash'; -// @ts-ignore -import { getInternalRepository } from '../kibana_server_services'; - -export const TELEMETRY_DOC_ID = 'file-upload-telemetry'; - -export interface Telemetry { - filesUploadedTotalCount: number; -} - -export interface TelemetrySavedObject { - attributes: Telemetry; -} - -export function initTelemetry(): Telemetry { - return { - filesUploadedTotalCount: 0, - }; -} - -export async function getTelemetry(internalRepo?: object): Promise { - const internalRepository = internalRepo || getInternalRepository(); - let telemetrySavedObject; - - try { - telemetrySavedObject = await internalRepository.get(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID); - } catch (e) { - // Fail silently - } - - return telemetrySavedObject ? telemetrySavedObject.attributes : null; -} - -export async function updateTelemetry(internalRepo?: any) { - const internalRepository = internalRepo || getInternalRepository(); - let telemetry = await getTelemetry(internalRepository); - // Create if doesn't exist - if (!telemetry || _.isEmpty(telemetry)) { - const newTelemetrySavedObject = await internalRepository.create( - TELEMETRY_DOC_ID, - initTelemetry(), - { id: TELEMETRY_DOC_ID } - ); - telemetry = newTelemetrySavedObject.attributes; - } - - await internalRepository.update(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID, incrementCounts(telemetry)); -} - -export function incrementCounts({ filesUploadedTotalCount }: { filesUploadedTotalCount: number }) { - return { - // TODO: get telemetry for app, total file counts, file type - filesUploadedTotalCount: filesUploadedTotalCount + 1, - }; -} diff --git a/x-pack/plugins/maps_file_upload/tsconfig.json b/x-pack/plugins/maps_file_upload/tsconfig.json deleted file mode 100644 index f068d62b71739d..00000000000000 --- a/x-pack/plugins/maps_file_upload/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["common/**/*", "public/**/*", "server/**/*", "mappings.ts"], - "references": [ - { "path": "../../../src/plugins/data/tsconfig.json" }, - { "path": "../../../src/plugins/usage_collection/tsconfig.json" } - ] -} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx index 0535b15912a9b3..0fa7de4732c391 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx @@ -12,7 +12,7 @@ import { EuiCallOut, EuiSpacer, EuiButtonEmpty, EuiHorizontalRule } from '@elast import numeral from '@elastic/numeral'; import { ErrorResponse } from '../../../../../../common/types/errors'; -import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../../../file_upload/common'; +import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../../../file_upload/public'; interface FileTooLargeProps { fileSize: number; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 47f262ef45a18e..4412390d62c1fc 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -15,7 +15,7 @@ import { MAX_FILE_SIZE_BYTES, ABSOLUTE_MAX_FILE_SIZE_BYTES, FILE_SIZE_DISPLAY_FORMAT, -} from '../../../../../../../file_upload/common'; +} from '../../../../../../../file_upload/public'; import { getUiSettings } from '../../../../util/dependency_cache'; import { FILE_DATA_VISUALIZER_MAX_FILE_SIZE } from '../../../../../../common/constants/settings'; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 9e6a0c06808bc8..d4b07203e8109e 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -2219,13 +2219,6 @@ } } }, - "fileUploadTelemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, "maps": { "properties": { "settings": { diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js index 9a6602ef923d36..3be24b273ae4c1 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js @@ -83,7 +83,7 @@ export default function ({ getService }) { expect(stats.stack_stats.kibana.plugins.reporting.enabled).to.be(true); expect(stats.stack_stats.kibana.plugins.rollups.index_patterns).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.spaces.available).to.be(true); - expect(stats.stack_stats.kibana.plugins.fileUploadTelemetry.filesUploadedTotalCount).to.be.a( + expect(stats.stack_stats.kibana.plugins.fileUpload.file_upload.index_creation_count).to.be.a( 'number' ); diff --git a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js index 4496b59393eece..0ce9b7022b38d6 100644 --- a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js +++ b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js @@ -11,7 +11,6 @@ import uuid from 'uuid/v4'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['maps', 'common']); - const testSubjects = getService('testSubjects'); const log = getService('log'); const security = getService('security'); @@ -99,20 +98,6 @@ export default function ({ getService, getPageObjects }) { expect(newIndexedLayerExists).to.be(false); }); - it('should create a link to new index in management', async () => { - const indexName = await indexPoint(); - - const layerAddReady = await PageObjects.maps.importLayerReadyForAdd(); - expect(layerAddReady).to.be(true); - - const newIndexLinkExists = await testSubjects.exists('indexManagementNewIndexLink'); - expect(newIndexLinkExists).to.be(true); - - const indexLink = await testSubjects.getAttribute('indexManagementNewIndexLink', 'href'); - const linkDirectsToNewIndex = indexLink.endsWith(indexName); - expect(linkDirectsToNewIndex).to.be(true); - }); - const GEO_POINT = 'geo_point'; const pointGeojsonFiles = ['point.json', 'multi_point.json']; pointGeojsonFiles.forEach(async (pointFile) => { diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index f4497487f6ff99..00286ac47da6ea 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -27,7 +27,6 @@ "plugins/licensing/**/*", "plugins/lens/**/*", "plugins/maps/**/*", - "plugins/maps_file_upload/**/*", "plugins/maps_legacy_licensing/**/*", "plugins/ml/**/*", "plugins/observability/**/*", @@ -130,7 +129,6 @@ { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, - { "path": "./plugins/maps_file_upload/tsconfig.json" }, { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./plugins/maps/tsconfig.json" }, { "path": "./plugins/ml/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index d4d7e8caa5088f..a36f4e205ab7da 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -29,7 +29,6 @@ { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, - { "path": "./plugins/maps_file_upload/tsconfig.json" }, { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./plugins/maps/tsconfig.json" }, { "path": "./plugins/ml/tsconfig.json" }, From 646732f96d4842bc1b66854f4a94c87bfc3959c0 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 9 Feb 2021 17:49:53 -0700 Subject: [PATCH 7/9] [core.logging] Uses host timezone as default (#90368) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/migration/migrate_8_0.asciidoc | 13 +++++++- docs/setup/settings.asciidoc | 3 +- .../deprecation/core_deprecations.test.ts | 20 ++++++++++++ .../config/deprecation/core_deprecations.ts | 13 ++++++++ .../legacy/integration_tests/logging.test.ts | 20 ++++++------ src/core/server/logging/README.md | 31 ++++++++++--------- .../__snapshots__/logging_system.test.ts.snap | 14 ++++----- .../__snapshots__/pattern_layout.test.ts.snap | 24 +++++++------- .../logging/layouts/conversions/date.ts | 11 ++++--- .../logging/layouts/pattern_layout.test.ts | 12 ++++--- 10 files changed, 106 insertions(+), 55 deletions(-) diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 14eff4594c8137..5452621440ed80 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -52,7 +52,18 @@ for example, `logstash-*`. ==== Default logging timezone is now the system's timezone *Details:* In prior releases the timezone used in logs defaulted to UTC. We now use the host machine's timezone by default. -*Impact:* To restore the previous behavior, in kibana.yml set `logging.timezone: UTC`. +*Impact:* To restore the previous behavior, in kibana.yml use the pattern layout, with a date modifier: +[source,yaml] +------------------- +logging: + appenders: + console: + kind: console + layout: + kind: pattern + pattern: "%date{ISO8601_TZ}{UTC}" +------------------- +See https://github.com/elastic/kibana/pull/90368 for more details. [float] ==== Responses are never logged by default diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index ecdb41c897b125..9b9c26fd0e1db3 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -309,7 +309,8 @@ suppress all logging output. *Default: `false`* | Set to the canonical time zone ID (for example, `America/Los_Angeles`) to log events using that time zone. For possible values, refer to -https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[database time zones]. *Default: `UTC`* +https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[database time zones]. +When not set, log events use the host timezone | [[logging-verbose]] `logging.verbose:` {ess-icon} | Set to `true` to log all events, including system usage information and all diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index 70ca91b0d63175..4d7dafd2162c2d 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -298,4 +298,24 @@ describe('core deprecations', () => { expect(messages).toEqual([]); }); }); + + describe('logging.timezone', () => { + it('warns when ops events are used', () => { + const { messages } = applyCoreDeprecations({ + logging: { timezone: 'GMT' }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.timezone\\" has been deprecated and will be removed in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md", + ] + `); + }); + + it('does not warn when other events are configured', () => { + const { messages } = applyCoreDeprecations({ + logging: { events: { log: '*' } }, + }); + expect(messages).toEqual([]); + }); + }); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 0db53cdb2e8bee..fbdbaeb14fd59f 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -127,6 +127,18 @@ const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, l return settings; }; +const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.timezone')) { + log( + '"logging.timezone" has been deprecated and will be removed ' + + 'in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern ' + + 'in your logging configuration. For more details, see ' + + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md' + ); + } + return settings; +}; + export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [ unusedFromRoot('savedObjects.indexCheckTimeout'), unusedFromRoot('server.xsrf.token'), @@ -163,4 +175,5 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unu mapManifestServiceUrlDeprecation, opsLoggingEventDeprecation, requestLoggingEventDeprecation, + timezoneLoggingDeprecation, ]; diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts index 6588f4270fe183..321eb81708f1e7 100644 --- a/src/core/server/legacy/integration_tests/logging.test.ts +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -87,7 +87,7 @@ describe('logging service', () => { const loggedString = getPlatformLogsFromMock(mockConsoleLog); expect(loggedString).toMatchInlineSnapshot(` Array [ - "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] handled by NP", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] handled by NP", ] `); }); @@ -131,9 +131,9 @@ describe('logging service', () => { expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` Array [ - "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error", ] `); @@ -162,9 +162,9 @@ describe('logging service', () => { expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` Array [ - "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error", ] `); @@ -199,9 +199,9 @@ describe('logging service', () => { expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` Array [ - "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error", ] `); diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index b0759defb88039..9e3da1f3e0d715 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -110,7 +110,8 @@ Example of `%meta` output: ##### date Outputs the date of the logging event. The date conversion specifier may be followed by a set of braces containing a name of predefined date format and canonical timezone name. -Timezone name is expected to be one from [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) +Timezone name is expected to be one from [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). +Timezone defaults to the host timezone when not explicitly specified. Example of `%date` output: | Conversion pattern | Example | @@ -410,22 +411,22 @@ loggerWithNestedContext.debug('Message with `debug` log level.'); And assuming logger for `server` context with `console` appender and `trace` level was used, console output will look like this: ```bash -[2017-07-25T18:54:41.639Z][TRACE][server] Message with `trace` log level. -[2017-07-25T18:54:41.639Z][DEBUG][server] Message with `debug` log level. -[2017-07-25T18:54:41.639Z][INFO ][server] Message with `info` log level. -[2017-07-25T18:54:41.639Z][WARN ][server] Message with `warn` log level. -[2017-07-25T18:54:41.639Z][ERROR][server] Message with `error` log level. -[2017-07-25T18:54:41.639Z][FATAL][server] Message with `fatal` log level. - -[2017-07-25T18:54:41.639Z][TRACE][server.http] Message with `trace` log level. -[2017-07-25T18:54:41.639Z][DEBUG][server.http] Message with `debug` log level. +[2017-07-25T11:54:41.639-07:00][TRACE][server] Message with `trace` log level. +[2017-07-25T11:54:41.639-07:00][DEBUG][server] Message with `debug` log level. +[2017-07-25T11:54:41.639-07:00][INFO ][server] Message with `info` log level. +[2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. +[2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. +[2017-07-25T11:54:41.639-07:00][FATAL][server] Message with `fatal` log level. + +[2017-07-25T11:54:41.639-07:00][TRACE][server.http] Message with `trace` log level. +[2017-07-25T11:54:41.639-07:00][DEBUG][server.http] Message with `debug` log level. ``` The log will be less verbose with `warn` level for the `server` context: ```bash -[2017-07-25T18:54:41.639Z][WARN ][server] Message with `warn` log level. -[2017-07-25T18:54:41.639Z][ERROR][server] Message with `error` log level. -[2017-07-25T18:54:41.639Z][FATAL][server] Message with `fatal` log level. +[2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. +[2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. +[2017-07-25T11:54:41.639-07:00][FATAL][server] Message with `fatal` log level. ``` ### Logging config migration @@ -488,7 +489,7 @@ logging.root.level: all #### logging.timezone Set to the canonical timezone id to log events using that timezone. New logging config allows -to [specify timezone](#date) for `layout: pattern`. +to [specify timezone](#date) for `layout: pattern`. Defaults to host timezone when not specified. ```yaml logging: appenders: @@ -530,7 +531,7 @@ TBD | Parameter | Platform log record in **pattern** format | Legacy Platform log record **text** format | | --------------- | ------------------------------------------ | ------------------------------------------ | -| @timestamp | ISO8601 `2012-01-31T23:33:22.011Z` | Absolute `23:33:22.011` | +| @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | Absolute `23:33:22.011` | | context | `parent.child` | `['parent', 'child']` | | level | `DEBUG` | `['debug']` | | meta | stringified JSON object `{"to": "v8"}` | N/A | diff --git a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap index cbe0e352a0f3a0..8013aec4a06fd3 100644 --- a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap +++ b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap @@ -1,20 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`appends records via multiple appenders.: console logs 1`] = `"[2012-01-31T23:33:22.011Z][INFO ][some-context] You know, just for your info."`; +exports[`appends records via multiple appenders.: console logs 1`] = `"[2012-01-31T18:33:22.011-05:00][INFO ][some-context] You know, just for your info."`; exports[`appends records via multiple appenders.: file logs 1`] = ` -"[2012-01-31T23:33:22.011Z][WARN ][tests] Config is not ready! +"[2012-01-31T13:33:22.011-05:00][WARN ][tests] Config is not ready! " `; exports[`appends records via multiple appenders.: file logs 2`] = ` -"[2012-01-31T23:33:22.011Z][ERROR][tests.child] Too bad that config is not ready :/ +"[2012-01-31T08:33:22.011-05:00][ERROR][tests.child] Too bad that config is not ready :/ " `; exports[`asLoggerFactory() only allows to create new loggers. 1`] = ` Object { - "@timestamp": "2012-01-31T18:33:22.011-05:00", + "@timestamp": "2012-01-30T22:33:22.011-05:00", "log": Object { "level": "TRACE", "logger": "test.context", @@ -28,7 +28,7 @@ Object { exports[`asLoggerFactory() only allows to create new loggers. 2`] = ` Object { - "@timestamp": "2012-01-31T13:33:22.011-05:00", + "@timestamp": "2012-01-30T17:33:22.011-05:00", "log": Object { "level": "INFO", "logger": "test.context", @@ -43,7 +43,7 @@ Object { exports[`asLoggerFactory() only allows to create new loggers. 3`] = ` Object { - "@timestamp": "2012-01-31T08:33:22.011-05:00", + "@timestamp": "2012-01-30T12:33:22.011-05:00", "log": Object { "level": "FATAL", "logger": "test.context", @@ -87,7 +87,7 @@ Object { exports[`uses \`root\` logger if context is not specified. 1`] = ` Array [ Array [ - "[2012-01-31T23:33:22.011Z][INFO ][root] This message goes to a root context.", + "[2012-01-31T03:33:22.011-05:00][INFO ][root] This message goes to a root context.", ], ] `; diff --git a/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap index 8988f3019d509d..54e46ca7f520e9 100644 --- a/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap +++ b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap @@ -12,29 +12,29 @@ exports[`\`format()\` correctly formats record with custom pattern. 5`] = `"mock exports[`\`format()\` correctly formats record with custom pattern. 6`] = `"mock-message-6-context-6-message-6"`; -exports[`\`format()\` correctly formats record with full pattern. 1`] = `"[2012-02-01T14:30:22.011Z][FATAL][context-1] Some error stack"`; +exports[`\`format()\` correctly formats record with full pattern. 1`] = `"[2012-02-01T09:30:22.011-05:00][FATAL][context-1] Some error stack"`; -exports[`\`format()\` correctly formats record with full pattern. 2`] = `"[2012-02-01T14:30:22.011Z][ERROR][context-2] message-2"`; +exports[`\`format()\` correctly formats record with full pattern. 2`] = `"[2012-02-01T09:30:22.011-05:00][ERROR][context-2] message-2"`; -exports[`\`format()\` correctly formats record with full pattern. 3`] = `"[2012-02-01T14:30:22.011Z][WARN ][context-3] message-3"`; +exports[`\`format()\` correctly formats record with full pattern. 3`] = `"[2012-02-01T09:30:22.011-05:00][WARN ][context-3] message-3"`; -exports[`\`format()\` correctly formats record with full pattern. 4`] = `"[2012-02-01T14:30:22.011Z][DEBUG][context-4] message-4"`; +exports[`\`format()\` correctly formats record with full pattern. 4`] = `"[2012-02-01T09:30:22.011-05:00][DEBUG][context-4] message-4"`; -exports[`\`format()\` correctly formats record with full pattern. 5`] = `"[2012-02-01T14:30:22.011Z][INFO ][context-5] message-5"`; +exports[`\`format()\` correctly formats record with full pattern. 5`] = `"[2012-02-01T09:30:22.011-05:00][INFO ][context-5] message-5"`; -exports[`\`format()\` correctly formats record with full pattern. 6`] = `"[2012-02-01T14:30:22.011Z][TRACE][context-6] message-6"`; +exports[`\`format()\` correctly formats record with full pattern. 6`] = `"[2012-02-01T09:30:22.011-05:00][TRACE][context-6] message-6"`; -exports[`\`format()\` correctly formats record with highlighting. 1`] = `[2012-02-01T14:30:22.011Z][FATAL][context-1] Some error stack`; +exports[`\`format()\` correctly formats record with highlighting. 1`] = `[2012-02-01T09:30:22.011-05:00][FATAL][context-1] Some error stack`; -exports[`\`format()\` correctly formats record with highlighting. 2`] = `[2012-02-01T14:30:22.011Z][ERROR][context-2] message-2`; +exports[`\`format()\` correctly formats record with highlighting. 2`] = `[2012-02-01T09:30:22.011-05:00][ERROR][context-2] message-2`; -exports[`\`format()\` correctly formats record with highlighting. 3`] = `[2012-02-01T14:30:22.011Z][WARN ][context-3] message-3`; +exports[`\`format()\` correctly formats record with highlighting. 3`] = `[2012-02-01T09:30:22.011-05:00][WARN ][context-3] message-3`; -exports[`\`format()\` correctly formats record with highlighting. 4`] = `[2012-02-01T14:30:22.011Z][DEBUG][context-4] message-4`; +exports[`\`format()\` correctly formats record with highlighting. 4`] = `[2012-02-01T09:30:22.011-05:00][DEBUG][context-4] message-4`; -exports[`\`format()\` correctly formats record with highlighting. 5`] = `[2012-02-01T14:30:22.011Z][INFO ][context-5] message-5`; +exports[`\`format()\` correctly formats record with highlighting. 5`] = `[2012-02-01T09:30:22.011-05:00][INFO ][context-5] message-5`; -exports[`\`format()\` correctly formats record with highlighting. 6`] = `[2012-02-01T14:30:22.011Z][TRACE][context-6] message-6`; +exports[`\`format()\` correctly formats record with highlighting. 6`] = `[2012-02-01T09:30:22.011-05:00][TRACE][context-6] message-6`; exports[`allows specifying the PID in custom pattern 1`] = `"5355-context-1-Some error stack"`; diff --git a/src/core/server/logging/layouts/conversions/date.ts b/src/core/server/logging/layouts/conversions/date.ts index c1f871282c5de9..66aad5b42354a1 100644 --- a/src/core/server/logging/layouts/conversions/date.ts +++ b/src/core/server/logging/layouts/conversions/date.ts @@ -22,11 +22,14 @@ const formats = { UNIX_MILLIS: 'UNIX_MILLIS', }; -function formatDate(date: Date, dateFormat: string = formats.ISO8601, timezone?: string): string { +function formatDate( + date: Date, + dateFormat: string = formats.ISO8601_TZ, + timezone?: string +): string { const momentDate = moment(date); - if (timezone) { - momentDate.tz(timezone); - } + momentDate.tz(timezone ?? moment.tz.guess()); + switch (dateFormat) { case formats.ISO8601: return momentDate.toISOString(); diff --git a/src/core/server/logging/layouts/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts index d291516524be0b..7dd3c7c51f833c 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -122,7 +122,9 @@ test('`format()` correctly formats record with meta data.', () => { to: 'v8', }, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta'); + ).toBe( + '[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta' + ); expect( layout.format({ @@ -133,7 +135,7 @@ test('`format()` correctly formats record with meta data.', () => { pid: 5355, meta: {}, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{} message-meta'); + ).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta]{} message-meta'); expect( layout.format({ @@ -143,7 +145,7 @@ test('`format()` correctly formats record with meta data.', () => { timestamp, pid: 5355, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta] message-meta'); + ).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta] message-meta'); }); test('`format()` correctly formats record with highlighting.', () => { @@ -187,10 +189,10 @@ describe('format', () => { timestamp, pid: 5355, }; - it('uses ISO8601 as default', () => { + it('uses ISO8601_TZ as default', () => { const layout = new PatternLayout(); - expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context] message'); + expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context] message'); }); describe('supports specifying a predefined format', () => { From 59cc39434ee43f7220092f47732550cc1efc7900 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 9 Feb 2021 17:57:05 -0700 Subject: [PATCH 8/9] remove ref to removed tsconfig file --- tsconfig.refs.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 86fdfad2f524d6..d5482a85856fef 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -83,7 +83,6 @@ { "path": "./x-pack/plugins/lens/tsconfig.json" }, { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_file_upload/tsconfig.json" }, { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, From 38a647539643700d66179532bec78d62ffd8676c Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 9 Feb 2021 20:30:25 -0600 Subject: [PATCH 9/9] 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 a89f82e931fd43..7a4edb8f49189d 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 589e57a1388b55..81e46d85ba220d 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 fac87e20dfe7de..57c6f695453ef7 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 a1cee1361a18f6..2bb98e83cbe70b 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 00000000000000..f1236c4fc2c2bb --- /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 a7b6c9fb7104ce..00000000000000 --- 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 815e1f2be33f27..33fe3c7af30c78 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 f02f98c49f01af..bd7812acac678c 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 00000000000000..ae2c6ed81badb4 --- /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 00000000000000..5938c7119616f0 --- /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 00000000000000..34a917a77dcf53 --- /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 00000000000000..6ddcf8fd5cb659 --- /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 00000000000000..2dc561ff172b9a --- /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 00000000000000..8e254fb2b67a8b --- /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 00000000000000..31fed514bdacc1 --- /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 3bbe8112258258..00000000000000 --- 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 929654ecb4693b..e7e4ade5257fc1 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 1bcc9e7157a514..db5a996c604fcd 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 c91c1d82afe9b0..00000000000000 --- 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 4b609a881bd18f..e63f43470497d2 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 72dc4da01d8678..661ce8f8a253ce 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 379ac9774c242a..1a759950f640d2 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 f0fdd79bcd93df..628df397998eec 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 4d06d23ef93ef7..5fd00527b8b704 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 52c2a70f2d3591..8fd32bda7fbc8f 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 44391568741f35..e22c6fa6611812 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 8e7d165f8a5356..d4bb83e8668ba5 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 b18b6e8a6eba6e..4d70676d25e40f 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 00000000000000..b7ef8ec7d23125 --- /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 00000000000000..ec95aac7268ad9 --- /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 00000000000000..98992701e3bb4e --- /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 00000000000000..8ac62c125515af --- /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 0b4df6805759ee..11fbe269b854d5 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 0182cb0e4099ac..686f27d714cc16 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 d346b71d76aa8e..82093b1a359d0a 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 7873fd8e43a7bb..f6e11f5294191b 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 0c87b2f0f8b536..34039e9107f007 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 b3676fc54aeaa5..6f996a672a44ad 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 45587cd258e5d7..7808851508a7c3 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 56a4b99e7236c6..54eea067177edf 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 99555fa56acd59..0ac49e05b36b92 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 cc2cf4092520a1..3da560135eaf48 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 8ec0b83994e1a8..6e227cfc12d113 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 d41fa0ffafecc5..1c2c4947a02ea3 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 6e9d0329eaff83..018d2d572eea06 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 eeda7091044797..5a9695b8ddc3de 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 应用中分析此类别。",