mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[RAC] Populate common rule fields in alert helpers (#108679)
Co-authored-by: mgiota <panagiota.mitsopoulou@elastic.co>
This commit is contained in:
parent
7d66cf9882
commit
137c182761
18 changed files with 289 additions and 222 deletions
12
packages/kbn-rule-data-utils/src/alerts_as_data_status.ts
Normal file
12
packages/kbn-rule-data-utils/src/alerts_as_data_status.ts
Normal 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;
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
});
|
|
@ -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>>;
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue