[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.

![image](dd14cf99-e44e-4531-a221-7ed40bb43c5a)

---------

Co-authored-by: Faisal Kanout <faisal@kanout.com>
Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
This commit is contained in:
Maryam Saeidi 2024-05-16 10:56:55 +02:00 committed by GitHub
parent 5acea44cc9
commit 62a0ce9d24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 300 additions and 45 deletions

View file

@ -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 }}
/>
);
}

View file

@ -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'}
/>,

View file

@ -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]),

View file

@ -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,

View file

@ -34,6 +34,9 @@ Array [
"groupBy": Array [
"host.hostname",
],
"groupInstance": Array [
"host-1",
],
"hideTitle": true,
"source": Object {
"id": "default",

View file

@ -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>
),

View file

@ -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}

View file

@ -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()}
/>

View file

@ -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>
);
})}
</>
);
}

View file

@ -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>
</>
);
}

View file

@ -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(() => {

View file

@ -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',

View file

@ -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,

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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,

View file

@ -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++;
}
}

View file

@ -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]),

View file

@ -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]),

View file

@ -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 = {

View file

@ -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(

View file

@ -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);