diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer/index.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts similarity index 98% rename from x-pack/plugins/infra/common/http_api/metrics_explorer/index.ts rename to x-pack/plugins/infra/common/http_api/metrics_explorer.ts index 93655f931f45df..eb5b0bdbcfbc50 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_explorer/index.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts @@ -13,6 +13,7 @@ export const METRIC_EXPLORER_AGGREGATIONS = [ 'cardinality', 'rate', 'count', + 'sum', ] as const; type MetricExplorerAggregations = typeof METRIC_EXPLORER_AGGREGATIONS[number]; @@ -54,6 +55,7 @@ export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({ afterKey: rt.union([rt.string, rt.null, rt.undefined]), limit: rt.union([rt.number, rt.null, rt.undefined]), filterQuery: rt.union([rt.string, rt.null, rt.undefined]), + forceInterval: rt.boolean, }); export const metricsExplorerRequestBodyRT = rt.intersection([ diff --git a/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts b/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts index 6eb08eabc15b5d..add6ab0f132b5c 100644 --- a/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts +++ b/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts @@ -35,6 +35,9 @@ export const metricsExplorerViewSavedObjectMappings: { }, options: { properties: { + forceInterval: { + type: 'boolean', + }, metrics: { type: 'nested', properties: { diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx rename to x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx rename to x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx similarity index 59% rename from x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx rename to x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index f2ac34ccb752fd..352ac1927479ea 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -6,9 +6,6 @@ import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react'; import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, EuiSpacer, EuiText, EuiFormRow, @@ -18,40 +15,28 @@ import { EuiIcon, EuiFieldSearch, } from '@elastic/eui'; -import { IFieldType } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { - MetricExpressionParams, Comparator, Aggregators, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../server/lib/alerting/metric_threshold/types'; -import { euiStyled } from '../../../../../observability/public'; import { - WhenExpression, - OfExpression, - 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 { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants'; -// 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 { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by'; import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; - -interface AlertContextMeta { - currentOptions?: Partial; - series?: MetricsExplorerSeries; -} +import { ExpressionRow } from './expression_row'; +import { AlertContextMeta, TimeUnit, MetricExpression } from '../types'; +import { ExpressionChart } from './expression_chart'; interface Props { errors: IErrorObject[]; @@ -67,11 +52,6 @@ interface Props { setAlertProperty(key: string, value: any): void; } -type TimeUnit = 's' | 'm' | 'h' | 'd'; -type MetricExpression = Omit & { - metric?: string; -}; - const defaultExpression = { aggType: Aggregators.AVERAGE, comparator: Comparator.GT, @@ -80,17 +60,6 @@ const defaultExpression = { timeUnit: 'm', } as MetricExpression; -const customComparators = { - ...builtInComparators, - [Comparator.OUTSIDE_RANGE]: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', { - defaultMessage: 'Is not between', - }), - value: Comparator.OUTSIDE_RANGE, - requiredValues: 2, - }, -}; - export const Expressions: React.FC = props => { const { setAlertParams, alertParams, errors, alertsContext } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ @@ -101,7 +70,6 @@ export const Expressions: React.FC = props => { }); const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ createDerivedIndexPattern, ]); @@ -127,14 +95,14 @@ export const Expressions: React.FC = props => { ); const addExpression = useCallback(() => { - const exp = alertParams.criteria.slice(); + const exp = alertParams.criteria?.slice() || []; exp.push(defaultExpression); setAlertParams('criteria', exp); }, [setAlertParams, alertParams.criteria]); const removeExpression = useCallback( (id: number) => { - const exp = alertParams.criteria.slice(); + const exp = alertParams.criteria?.slice() || []; if (exp.length > 1) { exp.splice(id, 1); setAlertParams('criteria', exp); @@ -167,10 +135,11 @@ export const Expressions: React.FC = props => { const updateTimeSize = useCallback( (ts: number | undefined) => { - const criteria = alertParams.criteria.map(c => ({ - ...c, - timeSize: ts, - })); + const criteria = + alertParams.criteria?.map(c => ({ + ...c, + timeSize: ts, + })) || []; setTimeSize(ts || undefined); setAlertParams('criteria', criteria); }, @@ -179,10 +148,11 @@ export const Expressions: React.FC = props => { const updateTimeUnit = useCallback( (tu: string) => { - const criteria = alertParams.criteria.map(c => ({ - ...c, - timeUnit: tu, - })); + const criteria = + alertParams.criteria?.map(c => ({ + ...c, + timeUnit: tu, + })) || []; setTimeUnit(tu as TimeUnit); setAlertParams('criteria', criteria); }, @@ -250,7 +220,7 @@ export const Expressions: React.FC = props => { alertParams.criteria.map((e, idx) => { return ( 1} + canDelete={(alertParams.criteria && alertParams.criteria.length > 1) || false} fields={derivedIndexPattern.fields} remove={removeExpression} addExpression={addExpression} @@ -259,17 +229,28 @@ export const Expressions: React.FC = props => { setAlertParams={updateParams} errors={errors[idx] || emptyError} expression={e || {}} - /> + > + + ); })} - +
+ +
= props => { ); }; - -interface ExpressionRowProps { - fields: IFieldType[]; - expressionId: number; - expression: MetricExpression; - errors: IErrorObject; - canDelete: boolean; - addExpression(): void; - remove(id: number): void; - setAlertParams(id: number, params: MetricExpression): 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, fields, canDelete } = props; - const { - aggType = Aggregators.MAX, - metric, - comparator = Comparator.GT, - threshold = [], - } = expression; - - const updateAggType = useCallback( - (at: string) => { - setAlertParams(expressionId, { - ...expression, - aggType: at as MetricExpression['aggType'], - metric: at === 'count' ? undefined : expression.metric, - }); - }, - [expressionId, expression, setAlertParams] - ); - - const updateMetric = useCallback( - (m?: MetricExpression['metric']) => { - setAlertParams(expressionId, { ...expression, metric: m }); - }, - [expressionId, expression, setAlertParams] - ); - - const updateComparator = useCallback( - (c?: string) => { - setAlertParams(expressionId, { ...expression, comparator: c as Comparator }); - }, - [expressionId, expression, setAlertParams] - ); - - const updateThreshold = useCallback( - t => { - if (t.join() !== expression.threshold.join()) { - setAlertParams(expressionId, { ...expression, threshold: t }); - } - }, - [expressionId, expression, setAlertParams] - ); - - return ( - <> - - - - - - - {aggType !== 'count' && ( - - ({ - normalizedType: f.type, - name: f.name, - }))} - aggType={aggType} - errors={errors} - onChangeSelectedAggField={updateMetric} - /> - - )} - - - - - - {canDelete && ( - - remove(expressionId)} - /> - - )} - - - - ); -}; - -export const aggregationType: { [key: string]: any } = { - avg: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { - defaultMessage: 'Average', - }), - fieldRequired: true, - validNormalizedTypes: ['number'], - value: Aggregators.AVERAGE, - }, - max: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.max', { - defaultMessage: 'Max', - }), - fieldRequired: true, - validNormalizedTypes: ['number', 'date'], - value: Aggregators.MAX, - }, - min: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.min', { - defaultMessage: 'Min', - }), - fieldRequired: true, - validNormalizedTypes: ['number', 'date'], - value: Aggregators.MIN, - }, - cardinality: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.cardinality', { - defaultMessage: 'Cardinality', - }), - fieldRequired: false, - value: Aggregators.CARDINALITY, - validNormalizedTypes: ['number'], - }, - rate: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.rate', { - defaultMessage: 'Rate', - }), - fieldRequired: false, - value: Aggregators.RATE, - validNormalizedTypes: ['number'], - }, - count: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.count', { - defaultMessage: 'Document count', - }), - fieldRequired: false, - value: Aggregators.COUNT, - validNormalizedTypes: ['number'], - }, -}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx new file mode 100644 index 00000000000000..a600d59865cccd --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -0,0 +1,302 @@ +/* + * 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, { useMemo, useCallback } from 'react'; +import { + Axis, + Chart, + niceTimeFormatter, + Position, + Settings, + TooltipValue, + RectAnnotation, +} from '@elastic/charts'; +import { first, last } from 'lodash'; +import moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { IIndexPattern } from 'src/plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { InfraSource } from '../../../../common/http_api/source_api'; +import { + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +import { MetricsExplorerColor, colorTransformer } from '../../../../common/color_palette'; +import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api'; +import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart'; +import { MetricExpression, AlertContextMeta } from '../types'; +import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { getChartTheme } from '../../../pages/metrics/metrics_explorer/components/helpers/get_chart_theme'; +import { createFormatterForMetric } from '../../../pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric'; +import { calculateDomain } from '../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain'; +import { useMetricsExplorerChartData } from '../hooks/use_metrics_explorer_chart_data'; + +interface Props { + context: AlertsContextValue; + expression: MetricExpression; + derivedIndexPattern: IIndexPattern; + source: InfraSource | null; + filterQuery?: string; + groupBy?: string; +} + +const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), +}; + +const TIME_LABELS = { + s: i18n.translate('xpack.infra.metrics.alerts.timeLabels.seconds', { defaultMessage: 'seconds' }), + m: i18n.translate('xpack.infra.metrics.alerts.timeLabels.minutes', { defaultMessage: 'minutes' }), + h: i18n.translate('xpack.infra.metrics.alerts.timeLabels.hours', { defaultMessage: 'hours' }), + d: i18n.translate('xpack.infra.metrics.alerts.timeLabels.days', { defaultMessage: 'days' }), +}; + +export const ExpressionChart: React.FC = ({ + expression, + context, + derivedIndexPattern, + source, + filterQuery, + groupBy, +}) => { + const { loading, data } = useMetricsExplorerChartData( + expression, + context, + derivedIndexPattern, + source, + filterQuery, + groupBy + ); + + const metric = { + field: expression.metric, + aggregation: expression.aggType as MetricsExplorerAggregation, + color: MetricsExplorerColor.color0, + }; + const isDarkMode = context.uiSettings?.get('theme:darkMode') || false; + const dateFormatter = useMemo(() => { + const firstSeries = data ? first(data.series) : null; + return firstSeries && firstSeries.rows.length > 0 + ? niceTimeFormatter([first(firstSeries.rows).timestamp, last(firstSeries.rows).timestamp]) + : (value: number) => `${value}`; + }, [data]); + + const yAxisFormater = useCallback(createFormatterForMetric(metric), [expression]); + + if (loading || !data) { + return ( + + + + + + ); + } + + const thresholds = expression.threshold.slice().sort(); + + // Creating a custom series where the ID is changed to 0 + // so that we can get a proper domian + const firstSeries = first(data.series); + if (!firstSeries) { + return ( + + Oops, no chart data available + + ); + } + + const series = { + ...firstSeries, + rows: firstSeries.rows.map(row => { + const newRow: MetricsExplorerRow = { + timestamp: row.timestamp, + metric_0: row.metric_0 || null, + }; + thresholds.forEach((thresholdValue, index) => { + newRow[`metric_threshold_${index}`] = thresholdValue; + }); + return newRow; + }), + }; + + const firstTimestamp = first(firstSeries.rows).timestamp; + const lastTimestamp = last(firstSeries.rows).timestamp; + const dataDomain = calculateDomain(series, [metric], false); + const domain = { + max: Math.max(dataDomain.max, last(thresholds) || dataDomain.max) * 1.1, // add 10% headroom. + min: Math.min(dataDomain.min, first(thresholds) || dataDomain.min), + }; + + if (domain.min === first(expression.threshold)) { + domain.min = domain.min * 0.9; + } + + const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(expression.comparator); + const opacity = 0.3; + const timeLabel = TIME_LABELS[expression.timeUnit]; + + return ( + <> + + + + {thresholds.length ? ( + `threshold_${i}`)} + series={series} + stack={false} + opacity={opacity} + /> + ) : null} + {thresholds.length && expression.comparator === Comparator.OUTSIDE_RANGE ? ( + <> + `threshold_${i}`)} + series={series} + stack={false} + opacity={opacity} + /> + + + + ) : null} + {isAbove ? ( + + ) : null} + + + + + +
+ {series.id !== 'ALL' ? ( + + + + ) : ( + + + + )} +
+ + ); +}; + +const EmptyContainer: React.FC = ({ children }) => ( +
+ {children} +
+); + +const ChartContainer: React.FC = ({ children }) => ( +
+ {children} +
+); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx new file mode 100644 index 00000000000000..8801df380b48d4 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx @@ -0,0 +1,237 @@ +/* + * 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, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiSpacer } from '@elastic/eui'; +import { IFieldType } from 'src/plugins/data/public'; +import { + WhenExpression, + OfExpression, + ThresholdExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/common'; +import { euiStyled } from '../../../../../observability/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { MetricExpression, AGGREGATION_TYPES } from '../types'; +import { + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants'; + +const customComparators = { + ...builtInComparators, + [Comparator.OUTSIDE_RANGE]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', { + defaultMessage: 'Is not between', + }), + value: Comparator.OUTSIDE_RANGE, + requiredValues: 2, + }, +}; + +interface ExpressionRowProps { + fields: IFieldType[]; + expressionId: number; + expression: MetricExpression; + errors: IErrorObject; + canDelete: boolean; + addExpression(): void; + remove(id: number): void; + setAlertParams(id: number, params: MetricExpression): 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 [isExpanded, setRowState] = useState(true); + const toggleRowState = useCallback(() => setRowState(!isExpanded), [isExpanded]); + const { + children, + setAlertParams, + expression, + errors, + expressionId, + remove, + fields, + canDelete, + } = props; + const { + aggType = AGGREGATION_TYPES.MAX, + metric, + comparator = Comparator.GT, + threshold = [], + } = expression; + + const updateAggType = useCallback( + (at: string) => { + setAlertParams(expressionId, { + ...expression, + aggType: at as MetricExpression['aggType'], + metric: at === 'count' ? undefined : expression.metric, + }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateMetric = useCallback( + (m?: MetricExpression['metric']) => { + setAlertParams(expressionId, { ...expression, metric: m }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateComparator = useCallback( + (c?: string) => { + setAlertParams(expressionId, { ...expression, comparator: c as Comparator }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateThreshold = useCallback( + t => { + if (t.join() !== expression.threshold.join()) { + setAlertParams(expressionId, { ...expression, threshold: t }); + } + }, + [expressionId, expression, setAlertParams] + ); + + return ( + <> + + + + + + + + + + {aggType !== 'count' && ( + + ({ + normalizedType: f.type, + name: f.name, + }))} + aggType={aggType} + errors={errors} + onChangeSelectedAggField={updateMetric} + /> + + )} + + + + + + {canDelete && ( + + remove(expressionId)} + /> + + )} + + {isExpanded ?
{children}
: null} + + + ); +}; + +export const aggregationType: { [key: string]: any } = { + avg: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { + defaultMessage: 'Average', + }), + fieldRequired: true, + validNormalizedTypes: ['number'], + value: AGGREGATION_TYPES.AVERAGE, + }, + max: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.max', { + defaultMessage: 'Max', + }), + fieldRequired: true, + validNormalizedTypes: ['number', 'date'], + value: AGGREGATION_TYPES.MAX, + }, + min: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.min', { + defaultMessage: 'Min', + }), + fieldRequired: true, + validNormalizedTypes: ['number', 'date'], + value: AGGREGATION_TYPES.MIN, + }, + cardinality: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.cardinality', { + defaultMessage: 'Cardinality', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.CARDINALITY, + validNormalizedTypes: ['number'], + }, + rate: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.rate', { + defaultMessage: 'Rate', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.RATE, + validNormalizedTypes: ['number'], + }, + count: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.count', { + defaultMessage: 'Document count', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.COUNT, + validNormalizedTypes: ['number'], + }, + sum: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.sum', { + defaultMessage: 'Sum', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.SUM, + validNormalizedTypes: ['number'], + }, +}; diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx rename to x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts new file mode 100644 index 00000000000000..67f66bf742f430 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IIndexPattern } from 'src/plugins/data/public'; +import { useMemo } from 'react'; +import { InfraSource } from '../../../../common/http_api/source_api'; +import { AlertContextMeta, MetricExpression } from '../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data'; + +export const useMetricsExplorerChartData = ( + expression: MetricExpression, + context: AlertsContextValue, + derivedIndexPattern: IIndexPattern, + source: InfraSource | null, + filterQuery?: string, + groupBy?: string +) => { + const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' }; + const options: MetricsExplorerOptions = useMemo( + () => ({ + limit: 1, + forceInterval: true, + groupBy, + filterQuery, + metrics: [ + { + field: expression.metric, + aggregation: expression.aggType, + }, + ], + aggregation: expression.aggType || 'avg', + }), + [expression.aggType, expression.metric, filterQuery, groupBy] + ); + const timerange = useMemo( + () => ({ + interval: `>=${timeSize || 1}${timeUnit}`, + from: `now-${(timeSize || 1) * 20}${timeUnit}`, + to: 'now', + }), + [timeSize, timeUnit] + ); + + return useMetricsExplorerData( + options, + source?.configuration, + derivedIndexPattern, + timerange, + null, + null, + context.http.fetch + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts similarity index 72% rename from x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts rename to x-pack/plugins/infra/public/alerting/metric_threshold/index.ts index 162102ebd86a80..91b9bafad50117 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts @@ -5,13 +5,13 @@ */ 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'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { Expressions } from './components/expression'; +import { validateMetricThreshold } from './components/validation'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/metric_threshold/types'; -export function getAlertType(): AlertTypeModel { +export function createMetricThresholdAlertType(): AlertTypeModel { return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.metrics.alertFlyout.alertName', { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts new file mode 100644 index 00000000000000..0e631b1e333d75 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts @@ -0,0 +1,31 @@ +/* + * 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 { first } from 'lodash'; +import { MetricsExplorerResponse } from '../../../../common/http_api/metrics_explorer'; +import { MetricThresholdAlertParams, ExpressionChartSeries } from '../types'; + +export const transformMetricsExplorerData = ( + params: MetricThresholdAlertParams, + data: MetricsExplorerResponse | null +) => { + const { criteria } = params; + if (criteria && data) { + const firstSeries = first(data.series); + const series = firstSeries.rows.reduce((acc, row) => { + const { timestamp } = row; + criteria.forEach((item, index) => { + if (!acc[index]) { + acc[index] = []; + } + const value = (row[`metric_${index}`] as number) || 0; + acc[index].push({ timestamp, value }); + }); + return acc; + }, [] as ExpressionChartSeries); + return { id: firstSeries.id, series }; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts new file mode 100644 index 00000000000000..af3baf191bed21 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + MetricExpressionParams, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../server/lib/alerting/metric_threshold/types'; +import { MetricsExplorerOptions } from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; + +export interface AlertContextMeta { + currentOptions?: Partial; + series?: MetricsExplorerSeries; +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; +export type MetricExpression = Omit & { + metric?: string; +}; + +export enum AGGREGATION_TYPES { + COUNT = 'count', + AVERAGE = 'avg', + SUM = 'sum', + MIN = 'min', + MAX = 'max', + RATE = 'rate', + CARDINALITY = 'cardinality', +} + +export interface MetricThresholdAlertParams { + criteria?: MetricExpression[]; + groupBy?: string; + filterQuery?: string; + sourceId?: string; +} + +export interface ExpressionChartRow { + timestamp: number; + value: number; +} + +export type ExpressionChartSeries = ExpressionChartRow[][]; + +export interface ExpressionChartData { + id: string; + series: ExpressionChartSeries; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index cc88dd9e0d0f8f..5dc9802fefd254 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -28,7 +28,7 @@ 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 { AlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; export const InfrastructurePage = ({ match }: RouteComponentProps) => { const uiCapabilities = useKibana().services.application?.capabilities; 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..c528aa885346e4 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 @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo, useState } from 'react'; -import { AlertFlyout } from '../../../../../components/alerting/metrics/alert_flyout'; +import { AlertFlyout } from '../../../../../alerting/metric_threshold/components/alert_flyout'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; import { getNodeDetailUrl, getNodeLogsUrl } from '../../../../link_to'; import { createUptimeLink } from '../../lib/create_uptime_link'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx index 0c0f7b33b3a4a0..5a84d204b3b25e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx @@ -26,6 +26,9 @@ export const MetricsExplorerAggregationPicker = ({ options, onChange }: Props) = ['avg']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.avg', { defaultMessage: 'Average', }), + ['sum']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.sum', { + defaultMessage: 'Sum', + }), ['max']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.max', { defaultMessage: 'Max', }), diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx index 31086a21ca13f8..e13c9dcc069845 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import DateMath from '@elastic/datemath'; import { Capabilities } from 'src/core/public'; -import { AlertFlyout } from '../../../../components/alerting/metrics/alert_flyout'; +import { AlertFlyout } from '../../../../alerting/metric_threshold/components/alert_flyout'; import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts index 1607302a6259a5..6343f2848054b7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options'; -export const createMetricLabel = (metric: MetricsExplorerMetric) => { +export const createMetricLabel = (metric: MetricsExplorerOptionsMetric) => { + if (metric.label) { + return metric.label; + } return `${metric.aggregation}(${metric.field || ''})`; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx index ad7ce83539526a..3b84fcbc34836b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx @@ -21,12 +21,15 @@ import { MetricsExplorerChartType, } from '../hooks/use_metrics_explorer_options'; +type NumberOrString = string | number; + interface Props { metric: MetricsExplorerOptionsMetric; - id: string | number; + id: NumberOrString | NumberOrString[]; series: MetricsExplorerSeries; type: MetricsExplorerChartType; stack: boolean; + opacity?: number; } export const MetricExplorerSeriesChart = (props: Props) => { @@ -36,13 +39,17 @@ export const MetricExplorerSeriesChart = (props: Props) => { return ; }; -export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack }: Props) => { +export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opacity }: Props) => { const color = (metric.color && colorTransformer(metric.color)) || colorTransformer(MetricsExplorerColor.color0); - const yAccessor = `metric_${id}`; - const chartId = `series-${series.id}-${yAccessor}`; + const yAccessors = Array.isArray(id) + ? id.map(i => `metric_${i}`).slice(id.length - 1, id.length) + : [`metric_${id}`]; + const y0Accessors = + Array.isArray(id) && id.length > 1 ? id.map(i => `metric_${i}`).slice(0, 1) : undefined; + const chartId = `series-${series.id}-${yAccessors.join('-')}`; const seriesAreaStyle: RecursivePartial = { line: { @@ -50,19 +57,21 @@ export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack }: Pr visible: true, }, area: { - opacity: 0.5, + opacity: opacity || 0.5, visible: type === MetricsExplorerChartType.area, }, }; + return ( (null); const [loading, setLoading] = useState(true); const [data, setData] = useState(null); @@ -46,13 +49,17 @@ export function useMetricsExplorerData( if (!from || !to) { throw new Error('Unalble to parse timerange'); } - if (!fetch) { + if (!fetchFn) { throw new Error('HTTP service is unavailable'); } + if (!source) { + throw new Error('Source is unavailable'); + } const response = decodeOrThrow(metricsExplorerResponseRT)( - await fetch('/api/infra/metrics_explorer', { + await fetchFn('/api/infra/metrics_explorer', { method: 'POST', body: JSON.stringify({ + forceInterval: options.forceInterval, metrics: options.aggregation === 'count' ? [{ aggregation: 'count' }] diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 9d124a6af80123..1b3e809fde61fd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -40,6 +40,7 @@ export interface MetricsExplorerOptions { groupBy?: string; filterQuery?: string; aggregation: MetricsExplorerAggregation; + forceInterval?: boolean; } export interface MetricsExplorerTimeOptions { diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 40366b2a54f243..8cdfc4f381f436 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -21,8 +21,8 @@ import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/p 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 { createMetricThresholdAlertType } from './alerting/metric_threshold'; export type ClientSetup = void; export type ClientStart = void; @@ -53,8 +53,8 @@ export class Plugin setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) { registerFeatures(pluginsSetup.home); - pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getMetricsAlertType()); pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createMetricThresholdAlertType()); core.application.register({ id: 'logs', 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..e735a26d96a91d 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 @@ -72,7 +72,9 @@ export const populateSeriesWithTSVBData = ( ); if (calculatedInterval) { - model.interval = `>=${calculatedInterval}s`; + model.interval = options.forceInterval + ? options.timerange.interval + : `>=${calculatedInterval}s`; } // Get TSVB results using the model, timerange and filters