[geo_containment alert] replace alertFactory with alertsClient (#173867)

Closes https://github.com/elastic/kibana/issues/167321

PR does not provide any custom fields for payload because geo
containment alert has very little usage and will be [disabled in
serverless](https://github.com/elastic/kibana/pull/174121), with the
goal of deprecating and removing geo containment alert in the future.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Mike Côté <mikecote@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-01-04 15:30:17 -07:00 committed by GitHub
parent 94bdc0d521
commit 2fcbea2d7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 97 additions and 129 deletions

View file

@ -5,50 +5,16 @@
* 2.0.
*/
import _ from 'lodash';
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks';
import sampleAggsJsonResponse from './tests/es_sample_response.json';
import sampleShapesJsonResponse from './tests/es_sample_response_shapes.json';
import { executor } from './executor';
import type {
GeoContainmentRuleParams,
GeoContainmentAlertInstanceState,
GeoContainmentAlertInstanceContext,
} from './types';
import type { GeoContainmentRuleParams, GeoContainmentAlertInstanceContext } from './types';
const alertFactory = (contextKeys: unknown[], testAlertActionArr: unknown[]) => ({
create: (instanceId: string) => {
const alertInstance = alertsMock.createAlertFactory.create<
GeoContainmentAlertInstanceState,
GeoContainmentAlertInstanceContext
>();
(alertInstance.scheduleActions as jest.Mock).mockImplementation(
(actionGroupId: string, context?: GeoContainmentAlertInstanceContext) => {
// Check subset of alert for comparison to expected results
// @ts-ignore
const contextSubset = _.pickBy(context, (v, k) => contextKeys.includes(k));
testAlertActionArr.push({
actionGroupId,
instanceId,
context: contextSubset,
});
}
);
return alertInstance;
},
alertLimit: {
getValue: () => 1000,
setLimitReached: () => {},
},
done: () => ({ getRecoveredAlerts: () => [] }),
});
describe('getGeoContainmentExecutor', () => {
// Params needed for all tests
const expectedAlertResults = [
describe('executor', () => {
const expectedAlerts = [
{
actionGroupId: 'Tracked entity contained',
context: {
containingBoundaryId: 'kFATGXkBsFLYN2Tj6AAk',
entityDocumentId: 'ZVBoGXkBsFLYN2Tj1wmV',
@ -58,7 +24,6 @@ describe('getGeoContainmentExecutor', () => {
instanceId: '0-kFATGXkBsFLYN2Tj6AAk',
},
{
actionGroupId: 'Tracked entity contained',
context: {
containingBoundaryId: 'kFATGXkBsFLYN2Tj6AAk',
entityDocumentId: 'ZlBoGXkBsFLYN2Tj1wmV',
@ -68,7 +33,7 @@ describe('getGeoContainmentExecutor', () => {
instanceId: '1-kFATGXkBsFLYN2Tj6AAk',
},
];
const testAlertActionArr: unknown[] = [];
const previousStartedAt = new Date('2021-04-27T16:56:11.923Z');
const startedAt = new Date('2021-04-29T16:56:11.923Z');
const geoContainmentParams: GeoContainmentRuleParams = {
@ -99,7 +64,6 @@ describe('getGeoContainmentExecutor', () => {
// Boundary test mocks
const boundaryCall = jest.fn();
const esAggCall = jest.fn();
const contextKeys = Object.keys(expectedAlertResults[0].context);
const esClient = elasticsearchServiceMock.createElasticsearchClient();
// @ts-ignore incomplete return type
esClient.search.mockResponseImplementation(({ index }) => {
@ -112,10 +76,26 @@ describe('getGeoContainmentExecutor', () => {
}
});
const alertServicesWithSearchMock: RuleExecutorServicesMock = {
const alerts: unknown[] = [];
const servicesMock: RuleExecutorServicesMock = {
...alertsMock.createRuleExecutorServices(),
// @ts-ignore
alertFactory: alertFactory(contextKeys, testAlertActionArr),
alertsClient: {
getRecoveredAlerts: () => {
return [];
},
report: ({ id, context }: { id: string; context: GeoContainmentAlertInstanceContext }) => {
alerts.push({
context: {
containingBoundaryId: context.containingBoundaryId,
entityDocumentId: context.entityDocumentId,
entityId: context.entityId,
entityLocation: context.entityLocation,
},
instanceId: id,
});
},
},
// @ts-ignore
scopedClusterClient: {
asCurrentUser: esClient,
@ -124,7 +104,7 @@ describe('getGeoContainmentExecutor', () => {
beforeEach(() => {
jest.clearAllMocks();
testAlertActionArr.length = 0;
alerts.length = 0;
});
test('should query for shapes if state does not contain shapes', async () => {
@ -132,7 +112,7 @@ describe('getGeoContainmentExecutor', () => {
previousStartedAt,
startedAt,
// @ts-ignore
services: alertServicesWithSearchMock,
services: servicesMock,
params: geoContainmentParams,
// @ts-ignore
rule: {
@ -145,7 +125,7 @@ describe('getGeoContainmentExecutor', () => {
expect(boundaryCall.mock.calls.length).toBe(1);
expect(esAggCall.mock.calls.length).toBe(1);
}
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
expect(alerts).toMatchObject(expectedAlerts);
});
test('should query for shapes if boundaries request meta changes', async () => {
@ -153,7 +133,7 @@ describe('getGeoContainmentExecutor', () => {
previousStartedAt,
startedAt,
// @ts-ignore
services: alertServicesWithSearchMock,
services: servicesMock,
params: geoContainmentParams,
// @ts-ignore
rule: {
@ -172,7 +152,7 @@ describe('getGeoContainmentExecutor', () => {
expect(boundaryCall.mock.calls.length).toBe(1);
expect(esAggCall.mock.calls.length).toBe(1);
}
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
expect(alerts).toMatchObject(expectedAlerts);
});
test('should not query for shapes if state contains shapes', async () => {
@ -180,7 +160,7 @@ describe('getGeoContainmentExecutor', () => {
previousStartedAt,
startedAt,
// @ts-ignore
services: alertServicesWithSearchMock,
services: servicesMock,
params: geoContainmentParams,
// @ts-ignore
rule: {
@ -192,7 +172,7 @@ describe('getGeoContainmentExecutor', () => {
expect(boundaryCall.mock.calls.length).toBe(0);
expect(esAggCall.mock.calls.length).toBe(1);
}
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
expect(alerts).toMatchObject(expectedAlerts);
});
test('should carry through shapes filters in state to next call unmodified', async () => {
@ -200,7 +180,7 @@ describe('getGeoContainmentExecutor', () => {
previousStartedAt,
startedAt,
// @ts-ignore
services: alertServicesWithSearchMock,
services: servicesMock,
params: geoContainmentParams,
// @ts-ignore
rule: {
@ -211,7 +191,7 @@ describe('getGeoContainmentExecutor', () => {
if (executionResult && executionResult.state.shapesFilters) {
expect(executionResult.state.shapesFilters).toEqual(geoContainmentState.shapesFilters);
}
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
expect(alerts).toMatchObject(expectedAlerts);
});
test('should return previous locations map', async () => {
@ -239,7 +219,7 @@ describe('getGeoContainmentExecutor', () => {
previousStartedAt,
startedAt,
// @ts-ignore
services: alertServicesWithSearchMock,
services: servicesMock,
params: geoContainmentParams,
// @ts-ignore
rule: {
@ -250,6 +230,6 @@ describe('getGeoContainmentExecutor', () => {
if (executionResult && executionResult.state.prevLocationMap) {
expect(executionResult.state.prevLocationMap).toEqual(expectedPrevLocationMap);
}
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
expect(alerts).toMatchObject(expectedAlerts);
});
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { AlertsClientError } from '@kbn/alerting-plugin/server';
import { RuleExecutorOptions } from '../../types';
import {
canSkipBoundariesFetch,
@ -45,6 +46,11 @@ export async function executor({
boundaryNameField: params.boundaryNameField,
boundaryIndexQuery: params.boundaryIndexQuery,
};
if (!services.alertsClient) {
throw new AlertsClientError();
}
const { shapesFilters, shapesIdsNamesMap } =
state.shapesFilters &&
canSkipBoundariesFetch(boundariesRequestMeta, state.boundariesRequestMeta)
@ -82,14 +88,13 @@ export async function executor({
const { activeEntities, inactiveEntities } = getEntitiesAndGenerateAlerts(
prevLocationMap,
currLocationMap,
services.alertFactory,
services.alertsClient,
shapesIdsNamesMap,
windowEnd
);
const { getRecoveredAlerts } = services.alertFactory.done();
for (const recoveredAlert of getRecoveredAlerts()) {
const recoveredAlertId = recoveredAlert.getId();
for (const recoveredAlert of services.alertsClient.getRecoveredAlerts()) {
const recoveredAlertId = recoveredAlert.alert.getId();
try {
const context = getRecoveredAlertContext({
alertId: recoveredAlertId,
@ -98,7 +103,10 @@ export async function executor({
windowEnd,
});
if (context) {
recoveredAlert.setContext(context);
services.alertsClient?.setAlertData({
id: recoveredAlertId,
context,
});
}
} catch (e) {
logger.warn(`Unable to set alert context for recovered alert, error: ${e.message}`);

View file

@ -5,47 +5,28 @@
* 2.0.
*/
import _ from 'lodash';
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
import { getEntitiesAndGenerateAlerts } from './get_entities_and_generate_alerts';
import { OTHER_CATEGORY } from '../constants';
import type {
GeoContainmentAlertInstanceState,
GeoContainmentAlertInstanceContext,
} from '../types';
const alertFactory = (contextKeys: unknown[], testAlertActionArr: unknown[]) => ({
create: (instanceId: string) => {
const alertInstance = alertsMock.createAlertFactory.create<
GeoContainmentAlertInstanceState,
GeoContainmentAlertInstanceContext
>();
(alertInstance.scheduleActions as jest.Mock).mockImplementation(
(actionGroupId: string, context?: GeoContainmentAlertInstanceContext) => {
// Check subset of alert for comparison to expected results
// @ts-ignore
const contextSubset = _.pickBy(context, (v, k) => contextKeys.includes(k));
testAlertActionArr.push({
actionGroupId,
instanceId,
context: contextSubset,
});
}
);
return alertInstance;
},
alertLimit: {
getValue: () => 1000,
setLimitReached: () => {},
},
done: () => ({ getRecoveredAlerts: () => [] }),
});
import type { GeoContainmentAlertInstanceContext } from '../types';
describe('getEntitiesAndGenerateAlerts', () => {
const testAlertActionArr: unknown[] = [];
const alerts: unknown[] = [];
const mockAlertsClient = {
report: ({ id, context }: { id: string; context: GeoContainmentAlertInstanceContext }) => {
alerts.push({
context: {
containingBoundaryId: context.containingBoundaryId,
entityDocumentId: context.entityDocumentId,
entityId: context.entityId,
entityLocation: context.entityLocation,
},
instanceId: id,
});
},
} as any; // eslint-disable-line @typescript-eslint/no-explicit-any
beforeEach(() => {
jest.clearAllMocks();
testAlertActionArr.length = 0;
alerts.length = 0;
});
const currLocationMap = new Map([
@ -87,9 +68,8 @@ describe('getEntitiesAndGenerateAlerts', () => {
],
]);
const expectedAlertResults = [
const expectedAlerts = [
{
actionGroupId: 'Tracked entity contained',
context: {
containingBoundaryId: '123',
entityDocumentId: 'docId1',
@ -99,7 +79,6 @@ describe('getEntitiesAndGenerateAlerts', () => {
instanceId: 'a-123',
},
{
actionGroupId: 'Tracked entity contained',
context: {
containingBoundaryId: '456',
entityDocumentId: 'docId2',
@ -109,7 +88,6 @@ describe('getEntitiesAndGenerateAlerts', () => {
instanceId: 'b-456',
},
{
actionGroupId: 'Tracked entity contained',
context: {
containingBoundaryId: '789',
entityDocumentId: 'docId3',
@ -119,7 +97,6 @@ describe('getEntitiesAndGenerateAlerts', () => {
instanceId: 'c-789',
},
];
const contextKeys = Object.keys(expectedAlertResults[0].context);
const emptyShapesIdsNamesMap = {};
const currentDateTime = new Date();
@ -129,12 +106,12 @@ describe('getEntitiesAndGenerateAlerts', () => {
const { activeEntities } = getEntitiesAndGenerateAlerts(
emptyPrevLocationMap,
currLocationMap,
alertFactory(contextKeys, testAlertActionArr),
mockAlertsClient,
emptyShapesIdsNamesMap,
currentDateTime
);
expect(activeEntities).toEqual(currLocationMap);
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
expect(alerts).toMatchObject(expectedAlerts);
});
test('should overwrite older identical entity entries', () => {
@ -155,12 +132,12 @@ describe('getEntitiesAndGenerateAlerts', () => {
const { activeEntities } = getEntitiesAndGenerateAlerts(
prevLocationMapWithIdenticalEntityEntry,
currLocationMap,
alertFactory(contextKeys, testAlertActionArr),
mockAlertsClient,
emptyShapesIdsNamesMap,
currentDateTime
);
expect(activeEntities).toEqual(currLocationMap);
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
expect(alerts).toMatchObject(expectedAlerts);
});
test('should preserve older non-identical entity entries', () => {
@ -178,9 +155,18 @@ describe('getEntitiesAndGenerateAlerts', () => {
],
],
]);
const expectedAlertResultsPlusD = [
const { activeEntities } = getEntitiesAndGenerateAlerts(
prevLocationMapWithNonIdenticalEntityEntry,
currLocationMap,
mockAlertsClient,
emptyShapesIdsNamesMap,
currentDateTime
);
expect(activeEntities).not.toEqual(currLocationMap);
expect(activeEntities.has('d')).toBeTruthy();
expect(alerts).toMatchObject([
{
actionGroupId: 'Tracked entity contained',
context: {
containingBoundaryId: '999',
entityDocumentId: 'docId7',
@ -189,19 +175,8 @@ describe('getEntitiesAndGenerateAlerts', () => {
},
instanceId: 'd-999',
},
...expectedAlertResults,
];
const { activeEntities } = getEntitiesAndGenerateAlerts(
prevLocationMapWithNonIdenticalEntityEntry,
currLocationMap,
alertFactory(contextKeys, testAlertActionArr),
emptyShapesIdsNamesMap,
currentDateTime
);
expect(activeEntities).not.toEqual(currLocationMap);
expect(activeEntities.has('d')).toBeTruthy();
expect(testAlertActionArr).toMatchObject(expectedAlertResultsPlusD);
...expectedAlerts,
]);
});
test('should remove "other" entries and schedule the expected number of actions', () => {
@ -219,7 +194,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
const { activeEntities, inactiveEntities } = getEntitiesAndGenerateAlerts(
emptyPrevLocationMap,
currLocationMapWithOther,
alertFactory(contextKeys, testAlertActionArr),
mockAlertsClient,
emptyShapesIdsNamesMap,
currentDateTime
);
@ -240,7 +215,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
],
])
);
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
expect(alerts).toMatchObject(expectedAlerts);
});
test('should generate multiple alerts per entity if found in multiple shapes in interval', () => {
@ -271,7 +246,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
getEntitiesAndGenerateAlerts(
emptyPrevLocationMap,
currLocationMapWithThreeMore,
alertFactory(contextKeys, testAlertActionArr),
mockAlertsClient,
emptyShapesIdsNamesMap,
currentDateTime
);
@ -279,7 +254,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
currLocationMapWithThreeMore.forEach((v) => {
numEntitiesInShapes += v.length;
});
expect(testAlertActionArr.length).toEqual(numEntitiesInShapes);
expect(alerts.length).toEqual(numEntitiesInShapes);
});
test('should not return entity as active entry if most recent location is "other"', () => {
@ -311,7 +286,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
const { activeEntities } = getEntitiesAndGenerateAlerts(
emptyPrevLocationMap,
currLocationMapWithOther,
alertFactory(contextKeys, testAlertActionArr),
mockAlertsClient,
emptyShapesIdsNamesMap,
currentDateTime
);
@ -346,7 +321,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
const { activeEntities } = getEntitiesAndGenerateAlerts(
emptyPrevLocationMap,
currLocationMapWithOther,
alertFactory(contextKeys, testAlertActionArr),
mockAlertsClient,
emptyShapesIdsNamesMap,
currentDateTime
);

View file

@ -17,11 +17,11 @@ import { getAlertId, getContainedAlertContext } from './alert_context';
export function getEntitiesAndGenerateAlerts(
prevLocationMap: Map<string, GeoContainmentAlertInstanceState[]>,
currLocationMap: Map<string, GeoContainmentAlertInstanceState[]>,
alertFactory: RuleExecutorServices<
alertsClient: RuleExecutorServices<
GeoContainmentAlertInstanceState,
GeoContainmentAlertInstanceContext,
typeof ActionGroupId
>['alertFactory'],
>['alertsClient'],
shapesIdsNamesMap: Record<string, unknown>,
windowEnd: Date
): {
@ -43,9 +43,11 @@ export function getEntitiesAndGenerateAlerts(
shapesIdsNamesMap,
windowEnd,
});
alertFactory
.create(getAlertId(entityName, context.containingBoundaryName))
.scheduleActions(ActionGroupId, context);
alertsClient!.report({
id: getAlertId(entityName, context.containingBoundaryName),
actionGroup: ActionGroupId,
context,
});
}
});

View file

@ -17,6 +17,7 @@ import type {
} from './types';
import { executor } from './executor';
import { ActionGroupId, RecoveryActionGroupId, GEO_CONTAINMENT_ID } from './constants';
import { STACK_ALERTS_AAD_CONFIG } from '../constants';
const actionVariables = {
context: [
@ -200,5 +201,7 @@ export function getRuleType(): GeoContainmentRuleType {
return injectEntityAndBoundaryIds(params, references);
},
},
// @ts-ignore
alerts: STACK_ALERTS_AAD_CONFIG,
};
}