mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Metric threshold] Persist group by information and apply it in the alert details page (#181689)
Resolves #178998 ## Summary This PR - Persists group by information and apply it in the alert details page - Adds source and tags to the alert summary field - Fixes annotation issue on the chart by adding a margin-top **Note** I showed the chart title temporarily in the screenshots below for verification: (You can do the same by removing hideTitle) | State | Screenshot | |---|---| |Before|<img src="46d39e04
-f871-476f-b06b-66e7eb77db5d" width=700 /><img src="243ea2aa
-8542-4a1f-91ff-d47c01b8452b" width=700 />| |After|<img src="3b632d4f
-690f-4a2f-90d6-3b9ec3d14e39" width=700 />| ### How to test - Create a metric threshold rule - make sure to enable the related feature flag ``` xpack.observability.unsafe.alertDetails.observability.enabled: true ``` - Go to the alert details page and verify the charts show data related to the selected group - either remove hideTitle - or make sure the data in the chart matches expectations for that specific group - or check the `metrics_explorer` <img src="20996859
-0e17-44fa-a294-0c124daf849e" width=500 /> <img src="21beda58
-0aff-42c2-a74e-df422eda347c" width=500 /> - Create an APM Latency threshold rule and check the active alert annotation to have the right color.  --------- Co-authored-by: Faisal Kanout <faisal@kanout.com> Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
This commit is contained in:
parent
5acea44cc9
commit
62a0ce9d24
22 changed files with 300 additions and 45 deletions
|
@ -37,7 +37,7 @@ export function AlertActiveTimeRangeAnnotation({ alertStart, alertEnd, color, id
|
|||
details: RECT_ANNOTATION_TITLE,
|
||||
},
|
||||
]}
|
||||
style={{ fill: color, opacity: 1 }}
|
||||
style={{ fill: color, opacity: 0.1 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Theme } from '@elastic/charts';
|
||||
import { RecursivePartial, transparentize } from '@elastic/eui';
|
||||
import { RecursivePartial } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -23,7 +24,6 @@ import { useEuiTheme } from '@elastic/eui';
|
|||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
import moment from 'moment';
|
||||
import chroma from 'chroma-js';
|
||||
import { filterNil } from '../../../shared/charts/latency_chart';
|
||||
import { LatencyAggregationTypeSelect } from '../../../shared/charts/latency_chart/latency_aggregation_type_select';
|
||||
import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
|
||||
|
@ -163,7 +163,7 @@ function LatencyChart({
|
|||
<AlertActiveTimeRangeAnnotation
|
||||
alertStart={alert.start}
|
||||
alertEnd={alertEnd}
|
||||
color={chroma(transparentize('#F04E981A', 0.2)).hex().toUpperCase()}
|
||||
color={euiTheme.colors.danger}
|
||||
id={'alertActiveRect'}
|
||||
key={'alertActiveRect'}
|
||||
/>,
|
||||
|
|
|
@ -91,6 +91,7 @@ export const afterKeyObjectRT = rt.record(rt.string, rt.union([rt.string, rt.nul
|
|||
|
||||
export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({
|
||||
groupBy: rt.union([groupByRT, rt.array(groupByRT)]),
|
||||
groupInstance: rt.union([groupByRT, rt.array(groupByRT)]),
|
||||
afterKey: rt.union([rt.string, rt.null, rt.undefined, afterKeyObjectRT]),
|
||||
limit: rt.union([rt.number, rt.null, rt.undefined]),
|
||||
filterQuery: rt.union([rt.string, rt.null, rt.undefined]),
|
||||
|
|
|
@ -69,6 +69,7 @@ export const metricExplorerOptionsRequiredRT = rt.type({
|
|||
export const metricExplorerOptionsOptionalRT = rt.partial({
|
||||
limit: rt.number,
|
||||
groupBy: rt.union([rt.string, rt.array(rt.string)]),
|
||||
groupInstance: rt.union([rt.string, rt.array(rt.string)]),
|
||||
filterQuery: rt.string,
|
||||
source: rt.string,
|
||||
forceInterval: rt.boolean,
|
||||
|
|
|
@ -34,6 +34,9 @@ Array [
|
|||
"groupBy": Array [
|
||||
"host.hostname",
|
||||
],
|
||||
"groupInstance": Array [
|
||||
"host-1",
|
||||
],
|
||||
"hideTitle": true,
|
||||
"source": Object {
|
||||
"id": "default",
|
||||
|
|
|
@ -18,6 +18,8 @@ import {
|
|||
} from '../mocks/metric_threshold_rule';
|
||||
import { AlertDetailsAppSection } from './alert_details_app_section';
|
||||
import { ExpressionChart } from './expression_chart';
|
||||
import { Groups } from './groups';
|
||||
import { Tags } from './tags';
|
||||
|
||||
const mockedChartStartContract = chartPluginMock.createStartContract();
|
||||
|
||||
|
@ -84,15 +86,32 @@ describe('AlertDetailsAppSection', () => {
|
|||
expect(result.getByTestId('threshold-2000-2500')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render rule link', async () => {
|
||||
it('should render alert summary fields', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(mockedSetAlertSummaryFields).toBeCalledTimes(1);
|
||||
expect(mockedSetAlertSummaryFields).toBeCalledWith([
|
||||
{
|
||||
label: 'Source',
|
||||
value: (
|
||||
<Groups
|
||||
groups={[
|
||||
{
|
||||
field: 'host.name',
|
||||
value: 'host-1',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Tags',
|
||||
value: <Tags tags={['tag 1', 'tag 2']} />,
|
||||
},
|
||||
{
|
||||
label: 'Rule',
|
||||
value: (
|
||||
<EuiLink data-test-subj="alertDetailsAppSectionRuleLink" href={ruleLink}>
|
||||
<EuiLink data-test-subj="metricsRuleAlertDetailsAppSectionRuleLink" href={ruleLink}>
|
||||
Monitoring hosts
|
||||
</EuiLink>
|
||||
),
|
||||
|
|
|
@ -20,7 +20,13 @@ import {
|
|||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { AlertSummaryField, TopAlert } from '@kbn/observability-plugin/public';
|
||||
import { ALERT_END, ALERT_START, ALERT_EVALUATION_VALUES } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
ALERT_END,
|
||||
ALERT_START,
|
||||
ALERT_EVALUATION_VALUES,
|
||||
ALERT_GROUP,
|
||||
TAGS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { Rule } from '@kbn/alerting-plugin/common';
|
||||
import { AlertAnnotation, AlertActiveTimeRangeAnnotation } from '@kbn/observability-alert-details';
|
||||
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
|
||||
|
@ -33,6 +39,8 @@ import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explore
|
|||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
||||
import { MetricThresholdRuleTypeParams } from '..';
|
||||
import { ExpressionChart } from './expression_chart';
|
||||
import { Groups } from './groups';
|
||||
import { Tags } from './tags';
|
||||
|
||||
// TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690
|
||||
export type MetricThresholdRule = Rule<
|
||||
|
@ -41,7 +49,18 @@ export type MetricThresholdRule = Rule<
|
|||
groupBy?: string | string[];
|
||||
}
|
||||
>;
|
||||
export type MetricThresholdAlert = TopAlert;
|
||||
|
||||
interface Group {
|
||||
field: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface MetricThresholdAlertField {
|
||||
[ALERT_EVALUATION_VALUES]?: Array<number | null>;
|
||||
[ALERT_GROUP]?: Group[];
|
||||
}
|
||||
|
||||
export type MetricThresholdAlert = TopAlert<MetricThresholdAlertField>;
|
||||
|
||||
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm';
|
||||
const ALERT_START_ANNOTATION_ID = 'alert_start_annotation';
|
||||
|
@ -63,6 +82,9 @@ export function AlertDetailsAppSection({
|
|||
const { uiSettings, charts } = useKibanaContextForPlugin().services;
|
||||
const { source, createDerivedIndexPattern } = useSourceContext();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const groupInstance = alert.fields[ALERT_GROUP]?.map((group: Group) => group.value);
|
||||
const groups = alert.fields[ALERT_GROUP];
|
||||
const tags = alert.fields[TAGS];
|
||||
|
||||
const derivedIndexPattern = useMemo(
|
||||
() => createDerivedIndexPattern(),
|
||||
|
@ -90,19 +112,36 @@ export function AlertDetailsAppSection({
|
|||
/>,
|
||||
];
|
||||
useEffect(() => {
|
||||
setAlertSummaryFields([
|
||||
{
|
||||
label: i18n.translate('xpack.infra.metrics.alertDetailsAppSection.summaryField.rule', {
|
||||
defaultMessage: 'Rule',
|
||||
const alertSummaryFields = [];
|
||||
if (groups) {
|
||||
alertSummaryFields.push({
|
||||
label: i18n.translate('xpack.infra.metrics.alertDetailsAppSection.summaryField.source', {
|
||||
defaultMessage: 'Source',
|
||||
}),
|
||||
value: (
|
||||
<EuiLink data-test-subj="alertDetailsAppSectionRuleLink" href={ruleLink}>
|
||||
{rule.name}
|
||||
</EuiLink>
|
||||
),
|
||||
},
|
||||
]);
|
||||
}, [rule, ruleLink, setAlertSummaryFields]);
|
||||
value: <Groups groups={groups} />,
|
||||
});
|
||||
}
|
||||
if (tags && tags.length > 0) {
|
||||
alertSummaryFields.push({
|
||||
label: i18n.translate('xpack.infra.metrics.alertDetailsAppSection.summaryField.tags', {
|
||||
defaultMessage: 'Tags',
|
||||
}),
|
||||
value: <Tags tags={tags} />,
|
||||
});
|
||||
}
|
||||
alertSummaryFields.push({
|
||||
label: i18n.translate('xpack.infra.metrics.alertDetailsAppSection.summaryField.rule', {
|
||||
defaultMessage: 'Rule',
|
||||
}),
|
||||
value: (
|
||||
<EuiLink data-test-subj="metricsRuleAlertDetailsAppSectionRuleLink" href={ruleLink}>
|
||||
{rule.name}
|
||||
</EuiLink>
|
||||
),
|
||||
});
|
||||
|
||||
setAlertSummaryFields(alertSummaryFields);
|
||||
}, [groups, tags, rule, ruleLink, setAlertSummaryFields]);
|
||||
|
||||
return !!rule.params.criteria ? (
|
||||
<EuiFlexGroup direction="column" data-test-subj="metricThresholdAppSection">
|
||||
|
@ -153,6 +192,7 @@ export function AlertDetailsAppSection({
|
|||
expression={criterion}
|
||||
filterQuery={rule.params.filterQueryText}
|
||||
groupBy={rule.params.groupBy}
|
||||
groupInstance={groupInstance}
|
||||
hideTitle
|
||||
source={source}
|
||||
timeRange={timeRange}
|
||||
|
|
|
@ -55,6 +55,7 @@ interface Props {
|
|||
chartType?: MetricsExplorerChartType;
|
||||
filterQuery?: string;
|
||||
groupBy?: string | string[];
|
||||
groupInstance?: string | string[];
|
||||
hideTitle?: boolean;
|
||||
source?: MetricsSourceConfiguration;
|
||||
timeRange?: TimeRange;
|
||||
|
@ -67,6 +68,7 @@ export const ExpressionChart: React.FC<Props> = ({
|
|||
chartType = MetricsExplorerChartType.bar,
|
||||
filterQuery,
|
||||
groupBy,
|
||||
groupInstance,
|
||||
hideTitle = false,
|
||||
source,
|
||||
timeRange,
|
||||
|
@ -80,6 +82,7 @@ export const ExpressionChart: React.FC<Props> = ({
|
|||
source,
|
||||
filterQuery,
|
||||
groupBy,
|
||||
groupInstance,
|
||||
timeRange
|
||||
);
|
||||
|
||||
|
@ -200,6 +203,7 @@ export const ExpressionChart: React.FC<Props> = ({
|
|||
externalPointerEvents={{
|
||||
tooltip: { visible: true },
|
||||
}}
|
||||
theme={{ chartMargins: { top: 10 } }}
|
||||
baseTheme={chartTheme.baseTheme}
|
||||
locale={i18n.getLocale()}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export function Groups({ groups }: { groups: Array<{ field: string; value: string }> }) {
|
||||
return (
|
||||
<>
|
||||
{groups &&
|
||||
groups.map((group) => {
|
||||
return (
|
||||
<span key={group.field}>
|
||||
{group.field}: <strong>{group.value}</strong>
|
||||
<br />
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiBadge, EuiPopover } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export function Tags({ tags }: { tags: string[] }) {
|
||||
const [isMoreTagsOpen, setIsMoreTagsOpen] = useState(false);
|
||||
const onMoreTagsClick = () => setIsMoreTagsOpen((isPopoverOpen) => !isPopoverOpen);
|
||||
const closePopover = () => setIsMoreTagsOpen(false);
|
||||
const moreTags = tags.length > 3 && (
|
||||
<EuiBadge
|
||||
key="more"
|
||||
onClick={onMoreTagsClick}
|
||||
onClickAriaLabel={i18n.translate(
|
||||
'xpack.infra.metrics.alertDetailsAppSection.summaryField.moreTags.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'more tags badge',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metrics.alertDetailsAppSection.summaryField.moreTags"
|
||||
defaultMessage="+{number} more"
|
||||
values={{ number: tags.length - 3 }}
|
||||
/>
|
||||
</EuiBadge>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<EuiBadge key={tag}>{tag}</EuiBadge>
|
||||
))}
|
||||
<br />
|
||||
<EuiPopover button={moreTags} isOpen={isMoreTagsOpen} closePopover={closePopover}>
|
||||
{tags.slice(3).map((tag) => (
|
||||
<EuiBadge key={tag}>{tag}</EuiBadge>
|
||||
))}
|
||||
</EuiPopover>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -26,6 +26,7 @@ export const useMetricsExplorerChartData = (
|
|||
source?: MetricsSourceConfiguration,
|
||||
filterQuery?: string,
|
||||
groupBy?: string | string[],
|
||||
groupInstance?: string | string[],
|
||||
timeRange: TimeRange = DEFAULT_TIME_RANGE
|
||||
) => {
|
||||
const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' };
|
||||
|
@ -36,6 +37,7 @@ export const useMetricsExplorerChartData = (
|
|||
forceInterval: true,
|
||||
dropLastBucket: false,
|
||||
groupBy,
|
||||
groupInstance,
|
||||
filterQuery,
|
||||
metrics: [
|
||||
expression.aggType === 'custom'
|
||||
|
@ -57,6 +59,7 @@ export const useMetricsExplorerChartData = (
|
|||
expression.customMetrics,
|
||||
filterQuery,
|
||||
groupBy,
|
||||
groupInstance,
|
||||
]
|
||||
);
|
||||
const timestamps: MetricsExplorerTimestamp = useMemo(() => {
|
||||
|
|
|
@ -153,6 +153,7 @@ export const buildMetricThresholdAlert = (
|
|||
alertOnGroupDisappear: true,
|
||||
},
|
||||
'kibana.alert.evaluation.values': [2500, 5],
|
||||
'kibana.alert.group': [{ field: 'host.name', value: 'host-1' }],
|
||||
'kibana.alert.rule.category': 'Metric threshold',
|
||||
'kibana.alert.rule.consumer': 'alerts',
|
||||
'kibana.alert.rule.execution.uuid': '62dd07ef-ead9-4b1f-a415-7c83d03925f7',
|
||||
|
@ -165,7 +166,7 @@ export const buildMetricThresholdAlert = (
|
|||
'@timestamp': '2023-03-28T14:40:00.000Z',
|
||||
'kibana.alert.reason': 'system.cpu.user.pct reported no data in the last 1m for ',
|
||||
'kibana.alert.action_group': 'metrics.threshold.nodata',
|
||||
tags: [],
|
||||
tags: ['tag 1', 'tag 2'],
|
||||
'kibana.alert.duration.us': 248391946000,
|
||||
'kibana.alert.time_range': {
|
||||
gte: '2023-03-13T14:06:23.695Z',
|
||||
|
|
|
@ -51,6 +51,7 @@ export function useMetricsExplorerData(
|
|||
dropLastBucket: options.dropLastBucket != null ? options.dropLastBucket : true,
|
||||
metrics: options.aggregation === 'count' ? [{ aggregation: 'count' }] : options.metrics,
|
||||
groupBy: options.groupBy,
|
||||
groupInstance: options.groupInstance,
|
||||
afterKey,
|
||||
limit: options.limit,
|
||||
indexPattern: source.metricAlias,
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface Group {
|
||||
field: string;
|
||||
value: string;
|
||||
}
|
|
@ -28,6 +28,7 @@ import {
|
|||
AlertExecutionDetails,
|
||||
InventoryMetricConditions,
|
||||
} from '../../../../common/alerting/metrics/types';
|
||||
import { Group } from './types';
|
||||
|
||||
const ALERT_CONTEXT_CONTAINER = 'container';
|
||||
const ALERT_CONTEXT_ORCHESTRATOR = 'orchestrator';
|
||||
|
@ -313,3 +314,22 @@ export const getGroupByObject = (
|
|||
}
|
||||
return groupByKeysObjectMapping;
|
||||
};
|
||||
|
||||
export const getFormattedGroupBy = (
|
||||
groupBy: string | string[] | undefined,
|
||||
groupSet: Set<string>
|
||||
): Record<string, Group[]> => {
|
||||
const groupByKeysObjectMapping: Record<string, Group[]> = {};
|
||||
if (groupBy) {
|
||||
groupSet.forEach((group) => {
|
||||
const groupSetKeys = group.split(',');
|
||||
groupByKeysObjectMapping[group] = Array.isArray(groupBy)
|
||||
? groupBy.reduce((result: Group[], groupByItem, index) => {
|
||||
result.push({ field: groupByItem, value: groupSetKeys[index]?.trim() });
|
||||
return result;
|
||||
}, [])
|
||||
: [{ field: groupBy, value: group }];
|
||||
});
|
||||
}
|
||||
return groupByKeysObjectMapping;
|
||||
};
|
||||
|
|
|
@ -28,7 +28,9 @@ import {
|
|||
ALERT_EVALUATION_THRESHOLD,
|
||||
ALERT_EVALUATION_VALUES,
|
||||
ALERT_REASON,
|
||||
ALERT_GROUP,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { Group } from '../common/types';
|
||||
|
||||
jest.mock('./lib/evaluate_rule', () => ({ evaluateRule: jest.fn() }));
|
||||
|
||||
|
@ -956,6 +958,7 @@ describe('The metric threshold rule type', () => {
|
|||
reason: 'test.metric.1 is 1 in the last 1 min for host-01. Alert when > 0.75.',
|
||||
tags: ['host-01_tag1', 'host-01_tag2', 'ruleTag1', 'ruleTag2'],
|
||||
groupByKeys: { host: { name: alertIdA } },
|
||||
group: [{ field: 'host.name', value: alertIdA }],
|
||||
});
|
||||
testAlertReported(2, {
|
||||
id: alertIdB,
|
||||
|
@ -967,6 +970,7 @@ describe('The metric threshold rule type', () => {
|
|||
reason: 'test.metric.1 is 3 in the last 1 min for host-02. Alert when > 0.75.',
|
||||
tags: ['host-02_tag1', 'host-02_tag2', 'ruleTag1', 'ruleTag2'],
|
||||
groupByKeys: { host: { name: alertIdB } },
|
||||
group: [{ field: 'host.name', value: alertIdB }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2325,6 +2329,7 @@ describe('The metric threshold rule type', () => {
|
|||
actionGroup,
|
||||
alertState,
|
||||
groupByKeys,
|
||||
group,
|
||||
conditions,
|
||||
reason,
|
||||
tags,
|
||||
|
@ -2342,6 +2347,7 @@ describe('The metric threshold rule type', () => {
|
|||
}>;
|
||||
reason: string;
|
||||
tags?: string[];
|
||||
group?: Group[];
|
||||
}
|
||||
) {
|
||||
expect(services.alertsClient.report).toHaveBeenNthCalledWith(index, {
|
||||
|
@ -2394,6 +2400,18 @@ describe('The metric threshold rule type', () => {
|
|||
? {
|
||||
[ALERT_EVALUATION_VALUES]: conditions.map((c) => c.evaluation_value),
|
||||
[ALERT_EVALUATION_THRESHOLD]: getThresholds(conditions),
|
||||
...(groupByKeys
|
||||
? group
|
||||
? {
|
||||
[ALERT_GROUP]: group,
|
||||
}
|
||||
: {
|
||||
[ALERT_GROUP]: Object.keys(groupByKeys).map((key) => ({
|
||||
field: key,
|
||||
value: groupByKeys[key],
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
[ALERT_REASON]: reason,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import {
|
||||
ALERT_EVALUATION_THRESHOLD,
|
||||
ALERT_EVALUATION_VALUES,
|
||||
ALERT_GROUP,
|
||||
ALERT_REASON,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { isEqual } from 'lodash';
|
||||
|
@ -43,8 +44,10 @@ import {
|
|||
validGroupByForContext,
|
||||
flattenAdditionalContext,
|
||||
getGroupByObject,
|
||||
getFormattedGroupBy,
|
||||
} from '../common/utils';
|
||||
import { getEvaluationValues, getThresholds } from '../common/get_values';
|
||||
import { Group } from '../common/types';
|
||||
|
||||
import { EvaluatedRuleParams, evaluateRule, Evaluation } from './lib/evaluate_rule';
|
||||
import { MissingGroupsRecord } from './lib/check_missing_group';
|
||||
|
@ -83,15 +86,16 @@ type MetricThresholdAllowedActionGroups = ActionGroupIdsOf<
|
|||
typeof FIRED_ACTIONS | typeof WARNING_ACTIONS | typeof NO_DATA_ACTIONS
|
||||
>;
|
||||
|
||||
type MetricThresholdAlertReporter = (
|
||||
id: string,
|
||||
reason: string,
|
||||
actionGroup: MetricThresholdActionGroup,
|
||||
context: MetricThresholdAlertContext,
|
||||
additionalContext?: AdditionalContext | null,
|
||||
evaluationValues?: Array<number | null>,
|
||||
thresholds?: Array<number | null>
|
||||
) => void;
|
||||
type MetricThresholdAlertReporter = (params: {
|
||||
id: string;
|
||||
reason: string;
|
||||
actionGroup: MetricThresholdActionGroup;
|
||||
context: MetricThresholdAlertContext;
|
||||
additionalContext?: AdditionalContext | null;
|
||||
evaluationValues?: Array<number | null>;
|
||||
groups?: object[];
|
||||
thresholds?: Array<number | null>;
|
||||
}) => void;
|
||||
|
||||
export const createMetricThresholdExecutor =
|
||||
(libs: InfraBackendLibs) =>
|
||||
|
@ -130,19 +134,21 @@ export const createMetricThresholdExecutor =
|
|||
throw new AlertsClientError();
|
||||
}
|
||||
|
||||
const alertReporter: MetricThresholdAlertReporter = async (
|
||||
const alertReporter: MetricThresholdAlertReporter = async ({
|
||||
id,
|
||||
reason,
|
||||
actionGroup,
|
||||
contextWithoutAlertDetailsUrl,
|
||||
context: contextWithoutAlertDetailsUrl,
|
||||
additionalContext,
|
||||
evaluationValues,
|
||||
thresholds
|
||||
) => {
|
||||
groups,
|
||||
thresholds,
|
||||
}) => {
|
||||
const { uuid, start } = alertsClient.report({
|
||||
id,
|
||||
actionGroup,
|
||||
});
|
||||
const groupsPayload = typeof groups !== 'undefined' ? { [ALERT_GROUP]: groups } : {};
|
||||
|
||||
alertsClient.setAlertData({
|
||||
id,
|
||||
|
@ -150,6 +156,7 @@ export const createMetricThresholdExecutor =
|
|||
[ALERT_REASON]: reason,
|
||||
[ALERT_EVALUATION_VALUES]: evaluationValues,
|
||||
[ALERT_EVALUATION_THRESHOLD]: thresholds,
|
||||
...groupsPayload,
|
||||
...flattenAdditionalContext(additionalContext),
|
||||
},
|
||||
context: {
|
||||
|
@ -197,7 +204,12 @@ export const createMetricThresholdExecutor =
|
|||
}),
|
||||
};
|
||||
|
||||
await alertReporter(UNGROUPED_FACTORY_KEY, reason, actionGroupId, alertContext);
|
||||
await alertReporter({
|
||||
id: UNGROUPED_FACTORY_KEY,
|
||||
reason,
|
||||
actionGroup: actionGroupId,
|
||||
context: alertContext,
|
||||
});
|
||||
|
||||
return {
|
||||
state: {
|
||||
|
@ -252,13 +264,14 @@ export const createMetricThresholdExecutor =
|
|||
}
|
||||
|
||||
const groupByKeysObjectMapping = getGroupByObject(params.groupBy, resultGroupSet);
|
||||
const groups = [...resultGroupSet];
|
||||
const groupByMapping = getFormattedGroupBy(params.groupBy, resultGroupSet);
|
||||
const groupArray = [...resultGroupSet];
|
||||
const nextMissingGroups = new Set<MissingGroupsRecord>();
|
||||
const hasGroups = !isEqual(groups, [UNGROUPED_FACTORY_KEY]);
|
||||
const hasGroups = !isEqual(groupArray, [UNGROUPED_FACTORY_KEY]);
|
||||
let scheduledActionsCount = 0;
|
||||
|
||||
// The key of `groups` is the alert instance ID.
|
||||
for (const group of groups) {
|
||||
// The key of `groupArray` is the alert instance ID.
|
||||
for (const group of groupArray) {
|
||||
// AND logic; all criteria must be across the threshold
|
||||
const shouldAlertFire = alertResults.every((result) => result[group]?.shouldFire);
|
||||
const shouldAlertWarn = alertResults.every((result) => result[group]?.shouldWarn);
|
||||
|
@ -340,6 +353,7 @@ export const createMetricThresholdExecutor =
|
|||
|
||||
const evaluationValues = getEvaluationValues<Evaluation>(alertResults, group);
|
||||
const thresholds = getThresholds<any>(criteria);
|
||||
const groups: Group[] = groupByMapping[group];
|
||||
|
||||
const alertContext = {
|
||||
alertState: stateToAlertMessage[nextState],
|
||||
|
@ -378,15 +392,16 @@ export const createMetricThresholdExecutor =
|
|||
...additionalContext,
|
||||
};
|
||||
|
||||
await alertReporter(
|
||||
`${group}`,
|
||||
await alertReporter({
|
||||
id: `${group}`,
|
||||
reason,
|
||||
actionGroupId,
|
||||
alertContext,
|
||||
actionGroup: actionGroupId,
|
||||
context: alertContext,
|
||||
additionalContext,
|
||||
evaluationValues,
|
||||
thresholds
|
||||
);
|
||||
groups,
|
||||
thresholds,
|
||||
});
|
||||
scheduledActionsCount++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export const MetricsAPIRequestRT = rt.intersection([
|
|||
}),
|
||||
rt.partial({
|
||||
groupBy: rt.array(groupByRT),
|
||||
groupInstance: rt.array(groupByRT),
|
||||
modules: rt.array(rt.string),
|
||||
afterKey: rt.union([rt.null, afterKeyObjectRT]),
|
||||
limit: rt.union([rt.number, rt.null]),
|
||||
|
|
|
@ -91,6 +91,7 @@ export const afterKeyObjectRT = rt.record(rt.string, rt.union([rt.string, rt.nul
|
|||
|
||||
export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({
|
||||
groupBy: rt.union([groupByRT, rt.array(groupByRT)]),
|
||||
groupInstance: rt.union([groupByRT, rt.array(groupByRT)]),
|
||||
afterKey: rt.union([rt.string, rt.null, rt.undefined, afterKeyObjectRT]),
|
||||
limit: rt.union([rt.number, rt.null, rt.undefined]),
|
||||
filterQuery: rt.union([rt.string, rt.null, rt.undefined]),
|
||||
|
|
|
@ -39,6 +39,14 @@ export const query = async (
|
|||
},
|
||||
};
|
||||
const hasGroupBy = Array.isArray(options.groupBy) && options.groupBy.length > 0;
|
||||
const groupInstanceFilter =
|
||||
options.groupInstance?.reduce<Array<Record<string, unknown>>>((acc, group, index) => {
|
||||
const key = options.groupBy?.[index];
|
||||
if (key && group) {
|
||||
acc.push({ term: { [key]: group } });
|
||||
}
|
||||
return acc;
|
||||
}, []) ?? [];
|
||||
const filter: Array<Record<string, any>> = [
|
||||
{
|
||||
range: {
|
||||
|
@ -50,6 +58,7 @@ export const query = async (
|
|||
},
|
||||
},
|
||||
...(options.groupBy?.map((field) => ({ exists: { field } })) ?? []),
|
||||
...groupInstanceFilter,
|
||||
];
|
||||
|
||||
const params = {
|
||||
|
|
|
@ -83,6 +83,34 @@ describe('convertRequestToMetricsAPIOptions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should work with groupBy and groupInstance as string', () => {
|
||||
expect(
|
||||
convertRequestToMetricsAPIOptions({
|
||||
...BASE_REQUEST,
|
||||
groupBy: 'host.name',
|
||||
groupInstance: 'host-1',
|
||||
})
|
||||
).toEqual({
|
||||
...BASE_METRICS_UI_OPTIONS,
|
||||
groupBy: ['host.name'],
|
||||
groupInstance: ['host-1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with groupInstance arrays', () => {
|
||||
expect(
|
||||
convertRequestToMetricsAPIOptions({
|
||||
...BASE_REQUEST,
|
||||
groupBy: ['host.name', 'cloud.availability_zone'],
|
||||
groupInstance: ['host-1', 'cloud.availability_zone-1'],
|
||||
})
|
||||
).toEqual({
|
||||
...BASE_METRICS_UI_OPTIONS,
|
||||
groupBy: ['host.name', 'cloud.availability_zone'],
|
||||
groupInstance: ['host-1', 'cloud.availability_zone-1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with filterQuery json string', () => {
|
||||
const filter = { bool: { filter: [{ match: { 'host.name': 'example-01' } }] } };
|
||||
expect(
|
||||
|
|
|
@ -40,6 +40,12 @@ export const convertRequestToMetricsAPIOptions = (
|
|||
metricsApiOptions.groupBy = isArray(options.groupBy) ? options.groupBy : [options.groupBy];
|
||||
}
|
||||
|
||||
if (options.groupInstance) {
|
||||
metricsApiOptions.groupInstance = isArray(options.groupInstance)
|
||||
? options.groupInstance
|
||||
: [options.groupInstance];
|
||||
}
|
||||
|
||||
if (options.filterQuery) {
|
||||
try {
|
||||
const filterObject = JSON.parse(options.filterQuery);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue