[Alerting] Active alerts do not recover after re-enabling a rule (#111671)

* [Alerting] Active alerts do not recover after re-enabling a rule

* created reusable lib file for generating event log object

* comment fix

* fixed tests

* fixed tests

* fixed typecheck

* fixed due to comments

* Apply suggestions from code review

Co-authored-by: ymao1 <ying.mao@elastic.co>

* fixed due to comments

* fixed due to comments

* fixed due to comments

* fixed tests

* Update disable.ts

* Update disable.ts

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: ymao1 <ying.mao@elastic.co>
This commit is contained in:
Yuliia Naumenko 2021-10-17 20:07:48 -07:00 committed by GitHub
parent 63a615f1f1
commit 84df5697cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 628 additions and 77 deletions

View file

@ -0,0 +1,210 @@
/*
* 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 { createAlertEventLogRecordObject } from './create_alert_event_log_record_object';
import { UntypedNormalizedAlertType } from '../rule_type_registry';
import { RecoveredActionGroup } from '../types';
describe('createAlertEventLogRecordObject', () => {
const ruleType: jest.Mocked<UntypedNormalizedAlertType> = {
id: 'test',
name: 'My test alert',
actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup],
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
recoveryActionGroup: RecoveredActionGroup,
executor: jest.fn(),
producer: 'alerts',
};
test('created alert event "execute-start"', async () => {
expect(
createAlertEventLogRecordObject({
ruleId: '1',
ruleType,
action: 'execute-start',
timestamp: '1970-01-01T00:00:00.000Z',
task: {
scheduled: '1970-01-01T00:00:00.000Z',
scheduleDelay: 0,
},
savedObjects: [
{
id: '1',
type: 'alert',
typeId: ruleType.id,
relation: 'primary',
},
],
})
).toStrictEqual({
'@timestamp': '1970-01-01T00:00:00.000Z',
event: {
action: 'execute-start',
category: ['alerts'],
kind: 'alert',
},
kibana: {
saved_objects: [
{
id: '1',
namespace: undefined,
rel: 'primary',
type: 'alert',
type_id: 'test',
},
],
task: {
schedule_delay: 0,
scheduled: '1970-01-01T00:00:00.000Z',
},
},
rule: {
category: 'test',
id: '1',
license: 'basic',
ruleset: 'alerts',
},
});
});
test('created alert event "recovered-instance"', async () => {
expect(
createAlertEventLogRecordObject({
ruleId: '1',
ruleName: 'test name',
ruleType,
action: 'recovered-instance',
instanceId: 'test1',
group: 'group 1',
message: 'message text here',
namespace: 'default',
subgroup: 'subgroup value',
state: {
start: '1970-01-01T00:00:00.000Z',
end: '1970-01-01T00:05:00.000Z',
duration: 5,
},
savedObjects: [
{
id: '1',
type: 'alert',
typeId: ruleType.id,
relation: 'primary',
},
],
})
).toStrictEqual({
event: {
action: 'recovered-instance',
category: ['alerts'],
duration: 5,
end: '1970-01-01T00:05:00.000Z',
kind: 'alert',
start: '1970-01-01T00:00:00.000Z',
},
kibana: {
alerting: {
action_group_id: 'group 1',
action_subgroup: 'subgroup value',
instance_id: 'test1',
},
saved_objects: [
{
id: '1',
namespace: 'default',
rel: 'primary',
type: 'alert',
type_id: 'test',
},
],
},
message: 'message text here',
rule: {
category: 'test',
id: '1',
license: 'basic',
ruleset: 'alerts',
name: 'test name',
},
});
});
test('created alert event "execute-action"', async () => {
expect(
createAlertEventLogRecordObject({
ruleId: '1',
ruleName: 'test name',
ruleType,
action: 'execute-action',
instanceId: 'test1',
group: 'group 1',
message: 'action execution start',
namespace: 'default',
subgroup: 'subgroup value',
state: {
start: '1970-01-01T00:00:00.000Z',
end: '1970-01-01T00:05:00.000Z',
duration: 5,
},
savedObjects: [
{
id: '1',
type: 'alert',
typeId: ruleType.id,
relation: 'primary',
},
{
id: '2',
type: 'action',
typeId: '.email',
},
],
})
).toStrictEqual({
event: {
action: 'execute-action',
category: ['alerts'],
duration: 5,
end: '1970-01-01T00:05:00.000Z',
kind: 'alert',
start: '1970-01-01T00:00:00.000Z',
},
kibana: {
alerting: {
action_group_id: 'group 1',
action_subgroup: 'subgroup value',
instance_id: 'test1',
},
saved_objects: [
{
id: '1',
namespace: 'default',
rel: 'primary',
type: 'alert',
type_id: 'test',
},
{
id: '2',
namespace: 'default',
type: 'action',
type_id: '.email',
},
],
},
message: 'action execution start',
rule: {
category: 'test',
id: '1',
license: 'basic',
ruleset: 'alerts',
name: 'test name',
},
});
});
});

View file

@ -0,0 +1,81 @@
/*
* 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 { AlertInstanceState } from '../types';
import { IEvent } from '../../../event_log/server';
import { UntypedNormalizedAlertType } from '../rule_type_registry';
export type Event = Exclude<IEvent, undefined>;
interface CreateAlertEventLogRecordParams {
ruleId: string;
ruleType: UntypedNormalizedAlertType;
action: string;
ruleName?: string;
instanceId?: string;
message?: string;
state?: AlertInstanceState;
group?: string;
subgroup?: string;
namespace?: string;
timestamp?: string;
task?: {
scheduled?: string;
scheduleDelay?: number;
};
savedObjects: Array<{
type: string;
id: string;
typeId: string;
relation?: string;
}>;
}
export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecordParams): Event {
const { ruleType, action, state, message, task, ruleId, group, subgroup, namespace } = params;
const alerting =
params.instanceId || group || subgroup
? {
alerting: {
...(params.instanceId ? { instance_id: params.instanceId } : {}),
...(group ? { action_group_id: group } : {}),
...(subgroup ? { action_subgroup: subgroup } : {}),
},
}
: undefined;
const event: Event = {
...(params.timestamp ? { '@timestamp': params.timestamp } : {}),
event: {
action,
kind: 'alert',
category: [ruleType.producer],
...(state?.start ? { start: state.start as string } : {}),
...(state?.end ? { end: state.end as string } : {}),
...(state?.duration !== undefined ? { duration: state.duration as number } : {}),
},
kibana: {
...(alerting ? alerting : {}),
saved_objects: params.savedObjects.map((so) => ({
...(so.relation ? { rel: so.relation } : {}),
type: so.type,
id: so.id,
type_id: so.typeId,
namespace,
})),
...(task ? { task: { scheduled: task.scheduled, schedule_delay: task.scheduleDelay } } : {}),
},
...(message ? { message } : {}),
rule: {
id: ruleId,
license: ruleType.minimumLicenseRequired,
category: ruleType.id,
ruleset: ruleType.producer,
...(params.ruleName ? { name: params.ruleName } : {}),
},
};
return event;
}

View file

@ -373,6 +373,7 @@ export class AlertingPlugin {
eventLog: plugins.eventLog,
kibanaVersion: this.kibanaVersion,
authorization: alertingAuthorizationClientFactory,
eventLogger: this.eventLogger,
});
const getRulesClientWithRequest = (request: KibanaRequest) => {

View file

@ -7,7 +7,7 @@
import Semver from 'semver';
import Boom from '@hapi/boom';
import { omit, isEqual, map, uniq, pick, truncate, trim } from 'lodash';
import { omit, isEqual, map, uniq, pick, truncate, trim, mapValues } from 'lodash';
import { i18n } from '@kbn/i18n';
import { estypes } from '@elastic/elasticsearch';
import {
@ -38,6 +38,7 @@ import {
AlertWithLegacyId,
SanitizedAlertWithLegacyId,
PartialAlertWithLegacyId,
RawAlertInstance,
} from '../types';
import {
validateAlertTypeParams,
@ -60,10 +61,14 @@ import {
AlertingAuthorizationFilterType,
AlertingAuthorizationFilterOpts,
} from '../authorization';
import { IEventLogClient } from '../../../event_log/server';
import {
IEvent,
IEventLogClient,
IEventLogger,
SAVED_OBJECT_REL_PRIMARY,
} from '../../../event_log/server';
import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date';
import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log';
import { IEvent } from '../../../event_log/server';
import { AuditLogger } from '../../../security/server';
import { parseDuration } from '../../common/parse_duration';
import { retryIfConflicts } from '../lib/retry_if_conflicts';
@ -73,6 +78,9 @@ import { ruleAuditEvent, RuleAuditAction } from './audit_events';
import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common';
import { mapSortField } from './lib';
import { getAlertExecutionStatusPending } from '../lib/alert_execution_status';
import { AlertInstance } from '../alert_instance';
import { EVENT_LOG_ACTIONS } from '../plugin';
import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object';
export interface RegistryAlertTypeWithAuth extends RegistryRuleType {
authorizedConsumers: string[];
@ -101,6 +109,7 @@ export interface ConstructorOptions {
getEventLogClient: () => Promise<IEventLogClient>;
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
auditLogger?: AuditLogger;
eventLogger?: IEventLogger;
}
export interface MuteOptions extends IndexType {
@ -215,6 +224,7 @@ export class RulesClient {
private readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version'];
private readonly auditLogger?: AuditLogger;
private readonly eventLogger?: IEventLogger;
constructor({
ruleTypeRegistry,
@ -232,6 +242,7 @@ export class RulesClient {
getEventLogClient,
kibanaVersion,
auditLogger,
eventLogger,
}: ConstructorOptions) {
this.logger = logger;
this.getUserName = getUserName;
@ -248,6 +259,7 @@ export class RulesClient {
this.getEventLogClient = getEventLogClient;
this.kibanaVersion = kibanaVersion;
this.auditLogger = auditLogger;
this.eventLogger = eventLogger;
}
public async create<Params extends AlertTypeParams = never>({
@ -1199,6 +1211,47 @@ export class RulesClient {
version = alert.version;
}
if (this.eventLogger && attributes.scheduledTaskId) {
const { state } = taskInstanceToAlertTaskInstance(
await this.taskManager.get(attributes.scheduledTaskId),
attributes as unknown as SanitizedAlert
);
const recoveredAlertInstances = mapValues<Record<string, RawAlertInstance>, AlertInstance>(
state.alertInstances ?? {},
(rawAlertInstance) => new AlertInstance(rawAlertInstance)
);
const recoveredAlertInstanceIds = Object.keys(recoveredAlertInstances);
for (const instanceId of recoveredAlertInstanceIds) {
const { group: actionGroup, subgroup: actionSubgroup } =
recoveredAlertInstances[instanceId].getLastScheduledActions() ?? {};
const instanceState = recoveredAlertInstances[instanceId].getState();
const message = `instance '${instanceId}' has recovered due to the rule was disabled`;
const event = createAlertEventLogRecordObject({
ruleId: id,
ruleName: attributes.name,
ruleType: this.ruleTypeRegistry.get(attributes.alertTypeId),
instanceId,
action: EVENT_LOG_ACTIONS.recoveredInstance,
message,
state: instanceState,
group: actionGroup,
subgroup: actionSubgroup,
namespace: this.namespace,
savedObjects: [
{
id,
type: 'alert',
typeId: attributes.alertTypeId,
relation: SAVED_OBJECT_REL_PRIMARY,
},
],
});
this.eventLogger.logEvent(event);
}
}
try {
await this.authorization.ensureAuthorized({
ruleTypeId: attributes.alertTypeId,

View file

@ -18,6 +18,8 @@ import { InvalidatePendingApiKey } from '../../types';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
import { eventLoggerMock } from '../../../../event_log/server/event_logger.mock';
import { TaskStatus } from '../../../../task_manager/server';
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
@ -26,6 +28,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const eventLogger = eventLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -44,10 +47,26 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
eventLogger,
};
beforeEach(() => {
getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry);
taskManager.get.mockResolvedValue({
id: 'task-123',
taskType: 'alerting:123',
scheduledAt: new Date(),
attempts: 1,
status: TaskStatus.Idle,
runAt: new Date(),
startedAt: null,
retryAt: null,
state: {},
params: {
alertId: '1',
},
ownerId: null,
});
(auditLogger.log as jest.Mock).mockClear();
});
@ -217,6 +236,120 @@ describe('disable()', () => {
).toBe('123');
});
test('disables the rule with calling event log to "recover" the alert instances from the task state', async () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'api_key_pending_invalidation',
attributes: {
apiKeyId: '123',
createdAt: '2019-02-12T21:01:22.479Z',
},
references: [],
});
const scheduledTaskId = 'task-123';
taskManager.get.mockResolvedValue({
id: scheduledTaskId,
taskType: 'alerting:123',
scheduledAt: new Date(),
attempts: 1,
status: TaskStatus.Idle,
runAt: new Date(),
startedAt: null,
retryAt: null,
state: {
alertInstances: {
'1': {
meta: {
lastScheduledActions: {
group: 'default',
subgroup: 'newSubgroup',
date: new Date().toISOString(),
},
},
state: { bar: false },
},
},
},
params: {
alertId: '1',
},
ownerId: null,
});
await rulesClient.disable({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
consumer: 'myApp',
schedule: { interval: '10s' },
alertTypeId: 'myType',
enabled: false,
meta: {
versionApiKeyLastmodified: kibanaVersion,
},
scheduledTaskId: null,
apiKey: null,
apiKeyOwner: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
actions: [
{
group: 'default',
id: '1',
actionTypeId: '1',
actionRef: '1',
params: {
foo: true,
},
},
],
},
{
version: '123',
}
);
expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123');
expect(
(unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId
).toBe('123');
expect(eventLogger.logEvent).toHaveBeenCalledTimes(1);
expect(eventLogger.logEvent.mock.calls[0][0]).toStrictEqual({
event: {
action: 'recovered-instance',
category: ['alerts'],
kind: 'alert',
},
kibana: {
alerting: {
action_group_id: 'default',
action_subgroup: 'newSubgroup',
instance_id: '1',
},
saved_objects: [
{
id: '1',
namespace: 'default',
rel: 'primary',
type: 'alert',
type_id: 'myType',
},
],
},
message: "instance '1' has recovered due to the rule was disabled",
rule: {
category: '123',
id: '1',
license: 'basic',
ruleset: 'alerts',
},
});
});
test('falls back when getDecryptedAsInternalUser throws an error', async () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({

View file

@ -325,6 +325,22 @@ beforeEach(() => {
params: {},
});
taskManager.get.mockResolvedValue({
id: 'task-123',
taskType: 'alerting:123',
scheduledAt: new Date(),
attempts: 1,
status: TaskStatus.Idle,
runAt: new Date(),
startedAt: null,
retryAt: null,
state: {},
params: {
alertId: '1',
},
ownerId: null,
});
const actionsClient = actionsClientMock.create();
actionsClient.getBulk.mockResolvedValue([]);
rulesClientParams.getActionsClient.mockResolvedValue(actionsClient);

View file

@ -17,7 +17,7 @@ import { RuleTypeRegistry, SpaceIdToNamespaceFunction } from './types';
import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server';
import { TaskManagerStartContract } from '../../task_manager/server';
import { IEventLogClientService } from '../../../plugins/event_log/server';
import { IEventLogClientService, IEventLogger } from '../../../plugins/event_log/server';
import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory';
export interface RulesClientFactoryOpts {
logger: Logger;
@ -32,6 +32,7 @@ export interface RulesClientFactoryOpts {
eventLog: IEventLogClientService;
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
authorization: AlertingAuthorizationClientFactory;
eventLogger?: IEventLogger;
}
export class RulesClientFactory {
@ -48,6 +49,7 @@ export class RulesClientFactory {
private eventLog!: IEventLogClientService;
private kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version'];
private authorization!: AlertingAuthorizationClientFactory;
private eventLogger?: IEventLogger;
public initialize(options: RulesClientFactoryOpts) {
if (this.isInitialized) {
@ -66,6 +68,7 @@ export class RulesClientFactory {
this.eventLog = options.eventLog;
this.kibanaVersion = options.kibanaVersion;
this.authorization = options.authorization;
this.eventLogger = options.eventLogger;
}
public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): RulesClient {
@ -123,6 +126,7 @@ export class RulesClientFactory {
async getEventLogClient() {
return eventLog.getClient(request);
},
eventLogger: this.eventLogger,
});
}
}

View file

@ -175,7 +175,6 @@ test('enqueues execution per selected action', async () => {
"kibana": Object {
"alerting": Object {
"action_group_id": "default",
"action_subgroup": undefined,
"instance_id": "2",
},
"saved_objects": Array [

View file

@ -10,7 +10,7 @@ import {
asSavedObjectExecutionSource,
PluginStartContract as ActionsPluginStartContract,
} from '../../../actions/server';
import { IEventLogger, IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { EVENT_LOG_ACTIONS } from '../plugin';
import { injectActionParams } from './inject_action_params';
import {
@ -21,8 +21,9 @@ import {
AlertInstanceContext,
RawAlert,
} from '../types';
import { NormalizedAlertType } from '../rule_type_registry';
import { NormalizedAlertType, UntypedNormalizedAlertType } from '../rule_type_registry';
import { isEphemeralTaskRejectedDueToCapacityError } from '../../../task_manager/server';
import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object';
export interface CreateExecutionHandlerOptions<
Params extends AlertTypeParams,
@ -201,43 +202,35 @@ export function createExecutionHandler<
await actionsClient.enqueueExecution(enqueueOptions);
}
const event: IEvent = {
event: {
action: EVENT_LOG_ACTIONS.executeAction,
kind: 'alert',
category: [alertType.producer],
},
kibana: {
alerting: {
instance_id: alertInstanceId,
action_group_id: actionGroup,
action_subgroup: actionSubgroup,
const event = createAlertEventLogRecordObject({
ruleId: alertId,
ruleType: alertType as UntypedNormalizedAlertType,
action: EVENT_LOG_ACTIONS.executeAction,
instanceId: alertInstanceId,
group: actionGroup,
subgroup: actionSubgroup,
ruleName: alertName,
savedObjects: [
{
type: 'alert',
id: alertId,
typeId: alertType.id,
relation: SAVED_OBJECT_REL_PRIMARY,
},
saved_objects: [
{
rel: SAVED_OBJECT_REL_PRIMARY,
type: 'alert',
id: alertId,
type_id: alertType.id,
...namespace,
},
{ type: 'action', id: action.id, type_id: action.actionTypeId, ...namespace },
],
},
rule: {
id: alertId,
license: alertType.minimumLicenseRequired,
category: alertType.id,
ruleset: alertType.producer,
name: alertName,
},
};
{
type: 'action',
id: action.id,
typeId: action.actionTypeId,
},
],
...namespace,
message: `alert: ${alertLabel} instanceId: '${alertInstanceId}' scheduled ${
actionSubgroup
? `actionGroup(subgroup): '${actionGroup}(${actionSubgroup})'`
: `actionGroup: '${actionGroup}'`
} action: ${actionLabel}`,
});
event.message = `alert: ${alertLabel} instanceId: '${alertInstanceId}' scheduled ${
actionSubgroup
? `actionGroup(subgroup): '${actionGroup}(${actionSubgroup})'`
: `actionGroup: '${actionGroup}'`
} action: ${actionLabel}`;
eventLogger.logEvent(event);
}
};

View file

@ -1379,7 +1379,6 @@ describe('Task Runner', () => {
"kibana": Object {
"alerting": Object {
"action_group_id": "default",
"action_subgroup": undefined,
"instance_id": "1",
},
"saved_objects": Array [
@ -1676,7 +1675,6 @@ describe('Task Runner', () => {
"kibana": Object {
"alerting": Object {
"action_group_id": "recovered",
"action_subgroup": undefined,
"instance_id": "2",
},
"saved_objects": Array [
@ -1717,7 +1715,6 @@ describe('Task Runner', () => {
"kibana": Object {
"alerting": Object {
"action_group_id": "default",
"action_subgroup": undefined,
"instance_id": "1",
},
"saved_objects": Array [

View file

@ -49,16 +49,18 @@ import {
AlertInstanceContext,
WithoutReservedActionGroups,
} from '../../common';
import { NormalizedAlertType } from '../rule_type_registry';
import { NormalizedAlertType, UntypedNormalizedAlertType } from '../rule_type_registry';
import { getEsErrorMessage } from '../lib/errors';
import {
createAlertEventLogRecordObject,
Event,
} from '../lib/create_alert_event_log_record_object';
const FALLBACK_RETRY_INTERVAL = '5m';
// 1,000,000 nanoseconds in 1 millisecond
const Millis2Nanos = 1000 * 1000;
type Event = Exclude<IEvent, undefined>;
interface AlertTaskRunResult {
state: AlertTaskState;
schedule: IntervalSchedule | undefined;
@ -517,37 +519,26 @@ export class TaskRunner<
const namespace = this.context.spaceIdToNamespace(spaceId);
const eventLogger = this.context.eventLogger;
const scheduleDelay = runDate.getTime() - this.taskInstance.runAt.getTime();
const event: IEvent = {
// explicitly set execute timestamp so it will be before other events
// generated here (new-instance, schedule-action, etc)
'@timestamp': runDateString,
event: {
action: EVENT_LOG_ACTIONS.execute,
kind: 'alert',
category: [this.alertType.producer],
const event = createAlertEventLogRecordObject({
timestamp: runDateString,
ruleId: alertId,
ruleType: this.alertType as UntypedNormalizedAlertType,
action: EVENT_LOG_ACTIONS.execute,
namespace,
task: {
scheduled: this.taskInstance.runAt.toISOString(),
scheduleDelay: Millis2Nanos * scheduleDelay,
},
kibana: {
saved_objects: [
{
rel: SAVED_OBJECT_REL_PRIMARY,
type: 'alert',
id: alertId,
type_id: this.alertType.id,
namespace,
},
],
task: {
scheduled: this.taskInstance.runAt.toISOString(),
schedule_delay: Millis2Nanos * scheduleDelay,
savedObjects: [
{
id: alertId,
type: 'alert',
typeId: this.alertType.id,
relation: SAVED_OBJECT_REL_PRIMARY,
},
},
rule: {
id: alertId,
license: this.alertType.minimumLicenseRequired,
category: this.alertType.id,
ruleset: this.alertType.producer,
},
};
],
});
eventLogger.startTiming(event);

View file

@ -14,12 +14,16 @@ import {
getUrlPrefix,
getTestAlertData,
ObjectRemover,
getEventLog,
} from '../../../common/lib';
import { validateEvent } from './event_log';
// eslint-disable-next-line import/no-default-export
export default function createDisableAlertTests({ getService }: FtrProviderContext) {
const es = getService('es');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const retry = getService('retry');
const supertest = getService('supertest');
describe('disable', () => {
const objectRemover = new ObjectRemover(supertestWithoutAuth);
@ -75,6 +79,75 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
});
});
it('should create recovered-instance events for all alert instances', async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send({
enabled: true,
name: 'abc',
tags: ['foo'],
rule_type_id: 'test.cumulative-firing',
consumer: 'alertsFixture',
schedule: { interval: '5s' },
throttle: '5s',
actions: [],
params: {},
notify_when: 'onThrottleInterval',
})
.expect(200);
objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting');
// wait for alert to actually execute
await retry.try(async () => {
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}/state`
);
expect(response.status).to.eql(200);
expect(response.body).to.key('alerts', 'rule_type_state', 'previous_started_at');
expect(response.body.rule_type_state.runCount).to.greaterThan(1);
});
await alertUtils.getDisableRequest(createdAlert.id);
const ruleId = createdAlert.id;
// wait for the events we're expecting
const events = await retry.try(async () => {
return await getEventLog({
getService,
spaceId: Spaces.space1.id,
type: 'alert',
id: ruleId,
provider: 'alerting',
actions: new Map([
// make sure the counts of the # of events per type are as expected
['recovered-instance', { equal: 2 }],
]),
});
});
const event = events[0];
expect(event).to.be.ok();
validateEvent(event, {
spaceId: Spaces.space1.id,
savedObjects: [
{ type: 'alert', id: ruleId, rel: 'primary', type_id: 'test.cumulative-firing' },
],
message: "instance 'instance-0' has recovered due to the rule was disabled",
shouldHaveEventEnd: false,
shouldHaveTask: false,
rule: {
id: ruleId,
category: createdAlert.rule_type_id,
license: 'basic',
ruleset: 'alertsFixture',
name: 'abc',
},
});
});
describe('legacy', () => {
it('should handle disable alert request appropriately', async () => {
const { body: createdAlert } = await supertestWithoutAuth