mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
63a615f1f1
commit
84df5697cc
12 changed files with 628 additions and 77 deletions
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -373,6 +373,7 @@ export class AlertingPlugin {
|
|||
eventLog: plugins.eventLog,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
authorization: alertingAuthorizationClientFactory,
|
||||
eventLogger: this.eventLogger,
|
||||
});
|
||||
|
||||
const getRulesClientWithRequest = (request: KibanaRequest) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue