adds createdAt and updatedAt fields to alerting (#53793) (#53952)

When users are writing UI's they need to see when an alert was created and when it was last updated, to this end we've added these two fields on the alert type.
This commit is contained in:
Gidi Meir Morris 2020-01-03 18:41:38 +00:00 committed by Mike Côté
parent 9f885ed2e6
commit 851f37d948
16 changed files with 139 additions and 25 deletions

View file

@ -54,6 +54,9 @@
"updatedBy": {
"type": "keyword"
},
"createdAt": {
"type": "date"
},
"apiKey": {
"type": "binary"
},

View file

@ -100,6 +100,7 @@ describe('create()', () => {
params: {
bar: true,
},
createdAt: '2019-02-12T21:01:22.479Z',
actions: [
{
group: 'default',
@ -160,6 +161,7 @@ describe('create()', () => {
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"params": Object {
"bar": true,
@ -168,6 +170,7 @@ describe('create()', () => {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
@ -189,6 +192,7 @@ describe('create()', () => {
"apiKey": null,
"apiKeyOwner": null,
"consumer": "bar",
"createdAt": "2019-02-12T21:01:22.479Z",
"createdBy": "elastic",
"enabled": true,
"muteAll": false,
@ -311,6 +315,7 @@ describe('create()', () => {
params: {
bar: true,
},
createdAt: new Date().toISOString(),
actions: [
{
group: 'default',
@ -407,6 +412,7 @@ describe('create()', () => {
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"params": Object {
"bar": true,
@ -415,6 +421,7 @@ describe('create()', () => {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([
@ -460,6 +467,7 @@ describe('create()', () => {
params: {
bar: true,
},
createdAt: new Date().toISOString(),
actions: [
{
group: 'default',
@ -493,6 +501,7 @@ describe('create()', () => {
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": false,
"id": "1",
"params": Object {
@ -501,6 +510,7 @@ describe('create()', () => {
"schedule": Object {
"interval": 10000,
},
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
@ -806,6 +816,7 @@ describe('create()', () => {
apiKey: Buffer.from('123:abc').toString('base64'),
apiKeyOwner: 'elastic',
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
enabled: true,
schedule: { interval: '10s' },
@ -1248,6 +1259,7 @@ describe('get()', () => {
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"params": Object {
"bar": true,
@ -1255,6 +1267,7 @@ describe('get()', () => {
"schedule": Object {
"interval": "10s",
},
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
@ -1347,6 +1360,7 @@ describe('find()', () => {
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"params": Object {
"bar": true,
@ -1354,6 +1368,7 @@ describe('find()', () => {
"schedule": Object {
"interval": "10s",
},
"updatedAt": 2019-02-12T21:01:22.479Z,
},
],
"page": 1,
@ -1476,7 +1491,9 @@ describe('update()', () => {
},
],
scheduledTaskId: 'task-123',
createdAt: new Date().toISOString(),
},
updated_at: new Date().toISOString(),
references: [
{
name: 'action_0',
@ -1517,6 +1534,7 @@ describe('update()', () => {
},
},
],
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": true,
"id": "1",
"params": Object {
@ -1526,6 +1544,7 @@ describe('update()', () => {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
@ -1624,6 +1643,7 @@ describe('update()', () => {
params: {
bar: true,
},
createdAt: new Date().toISOString(),
actions: [
{
group: 'default',
@ -1652,6 +1672,7 @@ describe('update()', () => {
],
scheduledTaskId: 'task-123',
},
updated_at: new Date().toISOString(),
references: [
{
name: 'action_0',
@ -1732,6 +1753,7 @@ describe('update()', () => {
},
},
],
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": true,
"id": "1",
"params": Object {
@ -1741,6 +1763,7 @@ describe('update()', () => {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([
@ -1799,6 +1822,7 @@ describe('update()', () => {
params: {
bar: true,
},
createdAt: new Date().toISOString(),
actions: [
{
group: 'default',
@ -1812,6 +1836,7 @@ describe('update()', () => {
apiKey: Buffer.from('123:abc').toString('base64'),
scheduledTaskId: 'task-123',
},
updated_at: new Date().toISOString(),
references: [
{
name: 'action_0',
@ -1853,6 +1878,7 @@ describe('update()', () => {
},
],
"apiKey": "MTIzOmFiYw==",
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": true,
"id": "1",
"params": Object {
@ -1862,6 +1888,7 @@ describe('update()', () => {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);

View file

@ -77,6 +77,8 @@ interface CreateOptions {
keyof Alert,
| 'createdBy'
| 'updatedBy'
| 'createdAt'
| 'updatedAt'
| 'apiKey'
| 'apiKeyOwner'
| 'muteAll'
@ -142,6 +144,7 @@ export class AlertsClient {
actions,
createdBy: username,
updatedBy: username,
createdAt: new Date().toISOString(),
params: validatedAlertTypeParams,
muteAll: false,
mutedInstanceIds: [],
@ -171,12 +174,17 @@ export class AlertsClient {
});
createdAlert.attributes.scheduledTaskId = scheduledTask.id;
}
return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references);
return this.getAlertFromRaw(
createdAlert.id,
createdAlert.attributes,
createdAlert.updated_at,
references
);
}
public async get({ id }: { id: string }) {
const result = await this.savedObjectsClient.get('alert', id);
return this.getAlertFromRaw(result.id, result.attributes, result.references);
return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references);
}
public async find({ options = {} }: FindOptions = {}): Promise<FindResult> {
@ -186,7 +194,7 @@ export class AlertsClient {
});
const data = results.saved_objects.map(result =>
this.getAlertFromRaw(result.id, result.attributes, result.references)
this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references)
);
return {
@ -214,7 +222,7 @@ export class AlertsClient {
updateResult.scheduledTaskId &&
!isEqual(alert.attributes.schedule, updateResult.schedule)
) {
this.taskManager.runNow(updateResult.scheduledTaskId).catch(err => {
this.taskManager.runNow(updateResult.scheduledTaskId).catch((err: Error) => {
this.logger.error(
`Alert update failed to run its underlying task. TaskManager runNow failed with Error: ${err.message}`
);
@ -254,7 +262,12 @@ export class AlertsClient {
references,
}
);
return this.getAlertFromRaw(id, updatedObject.attributes, updatedObject.references);
return this.getAlertFromRaw(
id,
updatedObject.attributes,
updatedObject.updated_at,
updatedObject.references
);
}
private apiKeyAsAlertAttributes(
@ -301,6 +314,7 @@ export class AlertsClient {
enabled: true,
...this.apiKeyAsAlertAttributes(await this.createAPIKey(), username),
updatedBy: username,
scheduledTaskId: scheduledTask.id,
},
{ version }
@ -382,6 +396,7 @@ export class AlertsClient {
alertId,
{
updatedBy: await this.getUserName(),
mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId),
},
{ version }
@ -424,6 +439,7 @@ export class AlertsClient {
private getAlertFromRaw(
id: string,
rawAlert: Partial<RawAlert>,
updatedAt: SavedObject['updated_at'],
references: SavedObjectReference[] | undefined
) {
if (!rawAlert.actions) {
@ -436,6 +452,8 @@ export class AlertsClient {
return {
id,
...rawAlert,
updatedAt: updatedAt ? new Date(updatedAt) : new Date(rawAlert.createdAt!),
createdAt: new Date(rawAlert.createdAt!),
actions,
};
}

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { omit } from 'lodash';
import { createMockServer } from './_mock_server';
import { createAlertRoute } from './create';
@ -39,8 +40,12 @@ test('creates an alert with proper parameters', async () => {
payload: mockedAlert,
};
const createdAt = new Date();
const updatedAt = new Date();
alertsClient.create.mockResolvedValueOnce({
...mockedAlert,
createdAt,
updatedAt,
id: '123',
actions: [
{
@ -52,7 +57,8 @@ test('creates an alert with proper parameters', async () => {
const { payload, statusCode } = await server.inject(request);
expect(statusCode).toBe(200);
const response = JSON.parse(payload);
expect(response).toMatchInlineSnapshot(`
expect(new Date(response.createdAt)).toEqual(createdAt);
expect(omit(response, 'createdAt', 'updatedAt')).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {

View file

@ -17,6 +17,8 @@ const mockedAlert = {
params: {
bar: true,
},
createdAt: new Date(),
updatedAt: new Date(),
actions: [
{
group: 'default',
@ -40,8 +42,10 @@ test('calls get with proper parameters', async () => {
alertsClient.get.mockResolvedValueOnce(mockedAlert);
const { payload, statusCode } = await server.inject(request);
expect(statusCode).toBe(200);
const response = JSON.parse(payload);
expect(response).toEqual(mockedAlert);
const { createdAt, updatedAt, ...response } = JSON.parse(payload);
expect({ createdAt: new Date(createdAt), updatedAt: new Date(updatedAt), ...response }).toEqual(
mockedAlert
);
expect(alertsClient.get).toHaveBeenCalledTimes(1);
expect(alertsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [

View file

@ -20,6 +20,8 @@ const mockedResponse = {
params: {
otherField: false,
},
createdAt: new Date(),
updatedAt: new Date(),
actions: [
{
group: 'default',
@ -59,8 +61,10 @@ test('calls the update function with proper parameters', async () => {
alertsClient.update.mockResolvedValueOnce(mockedResponse);
const { payload, statusCode } = await server.inject(request);
expect(statusCode).toBe(200);
const response = JSON.parse(payload);
expect(response).toEqual(mockedResponse);
const { createdAt, updatedAt, ...response } = JSON.parse(payload);
expect({ createdAt: new Date(createdAt), updatedAt: new Date(updatedAt), ...response }).toEqual(
mockedResponse
);
expect(alertsClient.update).toHaveBeenCalledTimes(1);
expect(alertsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
Array [

View file

@ -76,6 +76,8 @@ export interface Alert {
scheduledTaskId?: string;
createdBy: string | null;
updatedBy: string | null;
createdAt: Date;
updatedAt: Date;
apiKey: string | null;
apiKeyOwner: string | null;
throttle: string | null;
@ -95,6 +97,7 @@ export interface RawAlert extends SavedObjectAttributes {
scheduledTaskId?: string;
createdBy: string | null;
updatedBy: string | null;
createdAt: string;
apiKey: string | null;
apiKeyOwner: string | null;
throttle: string | null;

View file

@ -308,6 +308,8 @@ export const getResult = (): RuleAlertType => ({
references: ['http://www.example.com', 'https://ww.example.com'],
version: 1,
},
createdAt: new Date('2019-12-13T16:40:33.400Z'),
updatedAt: new Date('2019-12-13T16:40:33.400Z'),
schedule: { interval: '5m' },
enabled: true,
actions: [],

View file

@ -92,6 +92,8 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
createdBy: user.username,
schedule: { interval: '1m' },
scheduledTaskId: response.body.scheduledTaskId,
createdAt: response.body.createdAt,
updatedAt: response.body.updatedAt,
throttle: '1m',
updatedBy: user.username,
apiKeyOwner: user.username,
@ -99,6 +101,9 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
mutedInstanceIds: [],
});
expect(typeof response.body.scheduledTaskId).to.be('string');
expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
const { _source: taskRecord } = await getScheduledTask(response.body.scheduledTaskId);
expect(taskRecord.type).to.eql('task');
expect(taskRecord.task.taskType).to.eql('alerting:test.noop');

View file

@ -66,12 +66,16 @@ export default function createFindTests({ getService }: FtrProviderContext) {
params: {},
createdBy: 'elastic',
scheduledTaskId: match.scheduledTaskId,
createdAt: match.createdAt,
updatedAt: match.updatedAt,
throttle: '1m',
updatedBy: 'elastic',
apiKeyOwner: 'elastic',
muteAll: false,
mutedInstanceIds: [],
});
expect(Date.parse(match.createdAt)).to.be.greaterThan(0);
expect(Date.parse(match.updatedAt)).to.be.greaterThan(0);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
@ -157,7 +161,11 @@ export default function createFindTests({ getService }: FtrProviderContext) {
apiKeyOwner: 'elastic',
muteAll: false,
mutedInstanceIds: [],
createdAt: match.createdAt,
updatedAt: match.updatedAt,
});
expect(Date.parse(match.createdAt)).to.be.greaterThan(0);
expect(Date.parse(match.updatedAt)).to.be.greaterThan(0);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);

View file

@ -60,12 +60,16 @@ export default function createGetTests({ getService }: FtrProviderContext) {
params: {},
createdBy: 'elastic',
scheduledTaskId: response.body.scheduledTaskId,
updatedAt: response.body.updatedAt,
createdAt: response.body.createdAt,
throttle: '1m',
updatedBy: 'elastic',
apiKeyOwner: 'elastic',
muteAll: false,
mutedInstanceIds: [],
});
expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);

View file

@ -81,7 +81,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
muteAll: false,
mutedInstanceIds: [],
scheduledTaskId: createdAlert.scheduledTaskId,
createdAt: response.body.createdAt,
updatedAt: response.body.updatedAt,
});
expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(
Date.parse(response.body.createdAt)
);
// Ensure AAD isn't broken
await checkAAD({
supertest,

View file

@ -79,7 +79,12 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
throttle: '1m',
muteAll: false,
mutedInstanceIds: [],
createdAt: response.body.createdAt,
updatedAt: response.body.updatedAt,
});
expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
expect(typeof response.body.scheduledTaskId).to.be('string');
const { _source: taskRecord } = await getScheduledTask(response.body.scheduledTaskId);
expect(taskRecord.type).to.eql('task');

View file

@ -54,7 +54,11 @@ export default function createFindTests({ getService }: FtrProviderContext) {
throttle: '1m',
muteAll: false,
mutedInstanceIds: [],
createdAt: match.createdAt,
updatedAt: match.updatedAt,
});
expect(Date.parse(match.createdAt)).to.be.greaterThan(0);
expect(Date.parse(match.updatedAt)).to.be.greaterThan(0);
});
it(`shouldn't find alert from another space`, async () => {

View file

@ -48,7 +48,11 @@ export default function createGetTests({ getService }: FtrProviderContext) {
throttle: '1m',
muteAll: false,
mutedInstanceIds: [],
createdAt: response.body.createdAt,
updatedAt: response.body.updatedAt,
});
expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
});
it(`shouldn't find alert from another space`, async () => {

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect/expect.js';
import { Spaces } from '../../scenarios';
import { checkAAD, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
@ -35,24 +36,33 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
actions: [],
throttle: '1m',
};
await supertest
const response = await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`)
.set('kbn-xsrf', 'foo')
.send(updatedData)
.expect(200, {
...updatedData,
id: createdAlert.id,
tags: ['bar'],
alertTypeId: 'test.noop',
consumer: 'bar',
createdBy: null,
enabled: true,
updatedBy: null,
apiKeyOwner: null,
muteAll: false,
mutedInstanceIds: [],
scheduledTaskId: createdAlert.scheduledTaskId,
});
.expect(200);
expect(response.body).to.eql({
...updatedData,
id: createdAlert.id,
tags: ['bar'],
alertTypeId: 'test.noop',
consumer: 'bar',
createdBy: null,
enabled: true,
updatedBy: null,
apiKeyOwner: null,
muteAll: false,
mutedInstanceIds: [],
scheduledTaskId: createdAlert.scheduledTaskId,
createdAt: response.body.createdAt,
updatedAt: response.body.updatedAt,
});
expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(
Date.parse(response.body.createdAt)
);
// Ensure AAD isn't broken
await checkAAD({