[RAC] Populate common rule fields in alert helpers (#108679)

Co-authored-by: mgiota <panagiota.mitsopoulou@elastic.co>
This commit is contained in:
Felix Stürmer 2021-08-26 15:19:51 +02:00 committed by GitHub
parent 7d66cf9882
commit 137c182761
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 289 additions and 222 deletions

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export const ALERT_STATUS_ACTIVE = 'active';
export const ALERT_STATUS_RECOVERED = 'recovered';
export type AlertStatus = typeof ALERT_STATUS_ACTIVE | typeof ALERT_STATUS_RECOVERED;

View file

@ -9,3 +9,4 @@
export * from './technical_field_names';
export * from './alerts_as_data_rbac';
export * from './alerts_as_data_severity';
export * from './alerts_as_data_status';

View file

@ -6,11 +6,12 @@
*/
import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import { IndexPatternBase } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils';
import React, { useCallback, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import useAsync from 'react-use/lib/useAsync';
import { IndexPatternBase } from '@kbn/es-query';
import { ParsedTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields';
import type { AlertWorkflowStatus } from '../../../common/typings';
import { ExperimentalBadge } from '../../components/shared/experimental_badge';
@ -21,8 +22,8 @@ import { RouteParams } from '../../routes';
import { callObservabilityApi } from '../../services/call_observability_api';
import { AlertsSearchBar } from './alerts_search_bar';
import { AlertsTableTGrid } from './alerts_table_t_grid';
import { WorkflowStatusFilter } from './workflow_status_filter';
import './styles.scss';
import { WorkflowStatusFilter } from './workflow_status_filter';
export interface TopAlert {
fields: ParsedTechnicalFields;
@ -45,7 +46,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
query: {
rangeFrom = 'now-15m',
rangeTo = 'now',
kuery = 'kibana.alert.status: "open"', // TODO change hardcoded values as part of another PR
kuery = `${ALERT_STATUS}: "${ALERT_STATUS_ACTIVE}"`,
workflowStatus = 'open',
},
} = routeParams;

View file

@ -18,6 +18,7 @@ import {
ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED,
// @ts-expect-error
} from '@kbn/rule-data-utils/target_node/technical_field_names';
import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils';
import type { TopAlert } from '.';
import { parseTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields';
import { asDuration, asPercent } from '../../../common/utils/formatters';
@ -42,7 +43,7 @@ export const parseAlert = (observabilityRuleTypeRegistry: ObservabilityRuleTypeR
return {
...formatted,
fields: parsedFields,
active: parsedFields[ALERT_STATUS] !== 'closed',
active: parsedFields[ALERT_STATUS] === ALERT_STATUS_ACTIVE,
start: new Date(parsedFields[ALERT_START] ?? 0).getTime(),
};
};

View file

@ -26,7 +26,7 @@ import {
TIMESTAMP,
// @ts-expect-error importing from a place other than root because we want to limit what we import from this package
} from '@kbn/rule-data-utils/target_node/technical_field_names';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import type { CellValueElementProps, TimelineNonEcsData } from '../../../../timelines/common';
import { TimestampTooltip } from '../../components/shared/timestamp_tooltip';
import { asDuration } from '../../../common/utils/formatters';
@ -82,7 +82,7 @@ export const getRenderCellValue = ({
switch (columnId) {
case ALERT_STATUS:
switch (value) {
case 'open':
case ALERT_STATUS_ACTIVE:
return (
<EuiHealth color="primary" textSize="xs">
{i18n.translate('xpack.observability.alertsTGrid.statusActiveDescription', {
@ -90,7 +90,7 @@ export const getRenderCellValue = ({
})}
</EuiHealth>
);
case 'closed':
case ALERT_STATUS_RECOVERED:
return (
<EuiHealth color={theme.eui.euiColorLightShade} textSize="xs">
<EuiText color={theme.eui.euiColorLightShade} size="relative">

View file

@ -18,15 +18,15 @@ export const technicalRuleFieldMap = {
),
[Fields.ALERT_RULE_TYPE_ID]: { type: 'keyword', required: true },
[Fields.ALERT_RULE_CONSUMER]: { type: 'keyword', required: true },
[Fields.ALERT_RULE_PRODUCER]: { type: 'keyword' },
[Fields.ALERT_RULE_PRODUCER]: { type: 'keyword', required: true },
[Fields.SPACE_IDS]: { type: 'keyword', array: true, required: true },
[Fields.ALERT_UUID]: { type: 'keyword' },
[Fields.ALERT_ID]: { type: 'keyword' },
[Fields.ALERT_UUID]: { type: 'keyword', required: true },
[Fields.ALERT_ID]: { type: 'keyword', required: true },
[Fields.ALERT_START]: { type: 'date' },
[Fields.ALERT_END]: { type: 'date' },
[Fields.ALERT_DURATION]: { type: 'long' },
[Fields.ALERT_SEVERITY]: { type: 'keyword' },
[Fields.ALERT_STATUS]: { type: 'keyword' },
[Fields.ALERT_STATUS]: { type: 'keyword', required: true },
[Fields.ALERT_EVALUATION_THRESHOLD]: { type: 'scaled_float', scaling_factor: 100 },
[Fields.ALERT_EVALUATION_VALUE]: { type: 'scaled_float', scaling_factor: 100 },
[Fields.VERSION]: {
@ -87,12 +87,12 @@ export const technicalRuleFieldMap = {
[Fields.ALERT_RULE_CATEGORY]: {
type: 'keyword',
array: false,
required: false,
required: true,
},
[Fields.ALERT_RULE_UUID]: {
type: 'keyword',
array: false,
required: false,
required: true,
},
[Fields.ALERT_RULE_CREATED_AT]: {
type: 'date',
@ -132,7 +132,7 @@ export const technicalRuleFieldMap = {
[Fields.ALERT_RULE_NAME]: {
type: 'keyword',
array: false,
required: false,
required: true,
},
[Fields.ALERT_RULE_NOTE]: {
type: 'keyword',

View file

@ -19,7 +19,6 @@ export * from './config';
export * from './rule_data_plugin_service';
export * from './rule_data_client';
export { getRuleData, RuleExecutorData } from './utils/get_rule_executor_data';
export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory';
export {
LifecycleRuleExecutor,

View file

@ -6,16 +6,22 @@
*/
import {
ALERT_ID,
ALERT_RULE_CATEGORY,
ALERT_RULE_CONSUMER,
ALERT_RULE_NAME,
ALERT_RULE_PRODUCER,
ALERT_RULE_RISK_SCORE,
ALERT_STATUS,
ECS_VERSION,
ALERT_RULE_TYPE_ID,
ALERT_RULE_UUID,
ALERT_STATUS,
ALERT_STATUS_ACTIVE,
ALERT_UUID,
ECS_VERSION,
SPACE_IDS,
TIMESTAMP,
VERSION,
} from '@kbn/rule-data-utils';
import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants';
import { ParsedTechnicalFields } from '../../common/parse_technical_fields';
import { getAlertByIdRoute } from './get_alert_by_id';
@ -24,14 +30,20 @@ import { getReadRequest } from './__mocks__/request_responses';
import { requestMock, serverMock } from './__mocks__/server';
const getMockAlert = (): ParsedTechnicalFields => ({
[TIMESTAMP]: '2021-06-21T21:33:05.713Z',
[ECS_VERSION]: '1.0.0',
[VERSION]: '7.13.0',
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
[ALERT_ID]: 'fake-alert-id',
[ALERT_RULE_CATEGORY]: 'apm.error_rate',
[ALERT_RULE_CONSUMER]: 'apm',
[ALERT_STATUS]: 'open',
[ALERT_RULE_NAME]: 'Check error rate',
[ALERT_RULE_PRODUCER]: 'apm',
[ALERT_RULE_RISK_SCORE]: 20,
[ALERT_RULE_TYPE_ID]: 'fake-rule-type-id',
[ALERT_RULE_UUID]: 'fake-rule-uuid',
[ALERT_STATUS]: ALERT_STATUS_ACTIVE,
[ALERT_UUID]: 'fake-alert-uuid',
[ECS_VERSION]: '1.0.0',
[SPACE_IDS]: ['fake-space-id'],
[TIMESTAMP]: '2021-06-21T21:33:05.713Z',
[VERSION]: '7.13.0',
});
describe('getAlertByIdRoute', () => {

View file

@ -6,29 +6,25 @@
*/
import { loggerMock } from '@kbn/logging/mocks';
import {
elasticsearchServiceMock,
savedObjectsClientMock,
} from '../../../../../src/core/server/mocks';
import {
AlertExecutorOptions,
AlertInstanceContext,
AlertInstanceState,
AlertTypeParams,
AlertTypeState,
} from '../../../alerting/server';
import { alertsMock } from '../../../alerting/server/mocks';
import {
ALERT_ID,
ALERT_RULE_CATEGORY,
ALERT_RULE_CONSUMER,
ALERT_RULE_NAME,
ALERT_RULE_PRODUCER,
ALERT_RULE_TYPE_ID,
ALERT_RULE_UUID,
ALERT_STATUS,
ALERT_STATUS_ACTIVE,
ALERT_STATUS_RECOVERED,
ALERT_UUID,
EVENT_ACTION,
EVENT_KIND,
ALERT_RULE_TYPE_ID,
ALERT_RULE_CONSUMER,
SPACE_IDS,
} from '../../common/technical_rule_data_field_names';
import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock';
import { createLifecycleExecutor } from './create_lifecycle_executor';
import { createDefaultAlertExecutorOptions } from './rule_executor_test_utils';
describe('createLifecycleExecutor', () => {
it('wraps and unwraps the original executor state', async () => {
@ -95,14 +91,14 @@ describe('createLifecycleExecutor', () => {
{ index: { _id: expect.any(String) } },
expect.objectContaining({
[ALERT_ID]: 'TEST_ALERT_0',
[ALERT_STATUS]: 'open',
[ALERT_STATUS]: ALERT_STATUS_ACTIVE,
[EVENT_ACTION]: 'open',
[EVENT_KIND]: 'signal',
}),
{ index: { _id: expect.any(String) } },
expect.objectContaining({
[ALERT_ID]: 'TEST_ALERT_1',
[ALERT_STATUS]: 'open',
[ALERT_STATUS]: ALERT_STATUS_ACTIVE,
[EVENT_ACTION]: 'open',
[EVENT_KIND]: 'signal',
}),
@ -192,14 +188,14 @@ describe('createLifecycleExecutor', () => {
{ index: { _id: 'TEST_ALERT_0_UUID' } },
expect.objectContaining({
[ALERT_ID]: 'TEST_ALERT_0',
[ALERT_STATUS]: 'open',
[ALERT_STATUS]: ALERT_STATUS_ACTIVE,
[EVENT_ACTION]: 'active',
[EVENT_KIND]: 'signal',
}),
{ index: { _id: 'TEST_ALERT_1_UUID' } },
expect.objectContaining({
[ALERT_ID]: 'TEST_ALERT_1',
[ALERT_STATUS]: 'open',
[ALERT_STATUS]: ALERT_STATUS_ACTIVE,
[EVENT_ACTION]: 'active',
[EVENT_KIND]: 'signal',
}),
@ -220,6 +216,8 @@ describe('createLifecycleExecutor', () => {
});
it('updates existing documents for recovered alerts', async () => {
// NOTE: the documents should actually also be updated for recurring,
// active alerts (see elastic/kibana#108670)
const logger = loggerMock.create();
const ruleDataClientMock = createRuleDataClientMock();
ruleDataClientMock.getReader().search.mockResolvedValue({
@ -229,8 +227,14 @@ describe('createLifecycleExecutor', () => {
fields: {
'@timestamp': '',
[ALERT_ID]: 'TEST_ALERT_0',
[ALERT_UUID]: 'ALERT_0_UUID',
[ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME',
[ALERT_RULE_CONSUMER]: 'CONSUMER',
[ALERT_RULE_NAME]: 'NAME',
[ALERT_RULE_PRODUCER]: 'PRODUCER',
[ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID',
[ALERT_RULE_UUID]: 'RULE_UUID',
[ALERT_STATUS]: ALERT_STATUS_ACTIVE,
[SPACE_IDS]: ['fake-space-id'],
labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc
},
@ -239,8 +243,14 @@ describe('createLifecycleExecutor', () => {
fields: {
'@timestamp': '',
[ALERT_ID]: 'TEST_ALERT_1',
[ALERT_UUID]: 'ALERT_1_UUID',
[ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME',
[ALERT_RULE_CONSUMER]: 'CONSUMER',
[ALERT_RULE_NAME]: 'NAME',
[ALERT_RULE_PRODUCER]: 'PRODUCER',
[ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID',
[ALERT_RULE_UUID]: 'RULE_UUID',
[ALERT_STATUS]: ALERT_STATUS_ACTIVE,
[SPACE_IDS]: ['fake-space-id'],
labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc
},
@ -290,7 +300,7 @@ describe('createLifecycleExecutor', () => {
{ index: { _id: 'TEST_ALERT_0_UUID' } },
expect.objectContaining({
[ALERT_ID]: 'TEST_ALERT_0',
[ALERT_STATUS]: 'closed',
[ALERT_STATUS]: ALERT_STATUS_RECOVERED,
labels: { LABEL_0_KEY: 'LABEL_0_VALUE' },
[EVENT_ACTION]: 'close',
[EVENT_KIND]: 'signal',
@ -298,7 +308,7 @@ describe('createLifecycleExecutor', () => {
{ index: { _id: 'TEST_ALERT_1_UUID' } },
expect.objectContaining({
[ALERT_ID]: 'TEST_ALERT_1',
[ALERT_STATUS]: 'open',
[ALERT_STATUS]: ALERT_STATUS_ACTIVE,
[EVENT_ACTION]: 'active',
[EVENT_KIND]: 'signal',
}),
@ -326,62 +336,3 @@ type TestRuleState = Record<string, unknown> & {
const initialRuleState: TestRuleState = {
aRuleStateKey: 'INITIAL_RULE_STATE_VALUE',
};
const createDefaultAlertExecutorOptions = <
Params extends AlertTypeParams = never,
State extends AlertTypeState = never,
InstanceState extends AlertInstanceState = {},
InstanceContext extends AlertInstanceContext = {},
ActionGroupIds extends string = ''
>({
alertId = 'ALERT_ID',
ruleName = 'ALERT_RULE_NAME',
params,
state,
createdAt = new Date(),
startedAt = new Date(),
updatedAt = new Date(),
}: {
alertId?: string;
ruleName?: string;
params: Params;
state: State;
createdAt?: Date;
startedAt?: Date;
updatedAt?: Date;
}): AlertExecutorOptions<Params, State, InstanceState, InstanceContext, ActionGroupIds> => ({
alertId,
createdBy: 'CREATED_BY',
startedAt,
name: ruleName,
rule: {
updatedBy: null,
tags: [],
name: ruleName,
createdBy: null,
actions: [],
enabled: true,
consumer: 'CONSUMER',
producer: 'ALERT_PRODUCER',
schedule: { interval: '1m' },
throttle: null,
createdAt,
updatedAt,
notifyWhen: null,
ruleTypeId: 'RULE_TYPE_ID',
ruleTypeName: 'RULE_TYPE_NAME',
},
tags: [],
params,
spaceId: 'SPACE_ID',
services: {
alertInstanceFactory: alertsMock.createAlertServices<InstanceState, InstanceContext>()
.alertInstanceFactory,
savedObjectsClient: savedObjectsClientMock.create(),
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
},
state,
updatedBy: null,
previousStartedAt: null,
namespace: undefined,
});

View file

@ -9,7 +9,6 @@ import type { Logger } from '@kbn/logging';
import type { PublicContract } from '@kbn/utility-types';
import { getOrElse } from 'fp-ts/lib/Either';
import * as rt from 'io-ts';
import { Mutable } from 'utility-types';
import { v4 } from 'uuid';
import {
AlertExecutorOptions,
@ -24,22 +23,34 @@ import {
ALERT_DURATION,
ALERT_END,
ALERT_ID,
ALERT_RULE_CONSUMER,
ALERT_RULE_TYPE_ID,
ALERT_RULE_UUID,
ALERT_START,
ALERT_STATUS,
ALERT_STATUS_ACTIVE,
ALERT_STATUS_RECOVERED,
ALERT_UUID,
ALERT_WORKFLOW_STATUS,
EVENT_ACTION,
EVENT_KIND,
SPACE_IDS,
TIMESTAMP,
VERSION,
} from '../../common/technical_rule_data_field_names';
import { IRuleDataClient } from '../rule_data_client';
import { AlertExecutorOptionsWithExtraServices } from '../types';
import { getRuleData } from './get_rule_executor_data';
import {
CommonAlertFieldName,
CommonAlertIdFieldName,
getCommonAlertFields,
} from './get_common_alert_fields';
type ImplicitTechnicalFieldName = CommonAlertFieldName | CommonAlertIdFieldName;
type ExplicitTechnicalAlertFields = Partial<
Omit<ParsedTechnicalFields, ImplicitTechnicalFieldName>
>;
type ExplicitAlertFields = Record<string, unknown> & // every field can have values of arbitrary types
ExplicitTechnicalAlertFields; // but technical fields must obey their respective type
export type LifecycleAlertService<
InstanceState extends AlertInstanceState = never,
@ -47,7 +58,7 @@ export type LifecycleAlertService<
ActionGroupIds extends string = never
> = (alert: {
id: string;
fields: Record<string, unknown> & Partial<Omit<ParsedTechnicalFields, typeof ALERT_ID>>;
fields: ExplicitAlertFields;
}) => AlertInstance<InstanceState, InstanceContext, ActionGroupIds>;
export interface LifecycleAlertServices<
@ -129,14 +140,10 @@ export const createLifecycleExecutor = (
>
): Promise<WrappedLifecycleRuleState<State>> => {
const {
rule,
services: { alertInstanceFactory },
state: previousState,
spaceId,
} = options;
const ruleExecutorData = getRuleData(options);
const state = getOrElse(
(): WrappedLifecycleRuleState<State> => ({
wrapped: previousState as State,
@ -144,9 +151,9 @@ export const createLifecycleExecutor = (
})
)(wrappedStateRt<State>().decode(previousState));
const currentAlerts: Record<string, Partial<ParsedTechnicalFields>> = {};
const commonRuleFields = getCommonAlertFields(options);
const timestamp = options.startedAt.toISOString();
const currentAlerts: Record<string, ExplicitAlertFields> = {};
const lifecycleAlertServices: LifecycleAlertServices<
InstanceState,
@ -154,12 +161,8 @@ export const createLifecycleExecutor = (
ActionGroupIds
> = {
alertWithLifecycle: ({ id, fields }) => {
currentAlerts[id] = {
...fields,
[ALERT_ID]: id,
[ALERT_RULE_TYPE_ID]: rule.ruleTypeId,
[ALERT_RULE_CONSUMER]: rule.consumer,
};
currentAlerts[id] = fields;
return alertInstanceFactory(id);
},
};
@ -199,7 +202,7 @@ export const createLifecycleExecutor = (
filter: [
{
term: {
[ALERT_RULE_UUID]: ruleExecutorData[ALERT_RULE_UUID],
[ALERT_RULE_UUID]: commonRuleFields[ALERT_RULE_UUID],
},
},
{
@ -227,12 +230,10 @@ export const createLifecycleExecutor = (
hits.hits.forEach((hit) => {
const fields = parseTechnicalFields(hit.fields);
const alertId = fields[ALERT_ID]!;
const alertId = fields[ALERT_ID];
alertsDataMap[alertId] = {
...commonRuleFields,
...fields,
[ALERT_ID]: alertId,
[ALERT_RULE_TYPE_ID]: rule.ruleTypeId,
[ALERT_RULE_CONSUMER]: rule.consumer,
};
});
}
@ -244,59 +245,28 @@ export const createLifecycleExecutor = (
logger.warn(`Could not find alert data for ${alertId}`);
}
const event: Mutable<ParsedTechnicalFields> = {
...alertData,
...ruleExecutorData,
[TIMESTAMP]: timestamp,
[EVENT_KIND]: 'signal',
[ALERT_RULE_CONSUMER]: rule.consumer,
[ALERT_ID]: alertId,
[VERSION]: ruleDataClient.kibanaVersion,
} as ParsedTechnicalFields;
const isNew = !state.trackedAlerts[alertId];
const isRecovered = !currentAlerts[alertId];
const isActiveButNotNew = !isNew && !isRecovered;
const isActive = !isRecovered;
const { alertUuid, started } = state.trackedAlerts[alertId] ?? {
alertUuid: v4(),
started: timestamp,
started: commonRuleFields[TIMESTAMP],
};
const event: ParsedTechnicalFields = {
...alertData,
...commonRuleFields,
[ALERT_DURATION]: (options.startedAt.getTime() - new Date(started).getTime()) * 1000,
[ALERT_ID]: alertId,
[ALERT_START]: started,
[ALERT_STATUS]: isActive ? ALERT_STATUS_ACTIVE : ALERT_STATUS_RECOVERED,
[ALERT_WORKFLOW_STATUS]: alertData[ALERT_WORKFLOW_STATUS] ?? 'open',
[ALERT_UUID]: alertUuid,
[EVENT_KIND]: 'signal',
[EVENT_ACTION]: isNew ? 'open' : isActive ? 'active' : 'close',
[VERSION]: ruleDataClient.kibanaVersion,
...(isRecovered ? { [ALERT_END]: commonRuleFields[TIMESTAMP] } : {}),
};
event[ALERT_START] = started;
event[ALERT_UUID] = alertUuid;
event[ALERT_WORKFLOW_STATUS] = event[ALERT_WORKFLOW_STATUS] ?? 'open';
// not sure why typescript needs the non-null assertion here
// we already assert the value is not undefined with the ternary
// still getting an error with the ternary.. strange.
event[SPACE_IDS] =
event[SPACE_IDS] == null
? [spaceId]
: [spaceId, ...event[SPACE_IDS]!.filter((sid) => sid !== spaceId)];
if (isNew) {
event[EVENT_ACTION] = 'open';
}
if (isRecovered) {
event[ALERT_END] = timestamp;
event[EVENT_ACTION] = 'close';
event[ALERT_STATUS] = 'closed';
}
if (isActiveButNotNew) {
event[EVENT_ACTION] = 'active';
}
if (isActive) {
event[ALERT_STATUS] = 'open';
}
event[ALERT_DURATION] =
(options.startedAt.getTime() - new Date(event[ALERT_START]!).getTime()) * 1000;
return event;
});

View file

@ -6,7 +6,13 @@
*/
import { schema } from '@kbn/config-schema';
import { ALERT_DURATION, ALERT_STATUS, ALERT_UUID } from '@kbn/rule-data-utils';
import {
ALERT_DURATION,
ALERT_STATUS,
ALERT_STATUS_ACTIVE,
ALERT_STATUS_RECOVERED,
ALERT_UUID,
} from '@kbn/rule-data-utils';
import { loggerMock } from '@kbn/logging/mocks';
import { castArray, omit, mapValues } from 'lodash';
import { RuleDataClient } from '../rule_data_client';
@ -177,7 +183,9 @@ describe('createLifecycleRuleTypeFactory', () => {
expect(evaluationDocuments.length).toBe(0);
expect(alertDocuments.length).toBe(2);
expect(alertDocuments.every((doc) => doc[ALERT_STATUS] === 'open')).toBeTruthy();
expect(
alertDocuments.every((doc) => doc[ALERT_STATUS] === ALERT_STATUS_ACTIVE)
).toBeTruthy();
expect(alertDocuments.every((doc) => doc[ALERT_DURATION] === 0)).toBeTruthy();
@ -198,7 +206,7 @@ describe('createLifecycleRuleTypeFactory', () => {
"kibana.alert.rule.rule_type_id": "ruleTypeId",
"kibana.alert.rule.uuid": "alertId",
"kibana.alert.start": "2021-06-16T09:01:00.000Z",
"kibana.alert.status": "open",
"kibana.alert.status": "active",
"kibana.alert.workflow_status": "open",
"kibana.space_ids": Array [
"spaceId",
@ -222,7 +230,7 @@ describe('createLifecycleRuleTypeFactory', () => {
"kibana.alert.rule.rule_type_id": "ruleTypeId",
"kibana.alert.rule.uuid": "alertId",
"kibana.alert.start": "2021-06-16T09:01:00.000Z",
"kibana.alert.status": "open",
"kibana.alert.status": "active",
"kibana.alert.workflow_status": "open",
"kibana.space_ids": Array [
"spaceId",
@ -284,7 +292,9 @@ describe('createLifecycleRuleTypeFactory', () => {
expect(evaluationDocuments.length).toBe(0);
expect(alertDocuments.length).toBe(2);
expect(alertDocuments.every((doc) => doc[ALERT_STATUS] === 'open')).toBeTruthy();
expect(
alertDocuments.every((doc) => doc[ALERT_STATUS] === ALERT_STATUS_ACTIVE)
).toBeTruthy();
expect(alertDocuments.every((doc) => doc['event.action'] === 'active')).toBeTruthy();
expect(alertDocuments.every((doc) => doc[ALERT_DURATION] > 0)).toBeTruthy();
@ -362,10 +372,10 @@ describe('createLifecycleRuleTypeFactory', () => {
);
expect(opbeansJavaAlertDoc['event.action']).toBe('active');
expect(opbeansJavaAlertDoc[ALERT_STATUS]).toBe('open');
expect(opbeansJavaAlertDoc[ALERT_STATUS]).toBe(ALERT_STATUS_ACTIVE);
expect(opbeansNodeAlertDoc['event.action']).toBe('close');
expect(opbeansNodeAlertDoc[ALERT_STATUS]).toBe('closed');
expect(opbeansNodeAlertDoc[ALERT_STATUS]).toBe(ALERT_STATUS_RECOVERED);
});
});
});

View file

@ -6,6 +6,7 @@
*/
import { ALERT_ID, VERSION } from '@kbn/rule-data-utils';
import { getCommonAlertFields } from './get_common_alert_fields';
import { CreatePersistenceRuleTypeFactory } from './persistence_types';
export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory = ({
@ -24,13 +25,16 @@ export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory
logger.debug(`Found ${numAlerts} alerts.`);
if (ruleDataClient.isWriteEnabled() && numAlerts) {
const commonRuleFields = getCommonAlertFields(options);
const response = await ruleDataClient.getWriter().bulk({
body: alerts.flatMap((event) => [
{ index: {} },
{
...event.fields,
[ALERT_ID]: event.id,
[VERSION]: ruleDataClient.kibanaVersion,
...commonRuleFields,
...event.fields,
},
]),
refresh,

View file

@ -0,0 +1,57 @@
/*
* 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 { Values } from '@kbn/utility-types';
import { AlertExecutorOptions } from '../../../alerting/server';
import { ParsedTechnicalFields } from '../../common/parse_technical_fields';
import {
ALERT_ID,
ALERT_UUID,
ALERT_RULE_CATEGORY,
ALERT_RULE_CONSUMER,
ALERT_RULE_NAME,
ALERT_RULE_PRODUCER,
ALERT_RULE_TYPE_ID,
ALERT_RULE_UUID,
SPACE_IDS,
TAGS,
TIMESTAMP,
} from '../../common/technical_rule_data_field_names';
const commonAlertFieldNames = [
ALERT_RULE_CATEGORY,
ALERT_RULE_CONSUMER,
ALERT_RULE_NAME,
ALERT_RULE_PRODUCER,
ALERT_RULE_TYPE_ID,
ALERT_RULE_UUID,
SPACE_IDS,
TAGS,
TIMESTAMP,
];
export type CommonAlertFieldName = Values<typeof commonAlertFieldNames>;
const commonAlertIdFieldNames = [ALERT_ID, ALERT_UUID];
export type CommonAlertIdFieldName = Values<typeof commonAlertIdFieldNames>;
export type CommonAlertFields = Pick<ParsedTechnicalFields, CommonAlertFieldName>;
export const getCommonAlertFields = (
options: AlertExecutorOptions<any, any, any, any, any>
): CommonAlertFields => {
return {
[ALERT_RULE_CATEGORY]: options.rule.ruleTypeName,
[ALERT_RULE_CONSUMER]: options.rule.consumer,
[ALERT_RULE_NAME]: options.rule.name,
[ALERT_RULE_PRODUCER]: options.rule.producer,
[ALERT_RULE_TYPE_ID]: options.rule.ruleTypeId,
[ALERT_RULE_UUID]: options.alertId,
[SPACE_IDS]: [options.spaceId],
[TAGS]: options.tags,
[TIMESTAMP]: options.startedAt.toISOString(),
};
};

View file

@ -1,36 +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 { AlertExecutorOptions } from '../../../alerting/server';
import {
ALERT_RULE_PRODUCER,
ALERT_RULE_CATEGORY,
ALERT_RULE_TYPE_ID,
ALERT_RULE_NAME,
ALERT_RULE_UUID,
TAGS,
} from '../../common/technical_rule_data_field_names';
export interface RuleExecutorData {
[ALERT_RULE_CATEGORY]: string;
[ALERT_RULE_TYPE_ID]: string;
[ALERT_RULE_UUID]: string;
[ALERT_RULE_NAME]: string;
[ALERT_RULE_PRODUCER]: string;
[TAGS]: string[];
}
export function getRuleData(options: AlertExecutorOptions<any, any, any, any, any>) {
return {
[ALERT_RULE_TYPE_ID]: options.rule.ruleTypeId,
[ALERT_RULE_UUID]: options.alertId,
[ALERT_RULE_CATEGORY]: options.rule.ruleTypeName,
[ALERT_RULE_NAME]: options.rule.name,
[TAGS]: options.tags,
[ALERT_RULE_PRODUCER]: options.rule.producer,
};
}

View file

@ -0,0 +1,77 @@
/*
* 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 {
elasticsearchServiceMock,
savedObjectsClientMock,
} from '../../../../../src/core/server/mocks';
import {
AlertExecutorOptions,
AlertInstanceContext,
AlertInstanceState,
AlertTypeParams,
AlertTypeState,
} from '../../../alerting/server';
import { alertsMock } from '../../../alerting/server/mocks';
export const createDefaultAlertExecutorOptions = <
Params extends AlertTypeParams = never,
State extends AlertTypeState = never,
InstanceState extends AlertInstanceState = {},
InstanceContext extends AlertInstanceContext = {},
ActionGroupIds extends string = ''
>({
alertId = 'ALERT_ID',
ruleName = 'ALERT_RULE_NAME',
params,
state,
createdAt = new Date(),
startedAt = new Date(),
updatedAt = new Date(),
}: {
alertId?: string;
ruleName?: string;
params: Params;
state: State;
createdAt?: Date;
startedAt?: Date;
updatedAt?: Date;
}): AlertExecutorOptions<Params, State, InstanceState, InstanceContext, ActionGroupIds> => ({
alertId,
createdBy: 'CREATED_BY',
startedAt,
name: ruleName,
rule: {
updatedBy: null,
tags: [],
name: ruleName,
createdBy: null,
actions: [],
enabled: true,
consumer: 'CONSUMER',
producer: 'ALERT_PRODUCER',
schedule: { interval: '1m' },
throttle: null,
createdAt,
updatedAt,
notifyWhen: null,
ruleTypeId: 'RULE_TYPE_ID',
ruleTypeName: 'RULE_TYPE_NAME',
},
tags: [],
params,
spaceId: 'SPACE_ID',
services: {
alertInstanceFactory: alertsMock.createAlertServices<InstanceState, InstanceContext>()
.alertInstanceFactory,
savedObjectsClient: savedObjectsClientMock.create(),
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
},
state,
updatedBy: null,
previousStartedAt: null,
namespace: undefined,
});

View file

@ -33,5 +33,8 @@ export const parseRuleExecutionLog = (input: unknown) => {
/**
* @deprecated RuleExecutionEvent is kept here only as a reference. It will be superseded with EventLog implementation
*
* It's marked as `Partial` because the field map is not yet appropriate for
* execution log events.
*/
export type RuleExecutionEvent = ReturnType<typeof parseRuleExecutionLog>;
export type RuleExecutionEvent = Partial<ReturnType<typeof parseRuleExecutionLog>>;

View file

@ -19,6 +19,9 @@ import { AlertAttributes } from '../../signals/types';
import { createRuleMock } from './rule';
import { listMock } from '../../../../../../lists/server/mocks';
import { RuleParams } from '../../schemas/rule_schemas';
// this is only used in tests
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { createDefaultAlertExecutorOptions } from '../../../../../../rule_registry/server/utils/rule_executor_test_utils';
export const createRuleTypeMocks = (
ruleType: string = 'query',
@ -90,10 +93,12 @@ export const createRuleTypeMocks = (
scheduleActions,
executor: async ({ params }: { params: Record<string, unknown> }) => {
return alertExecutor({
...createDefaultAlertExecutorOptions({
params,
alertId: v4(),
state: {},
}),
services,
params,
alertId: v4(),
startedAt: new Date(),
});
},
};

View file

@ -420,7 +420,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
"apm.transaction_error_rate",
],
"kibana.alert.status": Array [
"open",
"active",
],
"kibana.alert.workflow_status": Array [
"open",
@ -489,7 +489,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
any
>;
expect(recoveredAlertEvent[ALERT_STATUS]?.[0]).to.eql('closed');
expect(recoveredAlertEvent[ALERT_STATUS]?.[0]).to.eql('recovered');
expect(recoveredAlertEvent[ALERT_DURATION]?.[0]).to.be.greaterThan(0);
expect(new Date(recoveredAlertEvent[ALERT_END]?.[0]).getTime()).to.be.greaterThan(0);
@ -530,7 +530,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
"apm.transaction_error_rate",
],
"kibana.alert.status": Array [
"closed",
"recovered",
],
"kibana.alert.workflow_status": Array [
"open",