Skip to content

Commit

Permalink
[EventLog] Populate alert instances view with event log data (elastic…
Browse files Browse the repository at this point in the history
…#68437) (elastic#75036)

resolves elastic#57446

Adds a new API (AlertClient and HTTP endpoint) `getAlertStatus()` which returns
alert data calculated from the event log.
  • Loading branch information
pmuellr authored Aug 14, 2020
1 parent 400f604 commit c3bea87
Show file tree
Hide file tree
Showing 41 changed files with 2,012 additions and 206 deletions.
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

0 comments on commit c3bea87

Please sign in to comment.