From fb67443e6dd6b049ecd7d88f8e90c0e8c26ff2a2 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 12 Jan 2021 16:25:23 -0800 Subject: [PATCH] [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 --- x-pack/plugins/actions/server/plugin.ts | 8 +- .../server/alerts_client/alerts_client.ts | 2 +- .../tests/get_alert_instance_summary.test.ts | 32 +++-- x-pack/plugins/alerts/server/plugin.ts | 6 +- .../server/es/cluster_client_adapter.mock.ts | 2 +- .../server/es/cluster_client_adapter.test.ts | 52 +++---- .../server/es/cluster_client_adapter.ts | 13 +- .../event_log/server/event_log_client.mock.ts | 2 +- .../event_log/server/event_log_client.test.ts | 32 ++--- .../event_log/server/event_log_client.ts | 18 +-- x-pack/plugins/event_log/server/plugin.ts | 4 +- .../event_log/server/routes/find.test.ts | 20 +-- .../plugins/event_log/server/routes/find.ts | 4 +- .../server/routes/find_by_ids.test.ts | 128 ++++++++++++++++++ .../event_log/server/routes/find_by_ids.ts | 64 +++++++++ .../saved_object_provider_registry.test.ts | 10 +- .../server/saved_object_provider_registry.ts | 21 ++- x-pack/plugins/event_log/server/types.ts | 4 +- 18 files changed, 320 insertions(+), 102 deletions(-) create mode 100644 x-pack/plugins/event_log/server/routes/find_by_ids.test.ts create mode 100644 x-pack/plugins/event_log/server/routes/find_by_ids.ts diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 7c41bf99af47..133e5f9c6aa2 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -19,6 +19,7 @@ import { ElasticsearchServiceStart, ILegacyClusterClient, SavedObjectsClientContract, + SavedObjectsBulkGetObject, } from '../../../../src/core/server'; import { @@ -333,7 +334,12 @@ export class ActionsPlugin implements Plugin, 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) => diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index a47af44d330c..b6606d9d7fe5 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -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(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 555c316038da..6c0612df030d 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -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' }); diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index d15ae0ca55ef..cb165fa56d04 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -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); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts index c1f60f2d6304..55871c975d35 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts @@ -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; diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 2b1d89f12be5..545b3b151714 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -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 { diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 0ac1193998ce..5d4c33f319fc 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -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 { @@ -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}` ); } } diff --git a/x-pack/plugins/event_log/server/event_log_client.mock.ts b/x-pack/plugins/event_log/server/event_log_client.mock.ts index 31cab802555d..65934d62a5a9 100644 --- a/x-pack/plugins/event_log/server/event_log_client.mock.ts +++ b/x-pack/plugins/event_log/server/event_log_client.mock.ts @@ -8,7 +8,7 @@ import { IEventLogClient } from './types'; const createEventLogClientMock = () => { const mock: jest.Mocked = { - findEventsBySavedObject: jest.fn(), + findEventsBySavedObjectIds: jest.fn(), }; return mock; }; diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts index d6793be42558..b9beebdc76db 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -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]`); diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 9b7d4e00b276..63453c6327da 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -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; export type AdminClusterClient$ = Observable; @@ -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 ): Promise { 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 ); } diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index d85de565b4d8..6471db7d5dd6 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -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; @@ -99,6 +100,7 @@ export class Plugin implements CorePlugin { const client = core.savedObjects.getScopedClient(request); - return client.get.bind(client); + return client.bulkGet.bind(client); }); this.eventLogClientService = new EventLogClientService({ diff --git a/x-pack/plugins/event_log/server/routes/find.test.ts b/x-pack/plugins/event_log/server/routes/find.test.ts index 07bb8329f78e..639995a7c379 100644 --- a/x-pack/plugins/event_log/server/routes/find.test.ts +++ b/x-pack/plugins/event_log/server/routes/find.test.ts @@ -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!' ); }); }); diff --git a/x-pack/plugins/event_log/server/routes/find.ts b/x-pack/plugins/event_log/server/routes/find.ts index 3880ac2c1012..50785de72cfc 100644 --- a/x-pack/plugins/event_log/server/routes/find.ts +++ b/x-pack/plugins/event_log/server/routes/find.ts @@ -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(); } diff --git a/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts b/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts new file mode 100644 index 000000000000..f28c36164e95 --- /dev/null +++ b/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts @@ -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!' + ); + }); +}); diff --git a/x-pack/plugins/event_log/server/routes/find_by_ids.ts b/x-pack/plugins/event_log/server/routes/find_by_ids.ts new file mode 100644 index 000000000000..a7ee0f35ac59 --- /dev/null +++ b/x-pack/plugins/event_log/server/routes/find_by_ids.ts @@ -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, FindOptionsType, TypeOf>, + res: KibanaResponseFactory + ): Promise { + 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(); + } + }) + ); +}; diff --git a/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts index 6a02d54c8751..f6d42a611351 100644 --- a/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts @@ -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); }); }); diff --git a/x-pack/plugins/event_log/server/saved_object_provider_registry.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.ts index 87a1da5dd6f4..3c3e93afec39 100644 --- a/x-pack/plugins/event_log/server/saved_object_provider_registry.ts +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.ts @@ -13,7 +13,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; export type SavedObjectGetter = ( ...params: Parameters ) => Promise; -export type SavedObjectProvider = (request: KibanaRequest) => SavedObjectGetter; + +export type SavedObjectBulkGetter = ( + ...params: Parameters +) => Promise; + +export type SavedObjectBulkGetterResult = (type: string, ids: string[]) => Promise; + +export type SavedObjectProvider = (request: KibanaRequest) => SavedObjectBulkGetter; export class SavedObjectProviderRegistry { private providers = new Map(); @@ -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(); + + // 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(); 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); }; } } diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 66030ee3910d..ff2ae8163292 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -51,9 +51,9 @@ export interface IEventLogClientService { } export interface IEventLogClient { - findEventsBySavedObject( + findEventsBySavedObjectIds( type: string, - id: string, + ids: string[], options?: Partial ): Promise; }