[EventLog] Added event log API to get events for multiple saved objects. (#87596)

* Added alerting API to get all active instances

* modofied event log findEventsBySavedObject to support bulk ids, renamed to findEventsBySavedObjectIds

* fixed faling typechecks

* fixed crash on zpd/api/event_log/alert/84c00970-5130-11eb-9fa7/_find for non existing id

* fixed faling typechecks

* fixed faling typechecks

* fixed due to comments

* fixed due to comments

* fixed failing test

* fixed due to comments
This commit is contained in:
Yuliia Naumenko 2021-01-12 16:25:23 -08:00 committed by GitHub
parent 5e4402c374
commit fb67443e6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 320 additions and 102 deletions

View file

@ -19,6 +19,7 @@ import {
ElasticsearchServiceStart,
ILegacyClusterClient,
SavedObjectsClientContract,
SavedObjectsBulkGetObject,
} from '../../../../src/core/server';
import {
@ -333,7 +334,12 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
this.eventLogService!.registerSavedObjectProvider('action', (request) => {
const client = secureGetActionsClientWithRequest(request);
return async (type: string, id: string) => (await client).get({ id });
return (objects?: SavedObjectsBulkGetObject[]) =>
objects
? Promise.all(
objects.map(async (objectItem) => await (await client).get({ id: objectItem.id }))
)
: Promise.resolve([]);
});
const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) =>

View file

@ -412,7 +412,7 @@ export class AlertsClient {
this.logger.debug(`getAlertInstanceSummary(): search the event log for alert ${id}`);
let events: IEvent[];
try {
const queryResults = await eventLogClient.findEventsBySavedObject('alert', id, {
const queryResults = await eventLogClient.findEventsBySavedObjectIds('alert', [id], {
page: 1,
per_page: 10000,
start: parsedDateStart.toISOString(),

View file

@ -131,7 +131,7 @@ describe('getAlertInstanceSummary()', () => {
total: events.length,
data: events,
};
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(eventsResult);
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(eventsResult);
const dateStart = new Date(Date.now() - 60 * 1000).toISOString();
@ -188,18 +188,20 @@ describe('getAlertInstanceSummary()', () => {
test('calls saved objects and event log client with default params', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(
AlertInstanceSummaryFindEventsResult
);
await alertsClient.getAlertInstanceSummary({ id: '1' });
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject.mock.calls[0]).toMatchInlineSnapshot(`
expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObjectIds.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alert",
"1",
Array [
"1",
],
Object {
"end": "2019-02-12T21:01:22.479Z",
"page": 1,
@ -210,7 +212,7 @@ describe('getAlertInstanceSummary()', () => {
]
`);
// calculate the expected start/end date for one test
const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!;
const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!;
expect(end).toBe(mockedDateString);
const startMillis = Date.parse(start!);
@ -222,7 +224,7 @@ describe('getAlertInstanceSummary()', () => {
test('calls event log client with start date', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(
AlertInstanceSummaryFindEventsResult
);
@ -232,8 +234,8 @@ describe('getAlertInstanceSummary()', () => {
await alertsClient.getAlertInstanceSummary({ id: '1', dateStart });
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!;
expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!;
expect({ start, end }).toMatchInlineSnapshot(`
Object {
@ -245,7 +247,7 @@ describe('getAlertInstanceSummary()', () => {
test('calls event log client with relative start date', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(
AlertInstanceSummaryFindEventsResult
);
@ -253,8 +255,8 @@ describe('getAlertInstanceSummary()', () => {
await alertsClient.getAlertInstanceSummary({ id: '1', dateStart });
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!;
expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!;
expect({ start, end }).toMatchInlineSnapshot(`
Object {
@ -266,7 +268,7 @@ describe('getAlertInstanceSummary()', () => {
test('invalid start date throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(
AlertInstanceSummaryFindEventsResult
);
@ -280,7 +282,7 @@ describe('getAlertInstanceSummary()', () => {
test('saved object get throws an error', async () => {
unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!'));
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(
AlertInstanceSummaryFindEventsResult
);
@ -291,7 +293,7 @@ describe('getAlertInstanceSummary()', () => {
test('findEvents throws an error', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('OMG 2!'));
eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('OMG 2!'));
// error eaten but logged
await alertsClient.getAlertInstanceSummary({ id: '1' });

View file

@ -33,6 +33,7 @@ import {
ILegacyClusterClient,
StatusServiceSetup,
ServiceStatus,
SavedObjectsBulkGetObject,
} from '../../../../src/core/server';
import {
@ -370,7 +371,10 @@ export class AlertingPlugin {
this.eventLogService!.registerSavedObjectProvider('alert', (request) => {
const client = getAlertsClientWithRequest(request);
return (type: string, id: string) => client.get({ id });
return (objects?: SavedObjectsBulkGetObject[]) =>
objects
? Promise.all(objects.map(async (objectItem) => await client.get({ id: objectItem.id })))
: Promise.resolve([]);
});
scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager);

View file

@ -16,7 +16,7 @@ const createClusterClientMock = () => {
createIndexTemplate: jest.fn(),
doesAliasExist: jest.fn(),
createIndex: jest.fn(),
queryEventsBySavedObject: jest.fn(),
queryEventsBySavedObjects: jest.fn(),
shutdown: jest.fn(),
};
return mock;

View file

@ -327,11 +327,11 @@ describe('queryEventsBySavedObject', () => {
total: { value: 0 },
},
});
await clusterClientAdapter.queryEventsBySavedObject(
await clusterClientAdapter.queryEventsBySavedObjects(
'index-name',
'namespace',
'saved-object-type',
'saved-object-id',
['saved-object-id'],
DEFAULT_OPTIONS
);
@ -365,10 +365,10 @@ describe('queryEventsBySavedObject', () => {
},
},
Object {
"term": Object {
"kibana.saved_objects.id": Object {
"value": "saved-object-id",
},
"terms": Object {
"kibana.saved_objects.id": Array [
"saved-object-id",
],
},
},
Object {
@ -406,11 +406,11 @@ describe('queryEventsBySavedObject', () => {
total: { value: 0 },
},
});
await clusterClientAdapter.queryEventsBySavedObject(
await clusterClientAdapter.queryEventsBySavedObjects(
'index-name',
undefined,
'saved-object-type',
'saved-object-id',
['saved-object-id'],
DEFAULT_OPTIONS
);
@ -444,10 +444,10 @@ describe('queryEventsBySavedObject', () => {
},
},
Object {
"term": Object {
"kibana.saved_objects.id": Object {
"value": "saved-object-id",
},
"terms": Object {
"kibana.saved_objects.id": Array [
"saved-object-id",
],
},
},
Object {
@ -487,11 +487,11 @@ describe('queryEventsBySavedObject', () => {
total: { value: 0 },
},
});
await clusterClientAdapter.queryEventsBySavedObject(
await clusterClientAdapter.queryEventsBySavedObjects(
'index-name',
'namespace',
'saved-object-type',
'saved-object-id',
['saved-object-id'],
{ ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' }
);
@ -515,11 +515,11 @@ describe('queryEventsBySavedObject', () => {
const start = '2020-07-08T00:52:28.350Z';
await clusterClientAdapter.queryEventsBySavedObject(
await clusterClientAdapter.queryEventsBySavedObjects(
'index-name',
'namespace',
'saved-object-type',
'saved-object-id',
['saved-object-id'],
{ ...DEFAULT_OPTIONS, start }
);
@ -553,10 +553,10 @@ describe('queryEventsBySavedObject', () => {
},
},
Object {
"term": Object {
"kibana.saved_objects.id": Object {
"value": "saved-object-id",
},
"terms": Object {
"kibana.saved_objects.id": Array [
"saved-object-id",
],
},
},
Object {
@ -605,11 +605,11 @@ describe('queryEventsBySavedObject', () => {
const start = '2020-07-08T00:52:28.350Z';
const end = '2020-07-08T00:00:00.000Z';
await clusterClientAdapter.queryEventsBySavedObject(
await clusterClientAdapter.queryEventsBySavedObjects(
'index-name',
'namespace',
'saved-object-type',
'saved-object-id',
['saved-object-id'],
{ ...DEFAULT_OPTIONS, start, end }
);
@ -643,10 +643,10 @@ describe('queryEventsBySavedObject', () => {
},
},
Object {
"term": Object {
"kibana.saved_objects.id": Object {
"value": "saved-object-id",
},
"terms": Object {
"kibana.saved_objects.id": Array [
"saved-object-id",
],
},
},
Object {

View file

@ -194,11 +194,11 @@ export class ClusterClientAdapter {
}
}
public async queryEventsBySavedObject(
public async queryEventsBySavedObjects(
index: string,
namespace: string | undefined,
type: string,
id: string,
ids: string[],
// eslint-disable-next-line @typescript-eslint/naming-convention
{ page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType
): Promise<QueryEventsBySavedObjectResult> {
@ -249,10 +249,9 @@ export class ClusterClientAdapter {
},
},
{
term: {
'kibana.saved_objects.id': {
value: id,
},
terms: {
// default maximum of 65,536 terms, configurable by index.max_terms_count
'kibana.saved_objects.id': ids,
},
},
namespaceQuery,
@ -298,7 +297,7 @@ export class ClusterClientAdapter {
};
} catch (err) {
throw new Error(
`querying for Event Log by for type "${type}" and id "${id}" failed with: ${err.message}`
`querying for Event Log by for type "${type}" and ids "${ids}" failed with: ${err.message}`
);
}
}

View file

@ -8,7 +8,7 @@ import { IEventLogClient } from './types';
const createEventLogClientMock = () => {
const mock: jest.Mocked<IEventLogClient> = {
findEventsBySavedObject: jest.fn(),
findEventsBySavedObjectIds: jest.fn(),
};
return mock;
};

View file

@ -11,7 +11,7 @@ import { merge } from 'lodash';
import moment from 'moment';
describe('EventLogStart', () => {
describe('findEventsBySavedObject', () => {
describe('findEventsBySavedObjectIds', () => {
test('verifies that the user can access the specified saved object', async () => {
const esContext = contextMock.create();
const savedObjectGetter = jest.fn();
@ -29,9 +29,9 @@ describe('EventLogStart', () => {
references: [],
});
await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id');
await eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id']);
expect(savedObjectGetter).toHaveBeenCalledWith('saved-object-type', 'saved-object-id');
expect(savedObjectGetter).toHaveBeenCalledWith('saved-object-type', ['saved-object-id']);
});
test('throws when the user doesnt have permission to access the specified saved object', async () => {
@ -48,7 +48,7 @@ describe('EventLogStart', () => {
savedObjectGetter.mockRejectedValue(new Error('Fail'));
expect(
eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id')
eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id'])
).rejects.toMatchInlineSnapshot(`[Error: Fail]`);
});
@ -107,17 +107,17 @@ describe('EventLogStart', () => {
total: expectedEvents.length,
data: expectedEvents,
};
esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue(result);
esContext.esAdapter.queryEventsBySavedObjects.mockResolvedValue(result);
expect(
await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id')
await eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id'])
).toEqual(result);
expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith(
expect(esContext.esAdapter.queryEventsBySavedObjects).toHaveBeenCalledWith(
esContext.esNames.indexPattern,
undefined,
'saved-object-type',
'saved-object-id',
['saved-object-id'],
{
page: 1,
per_page: 10,
@ -182,23 +182,23 @@ describe('EventLogStart', () => {
total: expectedEvents.length,
data: expectedEvents,
};
esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue(result);
esContext.esAdapter.queryEventsBySavedObjects.mockResolvedValue(result);
const start = moment().subtract(1, 'days').toISOString();
const end = moment().add(1, 'days').toISOString();
expect(
await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', {
await eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id'], {
start,
end,
})
).toEqual(result);
expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith(
expect(esContext.esAdapter.queryEventsBySavedObjects).toHaveBeenCalledWith(
esContext.esNames.indexPattern,
undefined,
'saved-object-type',
'saved-object-id',
['saved-object-id'],
{
page: 1,
per_page: 10,
@ -228,7 +228,7 @@ describe('EventLogStart', () => {
references: [],
});
esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue({
esContext.esAdapter.queryEventsBySavedObjects.mockResolvedValue({
page: 0,
per_page: 0,
total: 0,
@ -236,7 +236,7 @@ describe('EventLogStart', () => {
});
expect(
eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', {
eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id'], {
start: 'not a date string',
})
).rejects.toMatchInlineSnapshot(`[Error: [start]: Invalid Date]`);
@ -260,7 +260,7 @@ describe('EventLogStart', () => {
references: [],
});
esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue({
esContext.esAdapter.queryEventsBySavedObjects.mockResolvedValue({
page: 0,
per_page: 0,
total: 0,
@ -268,7 +268,7 @@ describe('EventLogStart', () => {
});
expect(
eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', {
eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id'], {
end: 'not a date string',
})
).rejects.toMatchInlineSnapshot(`[Error: [end]: Invalid Date]`);

View file

@ -12,7 +12,7 @@ import { SpacesServiceStart } from '../../spaces/server';
import { EsContext } from './es';
import { IEventLogClient } from './types';
import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter';
import { SavedObjectGetter } from './saved_object_provider_registry';
import { SavedObjectBulkGetterResult } from './saved_object_provider_registry';
export type PluginClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>;
export type AdminClusterClient$ = Observable<PluginClusterClient>;
@ -59,7 +59,7 @@ export type FindOptionsType = Pick<
interface EventLogServiceCtorParams {
esContext: EsContext;
savedObjectGetter: SavedObjectGetter;
savedObjectGetter: SavedObjectBulkGetterResult;
spacesService?: SpacesServiceStart;
request: KibanaRequest;
}
@ -67,7 +67,7 @@ interface EventLogServiceCtorParams {
// note that clusterClient may be null, indicating we can't write to ES
export class EventLogClient implements IEventLogClient {
private esContext: EsContext;
private savedObjectGetter: SavedObjectGetter;
private savedObjectGetter: SavedObjectBulkGetterResult;
private spacesService?: SpacesServiceStart;
private request: KibanaRequest;
@ -78,9 +78,9 @@ export class EventLogClient implements IEventLogClient {
this.request = request;
}
async findEventsBySavedObject(
async findEventsBySavedObjectIds(
type: string,
id: string,
ids: string[],
options?: Partial<FindOptionsType>
): Promise<QueryEventsBySavedObjectResult> {
const findOptions = findOptionsSchema.validate(options ?? {});
@ -88,14 +88,14 @@ export class EventLogClient implements IEventLogClient {
const space = await this.spacesService?.getActiveSpace(this.request);
const namespace = space && this.spacesService?.spaceIdToNamespace(space.id);
// verify the user has the required permissions to view this saved object
await this.savedObjectGetter(type, id);
// verify the user has the required permissions to view this saved objects
await this.savedObjectGetter(type, ids);
return await this.esContext.esAdapter.queryEventsBySavedObject(
return await this.esContext.esAdapter.queryEventsBySavedObjects(
this.esContext.esNames.indexPattern,
namespace,
type,
id,
ids,
findOptions
);
}

View file

@ -31,6 +31,7 @@ import { EventLogService } from './event_log_service';
import { createEsContext, EsContext } from './es';
import { EventLogClientService } from './event_log_start_service';
import { SavedObjectProviderRegistry } from './saved_object_provider_registry';
import { findByIdsRoute } from './routes/find_by_ids';
export type PluginClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>;
@ -99,6 +100,7 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
const router = core.http.createRouter();
// Register routes
findRoute(router, this.systemLogger);
findByIdsRoute(router, this.systemLogger);
return this.eventLogService;
}
@ -135,7 +137,7 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
this.savedObjectProviderRegistry.registerDefaultProvider((request) => {
const client = core.savedObjects.getScopedClient(request);
return client.get.bind(client);
return client.bulkGet.bind(client);
});
this.eventLogClientService = new EventLogClientService({

View file

@ -34,7 +34,7 @@ describe('find', () => {
total: events.length,
data: events,
};
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(result);
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(result);
const [context, req, res] = mockHandlerArguments(
eventLogClient,
@ -46,11 +46,11 @@ describe('find', () => {
await handler(context, req, res);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
const [type, id] = eventLogClient.findEventsBySavedObject.mock.calls[0];
const [type, ids] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0];
expect(type).toEqual(`action`);
expect(id).toEqual(`1`);
expect(ids).toEqual(['1']);
expect(res.ok).toHaveBeenCalledWith({
body: result,
@ -63,7 +63,7 @@ describe('find', () => {
findRoute(router, systemLogger);
const [, handler] = router.get.mock.calls[0];
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce({
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce({
page: 0,
per_page: 10,
total: 0,
@ -81,11 +81,11 @@ describe('find', () => {
await handler(context, req, res);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
const [type, id, options] = eventLogClient.findEventsBySavedObject.mock.calls[0];
const [type, ids, options] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0];
expect(type).toEqual(`action`);
expect(id).toEqual(`1`);
expect(ids).toEqual(['1']);
expect(options).toMatchObject({});
expect(res.ok).toHaveBeenCalledWith({
@ -104,7 +104,7 @@ describe('find', () => {
findRoute(router, systemLogger);
const [, handler] = router.get.mock.calls[0];
eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('oof!'));
eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('oof!'));
const [context, req, res] = mockHandlerArguments(
eventLogClient,
@ -119,7 +119,7 @@ describe('find', () => {
expect(systemLogger.debug).toHaveBeenCalledTimes(1);
expect(systemLogger.debug).toHaveBeenCalledWith(
'error calling eventLog findEventsBySavedObject(action, 1, {"page":3,"per_page":10}): oof!'
'error calling eventLog findEventsBySavedObjectIds(action, [1], {"page":3,"per_page":10}): oof!'
);
});
});

View file

@ -47,10 +47,10 @@ export const findRoute = (router: IRouter, systemLogger: Logger) => {
try {
return res.ok({
body: await eventLogClient.findEventsBySavedObject(type, id, query),
body: await eventLogClient.findEventsBySavedObjectIds(type, [id], query),
});
} catch (err) {
const call = `findEventsBySavedObject(${type}, ${id}, ${JSON.stringify(query)})`;
const call = `findEventsBySavedObjectIds(${type}, [${id}], ${JSON.stringify(query)})`;
systemLogger.debug(`error calling eventLog ${call}: ${err.message}`);
return res.notFound();
}

View file

@ -0,0 +1,128 @@
/*
* 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.
*/
import { httpServiceMock } from 'src/core/server/mocks';
import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments';
import { eventLogClientMock } from '../event_log_client.mock';
import { loggingSystemMock } from 'src/core/server/mocks';
import { findByIdsRoute } from './find_by_ids';
const eventLogClient = eventLogClientMock.create();
const systemLogger = loggingSystemMock.createLogger();
beforeEach(() => {
jest.resetAllMocks();
});
describe('find_by_ids', () => {
it('finds events with proper parameters', async () => {
const router = httpServiceMock.createRouter();
findByIdsRoute(router, systemLogger);
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/event_log/{type}/_find"`);
const events = [fakeEvent(), fakeEvent()];
const result = {
page: 0,
per_page: 10,
total: events.length,
data: events,
};
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(result);
const [context, req, res] = mockHandlerArguments(
eventLogClient,
{
params: { type: 'action' },
body: { ids: ['1'] },
},
['ok']
);
await handler(context, req, res);
expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
const [type, ids] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0];
expect(type).toEqual(`action`);
expect(ids).toEqual(['1']);
expect(res.ok).toHaveBeenCalledWith({
body: result,
});
});
it('supports optional pagination parameters', async () => {
const router = httpServiceMock.createRouter();
findByIdsRoute(router, systemLogger);
const [, handler] = router.post.mock.calls[0];
eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce({
page: 0,
per_page: 10,
total: 0,
data: [],
});
const [context, req, res] = mockHandlerArguments(
eventLogClient,
{
params: { type: 'action' },
body: { ids: ['1'] },
query: { page: 3, per_page: 10 },
},
['ok']
);
await handler(context, req, res);
expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1);
const [type, id, options] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0];
expect(type).toEqual(`action`);
expect(id).toEqual(['1']);
expect(options).toMatchObject({});
expect(res.ok).toHaveBeenCalledWith({
body: {
page: 0,
per_page: 10,
total: 0,
data: [],
},
});
});
it('logs a warning when the query throws an error', async () => {
const router = httpServiceMock.createRouter();
findByIdsRoute(router, systemLogger);
const [, handler] = router.post.mock.calls[0];
eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('oof!'));
const [context, req, res] = mockHandlerArguments(
eventLogClient,
{
params: { type: 'action' },
body: { ids: ['1'] },
query: { page: 3, per_page: 10 },
},
['ok']
);
await handler(context, req, res);
expect(systemLogger.debug).toHaveBeenCalledTimes(1);
expect(systemLogger.debug).toHaveBeenCalledWith(
'error calling eventLog findEventsBySavedObjectIds(action, [1], {"page":3,"per_page":10}): oof!'
);
});
});

View file

@ -0,0 +1,64 @@
/*
* 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.
*/
import { schema, TypeOf } from '@kbn/config-schema';
import {
IRouter,
RequestHandlerContext,
KibanaRequest,
IKibanaResponse,
KibanaResponseFactory,
Logger,
} from 'src/core/server';
import { BASE_EVENT_LOG_API_PATH } from '../../common';
import { findOptionsSchema, FindOptionsType } from '../event_log_client';
const paramSchema = schema.object({
type: schema.string(),
});
const bodySchema = schema.object({
ids: schema.arrayOf(schema.string(), { defaultValue: [] }),
});
export const findByIdsRoute = (router: IRouter, systemLogger: Logger) => {
router.post(
{
path: `${BASE_EVENT_LOG_API_PATH}/{type}/_find`,
validate: {
params: paramSchema,
query: findOptionsSchema,
body: bodySchema,
},
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
req: KibanaRequest<TypeOf<typeof paramSchema>, FindOptionsType, TypeOf<typeof bodySchema>>,
res: KibanaResponseFactory
): Promise<IKibanaResponse> {
if (!context.eventLog) {
return res.badRequest({ body: 'RouteHandlerContext is not registered for eventLog' });
}
const eventLogClient = context.eventLog.getEventLogClient();
const {
params: { type },
body: { ids },
query,
} = req;
try {
return res.ok({
body: await eventLogClient.findEventsBySavedObjectIds(type, ids, query),
});
} catch (err) {
const call = `findEventsBySavedObjectIds(${type}, [${ids}], ${JSON.stringify(query)})`;
systemLogger.debug(`error calling eventLog ${call}: ${err.message}`);
return res.notFound();
}
})
);
};

View file

@ -45,10 +45,10 @@ describe('SavedObjectProviderRegistry', () => {
getter.mockResolvedValue(alert);
expect(await registry.getProvidersClient(request)('alert', alert.id)).toMatchObject(alert);
expect(await registry.getProvidersClient(request)('alert', [alert.id])).toMatchObject(alert);
expect(provider).toHaveBeenCalledWith(request);
expect(getter).toHaveBeenCalledWith('alert', alert.id);
expect(getter).toHaveBeenCalledWith([{ id: alert.id, type: 'alert' }]);
});
test('should get SavedObject using the default provider for unregistered types', async () => {
@ -70,9 +70,11 @@ describe('SavedObjectProviderRegistry', () => {
defaultProvider.mockReturnValue(getter);
getter.mockResolvedValue(action);
expect(await registry.getProvidersClient(request)('action', action.id)).toMatchObject(action);
expect(await registry.getProvidersClient(request)('action', [action.id])).toMatchObject(
action
);
expect(getter).toHaveBeenCalledWith('action', action.id);
expect(getter).toHaveBeenCalledWith([{ id: action.id, type: 'action' }]);
expect(defaultProvider).toHaveBeenCalledWith(request);
});
});

View file

@ -13,7 +13,14 @@ import { pipe } from 'fp-ts/lib/pipeable';
export type SavedObjectGetter = (
...params: Parameters<SavedObjectsClientContract['get']>
) => Promise<unknown>;
export type SavedObjectProvider = (request: KibanaRequest) => SavedObjectGetter;
export type SavedObjectBulkGetter = (
...params: Parameters<SavedObjectsClientContract['bulkGet']>
) => Promise<unknown>;
export type SavedObjectBulkGetterResult = (type: string, ids: string[]) => Promise<unknown>;
export type SavedObjectProvider = (request: KibanaRequest) => SavedObjectBulkGetter;
export class SavedObjectProviderRegistry {
private providers = new Map<string, SavedObjectProvider>();
@ -34,7 +41,7 @@ export class SavedObjectProviderRegistry {
this.providers.set(type, provider);
}
public getProvidersClient(request: KibanaRequest): SavedObjectGetter {
public getProvidersClient(request: KibanaRequest): SavedObjectBulkGetterResult {
if (!this.defaultProvider) {
throw new Error(
i18n.translate(
@ -49,9 +56,13 @@ export class SavedObjectProviderRegistry {
// `scopedProviders` is a cache of providers which are scoped t othe current request.
// The client will only instantiate a provider on-demand and it will cache each
// one to enable the request to reuse each provider.
const scopedProviders = new Map<string, SavedObjectGetter>();
// would be nice to have a simple version support in API:
// curl -X GET "localhost:9200/my-index-000001/_mget?pretty" -H 'Content-Type: application/json' -d' { "ids" : ["1", "2"] } '
const scopedProviders = new Map<string, SavedObjectBulkGetter>();
const defaultGetter = this.defaultProvider(request);
return (type: string, id: string) => {
return (type: string, ids: string[]) => {
const objects = ids.map((id: string) => ({ type, id }));
const getter = pipe(
fromNullable(scopedProviders.get(type)),
getOrElse(() => {
@ -62,7 +73,7 @@ export class SavedObjectProviderRegistry {
return client;
})
);
return getter(type, id);
return getter(objects);
};
}
}

View file

@ -51,9 +51,9 @@ export interface IEventLogClientService {
}
export interface IEventLogClient {
findEventsBySavedObject(
findEventsBySavedObjectIds(
type: string,
id: string,
ids: string[],
options?: Partial<FindOptionsType>
): Promise<QueryEventsBySavedObjectResult>;
}