mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Alert details page][Custom threshold] Add history chart to custom threshold alert details page (#176513)
Closes #175200
## Summary
This PR adds a history chart to the custom threshold alert details page.
The history chart is only added if we have only 1 condition in the rule.
Also, this PR fixes the issue of not applying group by information on
the main chart that I mistakenly introduced during refactoring code in
this [PR](https://github.com/elastic/kibana/pull/175777).

## 🧪 How to test
- Create a custom threshold rule with only one condition
- Go to the alert details page from the alert table actions
- You should be able to see the history chart for the last 30 days with
the correct filtering both for optional KQL and group by information
This commit is contained in:
parent
873ae31687
commit
1aa5e3829e
13 changed files with 476 additions and 92 deletions
|
@ -150,4 +150,61 @@ describe('useAlertsHistory', () => {
|
|||
expect(result.current.data.histogramTriggeredAlerts?.length).toEqual(31);
|
||||
expect(result.current.data.totalTriggeredAlerts).toEqual(32);
|
||||
});
|
||||
|
||||
it('calls http post including term queries', async () => {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
const mockedHttpPost = jest.fn();
|
||||
const http = {
|
||||
post: mockedHttpPost.mockResolvedValue({
|
||||
hits: { total: { value: 32, relation: 'eq' }, max_score: null, hits: [] },
|
||||
aggregations: {
|
||||
avgTimeToRecoverUS: { doc_count: 28, recoveryTime: { value: 134959464.2857143 } },
|
||||
histogramTriggeredAlerts: {
|
||||
buckets: [
|
||||
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as unknown as HttpSetup;
|
||||
|
||||
const { result, waitFor } = renderHook<useAlertsHistoryProps, UseAlertsHistory>(
|
||||
() =>
|
||||
useAlertsHistory({
|
||||
http,
|
||||
featureIds: [AlertConsumers.APM],
|
||||
ruleId,
|
||||
dateRange: { from: start, to: end },
|
||||
queries: [
|
||||
{
|
||||
term: {
|
||||
'kibana.alert.group.value': {
|
||||
value: 'host=1',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await waitFor(() => result.current.isSuccess);
|
||||
});
|
||||
expect(mockedHttpPost).toBeCalledWith('/internal/rac/alerts/find', {
|
||||
body:
|
||||
'{"size":0,"feature_ids":["apm"],"query":{"bool":{"must":[' +
|
||||
'{"term":{"kibana.alert.rule.uuid":"cfd36e60-ef22-11ed-91eb-b7893acacfe2"}},' +
|
||||
'{"term":{"kibana.alert.group.value":{"value":"host=1"}}},' +
|
||||
'{"range":{"kibana.alert.time_range":{"from":"2023-04-10T00:00:00.000Z","to":"2023-05-10T00:00:00.000Z"}}}]}},' +
|
||||
'"aggs":{"histogramTriggeredAlerts":{"date_histogram":{"field":"kibana.alert.start","fixed_interval":"1d",' +
|
||||
'"extended_bounds":{"min":"2023-04-10T00:00:00.000Z","max":"2023-05-10T00:00:00.000Z"}}},' +
|
||||
'"avgTimeToRecoverUS":{"filter":{"term":{"kibana.alert.status":"recovered"}},' +
|
||||
'"aggs":{"recoveryTime":{"avg":{"field":"kibana.alert.duration.us"}}}}}}',
|
||||
signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AggregationsDateHistogramBucketKeys } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { type HttpSetup } from '@kbn/core/public';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { AggregationsDateHistogramBucketKeys } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
ALERT_DURATION,
|
||||
ALERT_RULE_UUID,
|
||||
|
@ -26,6 +27,7 @@ export interface Props {
|
|||
from: string;
|
||||
to: string;
|
||||
};
|
||||
queries?: QueryDslQueryContainer[];
|
||||
}
|
||||
|
||||
interface FetchAlertsHistory {
|
||||
|
@ -45,7 +47,13 @@ export const EMPTY_ALERTS_HISTORY = {
|
|||
histogramTriggeredAlerts: [] as AggregationsDateHistogramBucketKeys[],
|
||||
avgTimeToRecoverUS: 0,
|
||||
};
|
||||
export function useAlertsHistory({ featureIds, ruleId, dateRange, http }: Props): UseAlertsHistory {
|
||||
export function useAlertsHistory({
|
||||
featureIds,
|
||||
ruleId,
|
||||
dateRange,
|
||||
http,
|
||||
queries,
|
||||
}: Props): UseAlertsHistory {
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: ['useAlertsHistory'],
|
||||
queryFn: async ({ signal }) => {
|
||||
|
@ -58,6 +66,7 @@ export function useAlertsHistory({ featureIds, ruleId, dateRange, http }: Props)
|
|||
ruleId,
|
||||
dateRange,
|
||||
signal,
|
||||
queries,
|
||||
});
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
|
@ -87,12 +96,14 @@ interface AggsESResponse {
|
|||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchTriggeredAlertsHistory({
|
||||
featureIds,
|
||||
http,
|
||||
ruleId,
|
||||
dateRange,
|
||||
signal,
|
||||
queries = [],
|
||||
}: {
|
||||
featureIds: ValidFeatureId[];
|
||||
http: HttpSetup;
|
||||
|
@ -102,6 +113,7 @@ export async function fetchTriggeredAlertsHistory({
|
|||
to: string;
|
||||
};
|
||||
signal?: AbortSignal;
|
||||
queries?: QueryDslQueryContainer[];
|
||||
}): Promise<FetchAlertsHistory> {
|
||||
try {
|
||||
const responseES = await http.post<AggsESResponse>(`${BASE_RAC_ALERTS_API_PATH}/find`, {
|
||||
|
@ -117,6 +129,7 @@ export async function fetchTriggeredAlertsHistory({
|
|||
[ALERT_RULE_UUID]: ruleId,
|
||||
},
|
||||
},
|
||||
...queries,
|
||||
{
|
||||
range: {
|
||||
[ALERT_TIME_RANGE]: dateRange,
|
||||
|
|
|
@ -3,6 +3,18 @@
|
|||
exports[`AlertDetailsAppSection should render annotations 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"additionalFilters": Array [
|
||||
Object {
|
||||
"meta": Object {},
|
||||
"query": Object {
|
||||
"term": Object {
|
||||
"host.name": Object {
|
||||
"value": "host-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"color": "#BD271E",
|
||||
|
@ -27,6 +39,9 @@ Array [
|
|||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"chartOptions": Object {
|
||||
"seriesType": "bar_stacked",
|
||||
},
|
||||
"dataView": undefined,
|
||||
"groupBy": Array [
|
||||
"host.hostname",
|
||||
|
@ -52,7 +67,6 @@ Array [
|
|||
"query": "host.hostname: Users-System.local and service.type: system",
|
||||
},
|
||||
},
|
||||
"seriesType": "bar_stacked",
|
||||
"timeRange": Object {
|
||||
"from": "2023-03-28T10:43:13.802Z",
|
||||
"to": "2023-03-29T13:14:09.581Z",
|
||||
|
|
|
@ -19,7 +19,8 @@ import {
|
|||
} from '../../mocks/custom_threshold_rule';
|
||||
import { CustomThresholdAlertFields } from '../../types';
|
||||
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';
|
||||
import AlertDetailsAppSection, { CustomThresholdAlert } from './alert_details_app_section';
|
||||
import { CustomThresholdAlert } from '../types';
|
||||
import AlertDetailsAppSection from './alert_details_app_section';
|
||||
import { Groups } from './groups';
|
||||
import { Tags } from './tags';
|
||||
|
||||
|
@ -28,6 +29,17 @@ const mockedChartStartContract = chartPluginMock.createStartContract();
|
|||
jest.mock('@kbn/observability-alert-details', () => ({
|
||||
AlertAnnotation: () => {},
|
||||
AlertActiveTimeRangeAnnotation: () => {},
|
||||
useAlertsHistory: () => ({
|
||||
data: {
|
||||
histogramTriggeredAlerts: [
|
||||
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 2 },
|
||||
],
|
||||
avgTimeToRecoverUS: 0,
|
||||
totalTriggeredAlerts: 2,
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import chroma from 'chroma-js';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
@ -20,7 +21,7 @@ import {
|
|||
useEuiTheme,
|
||||
transparentize,
|
||||
} from '@elastic/eui';
|
||||
import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common';
|
||||
import { RuleTypeParams } from '@kbn/alerting-plugin/common';
|
||||
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
|
||||
import {
|
||||
ALERT_END,
|
||||
|
@ -30,34 +31,27 @@ import {
|
|||
TAGS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import chroma from 'chroma-js';
|
||||
import type {
|
||||
EventAnnotationConfig,
|
||||
PointInTimeEventAnnotationConfig,
|
||||
RangeEventAnnotationConfig,
|
||||
} from '@kbn/event-annotation-common';
|
||||
import moment from 'moment';
|
||||
import { AlertHistoryChart } from './alert_history';
|
||||
import { useLicense } from '../../../../hooks/use_license';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter';
|
||||
import { AlertSummaryField, TopAlert } from '../../../..';
|
||||
import {
|
||||
AlertParams,
|
||||
CustomThresholdAlertFields,
|
||||
CustomThresholdRuleTypeParams,
|
||||
MetricExpression,
|
||||
} from '../../types';
|
||||
import { AlertSummaryField } from '../../../..';
|
||||
import { AlertParams, MetricExpression } from '../../types';
|
||||
import { TIME_LABELS } from '../criterion_preview_chart/criterion_preview_chart';
|
||||
import { Threshold } from '../custom_threshold';
|
||||
import { getGroupFilters } from '../helpers/get_group';
|
||||
import { CustomThresholdRule, CustomThresholdAlert } from '../types';
|
||||
import { LogRateAnalysis } from './log_rate_analysis';
|
||||
import { Groups } from './groups';
|
||||
import { Tags } from './tags';
|
||||
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';
|
||||
|
||||
// TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690
|
||||
export type CustomThresholdRule = Rule<CustomThresholdRuleTypeParams>;
|
||||
export type CustomThresholdAlert = TopAlert<CustomThresholdAlertFields>;
|
||||
|
||||
interface AppSectionProps {
|
||||
alert: CustomThresholdAlert;
|
||||
rule: CustomThresholdRule;
|
||||
|
@ -261,14 +255,18 @@ export default function AlertDetailsAppSection({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={5}>
|
||||
<RuleConditionChart
|
||||
metricExpression={criterion}
|
||||
dataView={dataView}
|
||||
searchConfiguration={ruleParams.searchConfiguration}
|
||||
groupBy={ruleParams.groupBy}
|
||||
additionalFilters={getGroupFilters(groups)}
|
||||
annotations={annotations}
|
||||
chartOptions={{
|
||||
// For alert details page, the series type needs to be changed to 'bar_stacked'
|
||||
// due to https://github.com/elastic/elastic-charts/issues/2323
|
||||
seriesType: 'bar_stacked',
|
||||
}}
|
||||
dataView={dataView}
|
||||
groupBy={ruleParams.groupBy}
|
||||
metricExpression={criterion}
|
||||
searchConfiguration={ruleParams.searchConfiguration}
|
||||
timeRange={timeRange}
|
||||
// For alert details page, the series type needs to be changed to 'bar_stacked' due to https://github.com/elastic/elastic-charts/issues/2323
|
||||
seriesType={'bar_stacked'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -278,6 +276,7 @@ export default function AlertDetailsAppSection({
|
|||
{hasLogRateAnalysisLicense && (
|
||||
<LogRateAnalysis alert={alert} dataView={dataView} rule={rule} services={services} />
|
||||
)}
|
||||
<AlertHistoryChart alert={alert} dataView={dataView} rule={rule} />
|
||||
</EuiFlexGroup>
|
||||
) : null;
|
||||
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { RuleTypeParams } from '@kbn/alerting-plugin/common';
|
||||
import { EventAnnotationConfig } from '@kbn/event-annotation-common';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
EuiLoadingSpinner,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALERT_GROUP, ALERT_GROUP_VALUE, type AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { useAlertsHistory } from '@kbn/observability-alert-details';
|
||||
import { convertTo } from '../../../../../common/utils/formatters';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { AlertParams } from '../../types';
|
||||
import { getGroupFilters, getGroupQueries } from '../helpers/get_group';
|
||||
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';
|
||||
import { CustomThresholdAlert, CustomThresholdRule } from '../types';
|
||||
|
||||
const DEFAULT_INTERVAL = '1d';
|
||||
const SERIES_TYPE = 'bar_stacked';
|
||||
|
||||
interface Props {
|
||||
alert: CustomThresholdAlert;
|
||||
rule: CustomThresholdRule;
|
||||
dataView?: DataView;
|
||||
}
|
||||
|
||||
const dateRange = {
|
||||
from: 'now-30d',
|
||||
to: 'now',
|
||||
};
|
||||
|
||||
export function AlertHistoryChart({ rule, dataView, alert }: Props) {
|
||||
const { http, notifications } = useKibana().services;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const ruleParams = rule.params as RuleTypeParams & AlertParams;
|
||||
const criterion = rule.params.criteria[0];
|
||||
const groups = alert.fields[ALERT_GROUP];
|
||||
const featureIds = [rule.consumer as AlertConsumers];
|
||||
|
||||
const {
|
||||
data: { histogramTriggeredAlerts, avgTimeToRecoverUS, totalTriggeredAlerts },
|
||||
isLoading,
|
||||
isError,
|
||||
} = useAlertsHistory({
|
||||
http,
|
||||
featureIds,
|
||||
ruleId: rule.id,
|
||||
dateRange,
|
||||
queries: getGroupQueries(groups, ALERT_GROUP_VALUE),
|
||||
});
|
||||
|
||||
// Only show alert history chart if there is only one condition
|
||||
if (rule.params.criteria.length > 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
notifications?.toasts.addDanger({
|
||||
title: i18n.translate('xpack.observability.customThreshold.alertHistory.error.toastTitle', {
|
||||
defaultMessage: 'Alerts history chart error',
|
||||
}),
|
||||
text: i18n.translate(
|
||||
'xpack.observability.customThreshold.alertHistory.error.toastDescription',
|
||||
{
|
||||
defaultMessage: `An error occurred when fetching alert history chart data`,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const annotations: EventAnnotationConfig[] =
|
||||
histogramTriggeredAlerts
|
||||
?.filter((annotation) => annotation.doc_count > 0)
|
||||
.map((annotation) => {
|
||||
return {
|
||||
type: 'manual',
|
||||
id: uuidv4(),
|
||||
label: String(annotation.doc_count),
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: moment(new Date(annotation.key_as_string!)).toISOString(),
|
||||
},
|
||||
lineWidth: 2,
|
||||
color: euiTheme.colors.danger,
|
||||
icon: 'alert',
|
||||
textVisibility: true,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={true} data-test-subj="AlertDetails">
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.customThreshold.alertHistory.chartTitle', {
|
||||
defaultMessage: 'Alerts history',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
{i18n.translate('xpack.observability.customThreshold.alertHistory.last30days', {
|
||||
defaultMessage: 'Last 30 days',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="l">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="xs" direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="danger">
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
{isLoading ? <EuiLoadingSpinner size="s" /> : totalTriggeredAlerts || '-'}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
{i18n.translate(
|
||||
'xpack.observability.customThreshold.alertHistory.alertsTriggered',
|
||||
{
|
||||
defaultMessage: 'Alerts triggered',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="xs" direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
{isLoading ? (
|
||||
<EuiLoadingSpinner size="s" />
|
||||
) : avgTimeToRecoverUS ? (
|
||||
convertTo({
|
||||
unit: 'minutes',
|
||||
microseconds: avgTimeToRecoverUS,
|
||||
extended: true,
|
||||
}).formatted
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
{i18n.translate('xpack.observability.customThreshold.alertHistory.avgTimeToRecover', {
|
||||
defaultMessage: 'Avg time to recover',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<RuleConditionChart
|
||||
additionalFilters={getGroupFilters(groups)}
|
||||
annotations={annotations}
|
||||
chartOptions={{
|
||||
// For alert details page, the series type needs to be changed to 'bar_stacked'
|
||||
// due to https://github.com/elastic/elastic-charts/issues/2323
|
||||
seriesType: SERIES_TYPE,
|
||||
interval: DEFAULT_INTERVAL,
|
||||
}}
|
||||
dataView={dataView}
|
||||
groupBy={ruleParams.groupBy}
|
||||
metricExpression={criterion}
|
||||
searchConfiguration={ruleParams.searchConfiguration}
|
||||
timeRange={dateRange}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getFilterQuery } from './get_filter_query';
|
||||
|
||||
describe('getFilterQuery', () => {
|
||||
it('should generate correct filter query when original query is not empty', () => {
|
||||
const query = 'container.id: container-1';
|
||||
const groups = [
|
||||
{ field: 'container.id', value: 'container-0' },
|
||||
{ field: 'host.name', value: 'host-0' },
|
||||
];
|
||||
|
||||
expect(getFilterQuery(query, groups)).toBe(
|
||||
'(container.id: container-1) and container.id: container-0 and host.name: host-0'
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate correct filter query when original query is empty', () => {
|
||||
const query = '';
|
||||
const groups = [
|
||||
{ field: 'container.id', value: 'container-0' },
|
||||
{ field: 'host.name', value: 'host-0' },
|
||||
];
|
||||
|
||||
expect(getFilterQuery(query, groups)).toBe('container.id: container-0 and host.name: host-0');
|
||||
});
|
||||
|
||||
it('should generate correct filter query when original query and groups both are empty', () => {
|
||||
const query = '';
|
||||
const groups = undefined;
|
||||
|
||||
expect(getFilterQuery(query, groups)).toBe('');
|
||||
});
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const getFilterQuery = (
|
||||
filter: string,
|
||||
groups?: Array<{
|
||||
field: string;
|
||||
value: string;
|
||||
}>
|
||||
) => {
|
||||
let query = filter;
|
||||
if (groups) {
|
||||
const groupQueries = groups?.map(({ field, value }) => `${field}: ${value}`).join(' and ');
|
||||
query = query ? `(${query}) and ${groupQueries}` : groupQueries;
|
||||
}
|
||||
return query;
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getGroupQueries, getGroupFilters } from './get_group';
|
||||
|
||||
describe('getGroup', () => {
|
||||
describe('getGroupQueries', () => {
|
||||
it('should generate correct query with default field name', () => {
|
||||
const groups = [
|
||||
{ field: 'container.id', value: 'container-0' },
|
||||
{ field: 'host.name', value: 'host-0' },
|
||||
];
|
||||
|
||||
expect(getGroupQueries(groups)).toEqual([
|
||||
{ term: { 'container.id': { value: 'container-0' } } },
|
||||
{ term: { 'host.name': { value: 'host-0' } } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate correct query with custom field name', () => {
|
||||
const groups = [
|
||||
{ field: 'container.id', value: 'container-0' },
|
||||
{ field: 'host.name', value: 'host-0' },
|
||||
];
|
||||
const fieldName = 'custom.field';
|
||||
|
||||
expect(getGroupQueries(groups, fieldName)).toEqual([
|
||||
{ term: { 'custom.field': { value: 'container-0' } } },
|
||||
{ term: { 'custom.field': { value: 'host-0' } } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array when groups is empty', () => {
|
||||
const groups = undefined;
|
||||
|
||||
expect(getGroupQueries(groups)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupFilters', () => {
|
||||
it('should generate correct filter with default field name', () => {
|
||||
const groups = [
|
||||
{ field: 'container.id', value: 'container-0' },
|
||||
{ field: 'host.name', value: 'host-0' },
|
||||
];
|
||||
|
||||
expect(getGroupFilters(groups)).toEqual([
|
||||
{
|
||||
meta: {},
|
||||
query: { term: { 'container.id': { value: 'container-0' } } },
|
||||
},
|
||||
{
|
||||
meta: {},
|
||||
query: { term: { 'host.name': { value: 'host-0' } } },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate correct filter with custom field name', () => {
|
||||
const groups = [
|
||||
{ field: 'container.id', value: 'container-0' },
|
||||
{ field: 'host.name', value: 'host-0' },
|
||||
];
|
||||
const fieldName = 'custom.field';
|
||||
|
||||
expect(getGroupFilters(groups, fieldName)).toEqual([
|
||||
{
|
||||
meta: {},
|
||||
query: { term: { 'custom.field': { value: 'container-0' } } },
|
||||
},
|
||||
{
|
||||
meta: {},
|
||||
query: { term: { 'custom.field': { value: 'host-0' } } },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array when groups is empty', () => {
|
||||
const groups = undefined;
|
||||
|
||||
expect(getGroupFilters(groups)).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { Filter } from '@kbn/es-query';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { Group } from '../types';
|
||||
|
||||
/*
|
||||
* groupFieldName
|
||||
* In some cases, like AAD indices, the field name for group value is differen from group.field,
|
||||
* in AAD case, it is ALERT_GROUP_VALUE (`kibana.alert.group.value`). groupFieldName allows
|
||||
* passing a different field name to be used in the query.
|
||||
*/
|
||||
export const getGroupQueries = (
|
||||
groups?: Group[],
|
||||
groupFieldName?: string
|
||||
): QueryDslQueryContainer[] => {
|
||||
return (
|
||||
(groups &&
|
||||
groups.map((group) => ({
|
||||
term: {
|
||||
[groupFieldName || group.field]: {
|
||||
value: group.value,
|
||||
},
|
||||
},
|
||||
}))) ||
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
export const getGroupFilters = (groups?: Group[], groupFieldName?: string): Filter[] => {
|
||||
return getGroupQueries(groups, groupFieldName).map((query) => ({ meta: {}, query }));
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import { EuiEmptyPrompt, useEuiTheme } from '@elastic/eui';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { Query, Filter } from '@kbn/es-query';
|
||||
import { FillStyle, SeriesType } from '@kbn/lens-plugin/public';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -39,6 +39,11 @@ import {
|
|||
LensFieldFormat,
|
||||
} from './helpers';
|
||||
|
||||
interface ChartOptions {
|
||||
seriesType?: SeriesType;
|
||||
interval?: string;
|
||||
}
|
||||
|
||||
interface RuleConditionChartProps {
|
||||
metricExpression: MetricExpression;
|
||||
searchConfiguration: SerializedSearchSourceFields;
|
||||
|
@ -47,7 +52,8 @@ interface RuleConditionChartProps {
|
|||
error?: IErrorObject;
|
||||
timeRange: TimeRange;
|
||||
annotations?: EventAnnotationConfig[];
|
||||
seriesType?: SeriesType;
|
||||
chartOptions?: ChartOptions;
|
||||
additionalFilters?: Filter[];
|
||||
}
|
||||
|
||||
const defaultQuery: Query = {
|
||||
|
@ -63,7 +69,8 @@ export function RuleConditionChart({
|
|||
error,
|
||||
annotations,
|
||||
timeRange,
|
||||
seriesType,
|
||||
chartOptions: { seriesType, interval } = {},
|
||||
additionalFilters = [],
|
||||
}: RuleConditionChartProps) {
|
||||
const {
|
||||
services: { lens },
|
||||
|
@ -76,6 +83,7 @@ export function RuleConditionChart({
|
|||
const [thresholdReferenceLine, setThresholdReferenceLine] = useState<XYReferenceLinesLayer[]>();
|
||||
const [alertAnnotation, setAlertAnnotation] = useState<XYByValueAnnotationsLayer>();
|
||||
const [chartLoading, setChartLoading] = useState<boolean>(false);
|
||||
const filters = [...(searchConfiguration.filter || []), ...additionalFilters];
|
||||
const formulaAsync = useAsync(() => {
|
||||
return lens.stateHelperApi();
|
||||
}, [lens]);
|
||||
|
@ -231,7 +239,7 @@ export function RuleConditionChart({
|
|||
buckets: {
|
||||
type: 'date_histogram',
|
||||
params: {
|
||||
interval: `${timeSize}${timeUnit}`,
|
||||
interval: interval || `${timeSize}${timeUnit}`,
|
||||
},
|
||||
},
|
||||
seriesType: seriesType ? seriesType : 'bar',
|
||||
|
@ -295,6 +303,7 @@ export function RuleConditionChart({
|
|||
formula,
|
||||
formulaAsync.value,
|
||||
groupBy,
|
||||
interval,
|
||||
metrics,
|
||||
threshold,
|
||||
thresholdReferenceLine,
|
||||
|
@ -336,6 +345,7 @@ export function RuleConditionChart({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<lens.EmbeddableComponent
|
||||
|
@ -346,7 +356,7 @@ export function RuleConditionChart({
|
|||
attributes={attributes}
|
||||
disableTriggers={true}
|
||||
query={(searchConfiguration.query as Query) || defaultQuery}
|
||||
filters={searchConfiguration.filter}
|
||||
filters={filters}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { Rule } from '@kbn/alerting-plugin/common';
|
||||
import { TopAlert } from '../../..';
|
||||
import { CustomThresholdAlertFields, CustomThresholdRuleTypeParams } from '../types';
|
||||
|
||||
// TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690
|
||||
export type CustomThresholdRule = Rule<CustomThresholdRuleTypeParams>;
|
||||
export type CustomThresholdAlert = TopAlert<CustomThresholdAlertFields>;
|
||||
|
||||
export interface Group {
|
||||
field: string;
|
||||
value: string;
|
||||
}
|
|
@ -10,10 +10,7 @@ import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
|
|||
import { CustomThresholdAlertFields } from '../types';
|
||||
import { Aggregators, Comparator } from '../../../../common/custom_threshold_rule/types';
|
||||
|
||||
import {
|
||||
CustomThresholdAlert,
|
||||
CustomThresholdRule,
|
||||
} from '../components/alert_details_app_section/alert_details_app_section';
|
||||
import { CustomThresholdAlert, CustomThresholdRule } from '../components/types';
|
||||
|
||||
export const buildCustomThresholdRule = (
|
||||
rule: Partial<CustomThresholdRule> = {}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue