From 9c07f66e76b3a8ca91ee6d4d2c8f66b5c1d8d444 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Thu, 23 Apr 2020 05:44:47 -0500 Subject: [PATCH 01/17] Add new inventory metric threshold alert --- .../aws_ec2/toolbar_items.tsx | 32 +- .../aws_rds/toolbar_items.tsx | 30 +- .../inventory_models/aws_s3/toolbar_items.tsx | 22 +- .../aws_sqs/toolbar_items.tsx | 21 +- .../container/toolbar_items.tsx | 23 +- .../inventory_models/host/toolbar_items.tsx | 27 +- .../inventory_models/pod/toolbar_items.tsx | 9 +- .../alerting/inventory/alert_dropdown.tsx | 62 +++ .../alerting/inventory/alert_flyout.tsx | 51 ++ .../alerting/inventory/expression.tsx | 472 ++++++++++++++++++ .../components/alerting/inventory/metric.tsx | 151 ++++++ .../metric_inventory_threshold_alert_type.ts | 34 ++ .../alerting/inventory/validation.tsx | 89 ++++ .../alerting/metrics/alert_dropdown.tsx | 2 +- .../infra/public/pages/metrics/index.tsx | 6 +- .../components/waffle/node_context_menu.tsx | 2 +- x-pack/plugins/infra/public/plugin.ts | 2 + .../infra/server/graphql/sources/resolvers.ts | 9 +- .../metrics/kibana_metrics_adapter.ts | 10 +- .../inventory_metric_threshold_executor.ts | 174 +++++++ .../metric_threshold_executor.test.ts | 306 ++++++++++++ ...r_inventory_metric_threshold_alert_type.ts | 88 ++++ .../inventory_metric_threshold/test_mocks.ts | 129 +++++ .../inventory_metric_threshold/types.ts | 34 ++ .../metric_threshold_executor.ts | 63 +-- .../register_metric_threshold_alert_type.ts | 18 +- .../lib/alerting/register_alert_types.ts | 11 +- .../infra/server/lib/domains/fields_domain.ts | 2 +- .../log_entries_domain/log_entries_domain.ts | 6 +- .../create_timerange_with_interval.ts | 21 +- .../infra/server/lib/snapshot/snapshot.ts | 52 +- .../plugins/infra/server/lib/source_status.ts | 12 +- .../infra/server/lib/sources/sources.test.ts | 12 +- .../infra/server/lib/sources/sources.ts | 31 +- x-pack/plugins/infra/server/plugin.ts | 4 +- .../server/routes/inventory_metadata/index.ts | 2 +- .../infra/server/routes/log_entries/item.ts | 5 +- .../infra/server/routes/metadata/index.ts | 2 +- .../lib/get_dataset_for_field.ts | 13 +- .../lib/populate_series_with_tsvb_data.ts | 16 +- .../infra/server/routes/node_details/index.ts | 5 +- .../infra/server/routes/snapshot/index.ts | 14 +- .../infra/server/routes/source/index.ts | 5 +- .../server/utils/calculate_metric_interval.ts | 14 +- 44 files changed, 1849 insertions(+), 244 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/metric_threshold_executor.test.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/test_mocks.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx index b2da7dec3f2e08..764db2164b7118 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx @@ -11,27 +11,29 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_ import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const ec2MetricTypes: SnapshotMetricType[] = [ + 'cpu', + 'rx', + 'tx', + 'diskIOReadBytes', + 'diskIOWriteBytes', +]; + +export const ec2groupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'aws.ec2.instance.image.id', + 'aws.ec2.instance.state.name', +]; + export const AwsEC2ToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 'cpu', - 'rx', - 'tx', - 'diskIOReadBytes', - 'diskIOWriteBytes', - ]; - const groupByFields = [ - 'cloud.availability_zone', - 'cloud.machine.type', - 'aws.ec2.instance.image.id', - 'aws.ec2.instance.state.name', - ]; return ( <> ); diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx index 2a8394b9dd3a41..3eebdee22b2c30 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx @@ -11,26 +11,28 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_ import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const rdsMetricTypes: SnapshotMetricType[] = [ + 'cpu', + 'rdsConnections', + 'rdsQueriesExecuted', + 'rdsActiveTransactions', + 'rdsLatency', +]; + +export const rdsGroupByFields = [ + 'cloud.availability_zone', + 'aws.rds.db_instance.class', + 'aws.rds.db_instance.status', +]; + export const AwsRDSToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 'cpu', - 'rdsConnections', - 'rdsQueriesExecuted', - 'rdsActiveTransactions', - 'rdsLatency', - ]; - const groupByFields = [ - 'cloud.availability_zone', - 'aws.rds.db_instance.class', - 'aws.rds.db_instance.status', - ]; return ( <> ); diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx index 324bdd05860290..ede618b1bf19d0 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx @@ -11,22 +11,24 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_ import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const s3MetricTypes: SnapshotMetricType[] = [ + 's3BucketSize', + 's3NumberOfObjects', + 's3TotalRequests', + 's3DownloadBytes', + 's3UploadBytes', +]; + +export const s3GroupByFields = ['cloud.region']; + export const AwsS3ToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 's3BucketSize', - 's3NumberOfObjects', - 's3TotalRequests', - 's3DownloadBytes', - 's3UploadBytes', - ]; - const groupByFields = ['cloud.region']; return ( <> ); diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx index 3229c07034772c..e77f3af5781970 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx @@ -11,22 +11,23 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_ import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const sqsMetricTypes: SnapshotMetricType[] = [ + 'sqsMessagesVisible', + 'sqsMessagesDelayed', + 'sqsMessagesSent', + 'sqsMessagesEmpty', + 'sqsOldestMessage', +]; +export const sqsGroupByFields = ['cloud.region']; + export const AwsSQSToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 'sqsMessagesVisible', - 'sqsMessagesDelayed', - 'sqsMessagesSent', - 'sqsMessagesEmpty', - 'sqsOldestMessage', - ]; - const groupByFields = ['cloud.region']; return ( <> ); diff --git a/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx index f6c707726d9ca1..f193adbf6aadc4 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx @@ -10,21 +10,22 @@ import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/compo import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const containerMetricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; +export const containerGroupByFields = [ + 'host.name', + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', +]; + export const ContainerToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; - const groupByFields = [ - 'host.name', - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ]; return ( ); }; diff --git a/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx index 136264c0e26f4d..8ed684b3885de2 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx @@ -10,20 +10,27 @@ import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/compo import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const hostMetricTypes: SnapshotMetricType[] = [ + 'cpu', + 'memory', + 'load', + 'rx', + 'tx', + 'logRate', +]; +export const hostGroupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', +]; export const HostToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'load', 'rx', 'tx', 'logRate']; - const groupByFields = [ - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ]; return ( ); }; diff --git a/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx index c1cd375ff47bf9..54a32e3e0180a1 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx @@ -10,14 +10,15 @@ import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/compo import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const podMetricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; +export const podGroupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type']; + export const PodToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; - const groupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type']; return ( ); }; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx new file mode 100644 index 00000000000000..d2904206875c79 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertFlyout } from './alert_flyout'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + +export const InventoryAlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [flyoutVisible, setFlyoutVisible] = useState(false); + const kibana = useKibana(); + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + const menuItems = useMemo(() => { + return [ + setFlyoutVisible(true)}> + + , + + + , + ]; + }, [kibana.services]); + + return ( + <> + + + + } + isOpen={popoverOpen} + closePopover={closePopover} + > + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx new file mode 100644 index 00000000000000..69abda4f4d0e31 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; +import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths + +import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; + +interface Props { + visible?: boolean; + series?: MetricsExplorerSeries; + setVisible: React.Dispatch>; +} + +export const AlertFlyout = (props: Props) => { + const { triggersActionsUI } = useContext(TriggerActionsContext); + const { services } = useKibana(); + + return ( + <> + {triggersActionsUI && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx new file mode 100644 index 00000000000000..f84bc1b79d7f58 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -0,0 +1,472 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo, useEffect, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiSpacer, + EuiText, + EuiFormRow, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +import { euiStyled } from '../../../../../observability/public'; +import { + WhenExpression, + ThresholdExpression, + ForLastExpression, + // 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'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; +import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { toMetricOpt } from '../../../pages/metrics/inventory_view/components/toolbars/toolbar_wrapper'; +import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items'; +import { ec2MetricTypes } from '../../../../common/inventory_models/aws_ec2/toolbar_items'; +import { s3MetricTypes } from '../../../../common/inventory_models/aws_s3/toolbar_items'; +import { rdsMetricTypes } from '../../../../common/inventory_models/aws_rds/toolbar_items'; +import { hostMetricTypes } from '../../../../common/inventory_models/host/toolbar_items'; +import { containerMetricTypes } from '../../../../common/inventory_models/container/toolbar_items'; +import { podMetricTypes } from '../../../../common/inventory_models/pod/toolbar_items'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; +import { SnapshotMetricInput } from '../../../../common/http_api/snapshot_api'; +import { MetricExpression } from './metric'; + +interface AlertContextMeta { + currentOptions?: Partial; + series?: MetricsExplorerSeries; +} + +interface Props { + errors: IErrorObject[]; + alertParams: { + criteria: InventoryMetricConditions[]; + groupBy?: string; + filterQuery?: string; + sourceId?: string; + }; + alertsContext: AlertsContextValue; + setAlertParams(key: string, value: any): void; + setAlertProperty(key: string, value: any): void; +} + +type TimeUnit = 's' | 'm' | 'h' | 'd'; + +const defaultExpression = { + metric: 'cpu' as SnapshotMetricType, + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', +} as InventoryMetricConditions; + +export const Expressions: React.FC = props => { + const { setAlertParams, alertParams, errors, alertsContext } = props; + const { source, createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + fetch: alertsContext.http.fetch, + toastWarning: alertsContext.toastNotifications.addWarning, + }); + const [timeSize, setTimeSize] = useState(1); + const [timeUnit, setTimeUnit] = useState('m'); + const [nodeType, setNodeType] = useState('host'); + + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + createDerivedIndexPattern, + ]); + + const options = useMemo(() => { + if (alertsContext.metadata?.currentOptions?.metrics) { + return alertsContext.metadata.currentOptions as MetricsExplorerOptions; + } else { + return { + metrics: [], + aggregation: 'avg', + }; + } + }, [alertsContext.metadata]); + + const updateParams = useCallback( + (id, e: InventoryMetricConditions) => { + const exp = alertParams.criteria ? alertParams.criteria.slice() : []; + exp[id] = { ...exp[id], ...e }; + setAlertParams('criteria', exp); + }, + [setAlertParams, alertParams.criteria] + ); + + const addExpression = useCallback(() => { + const exp = alertParams.criteria.slice(); + exp.push(defaultExpression); + setAlertParams('criteria', exp); + }, [setAlertParams, alertParams.criteria]); + + const removeExpression = useCallback( + (id: number) => { + const exp = alertParams.criteria.slice(); + if (exp.length > 1) { + exp.splice(id, 1); + setAlertParams('criteria', exp); + } + }, + [setAlertParams, alertParams.criteria] + ); + + const onFilterQuerySubmit = useCallback( + (filter: any) => { + setAlertParams('filterQuery', filter); + }, + [setAlertParams] + ); + + const onGroupByChange = useCallback( + (group: string | null) => { + setAlertParams('groupBy', group || undefined); + }, + [setAlertParams] + ); + + const emptyError = useMemo(() => { + return { + aggField: [], + timeSizeUnit: [], + timeWindowSize: [], + }; + }, []); + + const updateTimeSize = useCallback( + (ts: number | undefined) => { + const criteria = alertParams.criteria.map(c => ({ + ...c, + timeSize: ts, + })); + setTimeSize(ts || undefined); + setAlertParams('criteria', criteria); + }, + [alertParams.criteria, setAlertParams] + ); + + const updateTimeUnit = useCallback( + (tu: string) => { + const criteria = alertParams.criteria.map(c => ({ + ...c, + timeUnit: tu, + })); + setTimeUnit(tu as TimeUnit); + setAlertParams('criteria', criteria); + }, + [alertParams.criteria, setAlertParams] + ); + + const updateNodeType = useCallback( + (nt: any) => { + setNodeType(nt); + setAlertParams('nodeType', nt); + }, + [setAlertParams] + ); + + useEffect(() => { + const md = alertsContext.metadata; + if (md) { + if (md.currentOptions?.metrics) { + setAlertParams( + 'criteria', + md.currentOptions.metrics.map(metric => ({ + metric: metric.field, + comparator: Comparator.GT, + threshold: [], + timeSize, + timeUnit, + aggType: metric.aggregation, + })) + ); + } else { + setAlertParams('criteria', [defaultExpression]); + } + + if (md.currentOptions) { + if (md.currentOptions.filterQuery) { + setAlertParams('filterQuery', md.currentOptions.filterQuery); + } else if (md.currentOptions.groupBy && md.series) { + const filter = `${md.currentOptions.groupBy}: "${md.series.id}"`; + setAlertParams('filterQuery', filter); + } + + setAlertParams('groupBy', md.currentOptions.groupBy); + } + setAlertParams('sourceId', source?.id); + } else { + setAlertParams('criteria', [defaultExpression]); + } + }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + + +

+ +

+
+ + + + + {alertParams.criteria && + alertParams.criteria.map((e, idx) => { + return ( + 1} + remove={removeExpression} + addExpression={addExpression} + key={idx} // idx's don't usually make good key's but here the index has semantic meaning + expressionId={idx} + setAlertParams={updateParams} + errors={errors[idx] || emptyError} + expression={e || {}} + /> + ); + })} + + + +
+ + + +
+ + + + {alertsContext.metadata && ( + <> + + + + + )} + + ); +}; + +interface ExpressionRowProps { + nodeType: InventoryItemType; + expressionId: number; + expression: Omit & { + metric?: SnapshotMetricType; + }; + errors: IErrorObject; + canDelete: boolean; + addExpression(): void; + remove(id: number): void; + setAlertParams(id: number, params: Partial): void; +} + +const StyledExpressionRow = euiStyled(EuiFlexGroup)` + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +`; + +const StyledExpression = euiStyled.div` + padding: 0 4px; +`; + +export const ExpressionRow: React.FC = props => { + const { setAlertParams, expression, errors, expressionId, remove, canDelete } = props; + const { metric, comparator = Comparator.GT, threshold = [] } = expression; + + const updateMetric = useCallback( + (m?: SnapshotMetricType) => { + setAlertParams(expressionId, { ...expression, metric: m }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateComparator = useCallback( + (c?: string) => { + setAlertParams(expressionId, { ...expression, comparator: c as Comparator | undefined }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateThreshold = useCallback( + t => { + if (t.join() !== expression.threshold.join()) { + setAlertParams(expressionId, { ...expression, threshold: t }); + } + }, + [expressionId, expression, setAlertParams] + ); + + const ofFields = useMemo(() => { + let myMetrics = hostMetricTypes; + + switch (props.nodeType) { + case 'awsEC2': + myMetrics = ec2MetricTypes; + break; + case 'awsRDS': + myMetrics = rdsMetricTypes; + break; + case 'awsS3': + myMetrics = s3MetricTypes; + break; + case 'awsSQS': + myMetrics = sqsMetricTypes; + break; + case 'host': + myMetrics = hostMetricTypes; + break; + case 'pod': + myMetrics = podMetricTypes; + break; + case 'container': + myMetrics = containerMetricTypes; + break; + } + return myMetrics.map(toMetricOpt); + }, [props.nodeType]); + + return ( + <> + + + + + v?.value === metric)?.text || '', + }} + metrics={ + ofFields.filter(m => m !== undefined && m.value !== undefined) as Array<{ + value: SnapshotMetricType; + text: string; + }> + } + onChange={updateMetric} + errors={errors} + /> + + + + + + + {canDelete && ( + + remove(expressionId)} + /> + + )} + + + + ); +}; + +const getDisplayNameForType = (type: InventoryItemType) => { + const inventoryModel = findInventoryModel(type); + return inventoryModel.displayName; +}; + +export const aggregationType: { [key: string]: any } = { + host: { + text: getDisplayNameForType('host'), + value: 'host', + }, + pod: { + text: getDisplayNameForType('pod'), + value: 'pod', + }, + container: { + text: getDisplayNameForType('container'), + value: 'container', + }, + awsEC2: { + text: getDisplayNameForType('awsEC2'), + value: 'awsEC2', + }, + awsS3: { + text: getDisplayNameForType('awsS3'), + value: 'awsS3', + }, + awsRDS: { + text: getDisplayNameForType('awsRDS'), + value: 'awsRDS', + }, + awsSQS: { + text: getDisplayNameForType('awsSQS'), + value: 'awsSQS', + }, +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx new file mode 100644 index 00000000000000..91c2487975ffea --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiExpression, + EuiPopover, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiComboBox, +} from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { SnapshotMetricType } from '../../../../common/inventory_models/types'; +// import { builtInAggregationTypes } from '../constants'; +// import { AggregationType } from '../types'; +// import { IErrorObject } from '../../types'; + +interface Props { + metric?: { value: SnapshotMetricType; text: string }; + metrics: Array<{ value: string; text: string }>; + errors: IErrorObject; + onChange: (metric: SnapshotMetricType) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosition }: Props) => { + const [aggFieldPopoverOpen, setAggFieldPopoverOpen] = useState(false); + const firstFieldOption = { + text: i18n.translate( + 'xpack.triggersActionsUI.common.expressionItems.of.selectTimeFieldOptionLabel', + { + defaultMessage: 'Select a field', + } + ), + value: '', + }; + + const availablefieldsOptions = metrics.map(m => { + return { label: m.text, value: m.value }; + }, []); + + return ( + { + setAggFieldPopoverOpen(true); + }} + color={metric ? 'secondary' : 'danger'} + /> + } + isOpen={aggFieldPopoverOpen} + closePopover={() => { + setAggFieldPopoverOpen(false); + }} + withTitle + anchorPosition={popupPosition ?? 'downRight'} + zIndex={8000} + > +
+ setAggFieldPopoverOpen(false)}> + + + + + 0 && metric !== undefined} + error={errors.aggField} + > + 0 && metric !== undefined} + placeholder={firstFieldOption.text} + options={availablefieldsOptions} + noSuggestions={!availablefieldsOptions.length} + selectedOptions={ + metric ? availablefieldsOptions.filter(a => a.value === metric.value) : [] + } + renderOption={(o: any) => o.label} + onChange={selectedOptions => { + if (selectedOptions.length > 0) { + onChange(selectedOptions[0].value as SnapshotMetricType); + setAggFieldPopoverOpen(false); + } + }} + /> + + + +
+
+ ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts b/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts new file mode 100644 index 00000000000000..5c89320788c627 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types'; +import { Expressions } from './expression'; +import { validateMetricThreshold } from './validation'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; + +export function getInventoryMetricAlertType(): AlertTypeModel { + return { + id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertName', { + defaultMessage: 'Inventory metric threshold', + }), + iconClass: 'bell', + alertParamsExpression: Expressions, + validate: validateMetricThreshold, + defaultActionMessage: i18n.translate( + 'xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\} + +\\{\\{context.metricOf.condition0\\}\\} has crossed a threshold of \\{\\{context.thresholdOf.condition0\\}\\} +Current value is \\{\\{context.valueOf.condition0\\}\\} +`, + } + ), + }; +} diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx new file mode 100644 index 00000000000000..d84e46d08a2879 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MetricExpressionParams } from '../../../../server/lib/alerting/metric_threshold/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; + +export function validateMetricThreshold({ + criteria, +}: { + criteria: MetricExpressionParams[]; +}): ValidationResult { + const validationResult = { errors: {} }; + const errors: { + [id: string]: { + aggField: string[]; + timeSizeUnit: string[]; + timeWindowSize: string[]; + threshold0: string[]; + threshold1: string[]; + metric: string[]; + }; + } = {}; + validationResult.errors = errors; + + if (!criteria || !criteria.length) { + return validationResult; + } + + criteria.forEach((c, idx) => { + // Create an id for each criteria, so we can map errors to specific criteria. + const id = idx.toString(); + + errors[id] = errors[id] || { + aggField: [], + timeSizeUnit: [], + timeWindowSize: [], + threshold0: [], + threshold1: [], + metric: [], + }; + if (!c.aggType) { + errors[id].aggField.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', { + defaultMessage: 'Aggreation is required.', + }) + ); + } + + if (!c.threshold || !c.threshold.length) { + errors[id].threshold0.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + + if (c.comparator === 'between' && (!c.threshold || c.threshold.length < 2)) { + errors[id].threshold1.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + + if (!c.timeSize) { + errors[id].timeWindowSize.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.timeRequred', { + defaultMessage: 'Time size is Required.', + }) + ); + } + + if (!c.metric && c.aggType !== 'count') { + errors[id].metric.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', { + defaultMessage: 'Metric is required.', + }) + ); + } + }); + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx index bb664f40676625..8bcf0e9ed5be5d 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AlertFlyout } from './alert_flyout'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -export const AlertDropdown = () => { +export const MetricsAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); const kibana = useKibana(); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index cc88dd9e0d0f8f..53a6f0a49c2ce1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -28,7 +28,8 @@ import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; 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 { AlertDropdown } from '../../components/alerting/metrics/alert_dropdown'; +import { MetricsAlertDropdown } from '../../components/alerting/metrics/alert_dropdown'; +import { InventoryAlertDropdown } from '../../components/alerting/inventory/alert_dropdown'; export const InfrastructurePage = ({ match }: RouteComponentProps) => { const uiCapabilities = useKibana().services.application?.capabilities; @@ -96,7 +97,8 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { /> - + + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 275635a33ec266..0a7cd4384070b4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -72,7 +72,7 @@ export const NodeContextMenu: React.FC = ({ } } else { if (options.fields) { - const { id } = findInventoryFields(nodeType, options.fields); + const { id } = findInventoryFields(nodeType, options.fields); // TODO: This will give me what I need to be able to filter alerts return { label: {id}, value: node.id, diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 3b6647b9bfbbeb..8f745471e242c5 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -22,6 +22,7 @@ import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; import { getAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; +import { getInventoryMetricAlertType } from './components/alerting/inventory/metric_inventory_threshold_alert_type'; export type ClientSetup = void; export type ClientStart = void; @@ -53,6 +54,7 @@ export class Plugin registerFeatures(pluginsSetup.home); pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getInventoryMetricAlertType()); core.application.register({ id: 'logs', diff --git a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts index 1fe1431392a389..da8a1099d45a99 100644 --- a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts +++ b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts @@ -93,12 +93,17 @@ export const createSourcesResolvers = ( } => ({ Query: { async source(root, args, { req }) { - const requestedSourceConfiguration = await libs.sources.getSourceConfiguration(req, args.id); + const requestedSourceConfiguration = await libs.sources.getSourceConfiguration( + req.core.savedObjects.client, + args.id + ); return requestedSourceConfiguration; }, async allSources(root, args, { req }) { - const sourceConfigurations = await libs.sources.getAllSourceConfigurations(req); + const sourceConfigurations = await libs.sources.getAllSourceConfigurations( + req.core.savedObjects.client + ); return sourceConfigurations; }, diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index 5a5f9d0f8f5293..62f324e01f8d99 100644 --- a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -18,6 +18,7 @@ import { InventoryMetricRT, } from '../../../../common/inventory_models/types'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; +import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../framework'; export class KibanaMetricsAdapter implements InfraMetricsAdapter { private framework: KibanaFramework; @@ -120,9 +121,14 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { indexPattern, options.timerange.interval ); + + const client = ( + opts: CallWithRequestParams + ): Promise> => + this.framework.callWithRequest(requestContext, 'search', opts); + const calculatedInterval = await calculateMetricInterval( - this.framework, - requestContext, + client, { indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, timestampField: options.sourceConfiguration.fields.timestamp, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts new file mode 100644 index 00000000000000..2958ea22e5645d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { mapValues, last } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + InfraDatabaseSearchResponse, + CallWithRequestParams, +} from '../../adapters/framework/adapter_types'; +import { Comparator, AlertStates, InventoryMetricConditions } from './types'; +import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; +import { InfraSnapshot } from '../../snapshot'; +import { parseFilterQuery } from '../../../utils/serialized_query'; +import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { SnapshotMetricInput, InfraTimerangeInput } from '../../../../common/http_api/snapshot_api'; +import { InfraSourceConfiguration } from '../../sources'; +import { InfraBackendLibs } from '../../infra_types'; + +interface InventoryMetricThresholdParams { + criteria: InventoryMetricConditions[]; + groupBy: string | undefined; + filterQuery: string | undefined; + nodeType: InventoryItemType; + sourceId?: string; +} + +export const createInventoryMetricThresholdExecutor = ( + libs: InfraBackendLibs, + alertId: string +) => async ({ services, params }: AlertExecutorOptions) => { + const { criteria, filterQuery, sourceId, nodeType } = params as InventoryMetricThresholdParams; + + const source = await libs.sources.getSourceConfiguration( + services.savedObjectsClient, + sourceId || 'default' + ); + + const results = await Promise.all( + criteria.map(c => { + return evaluateCondtion(c, nodeType, source.configuration, services, filterQuery); + }) + ); + + const invenotryItems = Object.keys(results[0]); + + for (const item of invenotryItems) { + const alertInstance = services.alertInstanceFactory(`${alertId}-${item}`); + // AND logic; all criteria must be across the threshold + const shouldAlertFire = results.every(result => result[item].shouldFire); + + // AND logic; because we need to evaluate all criteria, if one of them reports no data then the + // whole alert is in a No Data/Error state + const isNoData = results.some(result => result[item].isNoData); + const isError = results.some(result => result[item].isError); + + if (shouldAlertFire) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + item, + valueOf: mapToConditionsLookup(results, result => result[item].currentValue), + thresholdOf: mapToConditionsLookup(criteria, c => c.threshold), + metricOf: mapToConditionsLookup(criteria, c => c.metric), + }); + } + + alertInstance.replaceState({ + alertState: isError + ? AlertStates.ERROR + : isNoData + ? AlertStates.NO_DATA + : shouldAlertFire + ? AlertStates.ALERT + : AlertStates.OK, + }); + } +}; + +interface ConditionResult { + shouldFire: boolean; + currentValue?: number | null; + isNoData: boolean; + isError: boolean; +} + +const evaluateCondtion = async ( + condition: InventoryMetricConditions, + nodeType: InventoryItemType, + sourceConfiguration: InfraSourceConfiguration, + services: AlertServices, + filterQuery?: string +): Promise> => { + const { threshold, comparator, metric } = condition; + + const currentValues = await getData( + services, + nodeType, + metric, + { to: Date.now(), from: Date.now(), interval: condition.timeUnit }, + sourceConfiguration, + filterQuery + ); + + const comparisonFunction = comparatorMap[comparator]; + + return mapValues(currentValues, value => ({ + shouldFire: value !== undefined && value !== null && comparisonFunction(value, threshold), + currentValue: value, + isNoData: value === null, + isError: value === undefined, + })); +}; + +const getData = async ( + services: AlertServices, + nodeType: InventoryItemType, + metric: SnapshotMetricType, + timerange: InfraTimerangeInput, + sourceConfiguration: InfraSourceConfiguration, + filterQuery?: string +) => { + const snapshot = new InfraSnapshot(); + const esClient = ( + options: CallWithRequestParams + ): Promise> => + services.callCluster('search', options); + + const options = { + filterQuery: parseFilterQuery(filterQuery), + nodeType, + groupBy: [], + sourceConfiguration, + metric: { type: metric }, + timerange, + }; + + const { nodes } = await snapshot.getNodes(esClient, options); + + return nodes.reduce((acc, n) => { + const nodePathItem = last(n.path); + acc[nodePathItem.label] = n.metric && n.metric.value; + return acc; + }, {} as Record); +}; + +const comparatorMap = { + [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => + value >= Math.min(a, b) && value <= Math.max(a, b), + // `threshold` is always an array of numbers in case the BETWEEN comparator is + // used; all other compartors will just destructure the first value in the array + [Comparator.GT]: (a: number, [b]: number[]) => a > b, + [Comparator.LT]: (a: number, [b]: number[]) => a < b, + [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b, + [Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b, + [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b, +}; + +const mapToConditionsLookup = ( + list: any[], + mapFn: (value: any, index: number, array: any[]) => unknown +) => + list + .map(mapFn) + .reduce( + (result: Record, value, i) => ({ ...result, [`condition${i}`]: value }), + {} + ); + +export const FIRED_ACTIONS = { + id: 'metrics.invenotry_threshold.fired', + name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', { + defaultMessage: 'Fired', + }), +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/metric_threshold_executor.test.ts new file mode 100644 index 00000000000000..b081ab68bf61a9 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/metric_threshold_executor.test.ts @@ -0,0 +1,306 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// /* +// * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// * or more contributor license agreements. Licensed under the Elastic License; +// * you may not use this file except in compliance with the Elastic License. +// */ + +// import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; +// import { Comparator, AlertStates } from './types'; +// import * as mocks from './test_mocks'; +// import { AlertExecutorOptions } from '../../../../../alerting/server'; +// import { +// alertsMock, +// AlertServicesMock, +// AlertInstanceMock, +// } from '../../../../../alerting/server/mocks'; + +// const executor = createMetricThresholdExecutor('test') as (opts: { +// params: AlertExecutorOptions['params']; +// services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; +// }) => Promise; + +// const services: AlertServicesMock = alertsMock.createAlertServices(); +// services.callCluster.mockImplementation((_: string, { body, index }: any) => { +// if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; +// const metric = body.query.bool.filter[1]?.exists.field; +// if (body.aggs.groupings) { +// if (body.aggs.groupings.composite.after) { +// return mocks.compositeEndResponse; +// } +// if (metric === 'test.metric.2') { +// return mocks.alternateCompositeResponse; +// } +// return mocks.basicCompositeResponse; +// } +// if (metric === 'test.metric.2') { +// return mocks.alternateMetricResponse; +// } +// return mocks.basicMetricResponse; +// }); +// services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { +// if (sourceId === 'alternate') +// return { +// id: 'alternate', +// attributes: { metricAlias: 'alternatebeat-*' }, +// type, +// references: [], +// }; +// return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; +// }); + +// interface AlertTestInstance { +// instance: AlertInstanceMock; +// actionQueue: any[]; +// state: any; +// } +// const alertInstances = new Map(); +// services.alertInstanceFactory.mockImplementation((instanceID: string) => { +// const alertInstance: AlertTestInstance = { +// instance: alertsMock.createAlertInstanceFactory(), +// actionQueue: [], +// state: {}, +// }; +// alertInstances.set(instanceID, alertInstance); +// alertInstance.instance.replaceState.mockImplementation((newState: any) => { +// alertInstance.state = newState; +// return alertInstance.instance; +// }); +// alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { +// alertInstance.actionQueue.push({ id, action }); +// return alertInstance.instance; +// }); +// return alertInstance.instance; +// }); + +// function mostRecentAction(id: string) { +// return alertInstances.get(id)!.actionQueue.pop(); +// } + +// function getState(id: string) { +// return alertInstances.get(id)!.state; +// } + +// const baseCriterion = { +// aggType: 'avg', +// metric: 'test.metric.1', +// timeSize: 1, +// timeUnit: 'm', +// }; +// describe('The metric threshold alert type', () => { +// describe('querying the entire infrastructure', () => { +// const instanceID = 'test-*'; +// const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => +// executor({ +// services, +// params: { +// sourceId, +// criteria: [ +// { +// ...baseCriterion, +// comparator, +// threshold, +// }, +// ], +// }, +// }); +// test('alerts as expected with the > comparator', async () => { +// await execute(Comparator.GT, [0.75]); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// await execute(Comparator.GT, [1.5]); +// expect(mostRecentAction(instanceID)).toBe(undefined); +// expect(getState(instanceID).alertState).toBe(AlertStates.OK); +// }); +// test('alerts as expected with the < comparator', async () => { +// await execute(Comparator.LT, [1.5]); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// await execute(Comparator.LT, [0.75]); +// expect(mostRecentAction(instanceID)).toBe(undefined); +// expect(getState(instanceID).alertState).toBe(AlertStates.OK); +// }); +// test('alerts as expected with the >= comparator', async () => { +// await execute(Comparator.GT_OR_EQ, [0.75]); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// await execute(Comparator.GT_OR_EQ, [1.0]); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// await execute(Comparator.GT_OR_EQ, [1.5]); +// expect(mostRecentAction(instanceID)).toBe(undefined); +// expect(getState(instanceID).alertState).toBe(AlertStates.OK); +// }); +// test('alerts as expected with the <= comparator', async () => { +// await execute(Comparator.LT_OR_EQ, [1.5]); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// await execute(Comparator.LT_OR_EQ, [1.0]); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// await execute(Comparator.LT_OR_EQ, [0.75]); +// expect(mostRecentAction(instanceID)).toBe(undefined); +// expect(getState(instanceID).alertState).toBe(AlertStates.OK); +// }); +// test('alerts as expected with the between comparator', async () => { +// await execute(Comparator.BETWEEN, [0, 1.5]); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// await execute(Comparator.BETWEEN, [0, 0.75]); +// expect(mostRecentAction(instanceID)).toBe(undefined); +// expect(getState(instanceID).alertState).toBe(AlertStates.OK); +// }); +// test('reports expected values to the action context', async () => { +// await execute(Comparator.GT, [0.75]); +// const { action } = mostRecentAction(instanceID); +// expect(action.group).toBe('*'); +// expect(action.valueOf.condition0).toBe(1); +// expect(action.thresholdOf.condition0).toStrictEqual([0.75]); +// expect(action.metricOf.condition0).toBe('test.metric.1'); +// }); +// test('fetches the index pattern dynamically', async () => { +// await execute(Comparator.LT, [17], 'alternate'); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// await execute(Comparator.LT, [1.5], 'alternate'); +// expect(mostRecentAction(instanceID)).toBe(undefined); +// expect(getState(instanceID).alertState).toBe(AlertStates.OK); +// }); +// }); + +// describe('querying with a groupBy parameter', () => { +// const execute = (comparator: Comparator, threshold: number[]) => +// executor({ +// services, +// params: { +// groupBy: 'something', +// criteria: [ +// { +// ...baseCriterion, +// comparator, +// threshold, +// }, +// ], +// }, +// }); +// const instanceIdA = 'test-a'; +// const instanceIdB = 'test-b'; +// test('sends an alert when all groups pass the threshold', async () => { +// await execute(Comparator.GT, [0.75]); +// expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); +// expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceIdB).alertState).toBe(AlertStates.ALERT); +// }); +// test('sends an alert when only some groups pass the threshold', async () => { +// await execute(Comparator.LT, [1.5]); +// expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); +// expect(mostRecentAction(instanceIdB)).toBe(undefined); +// expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); +// }); +// test('sends no alert when no groups pass the threshold', async () => { +// await execute(Comparator.GT, [5]); +// expect(mostRecentAction(instanceIdA)).toBe(undefined); +// expect(getState(instanceIdA).alertState).toBe(AlertStates.OK); +// expect(mostRecentAction(instanceIdB)).toBe(undefined); +// expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); +// }); +// test('reports group values to the action context', async () => { +// await execute(Comparator.GT, [0.75]); +// expect(mostRecentAction(instanceIdA).action.group).toBe('a'); +// expect(mostRecentAction(instanceIdB).action.group).toBe('b'); +// }); +// }); + +// describe('querying with multiple criteria', () => { +// const execute = ( +// comparator: Comparator, +// thresholdA: number[], +// thresholdB: number[], +// groupBy: string = '' +// ) => +// executor({ +// services, +// params: { +// groupBy, +// criteria: [ +// { +// ...baseCriterion, +// comparator, +// threshold: thresholdA, +// }, +// { +// ...baseCriterion, +// comparator, +// threshold: thresholdB, +// metric: 'test.metric.2', +// }, +// ], +// }, +// }); +// test('sends an alert when all criteria cross the threshold', async () => { +// const instanceID = 'test-*'; +// await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// }); +// test('sends no alert when some, but not all, criteria cross the threshold', async () => { +// const instanceID = 'test-*'; +// await execute(Comparator.LT_OR_EQ, [1.0], [3.0]); +// expect(mostRecentAction(instanceID)).toBe(undefined); +// expect(getState(instanceID).alertState).toBe(AlertStates.OK); +// }); +// test('alerts only on groups that meet all criteria when querying with a groupBy parameter', async () => { +// const instanceIdA = 'test-a'; +// const instanceIdB = 'test-b'; +// await execute(Comparator.GT_OR_EQ, [1.0], [3.0], 'something'); +// expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); +// expect(mostRecentAction(instanceIdB)).toBe(undefined); +// expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); +// }); +// test('sends all criteria to the action context', async () => { +// const instanceID = 'test-*'; +// await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); +// const { action } = mostRecentAction(instanceID); +// expect(action.valueOf.condition0).toBe(1); +// expect(action.valueOf.condition1).toBe(3.5); +// expect(action.thresholdOf.condition0).toStrictEqual([1.0]); +// expect(action.thresholdOf.condition1).toStrictEqual([3.0]); +// expect(action.metricOf.condition0).toBe('test.metric.1'); +// expect(action.metricOf.condition1).toBe('test.metric.2'); +// }); +// }); +// describe('querying with the count aggregator', () => { +// const instanceID = 'test-*'; +// const execute = (comparator: Comparator, threshold: number[]) => +// executor({ +// services, +// params: { +// criteria: [ +// { +// ...baseCriterion, +// comparator, +// threshold, +// aggType: 'count', +// metric: undefined, +// }, +// ], +// }, +// }); +// test('alerts based on the doc_count value instead of the aggregatedValue', async () => { +// await execute(Comparator.GT, [2]); +// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); +// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); +// await execute(Comparator.LT, [1.5]); +// expect(mostRecentAction(instanceID)).toBe(undefined); +// expect(getState(instanceID).alertState).toBe(AlertStates.OK); +// }); +// }); +// }); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts new file mode 100644 index 00000000000000..994ba72cf6a56a --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { curry } from 'lodash'; +import uuid from 'uuid'; +import { + createInventoryMetricThresholdExecutor, + FIRED_ACTIONS, +} from './inventory_metric_threshold_executor'; +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './types'; +import { InfraBackendLibs } from '../../infra_types'; + +const condition = schema.object({ + threshold: schema.arrayOf(schema.number()), + comparator: schema.oneOf([ + schema.literal('>'), + schema.literal('<'), + schema.literal('>='), + schema.literal('<='), + schema.literal('between'), + ]), + timeUnit: schema.string(), + timeSize: schema.number(), + metric: schema.string(), +}); + +export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs) => ({ + id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + name: 'Inventory metric threshold', + validate: { + params: schema.object({ + criteria: schema.arrayOf(condition), + nodeType: schema.string(), + filterQuery: schema.maybe(schema.string()), + sourceId: schema.string(), + }), + }, + defaultActionGroupId: FIRED_ACTIONS.id, + actionGroups: [FIRED_ACTIONS], + executor: curry(createInventoryMetricThresholdExecutor)(libs, uuid.v4()), + actionVariables: { + context: [ + { + name: 'group', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.groupActionVariableDescription', + { + defaultMessage: 'Name of the group reporting data', + } + ), + }, + { + name: 'valueOf', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.valueOfActionVariableDescription', + { + defaultMessage: + 'Record of the current value of the watched metric; grouped by condition, i.e valueOf.condition0, valueOf.condition1, etc.', + } + ), + }, + { + name: 'thresholdOf', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.thresholdOfActionVariableDescription', + { + defaultMessage: + 'Record of the alerting threshold; grouped by condition, i.e thresholdOf.condition0, thresholdOf.condition1, etc.', + } + ), + }, + { + name: 'metricOf', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.metricOfActionVariableDescription', + { + defaultMessage: + 'Record of the watched metric; grouped by condition, i.e metricOf.condition0, metricOf.condition1, etc.', + } + ), + }, + ], + }, +}); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/test_mocks.ts new file mode 100644 index 00000000000000..66e0a363c89833 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/test_mocks.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const bucketsA = [ + { + doc_count: 2, + aggregatedValue: { value: 0.5 }, + }, + { + doc_count: 3, + aggregatedValue: { value: 1.0 }, + }, +]; + +const bucketsB = [ + { + doc_count: 4, + aggregatedValue: { value: 2.5 }, + }, + { + doc_count: 5, + aggregatedValue: { value: 3.5 }, + }, +]; + +const bucketsC = [ + { + doc_count: 2, + aggregatedValue: { value: 0.5 }, + }, + { + doc_count: 3, + aggregatedValue: { value: 16.0 }, + }, +]; + +export const basicMetricResponse = { + aggregations: { + aggregatedIntervals: { + buckets: bucketsA, + }, + }, +}; + +export const alternateMetricResponse = { + aggregations: { + aggregatedIntervals: { + buckets: bucketsB, + }, + }, +}; + +export const basicCompositeResponse = { + aggregations: { + groupings: { + after_key: 'foo', + buckets: [ + { + key: { + groupBy: 'a', + }, + aggregatedIntervals: { + buckets: bucketsA, + }, + }, + { + key: { + groupBy: 'b', + }, + aggregatedIntervals: { + buckets: bucketsB, + }, + }, + ], + }, + }, + hits: { + total: { + value: 2, + }, + }, +}; + +export const alternateCompositeResponse = { + aggregations: { + groupings: { + after_key: 'foo', + buckets: [ + { + key: { + groupBy: 'a', + }, + aggregatedIntervals: { + buckets: bucketsB, + }, + }, + { + key: { + groupBy: 'b', + }, + aggregatedIntervals: { + buckets: bucketsA, + }, + }, + ], + }, + }, + hits: { + total: { + value: 2, + }, + }, +}; + +export const compositeEndResponse = { + aggregations: {}, + hits: { total: { value: 0 } }, +}; + +export const changedSourceIdResponse = { + aggregations: { + aggregatedIntervals: { + buckets: bucketsC, + }, + }, +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts new file mode 100644 index 00000000000000..ee55730e18c876 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SnapshotMetricType } from '../../../../common/inventory_models/types'; + +export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; + +export enum Comparator { + GT = '>', + LT = '<', + GT_OR_EQ = '>=', + LT_OR_EQ = '<=', + BETWEEN = 'between', +} + +export enum AlertStates { + OK, + ALERT, + NO_DATA, + ERROR, +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; + +export interface InventoryMetricConditions { + metric: SnapshotMetricType; + timeSize: number; + timeUnit: TimeUnit; + sourceId?: string; + threshold: number[]; + comparator: Comparator; +} diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 946f1c14bf593a..6b2c09081ae956 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -5,8 +5,6 @@ */ import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { convertSavedObjectToSavedSourceConfiguration } from '../../sources/sources'; -import { infraSourceConfigurationSavedObjectType } from '../../sources/saved_object_mappings'; import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; @@ -15,9 +13,9 @@ import { MetricExpressionParams, Comparator, Aggregators, AlertStates } from './ import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; +import { InfraBackendLibs } from '../../infra_types'; const TOTAL_BUCKETS = 5; -const DEFAULT_INDEX_PATTERN = 'metricbeat-*'; interface Aggregation { aggregatedIntervals: { @@ -67,6 +65,7 @@ const getParsedFilterQuery: ( export const getElasticsearchMetricQuery = ( { metric, aggType, timeUnit, timeSize }: MetricExpressionParams, + timefield: string, groupBy?: string, filterQuery?: string ) => { @@ -100,7 +99,7 @@ export const getElasticsearchMetricQuery = ( const baseAggs = { aggregatedIntervals: { date_histogram: { - field: '@timestamp', + field: timefield, fixed_interval: interval, offset, extended_bounds: { @@ -168,43 +167,23 @@ export const getElasticsearchMetricQuery = ( }; }; -const getIndexPattern: ( - services: AlertServices, - sourceId?: string -) => Promise = async function({ savedObjectsClient }, sourceId = 'default') { - try { - const sourceConfiguration = await savedObjectsClient.get( - infraSourceConfigurationSavedObjectType, - sourceId - ); - const { metricAlias } = convertSavedObjectToSavedSourceConfiguration( - sourceConfiguration - ).configuration; - return metricAlias || DEFAULT_INDEX_PATTERN; - } catch (e) { - if (e.output.statusCode === 404) { - return DEFAULT_INDEX_PATTERN; - } else { - throw e; - } - } -}; - const getMetric: ( services: AlertServices, params: MetricExpressionParams, index: string, + timefield: string, groupBy: string | undefined, filterQuery: string | undefined ) => Promise> = async function( - { savedObjectsClient, callCluster }, + { callCluster }, params, index, + timefield, groupBy, filterQuery ) { const { aggType } = params; - const searchBody = getElasticsearchMetricQuery(params, groupBy, filterQuery); + const searchBody = getElasticsearchMetricQuery(params, timefield, groupBy, filterQuery); try { if (groupBy) { @@ -262,7 +241,7 @@ const mapToConditionsLookup = ( {} ); -export const createMetricThresholdExecutor = (alertUUID: string) => +export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: string) => async function({ services, params }: AlertExecutorOptions) { const { criteria, groupBy, filterQuery, sourceId } = params as { criteria: MetricExpressionParams[]; @@ -271,11 +250,22 @@ export const createMetricThresholdExecutor = (alertUUID: string) => sourceId?: string; }; + const source = await libs.sources.getSourceConfiguration( + services.savedObjectsClient, + sourceId || 'default' + ); + const config = source.configuration; const alertResults = await Promise.all( - criteria.map(criterion => - (async () => { - const index = await getIndexPattern(services, sourceId); - const currentValues = await getMetric(services, criterion, index, groupBy, filterQuery); + criteria.map(criterion => { + return (async () => { + const currentValues = await getMetric( + services, + criterion, + config.fields.timestamp, + config.metricAlias, + groupBy, + filterQuery + ); const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; return mapValues(currentValues, value => ({ @@ -285,13 +275,14 @@ export const createMetricThresholdExecutor = (alertUUID: string) => isNoData: value === null, isError: value === undefined, })); - })() - ) + })(); + }) ); + // Because each alert result has the same group definitions, just grap the groups from the first one. const groups = Object.keys(alertResults[0]); for (const group of groups) { - const alertInstance = services.alertInstanceFactory(`${alertUUID}-${group}`); + const alertInstance = services.alertInstanceFactory(`${alertId}-${group}`); // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every(result => result[group].shouldFire); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index b697af4fa4c3b2..d2b85366304269 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -6,10 +6,11 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; -import { PluginSetupContract } from '../../../../../alerting/server'; +import { curry } from 'lodash'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; +import { InfraBackendLibs } from '../../infra_types'; const oneOfLiterals = (arrayOfLiterals: Readonly) => schema.string({ @@ -17,14 +18,7 @@ const oneOfLiterals = (arrayOfLiterals: Readonly) => arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`, }); -export async function registerMetricThresholdAlertType(alertingPlugin: PluginSetupContract) { - if (!alertingPlugin) { - throw new Error( - 'Cannot register metric threshold alert type. Both the actions and alerting plugins need to be enabled.' - ); - } - const alertUUID = uuid.v4(); - +export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { const baseCriterion = { threshold: schema.arrayOf(schema.number()), comparator: oneOfLiterals(Object.values(Comparator)), @@ -75,7 +69,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet } ); - alertingPlugin.registerType({ + return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: 'Metric threshold', validate: { @@ -88,7 +82,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], - executor: createMetricThresholdExecutor(alertUUID), + executor: curry(createMetricThresholdExecutor)(libs, uuid.v4()), actionVariables: { context: [ { name: 'group', description: groupActionVariableDescription }, @@ -97,5 +91,5 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet { name: 'metricOf', description: metricOfActionVariableDescription }, ], }, - }); + }; } 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 6ec6f31256b787..5dd58d52e8d652 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 @@ -6,14 +6,13 @@ import { PluginSetupContract } from '../../../../alerting/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; +import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type'; +import { InfraBackendLibs } from '../infra_types'; -const registerAlertTypes = (alertingPlugin: PluginSetupContract) => { +const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => { if (alertingPlugin) { - const registerFns = [registerMetricThresholdAlertType]; - - registerFns.forEach(fn => { - fn(alertingPlugin); - }); + alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); + alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs)); } }; diff --git a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts index d2e151ca2c3f52..b6837e5b769a6f 100644 --- a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts @@ -21,7 +21,7 @@ export class InfraFieldsDomain { indexType: InfraIndexType ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const includeMetricIndices = [InfraIndexType.ANY, InfraIndexType.METRICS].includes(indexType); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 528b9a69327fa4..7f23278b448498 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -113,7 +113,7 @@ export class InfraLogEntriesDomain { params: LogEntriesParams ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); @@ -179,7 +179,7 @@ export class InfraLogEntriesDomain { filterQuery?: LogEntryQuery ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets( @@ -203,7 +203,7 @@ export class InfraLogEntriesDomain { filterQuery?: LogEntryQuery ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const messageFormattingRules = compileFormattingRules( diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts index cf2b1e59b2a225..c75ee6d6440442 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -5,26 +5,23 @@ */ import { uniq } from 'lodash'; -import { RequestHandlerContext } from 'kibana/server'; import { InfraSnapshotRequestOptions } from './types'; import { getMetricsAggregations } from './query_helpers'; import { calculateMetricInterval } from '../../utils/calculate_metric_interval'; import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/inventory_models/types'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field'; import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api'; +import { ESSearchClient } from '.'; export const createTimeRangeWithInterval = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, options: InfraSnapshotRequestOptions ): Promise => { const aggregations = getMetricsAggregations(options); - const modules = await aggregationsToModules(framework, requestContext, aggregations, options); + const modules = await aggregationsToModules(client, aggregations, options); const interval = Math.max( (await calculateMetricInterval( - framework, - requestContext, + client, { indexPattern: options.sourceConfiguration.metricAlias, timestampField: options.sourceConfiguration.fields.timestamp, @@ -43,8 +40,7 @@ export const createTimeRangeWithInterval = async ( }; const aggregationsToModules = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, aggregations: SnapshotModel, options: InfraSnapshotRequestOptions ): Promise => { @@ -59,12 +55,7 @@ const aggregationsToModules = async ( const fields = await Promise.all( uniqueFields.map( async field => - await getDatasetForField( - framework, - requestContext, - field as string, - options.sourceConfiguration.metricAlias - ) + await getDatasetForField(client, field as string, options.sourceConfiguration.metricAlias) ) ); return fields.filter(f => f) as string[]; diff --git a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts index 07abfa5fd474a9..4057ed246ccaf0 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts @@ -3,11 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { RequestHandlerContext } from 'src/core/server'; -import { InfraDatabaseSearchResponse } from '../adapters/framework'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; -import { InfraSources } from '../sources'; +import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../adapters/framework'; import { JsonObject } from '../../../common/typed_json'; import { SNAPSHOT_COMPOSITE_REQUEST_SIZE } from './constants'; @@ -31,36 +27,26 @@ import { InfraSnapshotRequestOptions } from './types'; import { createTimeRangeWithInterval } from './create_timerange_with_interval'; import { SnapshotNode } from '../../../common/http_api/snapshot_api'; +export type ESSearchClient = ( + options: CallWithRequestParams +) => Promise>; export class InfraSnapshot { - constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {} - public async getNodes( - requestContext: RequestHandlerContext, + client: ESSearchClient, options: InfraSnapshotRequestOptions ): Promise<{ nodes: SnapshotNode[]; interval: string }> { // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch // in order to page through the results of their respective composite aggregations. // Both chains of requests are supposed to run in parallel, and their results be merged // when they have both been completed. - const timeRangeWithIntervalApplied = await createTimeRangeWithInterval( - this.libs.framework, - requestContext, - options - ); + const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, options); const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied }; - const groupedNodesPromise = requestGroupedNodes( - requestContext, - optionsWithTimerange, - this.libs.framework - ); - const nodeMetricsPromise = requestNodeMetrics( - requestContext, - optionsWithTimerange, - this.libs.framework - ); + const groupedNodesPromise = requestGroupedNodes(client, optionsWithTimerange); + const nodeMetricsPromise = requestNodeMetrics(client, optionsWithTimerange); const groupedNodeBuckets = await groupedNodesPromise; const nodeMetricBuckets = await nodeMetricsPromise; + return { nodes: mergeNodeBuckets(groupedNodeBuckets, nodeMetricBuckets, options), interval: timeRangeWithIntervalApplied.interval, @@ -77,15 +63,12 @@ const handleAfterKey = createAfterKeyHandler( input => input?.aggregations?.nodes?.after_key ); -const callClusterFactory = (framework: KibanaFramework, requestContext: RequestHandlerContext) => ( - opts: any -) => - framework.callWithRequest<{}, InfraSnapshotAggregationResponse>(requestContext, 'search', opts); +const callClusterFactory = (search: ESSearchClient) => (opts: any) => + search<{}, InfraSnapshotAggregationResponse>(opts); const requestGroupedNodes = async ( - requestContext: RequestHandlerContext, - options: InfraSnapshotRequestOptions, - framework: KibanaFramework + client: ESSearchClient, + options: InfraSnapshotRequestOptions ): Promise => { const inventoryModel = findInventoryModel(options.nodeType); const query = { @@ -124,13 +107,12 @@ const requestGroupedNodes = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeGroupByBucket - >(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey); + >(callClusterFactory(client), query, bucketSelector, handleAfterKey); }; const requestNodeMetrics = async ( - requestContext: RequestHandlerContext, - options: InfraSnapshotRequestOptions, - framework: KibanaFramework + client: ESSearchClient, + options: InfraSnapshotRequestOptions ): Promise => { const index = options.metric.type === 'logRate' @@ -175,7 +157,7 @@ const requestNodeMetrics = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeMetricsBucket - >(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey); + >(callClusterFactory(client), query, bucketSelector, handleAfterKey); }; // buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[] diff --git a/x-pack/plugins/infra/server/lib/source_status.ts b/x-pack/plugins/infra/server/lib/source_status.ts index 1f0845b6b223f6..9bb953845e5a17 100644 --- a/x-pack/plugins/infra/server/lib/source_status.ts +++ b/x-pack/plugins/infra/server/lib/source_status.ts @@ -18,7 +18,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const indexNames = await this.adapter.getIndexNames( @@ -32,7 +32,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const indexNames = await this.adapter.getIndexNames( @@ -46,7 +46,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasAlias = await this.adapter.hasAlias( @@ -60,7 +60,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasAlias = await this.adapter.hasAlias( @@ -74,7 +74,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasIndices = await this.adapter.hasIndices( @@ -88,7 +88,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasIndices = await this.adapter.hasIndices( diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts index 4a83ca730ff83f..57efb0f676b2fe 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts @@ -29,7 +29,9 @@ describe('the InfraSources lib', () => { }, }); - expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ + expect( + await sourcesLib.getSourceConfiguration(request.core.savedObjects.client, 'TEST_ID') + ).toMatchObject({ id: 'TEST_ID', version: 'foo', updatedAt: 946684800000, @@ -74,7 +76,9 @@ describe('the InfraSources lib', () => { }, }); - expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ + expect( + await sourcesLib.getSourceConfiguration(request.core.savedObjects.client, 'TEST_ID') + ).toMatchObject({ id: 'TEST_ID', version: 'foo', updatedAt: 946684800000, @@ -104,7 +108,9 @@ describe('the InfraSources lib', () => { attributes: {}, }); - expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ + expect( + await sourcesLib.getSourceConfiguration(request.core.savedObjects.client, 'TEST_ID') + ).toMatchObject({ id: 'TEST_ID', version: 'foo', updatedAt: 946684800000, diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 99e062aa49ccf3..3ad39a83bd23fd 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -9,7 +9,7 @@ import { failure } from 'io-ts/lib/PathReporter'; import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; -import { RequestHandlerContext } from 'src/core/server'; +import { RequestHandlerContext, SavedObjectsClientContract } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; @@ -37,7 +37,7 @@ export class InfraSources { } public async getSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string ): Promise { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); @@ -55,7 +55,7 @@ export class InfraSources { })) .catch(err => err instanceof NotFoundError - ? this.getSavedSourceConfiguration(requestContext, sourceId).then(result => ({ + ? this.getSavedSourceConfiguration(savedObjectsClient, sourceId).then(result => ({ ...result, configuration: mergeSourceConfiguration( staticDefaultSourceConfiguration, @@ -65,7 +65,7 @@ export class InfraSources { : Promise.reject(err) ) .catch(err => - requestContext.core.savedObjects.client.errors.isNotFoundError(err) + savedObjectsClient.errors.isNotFoundError(err) ? Promise.resolve({ id: sourceId, version: undefined, @@ -79,10 +79,12 @@ export class InfraSources { return savedSourceConfiguration; } - public async getAllSourceConfigurations(requestContext: RequestHandlerContext) { + public async getAllSourceConfigurations(savedObjectsClient: SavedObjectsClientContract) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(requestContext); + const savedSourceConfigurations = await this.getAllSavedSourceConfigurations( + savedObjectsClient + ); return savedSourceConfigurations.map(savedSourceConfiguration => ({ ...savedSourceConfiguration, @@ -130,13 +132,16 @@ export class InfraSources { } public async updateSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string, sourceProperties: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const { configuration, version } = await this.getSourceConfiguration(requestContext, sourceId); + const { configuration, version } = await this.getSourceConfiguration( + savedObjectsClient, + sourceId + ); const updatedSourceConfigurationAttributes = mergeSourceConfiguration( configuration, @@ -144,7 +149,7 @@ export class InfraSources { ); const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await requestContext.core.savedObjects.client.update( + await savedObjectsClient.update( infraSourceConfigurationSavedObjectType, sourceId, pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, @@ -199,10 +204,10 @@ export class InfraSources { } private async getSavedSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string ) { - const savedObject = await requestContext.core.savedObjects.client.get( + const savedObject = await savedObjectsClient.get( infraSourceConfigurationSavedObjectType, sourceId ); @@ -210,8 +215,8 @@ export class InfraSources { return convertSavedObjectToSavedSourceConfiguration(savedObject); } - private async getAllSavedSourceConfigurations(requestContext: RequestHandlerContext) { - const savedObjects = await requestContext.core.savedObjects.client.find({ + private async getAllSavedSourceConfigurations(savedObjectsClient: SavedObjectsClientContract) { + const savedObjects = await savedObjectsClient.find({ type: infraSourceConfigurationSavedObjectType, }); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index e3804078604cc5..db34033c1d4f8d 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -109,7 +109,7 @@ export class InfraServerPlugin { sources, } ); - const snapshot = new InfraSnapshot({ sources, framework }); + const snapshot = new InfraSnapshot(); const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework }); const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework }); @@ -147,7 +147,7 @@ export class InfraServerPlugin { ]); initInfraServer(this.libs); - registerAlertTypes(plugins.alerting); + registerAlertTypes(plugins.alerting, this.libs); // Telemetry UsageCollector.registerUsageCollector(plugins.usageCollection); diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts index 7e9b7ada28c8e1..687e368736a410 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts @@ -39,7 +39,7 @@ export const initInventoryMetaRoute = (libs: InfraBackendLibs) => { ); const { configuration } = await libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const awsMetadata = await getCloudMetadata( diff --git a/x-pack/plugins/infra/server/routes/log_entries/item.ts b/x-pack/plugins/infra/server/routes/log_entries/item.ts index 3a6bdaf3804e33..85dba8f598a89d 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/item.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/item.ts @@ -37,8 +37,9 @@ export const initLogEntriesItemRoute = ({ framework, sources, logEntries }: Infr ); const { id, sourceId } = payload; - const sourceConfiguration = (await sources.getSourceConfiguration(requestContext, sourceId)) - .configuration; + const sourceConfiguration = ( + await sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId) + ).configuration; const logEntry = await logEntries.getLogItem(requestContext, id, sourceConfiguration); diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index c45f191b1130d8..fe142aa93dcda0 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -44,7 +44,7 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { ); const { configuration } = await libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const metricsMetadata = await getMetricMetadata( diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts index 66f0ca8fc706a3..94e91d32b14bb5 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from 'kibana/server'; -import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; +import { ESSearchClient } from '../../../lib/snapshot'; interface EventDatasetHit { _source: { @@ -16,8 +15,7 @@ interface EventDatasetHit { } export const getDatasetForField = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, field: string, indexPattern: string ) => { @@ -33,11 +31,8 @@ export const getDatasetForField = async ( }, }; - const response = await framework.callWithRequest( - requestContext, - 'search', - params - ); + const response = await client(params); + if (response.hits.total.value === 0) { return null; } diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index 3517800ea0dd10..01322e90a9b8ea 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -17,6 +17,10 @@ import { createMetricModel } from './create_metrics_model'; import { JsonObject } from '../../../../common/typed_json'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; import { getDatasetForField } from './get_dataset_for_field'; +import { + CallWithRequestParams, + InfraDatabaseSearchResponse, +} from '../../../lib/adapters/framework'; export const populateSeriesWithTSVBData = ( request: KibanaRequest, @@ -52,17 +56,21 @@ export const populateSeriesWithTSVBData = ( } const timerange = { min: options.timerange.from, max: options.timerange.to }; + const client = ( + opts: CallWithRequestParams + ): Promise> => + framework.callWithRequest(requestContext, 'search', opts); + // Create the TSVB model based on the request options const model = createMetricModel(options); const modules = await Promise.all( uniq(options.metrics.filter(m => m.field)).map( - async m => - await getDatasetForField(framework, requestContext, m.field as string, options.indexPattern) + async m => await getDatasetForField(client, m.field as string, options.indexPattern) ) ); + const calculatedInterval = await calculateMetricInterval( - framework, - requestContext, + client, { indexPattern: options.indexPattern, timestampField: options.timerange.field, diff --git a/x-pack/plugins/infra/server/routes/node_details/index.ts b/x-pack/plugins/infra/server/routes/node_details/index.ts index 36906f6f4125bf..a457ccac2416c3 100644 --- a/x-pack/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/plugins/infra/server/routes/node_details/index.ts @@ -37,7 +37,10 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { NodeDetailsRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); UsageCollector.countNode(nodeType); diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index e45b9884967d0d..2d951d426b03a0 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -13,6 +13,7 @@ import { UsageCollector } from '../../usage/usage_collector'; import { parseFilterQuery } from '../../utils/serialized_query'; import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; +import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../../lib/adapters/framework'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); @@ -42,7 +43,10 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { SnapshotRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); UsageCollector.countNode(nodeType); const options = { filterQuery: parseFilterQuery(filterQuery), @@ -54,7 +58,13 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { metric, timerange, }; - const nodesWithInterval = await libs.snapshot.getNodes(requestContext, options); + + const searchES = ( + opts: CallWithRequestParams + ): Promise> => + framework.callWithRequest(requestContext, 'search', opts); + + const nodesWithInterval = await libs.snapshot.getNodes(searchES, options); return response.ok({ body: SnapshotNodeResponseRT.encode(nodesWithInterval), }); diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index 2f29320d7bb81d..62b7fd7ba902f5 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -37,7 +37,10 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { try { const { type, sourceId } = request.params; - const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); if (!source) { return response.notFound(); } diff --git a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts index 7cbbdc0f2145be..43e109b009f48e 100644 --- a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from 'src/core/server'; +// import { RequestHandlerContext } from 'src/core/server'; import { findInventoryModel } from '../../common/inventory_models'; -import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; +// import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; import { InventoryItemType } from '../../common/inventory_models/types'; +import { ESSearchClient } from '../lib/snapshot'; interface Options { indexPattern: string; @@ -23,8 +24,7 @@ interface Options { * This is useful for visualizing metric modules like s3 that only send metrics once per day. */ export const calculateMetricInterval = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, options: Options, modules?: string[], nodeType?: InventoryItemType // TODO: check that this type still makes sense @@ -73,11 +73,7 @@ export const calculateMetricInterval = async ( }, }; - const resp = await framework.callWithRequest<{}, PeriodAggregationData>( - requestContext, - 'search', - query - ); + const resp = await client<{}, PeriodAggregationData>(query); // if ES doesn't return an aggregations key, something went seriously wrong. if (!resp.aggregations) { From e9862d167a4a1387b960bf89d06f37bc37a5f10d Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Thu, 23 Apr 2020 05:46:07 -0500 Subject: [PATCH 02/17] Add missed file --- .../server/lib/alerting/inventory_metric_threshold/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts index ee55730e18c876..73ee1ab6b76159 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts @@ -13,6 +13,7 @@ export enum Comparator { GT_OR_EQ = '>=', LT_OR_EQ = '<=', BETWEEN = 'between', + OUTSIDE_RANGE = 'outside', } export enum AlertStates { From 70b551386542a480ac1f2501b845f2e7435fcefe Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Thu, 23 Apr 2020 06:21:11 -0500 Subject: [PATCH 03/17] Fix some types --- .../alerting/inventory/expression.tsx | 28 ++-- .../inventory_metric_threshold_executor.ts | 2 +- .../metric_threshold_executor.test.ts | 152 ++++++++++-------- .../infra/server/lib/compose/kibana.ts | 2 +- .../apis/infra/metrics_alerting.ts | 3 +- 5 files changed, 99 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx index f84bc1b79d7f58..752845a0c88bf6 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -48,7 +48,6 @@ import { findInventoryModel } from '../../../../common/inventory_models'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; -import { SnapshotMetricInput } from '../../../../common/http_api/snapshot_api'; import { MetricExpression } from './metric'; interface AlertContextMeta { @@ -95,16 +94,16 @@ export const Expressions: React.FC = props => { createDerivedIndexPattern, ]); - const options = useMemo(() => { - if (alertsContext.metadata?.currentOptions?.metrics) { - return alertsContext.metadata.currentOptions as MetricsExplorerOptions; - } else { - return { - metrics: [], - aggregation: 'avg', - }; - } - }, [alertsContext.metadata]); + // const options = useMemo(() => { + // if (alertsContext.metadata?.currentOptions?.metrics) { + // return alertsContext.metadata.currentOptions as MetricsExplorerOptions; + // } else { + // return { + // metrics: [], + // aggregation: 'avg', + // }; + // } + // }, [alertsContext.metadata]); const updateParams = useCallback( (id, e: InventoryMetricConditions) => { @@ -139,13 +138,6 @@ export const Expressions: React.FC = props => { [setAlertParams] ); - const onGroupByChange = useCallback( - (group: string | null) => { - setAlertParams('groupBy', group || undefined); - }, - [setAlertParams] - ); - const emptyError = useMemo(() => { return { aggField: [], diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 2958ea22e5645d..d2f86cac06f54a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -14,7 +14,7 @@ import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/ser import { InfraSnapshot } from '../../snapshot'; import { parseFilterQuery } from '../../../utils/serialized_query'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { SnapshotMetricInput, InfraTimerangeInput } from '../../../../common/http_api/snapshot_api'; +import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api'; import { InfraSourceConfiguration } from '../../sources'; import { InfraBackendLibs } from '../../infra_types'; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index a52659dae01f12..a2e7d4bae46c87 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; @@ -13,79 +12,14 @@ import { AlertServicesMock, AlertInstanceMock, } from '../../../../../alerting/server/mocks'; - -const executor = createMetricThresholdExecutor('test') as (opts: { - params: AlertExecutorOptions['params']; - services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; -}) => Promise; - -const services: AlertServicesMock = alertsMock.createAlertServices(); -services.callCluster.mockImplementation((_: string, { body, index }: any) => { - if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; - const metric = body.query.bool.filter[1]?.exists.field; - if (body.aggs.groupings) { - if (body.aggs.groupings.composite.after) { - return mocks.compositeEndResponse; - } - if (metric === 'test.metric.2') { - return mocks.alternateCompositeResponse; - } - return mocks.basicCompositeResponse; - } - if (metric === 'test.metric.2') { - return mocks.alternateMetricResponse; - } - return mocks.basicMetricResponse; -}); -services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { - if (sourceId === 'alternate') - return { - id: 'alternate', - attributes: { metricAlias: 'alternatebeat-*' }, - type, - references: [], - }; - return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; -}); +import { InfraSources } from '../../sources'; interface AlertTestInstance { instance: AlertInstanceMock; actionQueue: any[]; state: any; } -const alertInstances = new Map(); -services.alertInstanceFactory.mockImplementation((instanceID: string) => { - const alertInstance: AlertTestInstance = { - instance: alertsMock.createAlertInstanceFactory(), - actionQueue: [], - state: {}, - }; - alertInstances.set(instanceID, alertInstance); - alertInstance.instance.replaceState.mockImplementation((newState: any) => { - alertInstance.state = newState; - return alertInstance.instance; - }); - alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { - alertInstance.actionQueue.push({ id, action }); - return alertInstance.instance; - }); - return alertInstance.instance; -}); -function mostRecentAction(id: string) { - return alertInstances.get(id)!.actionQueue.pop(); -} - -function getState(id: string) { - return alertInstances.get(id)!.state; -} - -const baseCriterion = { - aggType: 'avg', - metric: 'test.metric.1', - timeSize: 1, - timeUnit: 'm', -}; describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { const instanceID = 'test-*'; @@ -306,3 +240,87 @@ describe('The metric threshold alert type', () => { }); }); }); + +const createMockStaticConfiguration = (sources: any) => ({ + enabled: true, + query: { + partitionSize: 1, + partitionFactor: 1, + }, + sources, +}); + +const mockLibs: any = { + sources: new InfraSources({ + config: createMockStaticConfiguration({}), + }), + configuration: createMockStaticConfiguration({}), +}; + +const executor = createMetricThresholdExecutor(mockLibs, 'test') as (opts: { + params: AlertExecutorOptions['params']; + services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; +}) => Promise; + +const services: AlertServicesMock = alertsMock.createAlertServices(); +services.callCluster.mockImplementation((_: string, { body, index }: any) => { + if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; + const metric = body.query.bool.filter[1]?.exists.field; + if (body.aggs.groupings) { + if (body.aggs.groupings.composite.after) { + return mocks.compositeEndResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateCompositeResponse; + } + return mocks.basicCompositeResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateMetricResponse; + } + return mocks.basicMetricResponse; +}); +services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { + if (sourceId === 'alternate') + return { + id: 'alternate', + attributes: { metricAlias: 'alternatebeat-*' }, + type, + references: [], + }; + return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; +}); + +const alertInstances = new Map(); +services.alertInstanceFactory.mockImplementation((instanceID: string) => { + const alertInstance: AlertTestInstance = { + instance: alertsMock.createAlertInstanceFactory(), + actionQueue: [], + state: {}, + }; + alertInstances.set(instanceID, alertInstance); + alertInstance.instance.replaceState.mockImplementation((newState: any) => { + alertInstance.state = newState; + return alertInstance.instance; + }); + alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { + alertInstance.actionQueue.push({ id, action }); + return alertInstance.instance; + }); + return alertInstance.instance; +}); + +function mostRecentAction(id: string) { + return alertInstances.get(id)!.actionQueue.pop(); +} + +function getState(id: string) { + return alertInstances.get(id)!.state; +} + +const baseCriterion = { + aggType: 'avg', + metric: 'test.metric.1', + timeSize: 1, + timeUnit: 'm', +}; diff --git a/x-pack/plugins/infra/server/lib/compose/kibana.ts b/x-pack/plugins/infra/server/lib/compose/kibana.ts index f100726b5b92e7..d22ca2961cfa53 100644 --- a/x-pack/plugins/infra/server/lib/compose/kibana.ts +++ b/x-pack/plugins/infra/server/lib/compose/kibana.ts @@ -28,7 +28,7 @@ export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServ const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), { sources, }); - const snapshot = new InfraSnapshot({ sources, framework }); + const snapshot = new InfraSnapshot(); const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework }); const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework }); diff --git a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts index 4f17f9db674839..8923f062873553 100644 --- a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts @@ -32,7 +32,7 @@ export default function({ getService }: FtrProviderContext) { describe('querying the entire infrastructure', () => { for (const aggType of aggs) { it(`should work with the ${aggType} aggregator`, async () => { - const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType)); + const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType), '@timestamp'); const result = await client.search({ index, body: searchBody, @@ -44,6 +44,7 @@ export default function({ getService }: FtrProviderContext) { it('should work with a filterQuery', async () => { const searchBody = getElasticsearchMetricQuery( getSearchParams('avg'), + '@timestamp', undefined, '{"bool":{"should":[{"match_phrase":{"agent.hostname":"foo"}}],"minimum_should_match":1}}' ); From 5e033ff510d8bc69cec58b51f22cd336f70f6177 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Thu, 23 Apr 2020 18:56:10 -0500 Subject: [PATCH 04/17] Convert units on client and executor. --- .../alerting/inventory/expression.tsx | 49 +++++--- .../components/alerting/inventory/metric.tsx | 24 ++-- .../alerting/inventory/node_type.tsx | 117 ++++++++++++++++++ .../alerting/inventory/validation.tsx | 9 -- .../inventory_metric_threshold_executor.ts | 19 ++- ...r_inventory_metric_threshold_alert_type.ts | 2 +- 6 files changed, 179 insertions(+), 41 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx index 752845a0c88bf6..96d23184aa8d1a 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -49,6 +49,7 @@ import { InventoryItemType, SnapshotMetricType } from '../../../../common/invent // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; import { MetricExpression } from './metric'; +import { NodeTypeExpression } from './node_type'; interface AlertContextMeta { currentOptions?: Partial; @@ -94,17 +95,6 @@ export const Expressions: React.FC = props => { createDerivedIndexPattern, ]); - // const options = useMemo(() => { - // if (alertsContext.metadata?.currentOptions?.metrics) { - // return alertsContext.metadata.currentOptions as MetricsExplorerOptions; - // } else { - // return { - // metrics: [], - // aggregation: 'avg', - // }; - // } - // }, [alertsContext.metadata]); - const updateParams = useCallback( (id, e: InventoryMetricConditions) => { const exp = alertParams.criteria ? alertParams.criteria.slice() : []; @@ -207,11 +197,12 @@ export const Expressions: React.FC = props => { setAlertParams('groupBy', md.currentOptions.groupBy); } - setAlertParams('sourceId', source?.id); } else { setAlertParams('criteria', [defaultExpression]); } - }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + setAlertParams('sourceId', source?.id); + setAlertParams('nodeType', nodeType); + }, [alertsContext.metadata, defaultExpression, source, nodeType]); // eslint-disable-line react-hooks/exhaustive-deps return ( <> @@ -225,11 +216,7 @@ export const Expressions: React.FC = props => { - + {alertParams.criteria && @@ -407,6 +394,13 @@ export const ExpressionRow: React.FC = props => { errors={errors} /> + {metric && ( + +
+
{metricUnit[metric]?.label || ''}
+
+
+ )} {canDelete && ( @@ -432,7 +426,7 @@ const getDisplayNameForType = (type: InventoryItemType) => { return inventoryModel.displayName; }; -export const aggregationType: { [key: string]: any } = { +export const nodeTypes: { [key: string]: any } = { host: { text: getDisplayNameForType('host'), value: 'host', @@ -462,3 +456,20 @@ export const aggregationType: { [key: string]: any } = { value: 'awsSQS', }, }; + +const metricUnit: Record = { + count: { label: '' }, + cpu: { label: '%' }, + memory: { label: '%' }, + rx: { label: 'bits/s' }, + tx: { label: 'bits/s' }, + logRate: { label: '/s' }, + diskIOReadBytes: { label: 'bytes/s' }, + diskIOWriteBytes: { label: 'bytes/s' }, + s3BucketSize: { label: 'bytes' }, + s3TotalRequests: { label: '' }, + s3NumberOfObjects: { label: '' }, + s3UploadBytes: { label: 'bytes' }, + s3DownloadBytes: { label: 'bytes' }, + sqsOldestMessage: { label: 'seconds' }, +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx index 91c2487975ffea..92f258106728b7 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx @@ -46,12 +46,9 @@ interface Props { export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosition }: Props) => { const [aggFieldPopoverOpen, setAggFieldPopoverOpen] = useState(false); const firstFieldOption = { - text: i18n.translate( - 'xpack.triggersActionsUI.common.expressionItems.of.selectTimeFieldOptionLabel', - { - defaultMessage: 'Select a field', - } - ), + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.metric.selectFieldLabel', { + defaultMessage: 'Select a metric', + }), value: '', }; @@ -64,7 +61,12 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit id="aggFieldPopover" button={ { @@ -84,7 +86,7 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit
setAggFieldPopoverOpen(false)}> @@ -92,14 +94,14 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit 0 && metric !== undefined} - error={errors.aggField} + isInvalid={errors.metric.length > 0 && metric !== undefined} + error={errors.metric} > 0 && metric !== undefined} + isInvalid={errors.metric.length > 0 && metric !== undefined} placeholder={firstFieldOption.text} options={availablefieldsOptions} noSuggestions={!availablefieldsOptions.length} diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx new file mode 100644 index 00000000000000..392452a5de1d4c --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/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; + * you may not use this file except in compliance with the Elastic License. + */ + +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'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; + +interface WhenExpressionProps { + value: InventoryItemType; + options: { [key: string]: { text: string; value: InventoryItemType } }; + onChange: (value: InventoryItemType) => 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 + withTitle + anchorPosition={popupPosition ?? 'downLeft'} + > +
+ setAggTypePopoverOpen(false)}> + + + { + onChange(e.target.value as InventoryItemType); + 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/components/alerting/inventory/validation.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx index d84e46d08a2879..803893dd5a323f 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx @@ -18,7 +18,6 @@ export function validateMetricThreshold({ const validationResult = { errors: {} }; const errors: { [id: string]: { - aggField: string[]; timeSizeUnit: string[]; timeWindowSize: string[]; threshold0: string[]; @@ -37,20 +36,12 @@ export function validateMetricThreshold({ const id = idx.toString(); errors[id] = errors[id] || { - aggField: [], timeSizeUnit: [], timeWindowSize: [], threshold0: [], threshold1: [], metric: [], }; - if (!c.aggType) { - errors[id].aggField.push( - i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', { - defaultMessage: 'Aggreation is required.', - }) - ); - } if (!c.threshold || !c.threshold.length) { errors[id].threshold0.push( diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index d2f86cac06f54a..55ddacf88e7182 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -57,6 +57,7 @@ export const createInventoryMetricThresholdExecutor = ( if (shouldAlertFire) { alertInstance.scheduleActions(FIRED_ACTIONS.id, { + group: item, item, valueOf: mapToConditionsLookup(results, result => result[item].currentValue), thresholdOf: mapToConditionsLookup(criteria, c => c.threshold), @@ -90,7 +91,8 @@ const evaluateCondtion = async ( services: AlertServices, filterQuery?: string ): Promise> => { - const { threshold, comparator, metric } = condition; + const { comparator, metric } = condition; + let { threshold } = condition; const currentValues = await getData( services, @@ -101,6 +103,8 @@ const evaluateCondtion = async ( filterQuery ); + threshold = threshold.map(n => convertMetricValue(metric, n)); + const comparisonFunction = comparatorMap[comparator]; return mapValues(currentValues, value => ({ @@ -172,3 +176,16 @@ export const FIRED_ACTIONS = { defaultMessage: 'Fired', }), }; + +// Some metrics in the UI are in a different unit that what we store in ES. +const convertMetricValue = (metric: SnapshotMetricType, value: number) => { + if (converters[metric]) { + return converters[metric](value); + } else { + return value; + } +}; +const converters: Record number> = { + cpu: n => Number(n) / 100, + memory: n => Number(n) / 100, +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index 994ba72cf6a56a..6d057a612fc632 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -30,7 +30,7 @@ const condition = schema.object({ export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs) => ({ id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - name: 'Inventory metric threshold', + name: 'Inventory', validate: { params: schema.object({ criteria: schema.arrayOf(condition), From b0818d743c3004bb54a55b3dde3ee96c5e933334 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Fri, 24 Apr 2020 10:20:23 -0500 Subject: [PATCH 05/17] Move formatters to common. Properly format metrics in alert messages --- .../utils => common}/formatters/bytes.test.ts | 4 +- .../utils => common}/formatters/bytes.ts | 3 +- .../utils => common}/formatters/datetime.ts | 0 .../formatters/high_precision.ts | 0 .../utils => common}/formatters/index.ts | 9 ++- .../utils => common}/formatters/number.ts | 0 .../utils => common}/formatters/percent.ts | 0 .../formatters/snapshot_metric_formats.ts | 73 +++++++++++++++++++ .../alerting/inventory/node_type.tsx | 2 - .../log_text_stream/column_headers.tsx | 2 +- .../logging/log_text_stream/log_date_row.tsx | 2 +- x-pack/plugins/infra/public/index.ts | 2 +- x-pack/plugins/infra/public/lib/lib.ts | 6 -- .../components/nodes_overview.tsx | 2 +- .../components/gauges_section_vis.tsx | 2 +- .../metric_detail/components/helpers.ts | 2 +- .../helpers/create_formatter_for_metric.ts | 2 +- .../inventory_metric_threshold_executor.ts | 27 +++++-- 18 files changed, 111 insertions(+), 27 deletions(-) rename x-pack/plugins/infra/{public/utils => common}/formatters/bytes.test.ts (93%) rename x-pack/plugins/infra/{public/utils => common}/formatters/bytes.ts (96%) rename x-pack/plugins/infra/{public/utils => common}/formatters/datetime.ts (100%) rename x-pack/plugins/infra/{public/utils => common}/formatters/high_precision.ts (100%) rename x-pack/plugins/infra/{public/utils => common}/formatters/index.ts (86%) rename x-pack/plugins/infra/{public/utils => common}/formatters/number.ts (100%) rename x-pack/plugins/infra/{public/utils => common}/formatters/percent.ts (100%) create mode 100644 x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/bytes.test.ts b/x-pack/plugins/infra/common/formatters/bytes.test.ts similarity index 93% rename from x-pack/plugins/infra/public/utils/formatters/bytes.test.ts rename to x-pack/plugins/infra/common/formatters/bytes.test.ts index 4c872bcee057d7..78cb7c618cd7f4 100644 --- a/x-pack/plugins/infra/public/utils/formatters/bytes.test.ts +++ b/x-pack/plugins/infra/common/formatters/bytes.test.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { InfraWaffleMapDataFormat } from '../../lib/lib'; import { createBytesFormatter } from './bytes'; +import { InfraWaffleMapDataFormat } from '.'; + describe('createDataFormatter', () => { it('should format bytes as bytesDecimal', () => { const formatter = createBytesFormatter(InfraWaffleMapDataFormat.bytesDecimal); diff --git a/x-pack/plugins/infra/public/utils/formatters/bytes.ts b/x-pack/plugins/infra/common/formatters/bytes.ts similarity index 96% rename from x-pack/plugins/infra/public/utils/formatters/bytes.ts rename to x-pack/plugins/infra/common/formatters/bytes.ts index 80a5603ed6994b..5aa5281606ed2e 100644 --- a/x-pack/plugins/infra/public/utils/formatters/bytes.ts +++ b/x-pack/plugins/infra/common/formatters/bytes.ts @@ -3,9 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { InfraWaffleMapDataFormat } from '../../lib/lib'; import { formatNumber } from './number'; +import { InfraWaffleMapDataFormat } from '.'; /** * The labels are derived from these two Wikipedia articles. diff --git a/x-pack/plugins/infra/public/utils/formatters/datetime.ts b/x-pack/plugins/infra/common/formatters/datetime.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/datetime.ts rename to x-pack/plugins/infra/common/formatters/datetime.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/high_precision.ts b/x-pack/plugins/infra/common/formatters/high_precision.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/high_precision.ts rename to x-pack/plugins/infra/common/formatters/high_precision.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/index.ts b/x-pack/plugins/infra/common/formatters/index.ts similarity index 86% rename from x-pack/plugins/infra/public/utils/formatters/index.ts rename to x-pack/plugins/infra/common/formatters/index.ts index 3c60dba7478257..1f6a91e39c20fb 100644 --- a/x-pack/plugins/infra/public/utils/formatters/index.ts +++ b/x-pack/plugins/infra/common/formatters/index.ts @@ -5,13 +5,18 @@ */ import Mustache from 'mustache'; -import { InfraWaffleMapDataFormat } from '../../lib/lib'; import { createBytesFormatter } from './bytes'; import { formatNumber } from './number'; import { formatPercent } from './percent'; -import { InventoryFormatterType } from '../../../common/inventory_models/types'; +import { InventoryFormatterType } from '../inventory_models/types'; import { formatHighPercision } from './high_precision'; +export enum InfraWaffleMapDataFormat { + bytesDecimal = 'bytesDecimal', + bitsDecimal = 'bitsDecimal', + abbreviatedNumber = 'abbreviatedNumber', +} + export const FORMATTERS = { number: formatNumber, // Because the implimentation for formatting large numbers is the same as formatting diff --git a/x-pack/plugins/infra/public/utils/formatters/number.ts b/x-pack/plugins/infra/common/formatters/number.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/number.ts rename to x-pack/plugins/infra/common/formatters/number.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/percent.ts b/x-pack/plugins/infra/common/formatters/percent.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/percent.ts rename to x-pack/plugins/infra/common/formatters/percent.ts diff --git a/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts new file mode 100644 index 00000000000000..8b4ae27cb30614 --- /dev/null +++ b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +enum InfraFormatterType { + number = 'number', + abbreviatedNumber = 'abbreviatedNumber', + bytes = 'bytes', + bits = 'bits', + percent = 'percent', +} + +interface MetricFormatter { + formatter: InfraFormatterType; + template: string; + bounds?: { min: number; max: number }; +} + +interface MetricFormatters { + [key: string]: MetricFormatter; +} + +export const METRIC_FORMATTERS: MetricFormatters = { + ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, + ['cpu']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['memory']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['logRate']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}/s', + }, + ['diskIOReadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['diskIOWriteBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['s3BucketSize']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3TotalRequests']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3NumberOfObjects']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3UploadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3DownloadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['sqsOldestMessage']: { + formatter: InfraFormatterType.number, + template: '{{value}} seconds', + }, +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx index 392452a5de1d4c..1acec55b211ca3 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx @@ -9,8 +9,6 @@ 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'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; import { InventoryItemType } from '../../../../common/inventory_models/types'; interface WhenExpressionProps { diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx index 72d6aea5ecfc6a..c713839a1bba89 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx @@ -22,7 +22,7 @@ import { } from './log_entry_column'; import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel'; import { LogPositionState } from '../../../containers/logs/log_position'; -import { localizedDate } from '../../../utils/formatters/datetime'; +import { localizedDate } from '../../../../common/formatters/datetime'; export const LogColumnHeaders: React.FunctionComponent<{ columnConfigurations: LogColumnConfiguration[]; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx index fbc450950b8283..144caed744bab8 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiTitle } from '@elastic/eui'; -import { localizedDate } from '../../../utils/formatters/datetime'; +import { localizedDate } from '../../../../common/formatters/datetime'; interface LogDateRowProps { timestamp: number; diff --git a/x-pack/plugins/infra/public/index.ts b/x-pack/plugins/infra/public/index.ts index 4465bde377c124..1dfdf827f203b5 100644 --- a/x-pack/plugins/infra/public/index.ts +++ b/x-pack/plugins/infra/public/index.ts @@ -16,7 +16,7 @@ export const plugin: PluginInitializer< return new Plugin(context); }; -export { FORMATTERS } from './utils/formatters'; +export { FORMATTERS } from '../common/formatters'; export { InfraFormatterType } from './lib/lib'; export type InfraAppId = 'logs' | 'metrics'; diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index e4de0caf9bb8ba..9043b4d9f69796 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -186,12 +186,6 @@ export enum InfraFormatterType { percent = 'percent', } -export enum InfraWaffleMapDataFormat { - bytesDecimal = 'bytesDecimal', - bitsDecimal = 'bitsDecimal', - abbreviatedNumber = 'abbreviatedNumber', -} - export interface InfraGroupByOptions { text: string; field: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index afbfd2a079253d..86616c4bc2d29a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -16,7 +16,7 @@ import { InfraWaffleMapBounds, InfraWaffleMapOptions, } from '../../../../lib/lib'; -import { createFormatter } from '../../../../utils/formatters'; +import { createFormatter } from '../../../../../common/formatters'; import { NoData } from '../../../../components/empty_states'; import { InfraLoadingPanel } from '../../../../components/loading'; import { Map } from './waffle/map'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx index 0aab676b7d6c54..0f53ced80888b5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx @@ -17,7 +17,7 @@ import { get, last, max } from 'lodash'; import React, { ReactText } from 'react'; import { euiStyled } from '../../../../../../observability/public'; -import { createFormatter } from '../../../../utils/formatters'; +import { createFormatter } from '../../../../../common/formatters'; import { InventoryFormatterType } from '../../../../../common/inventory_models/types'; import { SeriesOverrides, VisSectionProps } from '../types'; import { getChartName } from './helpers'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts index bb4ad326609520..0b8773db2dddf0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts @@ -7,7 +7,7 @@ import { ReactText } from 'react'; import Color from 'color'; import { get, first, last, min, max } from 'lodash'; -import { createFormatter } from '../../../../utils/formatters'; +import { createFormatter } from '../../../../../common/formatters'; import { InfraDataSeries } from '../../../../graphql/types'; import { InventoryVisTypeRT, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts index d07a6b45f02bef..46bd7b006446af 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts @@ -5,7 +5,7 @@ */ import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; -import { createFormatter } from '../../../../../utils/formatters'; +import { createFormatter } from '../../../../../../common/formatters'; import { InfraFormatterType } from '../../../../../lib/lib'; import { metricToFormat } from './metric_to_format'; export const createFormatterForMetric = (metric?: MetricsExplorerMetric) => { diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 55ddacf88e7182..1e5fd4cb31d716 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { mapValues, last } from 'lodash'; +import { mapValues, last, get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { InfraDatabaseSearchResponse, @@ -17,6 +17,8 @@ import { InventoryItemType, SnapshotMetricType } from '../../../../common/invent import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api'; import { InfraSourceConfiguration } from '../../sources'; import { InfraBackendLibs } from '../../infra_types'; +import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats'; +import { createFormatter } from '../../../../common/formatters'; interface InventoryMetricThresholdParams { criteria: InventoryMetricConditions[]; @@ -38,13 +40,10 @@ export const createInventoryMetricThresholdExecutor = ( ); const results = await Promise.all( - criteria.map(c => { - return evaluateCondtion(c, nodeType, source.configuration, services, filterQuery); - }) + criteria.map(c => evaluateCondtion(c, nodeType, source.configuration, services, filterQuery)) ); const invenotryItems = Object.keys(results[0]); - for (const item of invenotryItems) { const alertInstance = services.alertInstanceFactory(`${alertId}-${item}`); // AND logic; all criteria must be across the threshold @@ -59,7 +58,9 @@ export const createInventoryMetricThresholdExecutor = ( alertInstance.scheduleActions(FIRED_ACTIONS.id, { group: item, item, - valueOf: mapToConditionsLookup(results, result => result[item].currentValue), + valueOf: mapToConditionsLookup(results, result => + formatMetric(result[item].metric, result[item].currentValue) + ), thresholdOf: mapToConditionsLookup(criteria, c => c.threshold), metricOf: mapToConditionsLookup(criteria, c => c.metric), }); @@ -109,6 +110,7 @@ const evaluateCondtion = async ( return mapValues(currentValues, value => ({ shouldFire: value !== undefined && value !== null && comparisonFunction(value, threshold), + metric, currentValue: value, isNoData: value === null, isError: value === undefined, @@ -189,3 +191,16 @@ const converters: Record number> = { cpu: n => Number(n) / 100, memory: n => Number(n) / 100, }; + +const formatMetric = (metric: SnapshotMetricType, value: number) => { + // if (SnapshotCustomMetricInputRT.is(metric)) { + // const formatter = createFormatterForMetric(metric); + // return formatter(val); + // } + const metricFormatter = get(METRIC_FORMATTERS, metric, METRIC_FORMATTERS.count); + if (value == null) { + return ''; + } + const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); + return formatter(value); +}; From 591e8226bdf22e74f16aea7bd81b82b0e46b5473 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 27 Apr 2020 13:46:15 -0500 Subject: [PATCH 06/17] Style changes --- .../alerting/inventory/expression.tsx | 1 + .../components/alerting/inventory/metric.tsx | 3 - .../alerting/inventory/node_type.tsx | 2 +- .../components/waffle/node_context_menu.tsx | 317 +++++++++--------- 4 files changed, 164 insertions(+), 159 deletions(-) diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx index 459cad718ec4ca..164521cdd7d921 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -284,6 +284,7 @@ export const Expressions: React.FC = props => {
)} + ); }; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx index 92f258106728b7..d8bc0054feb099 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx @@ -19,9 +19,6 @@ import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; import { SnapshotMetricType } from '../../../../common/inventory_models/types'; -// import { builtInAggregationTypes } from '../constants'; -// import { AggregationType } from '../types'; -// import { IErrorObject } from '../../types'; interface Props { metric?: { value: SnapshotMetricType; text: string }; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx index 1acec55b211ca3..ba63a8568baf17 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx @@ -42,7 +42,7 @@ export const NodeTypeExpression = ({ = ({ - options, - currentTime, - children, - node, - isPopoverOpen, - closePopover, - nodeType, - popoverPosition, -}) => { - const [flyoutVisible, setFlyoutVisible] = useState(false); - const inventoryModel = findInventoryModel(nodeType); - const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; - const uiCapabilities = useKibana().services.application?.capabilities; - // Due to the changing nature of the fields between APM and this UI, - // We need to have some exceptions until 7.0 & ECS is finalized. Reference - // #26620 for the details for these fields. - // TODO: This is tech debt, remove it after 7.0 & ECS migration. - const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id; +export const NodeContextMenu: React.FC = withTheme( + ({ + options, + currentTime, + children, + node, + isPopoverOpen, + closePopover, + nodeType, + popoverPosition, + theme, + }) => { + const [flyoutVisible, setFlyoutVisible] = useState(false); + const inventoryModel = findInventoryModel(nodeType); + const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; + const uiCapabilities = useKibana().services.application?.capabilities; + // Due to the changing nature of the fields between APM and this UI, + // We need to have some exceptions until 7.0 & ECS is finalized. Reference + // #26620 for the details for these fields. + // TODO: This is tech debt, remove it after 7.0 & ECS migration. + const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id; - const showDetail = inventoryModel.crosslinkSupport.details; - const showLogsLink = - inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show; - const showAPMTraceLink = - inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show; - const showUptimeLink = - inventoryModel.crosslinkSupport.uptime && (['pod', 'container'].includes(nodeType) || node.ip); + const showDetail = inventoryModel.crosslinkSupport.details; + const showLogsLink = + inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show; + const showAPMTraceLink = + inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show; + const showUptimeLink = + inventoryModel.crosslinkSupport.uptime && + (['pod', 'container'].includes(nodeType) || node.ip); - const inventoryId = useMemo(() => { - if (nodeType === 'host') { - if (node.ip) { - return { label: host.ip, value: node.ip }; + const inventoryId = useMemo(() => { + if (nodeType === 'host') { + if (node.ip) { + return { label: host.ip, value: node.ip }; + } + } else { + if (options.fields) { + const { id } = findInventoryFields(nodeType, options.fields); // TODO: This will give me what I need to be able to filter alerts + return { + label: {id}, + value: node.id, + }; + } } - } else { - if (options.fields) { - const { id } = findInventoryFields(nodeType, options.fields); // TODO: This will give me what I need to be able to filter alerts - return { - label: {id}, - value: node.id, - }; - } - } - return { label: '', value: '' }; - }, [nodeType, node.ip, node.id, options.fields]); + return { label: '', value: '' }; + }, [nodeType, node.ip, node.id, options.fields]); - const nodeLogsMenuItemLinkProps = useLinkProps({ - app: 'logs', - ...getNodeLogsUrl({ - nodeType, - nodeId: node.id, - time: currentTime, - }), - }); - const nodeDetailMenuItemLinkProps = useLinkProps({ - ...getNodeDetailUrl({ - nodeType, - nodeId: node.id, - from: nodeDetailFrom, - to: currentTime, - }), - }); - const apmTracesMenuItemLinkProps = useLinkProps({ - app: 'apm', - hash: 'traces', - search: { - kuery: `${apmField}:"${node.id}"`, - }, - }); - const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node)); + const nodeLogsMenuItemLinkProps = useLinkProps({ + app: 'logs', + ...getNodeLogsUrl({ + nodeType, + nodeId: node.id, + time: currentTime, + }), + }); + const nodeDetailMenuItemLinkProps = useLinkProps({ + ...getNodeDetailUrl({ + nodeType, + nodeId: node.id, + from: nodeDetailFrom, + to: currentTime, + }), + }); + const apmTracesMenuItemLinkProps = useLinkProps({ + app: 'apm', + hash: 'traces', + search: { + kuery: `${apmField}:"${node.id}"`, + }, + }); + const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node)); - const nodeLogsMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { - defaultMessage: '{inventoryName} logs', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...nodeLogsMenuItemLinkProps, - 'data-test-subj': 'viewLogsContextMenuItem', - isDisabled: !showLogsLink, - }; + const nodeLogsMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { + defaultMessage: '{inventoryName} logs', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...nodeLogsMenuItemLinkProps, + 'data-test-subj': 'viewLogsContextMenuItem', + isDisabled: !showLogsLink, + }; - const nodeDetailMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { - defaultMessage: '{inventoryName} metrics', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...nodeDetailMenuItemLinkProps, - isDisabled: !showDetail, - }; + const nodeDetailMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { + defaultMessage: '{inventoryName} metrics', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...nodeDetailMenuItemLinkProps, + isDisabled: !showDetail, + }; - const apmTracesMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { - defaultMessage: '{inventoryName} APM traces', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...apmTracesMenuItemLinkProps, - 'data-test-subj': 'viewApmTracesContextMenuItem', - isDisabled: !showAPMTraceLink, - }; + const apmTracesMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { + defaultMessage: '{inventoryName} APM traces', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...apmTracesMenuItemLinkProps, + 'data-test-subj': 'viewApmTracesContextMenuItem', + isDisabled: !showAPMTraceLink, + }; - const uptimeMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { - defaultMessage: '{inventoryName} in Uptime', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...uptimeMenuItemLinkProps, - isDisabled: !showUptimeLink, - }; + const uptimeMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { + defaultMessage: '{inventoryName} in Uptime', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...uptimeMenuItemLinkProps, + isDisabled: !showUptimeLink, + }; - const createAlertMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.createAlertLink', { - defaultMessage: 'Create alert', - }), - onClick: () => { - setFlyoutVisible(true); - }, - }; + const createAlertMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.createAlertLink', { + defaultMessage: 'Create alert', + }), + style: { color: theme?.eui.euiLinkColor || '#006BB4', fontWeight: 500, padding: 0 }, + onClick: () => { + setFlyoutVisible(true); + }, + }; - return ( - <> - -
-
- - - - {inventoryId.label && ( - -
- -
-
- )} - - - - - - - -
-
-
- - - ); -}; + return ( + <> + +
+
+ + + + {inventoryId.label && ( + +
+ +
+
+ )} + + + + + + + +
+
+
+ + + ); + } +); From 8a009bc14fdb0ada3c0e2793a41f51da4686680d Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 27 Apr 2020 13:51:55 -0500 Subject: [PATCH 07/17] Remove unused files --- .../lib/create_inventory_metric_formatter.ts | 1 - .../metric_threshold_executor.test.ts | 306 ------------------ .../inventory_metric_threshold/test_mocks.ts | 129 -------- 3 files changed, 436 deletions(-) delete mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/metric_threshold_executor.test.ts delete mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/test_mocks.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts index fac2398b791867..f8c7a10f12831d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts @@ -5,7 +5,6 @@ */ import { get } from 'lodash'; -// import { createFormatter } from '../../../../utils/formatters'; import { InfraFormatterType } from '../../../../lib/lib'; import { SnapshotMetricInput, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/metric_threshold_executor.test.ts deleted file mode 100644 index b081ab68bf61a9..00000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/metric_threshold_executor.test.ts +++ /dev/null @@ -1,306 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -// /* -// * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// * or more contributor license agreements. Licensed under the Elastic License; -// * you may not use this file except in compliance with the Elastic License. -// */ - -// import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; -// import { Comparator, AlertStates } from './types'; -// import * as mocks from './test_mocks'; -// import { AlertExecutorOptions } from '../../../../../alerting/server'; -// import { -// alertsMock, -// AlertServicesMock, -// AlertInstanceMock, -// } from '../../../../../alerting/server/mocks'; - -// const executor = createMetricThresholdExecutor('test') as (opts: { -// params: AlertExecutorOptions['params']; -// services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; -// }) => Promise; - -// const services: AlertServicesMock = alertsMock.createAlertServices(); -// services.callCluster.mockImplementation((_: string, { body, index }: any) => { -// if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; -// const metric = body.query.bool.filter[1]?.exists.field; -// if (body.aggs.groupings) { -// if (body.aggs.groupings.composite.after) { -// return mocks.compositeEndResponse; -// } -// if (metric === 'test.metric.2') { -// return mocks.alternateCompositeResponse; -// } -// return mocks.basicCompositeResponse; -// } -// if (metric === 'test.metric.2') { -// return mocks.alternateMetricResponse; -// } -// return mocks.basicMetricResponse; -// }); -// services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { -// if (sourceId === 'alternate') -// return { -// id: 'alternate', -// attributes: { metricAlias: 'alternatebeat-*' }, -// type, -// references: [], -// }; -// return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; -// }); - -// interface AlertTestInstance { -// instance: AlertInstanceMock; -// actionQueue: any[]; -// state: any; -// } -// const alertInstances = new Map(); -// services.alertInstanceFactory.mockImplementation((instanceID: string) => { -// const alertInstance: AlertTestInstance = { -// instance: alertsMock.createAlertInstanceFactory(), -// actionQueue: [], -// state: {}, -// }; -// alertInstances.set(instanceID, alertInstance); -// alertInstance.instance.replaceState.mockImplementation((newState: any) => { -// alertInstance.state = newState; -// return alertInstance.instance; -// }); -// alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { -// alertInstance.actionQueue.push({ id, action }); -// return alertInstance.instance; -// }); -// return alertInstance.instance; -// }); - -// function mostRecentAction(id: string) { -// return alertInstances.get(id)!.actionQueue.pop(); -// } - -// function getState(id: string) { -// return alertInstances.get(id)!.state; -// } - -// const baseCriterion = { -// aggType: 'avg', -// metric: 'test.metric.1', -// timeSize: 1, -// timeUnit: 'm', -// }; -// describe('The metric threshold alert type', () => { -// describe('querying the entire infrastructure', () => { -// const instanceID = 'test-*'; -// const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => -// executor({ -// services, -// params: { -// sourceId, -// criteria: [ -// { -// ...baseCriterion, -// comparator, -// threshold, -// }, -// ], -// }, -// }); -// test('alerts as expected with the > comparator', async () => { -// await execute(Comparator.GT, [0.75]); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// await execute(Comparator.GT, [1.5]); -// expect(mostRecentAction(instanceID)).toBe(undefined); -// expect(getState(instanceID).alertState).toBe(AlertStates.OK); -// }); -// test('alerts as expected with the < comparator', async () => { -// await execute(Comparator.LT, [1.5]); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// await execute(Comparator.LT, [0.75]); -// expect(mostRecentAction(instanceID)).toBe(undefined); -// expect(getState(instanceID).alertState).toBe(AlertStates.OK); -// }); -// test('alerts as expected with the >= comparator', async () => { -// await execute(Comparator.GT_OR_EQ, [0.75]); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// await execute(Comparator.GT_OR_EQ, [1.0]); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// await execute(Comparator.GT_OR_EQ, [1.5]); -// expect(mostRecentAction(instanceID)).toBe(undefined); -// expect(getState(instanceID).alertState).toBe(AlertStates.OK); -// }); -// test('alerts as expected with the <= comparator', async () => { -// await execute(Comparator.LT_OR_EQ, [1.5]); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// await execute(Comparator.LT_OR_EQ, [1.0]); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// await execute(Comparator.LT_OR_EQ, [0.75]); -// expect(mostRecentAction(instanceID)).toBe(undefined); -// expect(getState(instanceID).alertState).toBe(AlertStates.OK); -// }); -// test('alerts as expected with the between comparator', async () => { -// await execute(Comparator.BETWEEN, [0, 1.5]); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// await execute(Comparator.BETWEEN, [0, 0.75]); -// expect(mostRecentAction(instanceID)).toBe(undefined); -// expect(getState(instanceID).alertState).toBe(AlertStates.OK); -// }); -// test('reports expected values to the action context', async () => { -// await execute(Comparator.GT, [0.75]); -// const { action } = mostRecentAction(instanceID); -// expect(action.group).toBe('*'); -// expect(action.valueOf.condition0).toBe(1); -// expect(action.thresholdOf.condition0).toStrictEqual([0.75]); -// expect(action.metricOf.condition0).toBe('test.metric.1'); -// }); -// test('fetches the index pattern dynamically', async () => { -// await execute(Comparator.LT, [17], 'alternate'); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// await execute(Comparator.LT, [1.5], 'alternate'); -// expect(mostRecentAction(instanceID)).toBe(undefined); -// expect(getState(instanceID).alertState).toBe(AlertStates.OK); -// }); -// }); - -// describe('querying with a groupBy parameter', () => { -// const execute = (comparator: Comparator, threshold: number[]) => -// executor({ -// services, -// params: { -// groupBy: 'something', -// criteria: [ -// { -// ...baseCriterion, -// comparator, -// threshold, -// }, -// ], -// }, -// }); -// const instanceIdA = 'test-a'; -// const instanceIdB = 'test-b'; -// test('sends an alert when all groups pass the threshold', async () => { -// await execute(Comparator.GT, [0.75]); -// expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); -// expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceIdB).alertState).toBe(AlertStates.ALERT); -// }); -// test('sends an alert when only some groups pass the threshold', async () => { -// await execute(Comparator.LT, [1.5]); -// expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); -// expect(mostRecentAction(instanceIdB)).toBe(undefined); -// expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); -// }); -// test('sends no alert when no groups pass the threshold', async () => { -// await execute(Comparator.GT, [5]); -// expect(mostRecentAction(instanceIdA)).toBe(undefined); -// expect(getState(instanceIdA).alertState).toBe(AlertStates.OK); -// expect(mostRecentAction(instanceIdB)).toBe(undefined); -// expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); -// }); -// test('reports group values to the action context', async () => { -// await execute(Comparator.GT, [0.75]); -// expect(mostRecentAction(instanceIdA).action.group).toBe('a'); -// expect(mostRecentAction(instanceIdB).action.group).toBe('b'); -// }); -// }); - -// describe('querying with multiple criteria', () => { -// const execute = ( -// comparator: Comparator, -// thresholdA: number[], -// thresholdB: number[], -// groupBy: string = '' -// ) => -// executor({ -// services, -// params: { -// groupBy, -// criteria: [ -// { -// ...baseCriterion, -// comparator, -// threshold: thresholdA, -// }, -// { -// ...baseCriterion, -// comparator, -// threshold: thresholdB, -// metric: 'test.metric.2', -// }, -// ], -// }, -// }); -// test('sends an alert when all criteria cross the threshold', async () => { -// const instanceID = 'test-*'; -// await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// }); -// test('sends no alert when some, but not all, criteria cross the threshold', async () => { -// const instanceID = 'test-*'; -// await execute(Comparator.LT_OR_EQ, [1.0], [3.0]); -// expect(mostRecentAction(instanceID)).toBe(undefined); -// expect(getState(instanceID).alertState).toBe(AlertStates.OK); -// }); -// test('alerts only on groups that meet all criteria when querying with a groupBy parameter', async () => { -// const instanceIdA = 'test-a'; -// const instanceIdB = 'test-b'; -// await execute(Comparator.GT_OR_EQ, [1.0], [3.0], 'something'); -// expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); -// expect(mostRecentAction(instanceIdB)).toBe(undefined); -// expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); -// }); -// test('sends all criteria to the action context', async () => { -// const instanceID = 'test-*'; -// await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); -// const { action } = mostRecentAction(instanceID); -// expect(action.valueOf.condition0).toBe(1); -// expect(action.valueOf.condition1).toBe(3.5); -// expect(action.thresholdOf.condition0).toStrictEqual([1.0]); -// expect(action.thresholdOf.condition1).toStrictEqual([3.0]); -// expect(action.metricOf.condition0).toBe('test.metric.1'); -// expect(action.metricOf.condition1).toBe('test.metric.2'); -// }); -// }); -// describe('querying with the count aggregator', () => { -// const instanceID = 'test-*'; -// const execute = (comparator: Comparator, threshold: number[]) => -// executor({ -// services, -// params: { -// criteria: [ -// { -// ...baseCriterion, -// comparator, -// threshold, -// aggType: 'count', -// metric: undefined, -// }, -// ], -// }, -// }); -// test('alerts based on the doc_count value instead of the aggregatedValue', async () => { -// await execute(Comparator.GT, [2]); -// expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); -// expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); -// await execute(Comparator.LT, [1.5]); -// expect(mostRecentAction(instanceID)).toBe(undefined); -// expect(getState(instanceID).alertState).toBe(AlertStates.OK); -// }); -// }); -// }); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/test_mocks.ts deleted file mode 100644 index 66e0a363c89833..00000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/test_mocks.ts +++ /dev/null @@ -1,129 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -const bucketsA = [ - { - doc_count: 2, - aggregatedValue: { value: 0.5 }, - }, - { - doc_count: 3, - aggregatedValue: { value: 1.0 }, - }, -]; - -const bucketsB = [ - { - doc_count: 4, - aggregatedValue: { value: 2.5 }, - }, - { - doc_count: 5, - aggregatedValue: { value: 3.5 }, - }, -]; - -const bucketsC = [ - { - doc_count: 2, - aggregatedValue: { value: 0.5 }, - }, - { - doc_count: 3, - aggregatedValue: { value: 16.0 }, - }, -]; - -export const basicMetricResponse = { - aggregations: { - aggregatedIntervals: { - buckets: bucketsA, - }, - }, -}; - -export const alternateMetricResponse = { - aggregations: { - aggregatedIntervals: { - buckets: bucketsB, - }, - }, -}; - -export const basicCompositeResponse = { - aggregations: { - groupings: { - after_key: 'foo', - buckets: [ - { - key: { - groupBy: 'a', - }, - aggregatedIntervals: { - buckets: bucketsA, - }, - }, - { - key: { - groupBy: 'b', - }, - aggregatedIntervals: { - buckets: bucketsB, - }, - }, - ], - }, - }, - hits: { - total: { - value: 2, - }, - }, -}; - -export const alternateCompositeResponse = { - aggregations: { - groupings: { - after_key: 'foo', - buckets: [ - { - key: { - groupBy: 'a', - }, - aggregatedIntervals: { - buckets: bucketsB, - }, - }, - { - key: { - groupBy: 'b', - }, - aggregatedIntervals: { - buckets: bucketsA, - }, - }, - ], - }, - }, - hits: { - total: { - value: 2, - }, - }, -}; - -export const compositeEndResponse = { - aggregations: {}, - hits: { total: { value: 0 } }, -}; - -export const changedSourceIdResponse = { - aggregations: { - aggregatedIntervals: { - buckets: bucketsC, - }, - }, -}; From 4416d7040f93fda9e2f7858d94296c35edc85165 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 27 Apr 2020 18:41:33 -0500 Subject: [PATCH 08/17] fix test --- .../infra/public/components/alerting/inventory/metric.tsx | 2 +- .../public/components/alerting/inventory/node_type.tsx | 2 +- x-pack/plugins/infra/server/graphql/sources/resolvers.ts | 6 +++--- x-pack/plugins/infra/server/lib/sources/sources.ts | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx index d8bc0054feb099..faafdf1b81eeda 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx @@ -136,7 +136,7 @@ export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitle iconType="cross" color="danger" aria-label={i18n.translate( - 'xpack.triggersActionsUI.common.expressionItems.components.closablePopoverTitle.closeLabel', + 'xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel', { defaultMessage: 'Close', } diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx index ba63a8568baf17..1623fc4e24dcb7 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx @@ -101,7 +101,7 @@ export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitle iconType="cross" color="danger" aria-label={i18n.translate( - 'xpack.triggersActionsUI.common.expressionItems.components.closablePopoverTitle.closeLabel', + 'xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel', { defaultMessage: 'Close', } diff --git a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts index da8a1099d45a99..cffab4ba4f6f00 100644 --- a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts +++ b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts @@ -133,7 +133,7 @@ export const createSourcesResolvers = ( Mutation: { async createSource(root, args, { req }) { const sourceConfiguration = await libs.sources.createSourceConfiguration( - req, + req.core.savedObjects.client, args.id, compactObject({ ...args.sourceProperties, @@ -149,7 +149,7 @@ export const createSourcesResolvers = ( }; }, async deleteSource(root, args, { req }) { - await libs.sources.deleteSourceConfiguration(req, args.id); + await libs.sources.deleteSourceConfiguration(req.core.savedObjects.client, args.id); return { id: args.id, @@ -157,7 +157,7 @@ export const createSourcesResolvers = ( }, async updateSource(root, args, { req }) { const updatedSourceConfiguration = await libs.sources.updateSourceConfiguration( - req, + req.core.savedObjects.client, args.id, compactObject({ ...args.sourceProperties, diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 3ad39a83bd23fd..821267772e34d2 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -41,7 +41,6 @@ export class InfraSources { sourceId: string ): Promise { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId) .then(internalSourceConfiguration => ({ id: sourceId, From 3de0333398f5f97e5e5517d8d374ff1e6a1a31b0 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 27 Apr 2020 19:09:05 -0500 Subject: [PATCH 09/17] Update create --- x-pack/plugins/infra/server/lib/sources/sources.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 821267772e34d2..0aa705ec501ade 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -95,7 +95,7 @@ export class InfraSources { } public async createSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string, source: InfraSavedSourceConfiguration ) { @@ -107,7 +107,7 @@ export class InfraSources { ); const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await requestContext.core.savedObjects.client.create( + await savedObjectsClient.create( infraSourceConfigurationSavedObjectType, pickSavedSourceConfiguration(newSourceConfiguration) as any, { id: sourceId } From 6553a6d20ca992474e03a97f5f28b7b3fabb6572 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 27 Apr 2020 20:37:25 -0500 Subject: [PATCH 10/17] Fix signature --- x-pack/plugins/infra/server/lib/sources/sources.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 0aa705ec501ade..fcd837fdd9984c 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -123,11 +123,11 @@ export class InfraSources { }; } - public async deleteSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { - await requestContext.core.savedObjects.client.delete( - infraSourceConfigurationSavedObjectType, - sourceId - ); + public async deleteSourceConfiguration( + savedObjectsClient: SavedObjectsClientContract, + sourceId: string + ) { + await savedObjectsClient.delete(infraSourceConfigurationSavedObjectType, sourceId); } public async updateSourceConfiguration( From 3a64470bc84b3d53b1025458b46e274db314955a Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Tue, 28 Apr 2020 06:41:27 -0500 Subject: [PATCH 11/17] Remove old test. Remove unecessary import --- .../metric_threshold/metric_threshold_executor.test.ts | 8 -------- x-pack/plugins/infra/server/lib/sources/sources.ts | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 4bbcebf59e545c..1f7892f12f3832 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -99,14 +99,6 @@ describe('The metric threshold alert type', () => { expect(action.thresholdOf.condition0).toStrictEqual([0.75]); expect(action.metricOf.condition0).toBe('test.metric.1'); }); - test('fetches the index pattern dynamically', async () => { - await execute(Comparator.LT, [17], 'alternate'); - expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); - await execute(Comparator.LT, [1.5], 'alternate'); - expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); - }); }); describe('querying with a groupBy parameter', () => { diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index fcd837fdd9984c..71682c9e798a6f 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -9,7 +9,7 @@ import { failure } from 'io-ts/lib/PathReporter'; import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; -import { RequestHandlerContext, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; From 744060968226b5c866703510b5c92b8af649dba7 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Tue, 28 Apr 2020 14:34:16 -0500 Subject: [PATCH 12/17] Pass in filter when clicking create alert from context menu --- .../alerting/inventory/alert_flyout.tsx | 3 ++- .../components/alerting/inventory/expression.tsx | 15 ++++++--------- .../components/waffle/node_context_menu.tsx | 7 ++++++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx index f86ebff3452d0c..83298afd4fc5a5 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx @@ -17,6 +17,7 @@ interface Props { visible?: boolean; options?: Partial; nodeType?: InventoryItemType; + filter?: string; setVisible: React.Dispatch>; } @@ -29,7 +30,7 @@ export const AlertFlyout = (props: Props) => { {triggersActionsUI && ( ; nodeType?: InventoryItemType; + filter?: string; } interface Props { @@ -188,16 +189,12 @@ export const Expressions: React.FC = props => { } } - // if (md.currentOptions) { - // if (md.currentOptions.filterQuery) { - // setAlertParams('filterQuery', md.currentOptions.filterQuery); - // } else if (md.currentOptions.groupBy && md.series) { - // const filter = `${md.currentOptions.groupBy}: "${md.series.id}"`; - // setAlertParams('filterQuery', filter); - // } + if (!alertParams.filterQuery) { + if (md && md.filter) { + setAlertParams('filterQuery', md.filter); + } + } - // setAlertParams('groupBy', md.currentOptions.groupBy); - // } if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id); } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 8a4a3be5f56021..d576f08108649f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -77,7 +77,7 @@ export const NodeContextMenu: React.FC = withTheme } } else { if (options.fields) { - const { id } = findInventoryFields(nodeType, options.fields); // TODO: This will give me what I need to be able to filter alerts + const { id } = findInventoryFields(nodeType, options.fields); return { label: {id}, value: node.id, @@ -200,6 +200,11 @@ export const NodeContextMenu: React.FC = withTheme
Date: Tue, 28 Apr 2020 18:55:48 -0500 Subject: [PATCH 13/17] Fix filtering --- .../alerting/inventory/expression.tsx | 72 ++++++++++++------- .../alerting/metrics/expression.tsx | 36 +++++++--- .../containers/source/use_source_via_http.ts | 17 +++-- .../metrics_explorer/components/kuery_bar.tsx | 5 ++ .../inventory_metric_threshold_executor.ts | 10 ++- ...r_inventory_metric_threshold_alert_type.ts | 16 +++-- .../register_metric_threshold_alert_type.ts | 15 ++-- 7 files changed, 116 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx index a5d4f0e7ebf720..15cad770836bda 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useEffect, useState } from 'react'; +import React, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -13,6 +13,7 @@ import { EuiText, EuiFormRow, EuiButtonEmpty, + EuiFieldSearch, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -48,6 +49,7 @@ import { InventoryMetricConditions } from '../../../../server/lib/alerting/inven import { MetricExpression } from './metric'; import { NodeTypeExpression } from './node_type'; import { InfraWaffleMapOptions } from '../../../lib/lib'; +import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; interface AlertContextMeta { options?: Partial; @@ -62,6 +64,7 @@ interface Props { nodeType: InventoryItemType; groupBy?: string; filterQuery?: string; + filterQueryText?: string; sourceId?: string; }; alertsContext: AlertsContextValue; @@ -120,11 +123,15 @@ export const Expressions: React.FC = props => { [setAlertParams, alertParams.criteria] ); - const onFilterQuerySubmit = useCallback( + const onFilterChange = useCallback( (filter: any) => { - setAlertParams('filterQuery', filter); + setAlertParams('filterQueryText', filter || ''); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); }, - [setAlertParams] + [derivedIndexPattern, setAlertParams] ); const emptyError = useMemo(() => { @@ -166,6 +173,11 @@ export const Expressions: React.FC = props => { [setAlertParams] ); + const handleFieldSearchChange = useCallback( + (e: ChangeEvent) => onFilterChange(e.target.value), + [onFilterChange] + ); + useEffect(() => { const md = alertsContext.metadata; if (!alertParams.nodeType) { @@ -191,14 +203,18 @@ export const Expressions: React.FC = props => { if (!alertParams.filterQuery) { if (md && md.filter) { - setAlertParams('filterQuery', md.filter); + setAlertParams('filterQueryText', md.filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || '' + ); } } if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id); } - }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + }, [alertsContext.metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps return ( <> @@ -261,26 +277,32 @@ export const Expressions: React.FC = props => { - {alertsContext.metadata && ( - <> - + {(alertsContext.metadata && ( + + )) || ( + - - - - )} + /> + )} + + ); diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index d4d53b81109c61..50d81db86bf0a1 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -44,6 +44,7 @@ import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by'; import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; interface AlertContextMeta { currentOptions?: Partial; @@ -57,6 +58,7 @@ interface Props { groupBy?: string; filterQuery?: string; sourceId?: string; + filterQueryText?: string; }; alertsContext: AlertsContextValue; setAlertParams(key: string, value: any): void; @@ -139,11 +141,15 @@ export const Expressions: React.FC = props => { [setAlertParams, alertParams.criteria] ); - const onFilterQuerySubmit = useCallback( + const onFilterChange = useCallback( (filter: any) => { - setAlertParams('filterQuery', filter); + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); }, - [setAlertParams] + [setAlertParams, derivedIndexPattern] ); const onGroupByChange = useCallback( @@ -206,10 +212,19 @@ export const Expressions: React.FC = props => { if (md.currentOptions) { if (md.currentOptions.filterQuery) { - setAlertParams('filterQuery', md.currentOptions.filterQuery); + setAlertParams('filterQueryText', md.currentOptions.filterQuery); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(md.currentOptions.filterQuery, derivedIndexPattern) || + '' + ); } else if (md.currentOptions.groupBy && md.series) { const filter = `${md.currentOptions.groupBy}: "${md.series.id}"`; - setAlertParams('filterQuery', filter); + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); } setAlertParams('groupBy', md.currentOptions.groupBy); @@ -226,8 +241,8 @@ export const Expressions: React.FC = props => { }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps const handleFieldSearchChange = useCallback( - (e: ChangeEvent) => onFilterQuerySubmit(e.target.value), - [onFilterQuerySubmit] + (e: ChangeEvent) => onFilterChange(e.target.value), + [onFilterChange] ); return ( @@ -297,13 +312,14 @@ export const Expressions: React.FC = props => { {(alertsContext.metadata && ( )) || ( )} diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts index bc6374a6538e37..aad54bd2222b77 100644 --- a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts +++ b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useCallback } from 'react'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -69,12 +69,15 @@ export const useSourceViaHttp = ({ })(); }, [makeRequest]); - const createDerivedIndexPattern = (indexType: 'logs' | 'metrics' | 'both' = type) => { - return { - fields: response?.source ? response.status.indexFields : [], - title: pickIndexPattern(response?.source, indexType), - }; - }; + const createDerivedIndexPattern = useCallback( + (indexType: 'logs' | 'metrics' | 'both' = type) => { + return { + fields: response?.source ? response.status.indexFields : [], + title: pickIndexPattern(response?.source, indexType), + }; + }, + [response, type] + ); const source = useMemo(() => { return response ? { ...response.source, status: response.status } : null; 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 e9826e1ff39552..04661bbc377025 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 @@ -14,6 +14,7 @@ import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data interface Props { derivedIndexPattern: IIndexPattern; onSubmit: (query: string) => void; + onChange?: (query: string) => void; value?: string | null; placeholder?: string; } @@ -30,6 +31,7 @@ function validateQuery(query: string) { export const MetricsExplorerKueryBar = ({ derivedIndexPattern, onSubmit, + onChange, value, placeholder, }: Props) => { @@ -46,6 +48,9 @@ export const MetricsExplorerKueryBar = ({ const handleChange = (query: string) => { setValidation(validateQuery(query)); setDraftQuery(query); + if (onChange) { + onChange(query); + } }; const filteredDerivedIndexPattern = { diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 1e5fd4cb31d716..cc8a35f6e47a15 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -5,6 +5,7 @@ */ import { mapValues, last, get } from 'lodash'; import { i18n } from '@kbn/i18n'; +import moment from 'moment'; import { InfraDatabaseSearchResponse, CallWithRequestParams, @@ -99,7 +100,14 @@ const evaluateCondtion = async ( services, nodeType, metric, - { to: Date.now(), from: Date.now(), interval: condition.timeUnit }, + { + to: Date.now(), + from: moment() + .subtract(condition.timeSize, condition.timeUnit) + .toDate() + .getTime(), + interval: condition.timeUnit, + }, sourceConfiguration, filterQuery ); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index 6d057a612fc632..3b6a1b5557bc69 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -22,6 +22,7 @@ const condition = schema.object({ schema.literal('>='), schema.literal('<='), schema.literal('between'), + schema.literal('outside'), ]), timeUnit: schema.string(), timeSize: schema.number(), @@ -32,12 +33,15 @@ export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, name: 'Inventory', validate: { - params: schema.object({ - criteria: schema.arrayOf(condition), - nodeType: schema.string(), - filterQuery: schema.maybe(schema.string()), - sourceId: schema.string(), - }), + params: schema.object( + { + criteria: schema.arrayOf(condition), + nodeType: schema.string(), + filterQuery: schema.maybe(schema.string()), + sourceId: schema.string(), + }, + { unknowns: 'allow' } + ), }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index d2b85366304269..7291eb03fb2b4b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -73,12 +73,15 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: 'Metric threshold', validate: { - params: schema.object({ - criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])), - groupBy: schema.maybe(schema.string()), - filterQuery: schema.maybe(schema.string()), - sourceId: schema.string(), - }), + params: schema.object( + { + criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])), + groupBy: schema.maybe(schema.string()), + filterQuery: schema.maybe(schema.string()), + sourceId: schema.string(), + }, + { unknowns: 'allow' } + ), }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], From 698860c6c55b21ebfa160dab6e6814291a6cb260 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Tue, 28 Apr 2020 19:05:04 -0500 Subject: [PATCH 14/17] Fix more types --- x-pack/plugins/infra/common/formatters/bytes.ts | 7 +------ .../infra/server/routes/log_sources/configuration.ts | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/infra/common/formatters/bytes.ts b/x-pack/plugins/infra/common/formatters/bytes.ts index 2dbbaf9d342339..5aa5281606ed2e 100644 --- a/x-pack/plugins/infra/common/formatters/bytes.ts +++ b/x-pack/plugins/infra/common/formatters/bytes.ts @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { formatNumber } from './number'; - -export enum InfraWaffleMapDataFormat { - bytesDecimal = 'bytesDecimal', - bitsDecimal = 'bitsDecimal', - abbreviatedNumber = 'abbreviatedNumber', -} +import { InfraWaffleMapDataFormat } from '.'; /** * The labels are derived from these two Wikipedia articles. diff --git a/x-pack/plugins/infra/server/routes/log_sources/configuration.ts b/x-pack/plugins/infra/server/routes/log_sources/configuration.ts index 0ce594675773c2..46929954431f51 100644 --- a/x-pack/plugins/infra/server/routes/log_sources/configuration.ts +++ b/x-pack/plugins/infra/server/routes/log_sources/configuration.ts @@ -82,12 +82,12 @@ export const initLogSourceConfigurationRoutes = ({ framework, sources }: InfraBa const sourceConfigurationExists = sourceConfiguration.origin === 'stored'; const patchedSourceConfiguration = await (sourceConfigurationExists ? sources.updateSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId, patchedSourceConfigurationProperties ) : sources.createSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId, patchedSourceConfigurationProperties )); From 42da9948f12c39251914cba7bdf754fc22e613e2 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Tue, 28 Apr 2020 22:54:50 -0500 Subject: [PATCH 15/17] Fix tests --- x-pack/plugins/infra/common/formatters/bytes.test.ts | 2 +- x-pack/plugins/infra/common/formatters/bytes.ts | 2 +- x-pack/plugins/infra/common/formatters/index.ts | 7 +------ x-pack/plugins/infra/common/formatters/types.ts | 11 +++++++++++ 4 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/infra/common/formatters/types.ts diff --git a/x-pack/plugins/infra/common/formatters/bytes.test.ts b/x-pack/plugins/infra/common/formatters/bytes.test.ts index 78cb7c618cd7f4..ccdeed120acca1 100644 --- a/x-pack/plugins/infra/common/formatters/bytes.test.ts +++ b/x-pack/plugins/infra/common/formatters/bytes.test.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { InfraWaffleMapDataFormat } from './types'; import { createBytesFormatter } from './bytes'; -import { InfraWaffleMapDataFormat } from '.'; describe('createDataFormatter', () => { it('should format bytes as bytesDecimal', () => { diff --git a/x-pack/plugins/infra/common/formatters/bytes.ts b/x-pack/plugins/infra/common/formatters/bytes.ts index 5aa5281606ed2e..3a45caa8b5e150 100644 --- a/x-pack/plugins/infra/common/formatters/bytes.ts +++ b/x-pack/plugins/infra/common/formatters/bytes.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { formatNumber } from './number'; -import { InfraWaffleMapDataFormat } from '.'; +import { InfraWaffleMapDataFormat } from './types'; /** * The labels are derived from these two Wikipedia articles. diff --git a/x-pack/plugins/infra/common/formatters/index.ts b/x-pack/plugins/infra/common/formatters/index.ts index 1f6a91e39c20fb..096085696bd6bf 100644 --- a/x-pack/plugins/infra/common/formatters/index.ts +++ b/x-pack/plugins/infra/common/formatters/index.ts @@ -10,12 +10,7 @@ import { formatNumber } from './number'; import { formatPercent } from './percent'; import { InventoryFormatterType } from '../inventory_models/types'; import { formatHighPercision } from './high_precision'; - -export enum InfraWaffleMapDataFormat { - bytesDecimal = 'bytesDecimal', - bitsDecimal = 'bitsDecimal', - abbreviatedNumber = 'abbreviatedNumber', -} +import { InfraWaffleMapDataFormat } from './types'; export const FORMATTERS = { number: formatNumber, diff --git a/x-pack/plugins/infra/common/formatters/types.ts b/x-pack/plugins/infra/common/formatters/types.ts new file mode 100644 index 00000000000000..c438ec2d4205d1 --- /dev/null +++ b/x-pack/plugins/infra/common/formatters/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum InfraWaffleMapDataFormat { + bytesDecimal = 'bytesDecimal', + bitsDecimal = 'bitsDecimal', + abbreviatedNumber = 'abbreviatedNumber', +} From 34d1f0ba9d5511ed6331524e248943c854dbe4c2 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Wed, 29 Apr 2020 12:11:36 -0500 Subject: [PATCH 16/17] Fix merge --- x-pack/plugins/infra/public/plugin.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 2d6bab124104d6..5c568caa7badf2 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -23,10 +23,7 @@ import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; import { getAlertType as getMetricsAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type'; - import { getInventoryMetricAlertType } from './components/alerting/inventory/metric_inventory_threshold_alert_type'; -import { getAlertType as getMetricsAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; -import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type'; export type ClientSetup = void; export type ClientStart = void; From bc7fa672cbea20298db43d3cf200fc03f35cc155 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Thu, 30 Apr 2020 14:52:48 -0500 Subject: [PATCH 17/17] Fix merge --- .../alerting/metric_threshold/metric_threshold_executor.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index e2f7fb9427975e..2531e939792af2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -327,6 +327,8 @@ services.callCluster.mockImplementation(async (_: string, { body, index }: any) } if (metric === 'test.metric.2') { return mocks.alternateMetricResponse; + } else if (metric === 'test.metric.3') { + return mocks.emptyMetricResponse; } return mocks.basicMetricResponse; });