mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Save ES Query Rule type alerts in alert-as-data index (#161685)
Resolves: #159493 This PR replaces `AlertFactory` in ES Query rule type with `AlertsClient` so the alerts are persistent in an alert-as-data index. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4b8e9285cd
commit
458c67e8c4
20 changed files with 643 additions and 178 deletions
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
// ---------------------------------- WARNING ----------------------------------
|
||||
// this file was generated, and should not be edited by hand
|
||||
// ---------------------------------- WARNING ----------------------------------
|
||||
import * as rt from 'io-ts';
|
||||
import { Either } from 'fp-ts/lib/Either';
|
||||
import { AlertSchema } from './alert_schema';
|
||||
const ISO_DATE_PATTERN = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/;
|
||||
export const IsoDateString = new rt.Type<string, string, unknown>(
|
||||
'IsoDateString',
|
||||
rt.string.is,
|
||||
(input, context): Either<rt.Errors, string> => {
|
||||
if (typeof input === 'string' && ISO_DATE_PATTERN.test(input)) {
|
||||
return rt.success(input);
|
||||
} else {
|
||||
return rt.failure(input, context);
|
||||
}
|
||||
},
|
||||
rt.identity
|
||||
);
|
||||
export type IsoDateStringC = typeof IsoDateString;
|
||||
export const schemaDate = IsoDateString;
|
||||
export const schemaDateArray = rt.array(IsoDateString);
|
||||
export const schemaDateRange = rt.partial({
|
||||
gte: schemaDate,
|
||||
lte: schemaDate,
|
||||
});
|
||||
export const schemaDateRangeArray = rt.array(schemaDateRange);
|
||||
export const schemaUnknown = rt.unknown;
|
||||
export const schemaUnknownArray = rt.array(rt.unknown);
|
||||
export const schemaString = rt.string;
|
||||
export const schemaStringArray = rt.array(schemaString);
|
||||
export const schemaNumber = rt.number;
|
||||
export const schemaNumberArray = rt.array(schemaNumber);
|
||||
export const schemaStringOrNumber = rt.union([schemaString, schemaNumber]);
|
||||
export const schemaStringOrNumberArray = rt.array(schemaStringOrNumber);
|
||||
export const schemaBoolean = rt.boolean;
|
||||
export const schemaBooleanArray = rt.array(schemaBoolean);
|
||||
const schemaGeoPointCoords = rt.type({
|
||||
type: schemaString,
|
||||
coordinates: schemaNumberArray,
|
||||
});
|
||||
const schemaGeoPointString = schemaString;
|
||||
const schemaGeoPointLatLon = rt.type({
|
||||
lat: schemaNumber,
|
||||
lon: schemaNumber,
|
||||
});
|
||||
const schemaGeoPointLocation = rt.type({
|
||||
location: schemaNumberArray,
|
||||
});
|
||||
const schemaGeoPointLocationString = rt.type({
|
||||
location: schemaString,
|
||||
});
|
||||
export const schemaGeoPoint = rt.union([
|
||||
schemaGeoPointCoords,
|
||||
schemaGeoPointString,
|
||||
schemaGeoPointLatLon,
|
||||
schemaGeoPointLocation,
|
||||
schemaGeoPointLocationString,
|
||||
]);
|
||||
export const schemaGeoPointArray = rt.array(schemaGeoPoint);
|
||||
// prettier-ignore
|
||||
const StackAlertRequired = rt.type({
|
||||
});
|
||||
const StackAlertOptional = rt.partial({
|
||||
kibana: rt.partial({
|
||||
alert: rt.partial({
|
||||
evaluation: rt.partial({
|
||||
conditions: schemaString,
|
||||
value: schemaString,
|
||||
}),
|
||||
title: schemaString,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
// prettier-ignore
|
||||
export const StackAlertSchema = rt.intersection([StackAlertRequired, StackAlertOptional, AlertSchema]);
|
||||
// prettier-ignore
|
||||
export type StackAlert = rt.TypeOf<typeof StackAlertSchema>;
|
|
@ -23,6 +23,7 @@ export type { ObservabilityMetricsAlert } from './generated/observability_metric
|
|||
export type { ObservabilitySloAlert } from './generated/observability_slo_schema';
|
||||
export type { ObservabilityUptimeAlert } from './generated/observability_uptime_schema';
|
||||
export type { SecurityAlert } from './generated/security_schema';
|
||||
export type { StackAlert } from './generated/stack_schema';
|
||||
|
||||
export type AADAlert =
|
||||
| Alert
|
||||
|
|
|
@ -30,9 +30,11 @@ const createPublicAlertsClientMock = () => {
|
|||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
create: jest.fn(),
|
||||
getAlertLimitValue: jest.fn(),
|
||||
report: jest.fn(),
|
||||
getAlertLimitValue: jest.fn().mockReturnValue(1000),
|
||||
setAlertLimitReached: jest.fn(),
|
||||
getRecoveredAlerts: jest.fn(),
|
||||
getRecoveredAlerts: jest.fn().mockReturnValue([]),
|
||||
setAlertData: jest.fn(),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -14,4 +14,5 @@ export {
|
|||
getHitsWithCount,
|
||||
getLifecycleAlertsQueries,
|
||||
getContinualAlertsQuery,
|
||||
expandFlattenedAlert,
|
||||
} from './get_summarized_alerts_query';
|
||||
|
|
|
@ -6,11 +6,16 @@
|
|||
*/
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { ALERT_REASON, ALERT_WORKFLOW_STATUS, TAGS } from '@kbn/rule-data-utils';
|
||||
import { ALERT_REASON, ALERT_WORKFLOW_STATUS, TAGS, ALERT_URL } from '@kbn/rule-data-utils';
|
||||
import { alertFieldMap } from '@kbn/alerts-as-data-utils';
|
||||
import { RuleAlertData } from '../../types';
|
||||
|
||||
const allowedFrameworkFields = new Set<string>([ALERT_REASON, ALERT_WORKFLOW_STATUS, TAGS]);
|
||||
const allowedFrameworkFields = new Set<string>([
|
||||
ALERT_REASON,
|
||||
ALERT_WORKFLOW_STATUS,
|
||||
TAGS,
|
||||
ALERT_URL,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Remove framework fields from the alert payload reported by
|
||||
|
|
|
@ -292,7 +292,7 @@ export class ExecutionHandler<
|
|||
alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!,
|
||||
context: alert.getContext(),
|
||||
actionId: action.id,
|
||||
state: alert.getScheduledActionOptions()?.state || {},
|
||||
state: alert.getState(),
|
||||
kibanaBaseUrl: this.taskRunnerContext.kibanaBaseUrl,
|
||||
alertParams: this.rule.params,
|
||||
actionParams: action.params,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 STACK_AAD_INDEX_NAME = 'stack';
|
|
@ -45,11 +45,19 @@ jest.mock('./lib/fetch_search_source_query', () => ({
|
|||
mockFetchSearchSourceQuery(...args),
|
||||
}));
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const replaceState = jest.fn(() => ({ scheduleActions }));
|
||||
const mockCreateAlert = jest.fn(() => ({ replaceState }));
|
||||
const mockGetRecoveredAlerts = jest.fn().mockReturnValue([]);
|
||||
const mockSetLimitReached = jest.fn();
|
||||
const mockReport = jest.fn();
|
||||
const mockSetAlertData = jest.fn();
|
||||
const mockGetAlertLimitValue = jest.fn().mockReturnValue(1000);
|
||||
|
||||
const mockAlertClient = {
|
||||
report: mockReport,
|
||||
getAlertLimitValue: mockGetAlertLimitValue,
|
||||
setAlertLimitReached: mockSetLimitReached,
|
||||
getRecoveredAlerts: mockGetRecoveredAlerts,
|
||||
setAlertData: mockSetAlertData,
|
||||
};
|
||||
|
||||
const mockNow = jest.getRealSystemTime();
|
||||
|
||||
|
@ -87,16 +95,7 @@ describe('es_query executor', () => {
|
|||
get: () => ({ attributes: { consumer: 'alerts' } }),
|
||||
},
|
||||
searchSourceClient: searchSourceClientMock,
|
||||
alertFactory: {
|
||||
create: mockCreateAlert,
|
||||
alertLimit: {
|
||||
getValue: jest.fn().mockReturnValue(1000),
|
||||
setLimitReached: mockSetLimitReached,
|
||||
},
|
||||
done: () => ({
|
||||
getRecoveredAlerts: mockGetRecoveredAlerts,
|
||||
}),
|
||||
},
|
||||
alertsClient: mockAlertClient,
|
||||
alertWithLifecycle: jest.fn(),
|
||||
logger,
|
||||
shouldWriteAlerts: () => true,
|
||||
|
@ -210,7 +209,7 @@ describe('es_query executor', () => {
|
|||
params: { ...defaultProps, threshold: [500], thresholdComparator: '>=' as Comparator },
|
||||
});
|
||||
|
||||
expect(mockCreateAlert).not.toHaveBeenCalled();
|
||||
expect(mockReport).not.toHaveBeenCalled();
|
||||
expect(mockSetLimitReached).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetLimitReached).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
@ -237,22 +236,47 @@ describe('es_query executor', () => {
|
|||
params: { ...defaultProps, threshold: [200], thresholdComparator: '>=' as Comparator },
|
||||
});
|
||||
|
||||
expect(mockCreateAlert).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateAlert).toHaveBeenNthCalledWith(1, 'query matched');
|
||||
expect(scheduleActions).toHaveBeenCalledTimes(1);
|
||||
expect(scheduleActions).toHaveBeenNthCalledWith(1, 'query matched', {
|
||||
conditions: 'Number of matching documents is greater than or equal to 200',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is active:
|
||||
expect(mockReport).toHaveBeenCalledTimes(1);
|
||||
expect(mockReport).toHaveBeenNthCalledWith(1, {
|
||||
actionGroup: 'query matched',
|
||||
context: {
|
||||
conditions: 'Number of matching documents is greater than or equal to 200',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is active:
|
||||
|
||||
- Value: 491
|
||||
- Conditions Met: Number of matching documents is greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' matched query",
|
||||
value: 491,
|
||||
title: "rule 'test-rule-name' matched query",
|
||||
value: 491,
|
||||
},
|
||||
id: 'query matched',
|
||||
state: {
|
||||
dateEnd: new Date(mockNow).toISOString(),
|
||||
dateStart: new Date(mockNow).toISOString(),
|
||||
latestTimestamp: undefined,
|
||||
},
|
||||
payload: {
|
||||
kibana: {
|
||||
alert: {
|
||||
url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
reason: `rule 'test-rule-name' is active:
|
||||
|
||||
- Value: 491
|
||||
- Conditions Met: Number of matching documents is greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' matched query",
|
||||
evaluation: {
|
||||
conditions: 'Number of matching documents is greater than or equal to 200',
|
||||
value: 491,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockSetLimitReached).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetLimitReached).toHaveBeenCalledWith(false);
|
||||
|
@ -297,55 +321,135 @@ describe('es_query executor', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(mockCreateAlert).toHaveBeenCalledTimes(3);
|
||||
expect(mockCreateAlert).toHaveBeenNthCalledWith(1, 'host-1');
|
||||
expect(mockCreateAlert).toHaveBeenNthCalledWith(2, 'host-2');
|
||||
expect(mockCreateAlert).toHaveBeenNthCalledWith(3, 'host-3');
|
||||
expect(scheduleActions).toHaveBeenCalledTimes(3);
|
||||
expect(scheduleActions).toHaveBeenNthCalledWith(1, 'query matched', {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-1" is greater than or equal to 200',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is active:
|
||||
expect(mockReport).toHaveBeenCalledTimes(3);
|
||||
expect(mockReport).toHaveBeenNthCalledWith(1, {
|
||||
actionGroup: 'query matched',
|
||||
context: {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-1" is greater than or equal to 200',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is active:
|
||||
|
||||
- Value: 291
|
||||
- Conditions Met: Number of matching documents for group "host-1" is greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' matched query for group host-1",
|
||||
value: 291,
|
||||
title: "rule 'test-rule-name' matched query for group host-1",
|
||||
value: 291,
|
||||
},
|
||||
id: 'host-1',
|
||||
state: {
|
||||
dateEnd: new Date(mockNow).toISOString(),
|
||||
dateStart: new Date(mockNow).toISOString(),
|
||||
latestTimestamp: undefined,
|
||||
},
|
||||
payload: {
|
||||
kibana: {
|
||||
alert: {
|
||||
url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
reason: `rule 'test-rule-name' is active:
|
||||
|
||||
- Value: 291
|
||||
- Conditions Met: Number of matching documents for group "host-1" is greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' matched query for group host-1",
|
||||
evaluation: {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-1" is greater than or equal to 200',
|
||||
value: 291,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenNthCalledWith(2, 'query matched', {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-2" is greater than or equal to 200',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is active:
|
||||
expect(mockReport).toHaveBeenNthCalledWith(2, {
|
||||
actionGroup: 'query matched',
|
||||
context: {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-2" is greater than or equal to 200',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is active:
|
||||
|
||||
- Value: 477
|
||||
- Conditions Met: Number of matching documents for group "host-2" is greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' matched query for group host-2",
|
||||
value: 477,
|
||||
title: "rule 'test-rule-name' matched query for group host-2",
|
||||
value: 477,
|
||||
},
|
||||
id: 'host-2',
|
||||
state: {
|
||||
dateEnd: new Date(mockNow).toISOString(),
|
||||
dateStart: new Date(mockNow).toISOString(),
|
||||
latestTimestamp: undefined,
|
||||
},
|
||||
payload: {
|
||||
kibana: {
|
||||
alert: {
|
||||
url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
reason: `rule 'test-rule-name' is active:
|
||||
|
||||
- Value: 477
|
||||
- Conditions Met: Number of matching documents for group "host-2" is greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' matched query for group host-2",
|
||||
evaluation: {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-2" is greater than or equal to 200',
|
||||
value: 477,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenNthCalledWith(3, 'query matched', {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-3" is greater than or equal to 200',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is active:
|
||||
expect(mockReport).toHaveBeenNthCalledWith(3, {
|
||||
actionGroup: 'query matched',
|
||||
context: {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-3" is greater than or equal to 200',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is active:
|
||||
|
||||
- Value: 999
|
||||
- Conditions Met: Number of matching documents for group "host-3" is greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' matched query for group host-3",
|
||||
value: 999,
|
||||
title: "rule 'test-rule-name' matched query for group host-3",
|
||||
value: 999,
|
||||
},
|
||||
id: 'host-3',
|
||||
state: {
|
||||
dateEnd: new Date(mockNow).toISOString(),
|
||||
dateStart: new Date(mockNow).toISOString(),
|
||||
latestTimestamp: undefined,
|
||||
},
|
||||
payload: {
|
||||
kibana: {
|
||||
alert: {
|
||||
url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
reason: `rule 'test-rule-name' is active:
|
||||
|
||||
- Value: 999
|
||||
- Conditions Met: Number of matching documents for group \"host-3\" is greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' matched query for group host-3",
|
||||
evaluation: {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-3" is greater than or equal to 200',
|
||||
value: 999,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockSetLimitReached).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetLimitReached).toHaveBeenCalledWith(false);
|
||||
|
@ -389,21 +493,20 @@ describe('es_query executor', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(mockCreateAlert).toHaveBeenCalledTimes(3);
|
||||
expect(mockCreateAlert).toHaveBeenNthCalledWith(1, 'host-1');
|
||||
expect(mockCreateAlert).toHaveBeenNthCalledWith(2, 'host-2');
|
||||
expect(mockCreateAlert).toHaveBeenNthCalledWith(3, 'host-3');
|
||||
expect(scheduleActions).toHaveBeenCalledTimes(3);
|
||||
expect(mockReport).toHaveBeenCalledTimes(3);
|
||||
expect(mockReport).toHaveBeenNthCalledWith(1, expect.objectContaining({ id: 'host-1' }));
|
||||
expect(mockReport).toHaveBeenNthCalledWith(2, expect.objectContaining({ id: 'host-2' }));
|
||||
expect(mockReport).toHaveBeenNthCalledWith(3, expect.objectContaining({ id: 'host-3' }));
|
||||
expect(mockSetLimitReached).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetLimitReached).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should correctly handle recovered alerts for ungrouped alert', async () => {
|
||||
const mockSetContext = jest.fn();
|
||||
mockGetRecoveredAlerts.mockReturnValueOnce([
|
||||
{
|
||||
getId: () => 'query matched',
|
||||
setContext: mockSetContext,
|
||||
alert: {
|
||||
getId: () => 'query matched',
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockFetchEsQuery.mockResolvedValueOnce({
|
||||
|
@ -427,36 +530,58 @@ describe('es_query executor', () => {
|
|||
params: { ...defaultProps, threshold: [500], thresholdComparator: '>=' as Comparator },
|
||||
});
|
||||
|
||||
expect(mockCreateAlert).not.toHaveBeenCalled();
|
||||
expect(mockSetContext).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetContext).toHaveBeenNthCalledWith(1, {
|
||||
conditions: 'Number of matching documents is NOT greater than or equal to 500',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is recovered:
|
||||
expect(mockReport).not.toHaveBeenCalled();
|
||||
expect(mockSetAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetAlertData).toHaveBeenNthCalledWith(1, {
|
||||
id: 'query matched',
|
||||
context: {
|
||||
conditions: 'Number of matching documents is NOT greater than or equal to 500',
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is recovered:
|
||||
|
||||
- Value: 0
|
||||
- Conditions Met: Number of matching documents is NOT greater than or equal to 500 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' recovered",
|
||||
value: 0,
|
||||
title: "rule 'test-rule-name' recovered",
|
||||
value: 0,
|
||||
},
|
||||
payload: {
|
||||
kibana: {
|
||||
alert: {
|
||||
evaluation: {
|
||||
conditions: 'Number of matching documents is NOT greater than or equal to 500',
|
||||
value: 0,
|
||||
},
|
||||
reason: `rule 'test-rule-name' is recovered:
|
||||
|
||||
- Value: 0
|
||||
- Conditions Met: Number of matching documents is NOT greater than or equal to 500 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' recovered",
|
||||
url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockSetLimitReached).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetLimitReached).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should correctly handle recovered alerts for grouped alerts', async () => {
|
||||
const mockSetContext = jest.fn();
|
||||
mockGetRecoveredAlerts.mockReturnValueOnce([
|
||||
{
|
||||
getId: () => 'host-1',
|
||||
setContext: mockSetContext,
|
||||
alert: {
|
||||
getId: () => 'host-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
getId: () => 'host-2',
|
||||
setContext: mockSetContext,
|
||||
alert: {
|
||||
getId: () => 'host-2',
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockFetchEsQuery.mockResolvedValueOnce({
|
||||
|
@ -478,35 +603,79 @@ describe('es_query executor', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(mockCreateAlert).not.toHaveBeenCalled();
|
||||
expect(mockSetContext).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetContext).toHaveBeenNthCalledWith(1, {
|
||||
conditions: `Number of matching documents for group "host-1" is NOT greater than or equal to 200`,
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is recovered:
|
||||
expect(mockReport).not.toHaveBeenCalled();
|
||||
expect(mockSetAlertData).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetAlertData).toHaveBeenNthCalledWith(1, {
|
||||
id: 'host-1',
|
||||
context: {
|
||||
conditions: `Number of matching documents for group "host-1" is NOT greater than or equal to 200`,
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is recovered:
|
||||
|
||||
- Value: 0
|
||||
- Conditions Met: Number of matching documents for group "host-1" is NOT greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' recovered",
|
||||
value: 0,
|
||||
title: "rule 'test-rule-name' recovered",
|
||||
value: 0,
|
||||
},
|
||||
payload: {
|
||||
kibana: {
|
||||
alert: {
|
||||
evaluation: {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-1" is NOT greater than or equal to 200',
|
||||
value: 0,
|
||||
},
|
||||
reason: `rule 'test-rule-name' is recovered:
|
||||
|
||||
- Value: 0
|
||||
- Conditions Met: Number of matching documents for group \"host-1\" is NOT greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' recovered",
|
||||
url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockSetContext).toHaveBeenNthCalledWith(2, {
|
||||
conditions: `Number of matching documents for group "host-2" is NOT greater than or equal to 200`,
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is recovered:
|
||||
expect(mockSetAlertData).toHaveBeenNthCalledWith(2, {
|
||||
id: 'host-2',
|
||||
context: {
|
||||
conditions: `Number of matching documents for group "host-2" is NOT greater than or equal to 200`,
|
||||
date: new Date(mockNow).toISOString(),
|
||||
hits: [],
|
||||
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
message: `rule 'test-rule-name' is recovered:
|
||||
|
||||
- Value: 0
|
||||
- Conditions Met: Number of matching documents for group "host-2" is NOT greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' recovered",
|
||||
value: 0,
|
||||
title: "rule 'test-rule-name' recovered",
|
||||
value: 0,
|
||||
},
|
||||
payload: {
|
||||
kibana: {
|
||||
alert: {
|
||||
evaluation: {
|
||||
conditions:
|
||||
'Number of matching documents for group "host-2" is NOT greater than or equal to 200',
|
||||
value: 0,
|
||||
},
|
||||
reason: `rule 'test-rule-name' is recovered:
|
||||
|
||||
- Value: 0
|
||||
- Conditions Met: Number of matching documents for group \"host-2\" is NOT greater than or equal to 200 over 5m
|
||||
- Timestamp: ${new Date(mockNow).toISOString()}
|
||||
- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`,
|
||||
title: "rule 'test-rule-name' recovered",
|
||||
url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockSetLimitReached).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetLimitReached).toHaveBeenCalledWith(false);
|
||||
|
|
|
@ -9,6 +9,10 @@ import { i18n } from '@kbn/i18n';
|
|||
import { CoreSetup } from '@kbn/core/server';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/server';
|
||||
import { isGroupAggregation, UngroupedGroupId } from '@kbn/triggers-actions-ui-plugin/common';
|
||||
import { ALERT_EVALUATION_VALUE, ALERT_REASON, ALERT_URL } from '@kbn/rule-data-utils';
|
||||
|
||||
import { expandFlattenedAlert } from '@kbn/alerting-plugin/server/alerts_client/lib';
|
||||
import { ALERT_TITLE, ALERT_EVALUATION_CONDITIONS } from './fields';
|
||||
import { ComparatorFns } from '../../../common';
|
||||
import {
|
||||
addMessages,
|
||||
|
@ -32,11 +36,11 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
spaceId,
|
||||
logger,
|
||||
} = options;
|
||||
const { alertFactory, scopedClusterClient, searchSourceClient, share, dataViews } = services;
|
||||
const { alertsClient, scopedClusterClient, searchSourceClient, share, dataViews } = services;
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
const publicBaseUrl = core.http.basePath.publicBaseUrl ?? '';
|
||||
const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : '';
|
||||
const alertLimit = alertFactory.alertLimit.getValue();
|
||||
const alertLimit = alertsClient?.getAlertLimitValue();
|
||||
const compareFn = ComparatorFns.get(params.thresholdComparator);
|
||||
if (compareFn == null) {
|
||||
throw new Error(getInvalidComparatorError(params.thresholdComparator));
|
||||
|
@ -108,19 +112,29 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
...(isGroupAgg ? { group: alertId } : {}),
|
||||
}),
|
||||
} as EsQueryRuleActionContext;
|
||||
|
||||
const actionContext = addMessages({
|
||||
ruleName: name,
|
||||
baseContext: baseActiveContext,
|
||||
params,
|
||||
...(isGroupAgg ? { group: alertId } : {}),
|
||||
});
|
||||
const alert = alertFactory.create(
|
||||
alertId === UngroupedGroupId && !isGroupAgg ? ConditionMetAlertInstanceId : alertId
|
||||
);
|
||||
alert
|
||||
// store the params we would need to recreate the query that led to this alert instance
|
||||
.replaceState({ latestTimestamp, dateStart, dateEnd })
|
||||
.scheduleActions(ActionGroupId, actionContext);
|
||||
|
||||
const id = alertId === UngroupedGroupId && !isGroupAgg ? ConditionMetAlertInstanceId : alertId;
|
||||
|
||||
alertsClient!.report({
|
||||
id,
|
||||
actionGroup: ActionGroupId,
|
||||
state: { latestTimestamp, dateStart, dateEnd },
|
||||
context: actionContext,
|
||||
payload: expandFlattenedAlert({
|
||||
[ALERT_URL]: actionContext.link,
|
||||
[ALERT_REASON]: actionContext.message,
|
||||
[ALERT_TITLE]: actionContext.title,
|
||||
[ALERT_EVALUATION_CONDITIONS]: actionContext.conditions,
|
||||
[ALERT_EVALUATION_VALUE]: actionContext.value,
|
||||
}),
|
||||
});
|
||||
if (!isGroupAgg) {
|
||||
// update the timestamp based on the current search results
|
||||
const firstValidTimefieldSort = getValidTimefieldSort(
|
||||
|
@ -131,12 +145,11 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
}
|
||||
}
|
||||
}
|
||||
alertsClient!.setAlertLimitReached(parsedResults.truncated);
|
||||
|
||||
alertFactory.alertLimit.setLimitReached(parsedResults.truncated);
|
||||
|
||||
const { getRecoveredAlerts } = alertFactory.done();
|
||||
const { getRecoveredAlerts } = alertsClient!;
|
||||
for (const recoveredAlert of getRecoveredAlerts()) {
|
||||
const alertId = recoveredAlert.getId();
|
||||
const alertId = recoveredAlert.alert.getId();
|
||||
const baseRecoveryContext: EsQueryRuleActionContext = {
|
||||
title: name,
|
||||
date: currentTimestamp,
|
||||
|
@ -159,7 +172,17 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
isRecovered: true,
|
||||
...(isGroupAgg ? { group: alertId } : {}),
|
||||
});
|
||||
recoveredAlert.setContext(recoveryContext);
|
||||
alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: recoveryContext,
|
||||
payload: expandFlattenedAlert({
|
||||
[ALERT_URL]: recoveryContext.link,
|
||||
[ALERT_REASON]: recoveryContext.message,
|
||||
[ALERT_TITLE]: recoveryContext.title,
|
||||
[ALERT_EVALUATION_CONDITIONS]: recoveryContext.conditions,
|
||||
[ALERT_EVALUATION_VALUE]: recoveryContext.value,
|
||||
}),
|
||||
});
|
||||
}
|
||||
return { state: { latestTimestamp } };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { ALERT_NAMESPACE } from '@kbn/rule-data-utils';
|
||||
|
||||
const ALERT_TITLE = `${ALERT_NAMESPACE}.title` as const;
|
||||
// kibana.alert.evaluation.conditions - human readable string that shows the consditions set by the user
|
||||
const ALERT_EVALUATION_CONDITIONS = `${ALERT_NAMESPACE}.evaluation.conditions` as const;
|
||||
|
||||
export { ALERT_TITLE, ALERT_EVALUATION_CONDITIONS };
|
|
@ -8,11 +8,7 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Writable } from '@kbn/utility-types';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import {
|
||||
RuleExecutorServicesMock,
|
||||
alertsMock,
|
||||
AlertInstanceMock,
|
||||
} from '@kbn/alerting-plugin/server/mocks';
|
||||
import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import type { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
|
@ -31,8 +27,23 @@ import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_set
|
|||
const logger = loggingSystemMock.create().get();
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const ruleType = getRuleType(coreSetup);
|
||||
const mockNow = jest.getRealSystemTime();
|
||||
|
||||
describe('ruleType', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(mockNow);
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('rule type creation structure is the expected value', async () => {
|
||||
expect(ruleType.id).toBe('.es-query');
|
||||
expect(ruleType.name).toBe('Elasticsearch query');
|
||||
|
@ -168,7 +179,7 @@ describe('ruleType', () => {
|
|||
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
expect(ruleServices.alertFactory.create).not.toHaveBeenCalled();
|
||||
expect(ruleServices.alertsClient.report).not.toHaveBeenCalled();
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -215,13 +226,17 @@ describe('ruleType', () => {
|
|||
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
expect(ruleServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId);
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
});
|
||||
expect(ruleServices.alertsClient.report).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ConditionMetAlertInstanceId,
|
||||
actionGroup: ActionGroupId,
|
||||
state: {
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
state: {
|
||||
|
@ -269,13 +284,18 @@ describe('ruleType', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
// ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward
|
||||
latestTimestamp: new Date(previousTimestamp).toISOString(),
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
});
|
||||
expect(ruleServices.alertsClient.report).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ConditionMetAlertInstanceId,
|
||||
actionGroup: ActionGroupId,
|
||||
state: {
|
||||
// ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward
|
||||
latestTimestamp: new Date(previousTimestamp).toISOString(),
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
state: {
|
||||
|
@ -318,12 +338,17 @@ describe('ruleType', () => {
|
|||
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
});
|
||||
expect(ruleServices.alertsClient.report).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ConditionMetAlertInstanceId,
|
||||
actionGroup: ActionGroupId,
|
||||
state: {
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
state: {
|
||||
|
@ -363,12 +388,17 @@ describe('ruleType', () => {
|
|||
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
});
|
||||
expect(ruleServices.alertsClient.report).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ConditionMetAlertInstanceId,
|
||||
actionGroup: ActionGroupId,
|
||||
state: {
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(result?.state).toMatchObject({
|
||||
latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(),
|
||||
|
@ -394,13 +424,17 @@ describe('ruleType', () => {
|
|||
state: result?.state as EsQueryRuleState,
|
||||
});
|
||||
|
||||
const existingInstance: AlertInstanceMock =
|
||||
ruleServices.alertFactory.create.mock.results[1].value;
|
||||
expect(existingInstance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(),
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
});
|
||||
expect(ruleServices.alertsClient.report).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ConditionMetAlertInstanceId,
|
||||
actionGroup: ActionGroupId,
|
||||
state: {
|
||||
latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(),
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(secondResult).toMatchObject({
|
||||
state: {
|
||||
|
@ -446,12 +480,17 @@ describe('ruleType', () => {
|
|||
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
});
|
||||
expect(ruleServices.alertsClient.report).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ConditionMetAlertInstanceId,
|
||||
actionGroup: ActionGroupId,
|
||||
state: {
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
state: {
|
||||
|
@ -498,12 +537,17 @@ describe('ruleType', () => {
|
|||
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
});
|
||||
expect(ruleServices.alertsClient.report).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ConditionMetAlertInstanceId,
|
||||
actionGroup: ActionGroupId,
|
||||
state: {
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
dateEnd: expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
state: {
|
||||
|
@ -599,7 +643,7 @@ describe('ruleType', () => {
|
|||
|
||||
await invokeExecutor({ params, ruleServices });
|
||||
|
||||
expect(ruleServices.alertFactory.create).not.toHaveBeenCalled();
|
||||
expect(ruleServices.alertsClient.report).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rule executor throws an error when index does not have time field', async () => {
|
||||
|
@ -637,10 +681,33 @@ describe('ruleType', () => {
|
|||
hits: { total: 3, hits: [{}, {}, {}] },
|
||||
});
|
||||
|
||||
await invokeExecutor({ params, ruleServices });
|
||||
await invokeExecutor({
|
||||
params,
|
||||
ruleServices,
|
||||
state: { latestTimestamp: new Date(mockNow).toISOString(), dateStart: '', dateEnd: '' },
|
||||
});
|
||||
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.scheduleActions).toHaveBeenCalled();
|
||||
expect(ruleServices.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(ruleServices.alertsClient.report).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
actionGroup: 'query matched',
|
||||
id: 'query matched',
|
||||
payload: expect.objectContaining({
|
||||
kibana: {
|
||||
alert: {
|
||||
url: expect.any(String),
|
||||
reason: expect.any(String),
|
||||
title: "rule 'rule-name' matched query",
|
||||
evaluation: {
|
||||
conditions: 'Number of matching documents is greater than or equal to 3',
|
||||
value: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -711,7 +778,7 @@ async function invokeExecutor({
|
|||
spaceId: uuidv4(),
|
||||
rule: {
|
||||
id: uuidv4(),
|
||||
name: uuidv4(),
|
||||
name: 'rule-name',
|
||||
tags: [],
|
||||
consumer: '',
|
||||
producer: '',
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreSetup } from '@kbn/core/server';
|
||||
import { extractReferences, injectReferences } from '@kbn/data-plugin/common';
|
||||
import { IRuleTypeAlerts } from '@kbn/alerting-plugin/server';
|
||||
import { ALERT_EVALUATION_VALUE } from '@kbn/rule-data-utils';
|
||||
import { StackAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { STACK_AAD_INDEX_NAME } from '..';
|
||||
import { ALERT_TITLE, ALERT_EVALUATION_CONDITIONS } from './fields';
|
||||
import { RuleType } from '../../types';
|
||||
import { ActionContext } from './action_context';
|
||||
import {
|
||||
|
@ -30,7 +35,9 @@ export function getRuleType(
|
|||
EsQueryRuleState,
|
||||
{},
|
||||
ActionContext,
|
||||
typeof ActionGroupId
|
||||
typeof ActionGroupId,
|
||||
never,
|
||||
StackAlert
|
||||
> {
|
||||
const ruleTypeName = i18n.translate('xpack.stackAlerts.esQuery.alertTypeTitle', {
|
||||
defaultMessage: 'Elasticsearch query',
|
||||
|
@ -135,6 +142,18 @@ export function getRuleType(
|
|||
}
|
||||
);
|
||||
|
||||
const alerts: IRuleTypeAlerts<StackAlert> = {
|
||||
context: STACK_AAD_INDEX_NAME,
|
||||
mappings: {
|
||||
fieldMap: {
|
||||
[ALERT_TITLE]: { type: 'keyword', array: false, required: false },
|
||||
[ALERT_EVALUATION_CONDITIONS]: { type: 'keyword', array: false, required: false },
|
||||
[ALERT_EVALUATION_VALUE]: { type: 'keyword', array: false, required: false },
|
||||
},
|
||||
},
|
||||
shouldWrite: true,
|
||||
};
|
||||
|
||||
return {
|
||||
id: ES_QUERY_ID,
|
||||
name: ruleTypeName,
|
||||
|
@ -188,5 +207,6 @@ export function getRuleType(
|
|||
},
|
||||
producer: STACK_ALERTS_FEATURE_ID,
|
||||
doesSetRecoveryContext: true,
|
||||
alerts,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { StackAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { RuleExecutorOptions, RuleTypeParams } from '../../types';
|
||||
import { ActionContext } from './action_context';
|
||||
import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params';
|
||||
|
@ -24,5 +25,6 @@ export type ExecutorOptions<P extends RuleTypeParams> = RuleExecutorOptions<
|
|||
EsQueryRuleState,
|
||||
{},
|
||||
ActionContext,
|
||||
typeof ActionGroupId
|
||||
typeof ActionGroupId,
|
||||
StackAlert
|
||||
>;
|
||||
|
|
|
@ -10,6 +10,8 @@ import { register as registerIndexThreshold } from './index_threshold';
|
|||
import { register as registerGeoContainment } from './geo_containment';
|
||||
import { register as registerEsQuery } from './es_query';
|
||||
|
||||
export * from './constants';
|
||||
|
||||
export function registerBuiltInRuleTypes(params: RegisterRuleTypesParams) {
|
||||
registerIndexThreshold(params);
|
||||
registerGeoContainment(params);
|
||||
|
|
|
@ -42,6 +42,8 @@
|
|||
"@kbn/logging-mocks",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/rule-data-utils",
|
||||
"@kbn/alerts-as-data-utils",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -110,6 +110,31 @@ export class ESTestIndexTool {
|
|||
return await this.es.search(params, { meta: true });
|
||||
}
|
||||
|
||||
async getAll(size: number = 10) {
|
||||
const params = {
|
||||
index: this.index,
|
||||
size,
|
||||
body: {
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
return await this.es.search(params, { meta: true });
|
||||
}
|
||||
|
||||
async removeAll() {
|
||||
const params = {
|
||||
index: this.index,
|
||||
body: {
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
return await this.es.deleteByQuery(params);
|
||||
}
|
||||
|
||||
async waitForDocs(source: string, reference: string, numDocs: number = 1) {
|
||||
return await this.retry.try(async () => {
|
||||
const searchResult = await this.search(source, reference);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
|
||||
import { STACK_AAD_INDEX_NAME } from '@kbn/stack-alerts-plugin/server/rule_types';
|
||||
import { FtrProviderContext } from '../../../../../../common/ftr_provider_context';
|
||||
import { Spaces } from '../../../../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../../../../common/lib';
|
||||
|
@ -69,6 +70,11 @@ export function getRuleServices(getService: FtrProviderContext['getService']) {
|
|||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME);
|
||||
const esTestIndexToolDataStream = new ESTestIndexTool(es, retry, ES_TEST_DATA_STREAM_NAME);
|
||||
const esTestIndexToolAAD = new ESTestIndexTool(
|
||||
es,
|
||||
retry,
|
||||
`.internal.alerts-${STACK_AAD_INDEX_NAME}.alerts-default-000001`
|
||||
);
|
||||
|
||||
async function createEsDocumentsInGroups(
|
||||
groups: number,
|
||||
|
@ -112,6 +118,14 @@ export function getRuleServices(getService: FtrProviderContext['getService']) {
|
|||
);
|
||||
}
|
||||
|
||||
async function getAllAADDocs(size: number): Promise<any> {
|
||||
return await esTestIndexToolAAD.getAll(size);
|
||||
}
|
||||
|
||||
async function removeAllAADDocs(): Promise<any> {
|
||||
return await esTestIndexToolAAD.removeAll();
|
||||
}
|
||||
|
||||
return {
|
||||
retry,
|
||||
es,
|
||||
|
@ -121,5 +135,7 @@ export function getRuleServices(getService: FtrProviderContext['getService']) {
|
|||
createEsDocumentsInGroups,
|
||||
createGroupedEsDocumentsInGroups,
|
||||
waitForDocs,
|
||||
getAllAADDocs,
|
||||
removeAllAADDocs,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
esTestIndexToolDataStream,
|
||||
createEsDocumentsInGroups,
|
||||
createGroupedEsDocumentsInGroups,
|
||||
removeAllAADDocs,
|
||||
getAllAADDocs,
|
||||
} = getRuleServices(getService);
|
||||
|
||||
describe('rule', async () => {
|
||||
|
@ -66,6 +68,7 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
await esTestIndexTool.destroy();
|
||||
await esTestIndexToolOutput.destroy();
|
||||
await deleteDataStream(es, ES_TEST_DATA_STREAM_NAME);
|
||||
await removeAllAADDocs();
|
||||
});
|
||||
|
||||
[
|
||||
|
@ -135,6 +138,9 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
await initData();
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
const messagePattern =
|
||||
/rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
const { previousTimestamp, hits } = doc._source;
|
||||
|
@ -142,8 +148,6 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(name).to.be('always fire');
|
||||
expect(title).to.be(`rule 'always fire' matched query`);
|
||||
const messagePattern =
|
||||
/rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
expect(message).to.match(messagePattern);
|
||||
expect(hits).not.to.be.empty();
|
||||
|
||||
|
@ -155,6 +159,17 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
expect(previousTimestamp).not.to.be.empty();
|
||||
}
|
||||
}
|
||||
|
||||
const aadDocs = await getAllAADDocs(1);
|
||||
|
||||
const alertDoc = aadDocs.body.hits.hits[0]._source.kibana.alert;
|
||||
expect(alertDoc.reason).to.match(messagePattern);
|
||||
expect(alertDoc.title).to.be("rule 'always fire' matched query");
|
||||
expect(alertDoc.evaluation.conditions).to.be(
|
||||
'Number of matching documents is greater than -1'
|
||||
);
|
||||
expect(alertDoc.evaluation.value).greaterThan(0);
|
||||
expect(alertDoc.url).to.contain('/s/space1/app/');
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -84,7 +84,13 @@ export default function checkAlertSchemasTest({ getService }: FtrProviderContext
|
|||
}
|
||||
});
|
||||
|
||||
const { stdout } = await execa('git', ['ls-files', '--modified']);
|
||||
const { stdout } = await execa('git', [
|
||||
'ls-files',
|
||||
'--modified',
|
||||
'--others',
|
||||
'--exclude-standard',
|
||||
]);
|
||||
|
||||
expect(stdout).not.to.contain('packages/kbn-alerts-as-data-utils/src/schemas/generated');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -138,6 +138,7 @@
|
|||
"@kbn/ml-category-validator",
|
||||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/stack-connectors-plugin",
|
||||
"@kbn/stack-alerts-plugin",
|
||||
"@kbn/aiops-utils"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue