[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, ElasticsearchServiceStart,
ILegacyClusterClient, ILegacyClusterClient,
SavedObjectsClientContract, SavedObjectsClientContract,
SavedObjectsBulkGetObject,
} from '../../../../src/core/server'; } from '../../../../src/core/server';
import { import {
@ -333,7 +334,12 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
this.eventLogService!.registerSavedObjectProvider('action', (request) => { this.eventLogService!.registerSavedObjectProvider('action', (request) => {
const client = secureGetActionsClientWithRequest(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) => const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) =>

View file

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

View file

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

View file

@ -33,6 +33,7 @@ import {
ILegacyClusterClient, ILegacyClusterClient,
StatusServiceSetup, StatusServiceSetup,
ServiceStatus, ServiceStatus,
SavedObjectsBulkGetObject,
} from '../../../../src/core/server'; } from '../../../../src/core/server';
import { import {
@ -370,7 +371,10 @@ export class AlertingPlugin {
this.eventLogService!.registerSavedObjectProvider('alert', (request) => { this.eventLogService!.registerSavedObjectProvider('alert', (request) => {
const client = getAlertsClientWithRequest(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); scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager);

View file

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

View file

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

View file

@ -194,11 +194,11 @@ export class ClusterClientAdapter {
} }
} }
public async queryEventsBySavedObject( public async queryEventsBySavedObjects(
index: string, index: string,
namespace: string | undefined, namespace: string | undefined,
type: string, type: string,
id: string, ids: string[],
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
{ page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType { page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType
): Promise<QueryEventsBySavedObjectResult> { ): Promise<QueryEventsBySavedObjectResult> {
@ -249,10 +249,9 @@ export class ClusterClientAdapter {
}, },
}, },
{ {
term: { terms: {
'kibana.saved_objects.id': { // default maximum of 65,536 terms, configurable by index.max_terms_count
value: id, 'kibana.saved_objects.id': ids,
},
}, },
}, },
namespaceQuery, namespaceQuery,
@ -298,7 +297,7 @@ export class ClusterClientAdapter {
}; };
} catch (err) { } catch (err) {
throw new Error( 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 createEventLogClientMock = () => {
const mock: jest.Mocked<IEventLogClient> = { const mock: jest.Mocked<IEventLogClient> = {
findEventsBySavedObject: jest.fn(), findEventsBySavedObjectIds: jest.fn(),
}; };
return mock; return mock;
}; };

View file

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

View file

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

View file

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

View file

@ -34,7 +34,7 @@ describe('find', () => {
total: events.length, total: events.length,
data: events, data: events,
}; };
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(result); eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(result);
const [context, req, res] = mockHandlerArguments( const [context, req, res] = mockHandlerArguments(
eventLogClient, eventLogClient,
@ -46,11 +46,11 @@ describe('find', () => {
await handler(context, req, res); 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(type).toEqual(`action`);
expect(id).toEqual(`1`); expect(ids).toEqual(['1']);
expect(res.ok).toHaveBeenCalledWith({ expect(res.ok).toHaveBeenCalledWith({
body: result, body: result,
@ -63,7 +63,7 @@ describe('find', () => {
findRoute(router, systemLogger); findRoute(router, systemLogger);
const [, handler] = router.get.mock.calls[0]; const [, handler] = router.get.mock.calls[0];
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce({ eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce({
page: 0, page: 0,
per_page: 10, per_page: 10,
total: 0, total: 0,
@ -81,11 +81,11 @@ describe('find', () => {
await handler(context, req, res); 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(type).toEqual(`action`);
expect(id).toEqual(`1`); expect(ids).toEqual(['1']);
expect(options).toMatchObject({}); expect(options).toMatchObject({});
expect(res.ok).toHaveBeenCalledWith({ expect(res.ok).toHaveBeenCalledWith({
@ -104,7 +104,7 @@ describe('find', () => {
findRoute(router, systemLogger); findRoute(router, systemLogger);
const [, handler] = router.get.mock.calls[0]; const [, handler] = router.get.mock.calls[0];
eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('oof!')); eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('oof!'));
const [context, req, res] = mockHandlerArguments( const [context, req, res] = mockHandlerArguments(
eventLogClient, eventLogClient,
@ -119,7 +119,7 @@ describe('find', () => {
expect(systemLogger.debug).toHaveBeenCalledTimes(1); expect(systemLogger.debug).toHaveBeenCalledTimes(1);
expect(systemLogger.debug).toHaveBeenCalledWith( 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 { try {
return res.ok({ return res.ok({
body: await eventLogClient.findEventsBySavedObject(type, id, query), body: await eventLogClient.findEventsBySavedObjectIds(type, [id], query),
}); });
} catch (err) { } 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}`); systemLogger.debug(`error calling eventLog ${call}: ${err.message}`);
return res.notFound(); 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); 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(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 () => { test('should get SavedObject using the default provider for unregistered types', async () => {
@ -70,9 +70,11 @@ describe('SavedObjectProviderRegistry', () => {
defaultProvider.mockReturnValue(getter); defaultProvider.mockReturnValue(getter);
getter.mockResolvedValue(action); 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); expect(defaultProvider).toHaveBeenCalledWith(request);
}); });
}); });

View file

@ -13,7 +13,14 @@ import { pipe } from 'fp-ts/lib/pipeable';
export type SavedObjectGetter = ( export type SavedObjectGetter = (
...params: Parameters<SavedObjectsClientContract['get']> ...params: Parameters<SavedObjectsClientContract['get']>
) => Promise<unknown>; ) => 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 { export class SavedObjectProviderRegistry {
private providers = new Map<string, SavedObjectProvider>(); private providers = new Map<string, SavedObjectProvider>();
@ -34,7 +41,7 @@ export class SavedObjectProviderRegistry {
this.providers.set(type, provider); this.providers.set(type, provider);
} }
public getProvidersClient(request: KibanaRequest): SavedObjectGetter { public getProvidersClient(request: KibanaRequest): SavedObjectBulkGetterResult {
if (!this.defaultProvider) { if (!this.defaultProvider) {
throw new Error( throw new Error(
i18n.translate( i18n.translate(
@ -49,9 +56,13 @@ export class SavedObjectProviderRegistry {
// `scopedProviders` is a cache of providers which are scoped t othe current request. // `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 // The client will only instantiate a provider on-demand and it will cache each
// one to enable the request to reuse each provider. // 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); 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( const getter = pipe(
fromNullable(scopedProviders.get(type)), fromNullable(scopedProviders.get(type)),
getOrElse(() => { getOrElse(() => {
@ -62,7 +73,7 @@ export class SavedObjectProviderRegistry {
return client; return client;
}) })
); );
return getter(type, id); return getter(objects);
}; };
} }
} }

View file

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