Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [EventLog] Populate alert instances view with event log data (#68437) #75036

Merged
merged 1 commit into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions x-pack/plugins/alerts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Table of Contents
- [`GET /api/alerts/_find`: Find alerts](#get-apialertfind-find-alerts)
- [`GET /api/alerts/alert/{id}`: Get alert](#get-apialertid-get-alert)
- [`GET /api/alerts/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state)
- [`GET /api/alerts/alert/{id}/status`: Get alert status](#get-apialertidstate-get-alert-status)
- [`GET /api/alerts/list_alert_types`: List alert types](#get-apialerttypes-list-alert-types)
- [`PUT /api/alerts/alert/{id}`: Update alert](#put-apialertid-update-alert)
- [`POST /api/alerts/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert)
Expand Down Expand Up @@ -504,6 +505,23 @@ Params:
|---|---|---|
|id|The id of the alert whose state you're trying to get.|string|

### `GET /api/alerts/alert/{id}/status`: Get alert status

Similar to the `GET state` call, but collects additional information from
the event log.

Params:

|Property|Description|Type|
|---|---|---|
|id|The id of the alert whose status you're trying to get.|string|

Query:

|Property|Description|Type|
|---|---|---|
|dateStart|The date to start looking for alert events in the event log. Either an ISO date string, or a duration string indicating time since now.|string|

### `GET /api/alerts/list_alert_types`: List alert types

No parameters.
Expand Down
31 changes: 31 additions & 0 deletions x-pack/plugins/alerts/common/alert_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

type AlertStatusValues = 'OK' | 'Active' | 'Error';
type AlertInstanceStatusValues = 'OK' | 'Active';

export interface AlertStatus {
id: string;
name: string;
tags: string[];
alertTypeId: string;
consumer: string;
muteAll: boolean;
throttle: string | null;
enabled: boolean;
statusStartDate: string;
statusEndDate: string;
status: AlertStatusValues;
lastRun?: string;
errorMessages: Array<{ date: string; message: string }>;
instances: Record<string, AlertInstanceStatus>;
}

export interface AlertInstanceStatus {
status: AlertInstanceStatusValues;
muted: boolean;
activeStartDate?: string;
}
1 change: 1 addition & 0 deletions x-pack/plugins/alerts/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './alert_type';
export * from './alert_instance';
export * from './alert_task_instance';
export * from './alert_navigation';
export * from './alert_status';

export interface ActionGroup {
id: string;
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/alerts/server/alerts_client.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const createAlertsClientMock = () => {
muteInstance: jest.fn(),
unmuteInstance: jest.fn(),
listAlertTypes: jest.fn(),
getAlertStatus: jest.fn(),
};
return mocked;
};
Expand Down
246 changes: 241 additions & 5 deletions x-pack/plugins/alerts/server/alerts_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
import { alertTypeRegistryMock } from './alert_type_registry.mock';
import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock';
import { TaskStatus } from '../../task_manager/server';
import { IntervalSchedule } from './types';
import { IntervalSchedule, RawAlert } from './types';
import { resolvable } from './test_utils';
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
import { actionsClientMock, actionsAuthorizationMock } from '../../actions/server/mocks';
import { AlertsAuthorization } from './authorization/alerts_authorization';
import { ActionsAuthorization } from '../../actions/server';
import { eventLogClientMock } from '../../event_log/server/mocks';
import { QueryEventsBySavedObjectResult } from '../../event_log/server';
import { SavedObject } from 'kibana/server';
import { EventsFactory } from './lib/alert_status_from_event_log.test';

const taskManager = taskManagerMock.start();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const eventLogClient = eventLogClientMock.create();

const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
Expand All @@ -39,6 +45,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
logger: loggingSystemMock.create().get(),
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
};

beforeEach(() => {
Expand Down Expand Up @@ -91,17 +98,33 @@ beforeEach(() => {
async executor() {},
producer: 'alerts',
}));
alertsClientParams.getEventLogClient.mockResolvedValue(eventLogClient);
});

const mockedDate = new Date('2019-02-12T21:01:22.479Z');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockedDateString = '2019-02-12T21:01:22.479Z';
const mockedDate = new Date(mockedDateString);
const DateOriginal = Date;

// A version of date that responds to `new Date(null|undefined)` and `Date.now()`
// by returning a fixed date, otherwise should be same as Date.
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
(global as any).Date = class Date {
constructor() {
return mockedDate;
constructor(...args: unknown[]) {
// sometimes the ctor has no args, sometimes has a single `null` arg
if (args[0] == null) {
// @ts-ignore
return mockedDate;
} else {
// @ts-ignore
return new DateOriginal(...args);
}
}
static now() {
return mockedDate.getTime();
}
static parse(string: string) {
return DateOriginal.parse(string);
}
};

function getMockData(overwrites: Record<string, unknown> = {}): CreateOptions['data'] {
Expand Down Expand Up @@ -2295,6 +2318,219 @@ describe('getAlertState()', () => {
});
});

const AlertStatusFindEventsResult: QueryEventsBySavedObjectResult = {
page: 1,
per_page: 10000,
total: 0,
data: [],
};

const AlertStatusIntervalSeconds = 1;

const BaseAlertStatusSavedObject: SavedObject<RawAlert> = {
id: '1',
type: 'alert',
attributes: {
enabled: true,
name: 'alert-name',
tags: ['tag-1', 'tag-2'],
alertTypeId: '123',
consumer: 'alert-consumer',
schedule: { interval: `${AlertStatusIntervalSeconds}s` },
actions: [],
params: {},
createdBy: null,
updatedBy: null,
createdAt: mockedDateString,
apiKey: null,
apiKeyOwner: null,
throttle: null,
muteAll: false,
mutedInstanceIds: [],
},
references: [],
};

function getAlertStatusSavedObject(attributes: Partial<RawAlert> = {}): SavedObject<RawAlert> {
return {
...BaseAlertStatusSavedObject,
attributes: { ...BaseAlertStatusSavedObject.attributes, ...attributes },
};
}

describe('getAlertStatus()', () => {
let alertsClient: AlertsClient;

beforeEach(() => {
alertsClient = new AlertsClient(alertsClientParams);
});

test('runs as expected with some event log data', async () => {
const alertSO = getAlertStatusSavedObject({ mutedInstanceIds: ['instance-muted-no-activity'] });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(alertSO);

const eventsFactory = new EventsFactory(mockedDateString);
const events = eventsFactory
.addExecute()
.addNewInstance('instance-currently-active')
.addNewInstance('instance-previously-active')
.addActiveInstance('instance-currently-active')
.addActiveInstance('instance-previously-active')
.advanceTime(10000)
.addExecute()
.addResolvedInstance('instance-previously-active')
.addActiveInstance('instance-currently-active')
.getEvents();
const eventsResult = {
...AlertStatusFindEventsResult,
total: events.length,
data: events,
};
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(eventsResult);

const dateStart = new Date(Date.now() - 60 * 1000).toISOString();

const result = await alertsClient.getAlertStatus({ id: '1', dateStart });
expect(result).toMatchInlineSnapshot(`
Object {
"alertTypeId": "123",
"consumer": "alert-consumer",
"enabled": true,
"errorMessages": Array [],
"id": "1",
"instances": Object {
"instance-currently-active": Object {
"activeStartDate": "2019-02-12T21:01:22.479Z",
"muted": false,
"status": "Active",
},
"instance-muted-no-activity": Object {
"activeStartDate": undefined,
"muted": true,
"status": "OK",
},
"instance-previously-active": Object {
"activeStartDate": undefined,
"muted": false,
"status": "OK",
},
},
"lastRun": "2019-02-12T21:01:32.479Z",
"muteAll": false,
"name": "alert-name",
"status": "Active",
"statusEndDate": "2019-02-12T21:01:22.479Z",
"statusStartDate": "2019-02-12T21:00:22.479Z",
"tags": Array [
"tag-1",
"tag-2",
],
"throttle": null,
}
`);
});

// Further tests don't check the result of `getAlertStatus()`, as the result
// is just the result from the `alertStatusFromEventLog()`, which itself
// has a complete set of tests. These tests just make sure the data gets
// sent into `getAlertStatus()` as appropriate.

test('calls saved objects and event log client with default params', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);

await alertsClient.getAlertStatus({ id: '1' });

expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alert",
"1",
Object {
"end": "2019-02-12T21:01:22.479Z",
"page": 1,
"per_page": 10000,
"sort_order": "desc",
"start": "2019-02-12T21:00:22.479Z",
},
]
`);
// calculate the expected start/end date for one test
const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!;
expect(end).toBe(mockedDateString);

const startMillis = Date.parse(start!);
const endMillis = Date.parse(end!);
const expectedDuration = 60 * AlertStatusIntervalSeconds * 1000;
expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2);
expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2);
});

test('calls event log client with start date', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);

const dateStart = new Date(Date.now() - 60 * AlertStatusIntervalSeconds * 1000).toISOString();
await alertsClient.getAlertStatus({ id: '1', dateStart });

expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!;

expect({ start, end }).toMatchInlineSnapshot(`
Object {
"end": "2019-02-12T21:01:22.479Z",
"start": "2019-02-12T21:00:22.479Z",
}
`);
});

test('calls event log client with relative start date', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);

const dateStart = '2m';
await alertsClient.getAlertStatus({ id: '1', dateStart });

expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!;

expect({ start, end }).toMatchInlineSnapshot(`
Object {
"end": "2019-02-12T21:01:22.479Z",
"start": "2019-02-12T20:59:22.479Z",
}
`);
});

test('invalid start date throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);

const dateStart = 'ain"t no way this will get parsed as a date';
expect(alertsClient.getAlertStatus({ id: '1', dateStart })).rejects.toMatchInlineSnapshot(
`[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]`
);
});

test('saved object get throws an error', async () => {
unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!'));
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);

expect(alertsClient.getAlertStatus({ id: '1' })).rejects.toMatchInlineSnapshot(`[Error: OMG!]`);
});

test('findEvents throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('OMG 2!'));

// error eaten but logged
await alertsClient.getAlertStatus({ id: '1' });
});
});

describe('find()', () => {
const listedTypes = new Set([
{
Expand Down
Loading