[Event Log] Added KQL queries support for Event Log API. (#89394)

* [Event Log] Added KQL queries support for Event Log API.

* refactored to use core.elasticsearch.client

* Fixed tests

* removed get index pattern for event log

* Fixed tests

* Fixed due to comments.
This commit is contained in:
Yuliia Naumenko 2021-01-28 11:19:59 -08:00 committed by GitHub
parent 62a4266ab6
commit 4de729f3c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 199 additions and 164 deletions

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { LegacyClusterClient } from 'src/core/server'; import { ElasticsearchClient } from 'src/core/server';
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
import { import {
ClusterClientAdapter, ClusterClientAdapter,
@ -15,20 +15,21 @@ import { contextMock } from './context.mock';
import { findOptionsSchema } from '../event_log_client'; import { findOptionsSchema } from '../event_log_client';
import { delay } from '../lib/delay'; import { delay } from '../lib/delay';
import { times } from 'lodash'; import { times } from 'lodash';
import { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import { RequestEvent } from '@elastic/elasticsearch';
type EsClusterClient = Pick<jest.Mocked<LegacyClusterClient>, 'callAsInternalUser' | 'asScoped'>;
type MockedLogger = ReturnType<typeof loggingSystemMock['createLogger']>; type MockedLogger = ReturnType<typeof loggingSystemMock['createLogger']>;
let logger: MockedLogger; let logger: MockedLogger;
let clusterClient: EsClusterClient; let clusterClient: DeeplyMockedKeys<ElasticsearchClient>;
let clusterClientAdapter: IClusterClientAdapter; let clusterClientAdapter: IClusterClientAdapter;
beforeEach(() => { beforeEach(() => {
logger = loggingSystemMock.createLogger(); logger = loggingSystemMock.createLogger();
clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
clusterClientAdapter = new ClusterClientAdapter({ clusterClientAdapter = new ClusterClientAdapter({
logger, logger,
clusterClientPromise: Promise.resolve(clusterClient), elasticsearchClientPromise: Promise.resolve(clusterClient),
context: contextMock.create(), context: contextMock.create(),
}); });
}); });
@ -38,16 +39,16 @@ describe('indexDocument', () => {
clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' }); clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' });
await retryUntil('cluster client bulk called', () => { await retryUntil('cluster client bulk called', () => {
return clusterClient.callAsInternalUser.mock.calls.length !== 0; return clusterClient.bulk.mock.calls.length !== 0;
}); });
expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('bulk', { expect(clusterClient.bulk).toHaveBeenCalledWith({
body: [{ create: { _index: 'event-log' } }, { message: 'foo' }], body: [{ create: { _index: 'event-log' } }, { message: 'foo' }],
}); });
}); });
test('should log an error when cluster client throws an error', async () => { test('should log an error when cluster client throws an error', async () => {
clusterClient.callAsInternalUser.mockRejectedValue(new Error('expected failure')); clusterClient.bulk.mockRejectedValue(new Error('expected failure'));
clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' }); clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' });
await retryUntil('cluster client bulk called', () => { await retryUntil('cluster client bulk called', () => {
return logger.error.mock.calls.length !== 0; return logger.error.mock.calls.length !== 0;
@ -69,7 +70,7 @@ describe('shutdown()', () => {
const resultPromise = clusterClientAdapter.shutdown(); const resultPromise = clusterClientAdapter.shutdown();
await retryUntil('cluster client bulk called', () => { await retryUntil('cluster client bulk called', () => {
return clusterClient.callAsInternalUser.mock.calls.length !== 0; return clusterClient.bulk.mock.calls.length !== 0;
}); });
const result = await resultPromise; const result = await resultPromise;
@ -85,7 +86,7 @@ describe('buffering documents', () => {
} }
await retryUntil('cluster client bulk called', () => { await retryUntil('cluster client bulk called', () => {
return clusterClient.callAsInternalUser.mock.calls.length !== 0; return clusterClient.bulk.mock.calls.length !== 0;
}); });
const expectedBody = []; const expectedBody = [];
@ -93,7 +94,7 @@ describe('buffering documents', () => {
expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` }); expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` });
} }
expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('bulk', { expect(clusterClient.bulk).toHaveBeenCalledWith({
body: expectedBody, body: expectedBody,
}); });
}); });
@ -105,7 +106,7 @@ describe('buffering documents', () => {
} }
await retryUntil('cluster client bulk called', () => { await retryUntil('cluster client bulk called', () => {
return clusterClient.callAsInternalUser.mock.calls.length >= 2; return clusterClient.bulk.mock.calls.length >= 2;
}); });
const expectedBody = []; const expectedBody = [];
@ -113,18 +114,18 @@ describe('buffering documents', () => {
expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` }); expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` });
} }
expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(1, 'bulk', { expect(clusterClient.bulk).toHaveBeenNthCalledWith(1, {
body: expectedBody, body: expectedBody,
}); });
expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(2, 'bulk', { expect(clusterClient.bulk).toHaveBeenNthCalledWith(2, {
body: [{ create: { _index: 'event-log' } }, { message: `foo 100` }], body: [{ create: { _index: 'event-log' } }, { message: `foo 100` }],
}); });
}); });
test('should handle lots of docs correctly with a delay in the bulk index', async () => { test('should handle lots of docs correctly with a delay in the bulk index', async () => {
// @ts-ignore // @ts-ignore
clusterClient.callAsInternalUser.mockImplementation = async () => await delay(100); clusterClient.bulk.mockImplementation = async () => await delay(100);
const docs = times(EVENT_BUFFER_LENGTH * 10, (i) => ({ const docs = times(EVENT_BUFFER_LENGTH * 10, (i) => ({
body: { message: `foo ${i}` }, body: { message: `foo ${i}` },
@ -137,7 +138,7 @@ describe('buffering documents', () => {
} }
await retryUntil('cluster client bulk called', () => { await retryUntil('cluster client bulk called', () => {
return clusterClient.callAsInternalUser.mock.calls.length >= 10; return clusterClient.bulk.mock.calls.length >= 10;
}); });
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
@ -149,7 +150,7 @@ describe('buffering documents', () => {
); );
} }
expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(i + 1, 'bulk', { expect(clusterClient.bulk).toHaveBeenNthCalledWith(i + 1, {
body: expectedBody, body: expectedBody,
}); });
} }
@ -164,19 +165,19 @@ describe('doesIlmPolicyExist', () => {
test('should call cluster with proper arguments', async () => { test('should call cluster with proper arguments', async () => {
await clusterClientAdapter.doesIlmPolicyExist('foo'); await clusterClientAdapter.doesIlmPolicyExist('foo');
expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', { expect(clusterClient.transport.request).toHaveBeenCalledWith({
method: 'GET', method: 'GET',
path: '/_ilm/policy/foo', path: '/_ilm/policy/foo',
}); });
}); });
test('should return false when 404 error is returned by Elasticsearch', async () => { test('should return false when 404 error is returned by Elasticsearch', async () => {
clusterClient.callAsInternalUser.mockRejectedValue(notFoundError); clusterClient.transport.request.mockRejectedValue(notFoundError);
await expect(clusterClientAdapter.doesIlmPolicyExist('foo')).resolves.toEqual(false); await expect(clusterClientAdapter.doesIlmPolicyExist('foo')).resolves.toEqual(false);
}); });
test('should throw error when error is not 404', async () => { test('should throw error when error is not 404', async () => {
clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); clusterClient.transport.request.mockRejectedValue(new Error('Fail'));
await expect( await expect(
clusterClientAdapter.doesIlmPolicyExist('foo') clusterClientAdapter.doesIlmPolicyExist('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(`"error checking existance of ilm policy: Fail"`); ).rejects.toThrowErrorMatchingInlineSnapshot(`"error checking existance of ilm policy: Fail"`);
@ -189,9 +190,9 @@ describe('doesIlmPolicyExist', () => {
describe('createIlmPolicy', () => { describe('createIlmPolicy', () => {
test('should call cluster client with given policy', async () => { test('should call cluster client with given policy', async () => {
clusterClient.callAsInternalUser.mockResolvedValue({ success: true }); clusterClient.transport.request.mockResolvedValue(asApiResponse({ success: true }));
await clusterClientAdapter.createIlmPolicy('foo', { args: true }); await clusterClientAdapter.createIlmPolicy('foo', { args: true });
expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', { expect(clusterClient.transport.request).toHaveBeenCalledWith({
method: 'PUT', method: 'PUT',
path: '/_ilm/policy/foo', path: '/_ilm/policy/foo',
body: { args: true }, body: { args: true },
@ -199,7 +200,7 @@ describe('createIlmPolicy', () => {
}); });
test('should throw error when call cluster client throws', async () => { test('should throw error when call cluster client throws', async () => {
clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); clusterClient.transport.request.mockRejectedValue(new Error('Fail'));
await expect( await expect(
clusterClientAdapter.createIlmPolicy('foo', { args: true }) clusterClientAdapter.createIlmPolicy('foo', { args: true })
).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating ilm policy: Fail"`); ).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating ilm policy: Fail"`);
@ -209,23 +210,23 @@ describe('createIlmPolicy', () => {
describe('doesIndexTemplateExist', () => { describe('doesIndexTemplateExist', () => {
test('should call cluster with proper arguments', async () => { test('should call cluster with proper arguments', async () => {
await clusterClientAdapter.doesIndexTemplateExist('foo'); await clusterClientAdapter.doesIndexTemplateExist('foo');
expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.existsTemplate', { expect(clusterClient.indices.existsTemplate).toHaveBeenCalledWith({
name: 'foo', name: 'foo',
}); });
}); });
test('should return true when call cluster returns true', async () => { test('should return true when call cluster returns true', async () => {
clusterClient.callAsInternalUser.mockResolvedValue(true); clusterClient.indices.existsTemplate.mockResolvedValue(asApiResponse(true));
await expect(clusterClientAdapter.doesIndexTemplateExist('foo')).resolves.toEqual(true); await expect(clusterClientAdapter.doesIndexTemplateExist('foo')).resolves.toEqual(true);
}); });
test('should return false when call cluster returns false', async () => { test('should return false when call cluster returns false', async () => {
clusterClient.callAsInternalUser.mockResolvedValue(false); clusterClient.indices.existsTemplate.mockResolvedValue(asApiResponse(false));
await expect(clusterClientAdapter.doesIndexTemplateExist('foo')).resolves.toEqual(false); await expect(clusterClientAdapter.doesIndexTemplateExist('foo')).resolves.toEqual(false);
}); });
test('should throw error when call cluster throws an error', async () => { test('should throw error when call cluster throws an error', async () => {
clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); clusterClient.indices.existsTemplate.mockRejectedValue(new Error('Fail'));
await expect( await expect(
clusterClientAdapter.doesIndexTemplateExist('foo') clusterClientAdapter.doesIndexTemplateExist('foo')
).rejects.toThrowErrorMatchingInlineSnapshot( ).rejects.toThrowErrorMatchingInlineSnapshot(
@ -237,7 +238,7 @@ describe('doesIndexTemplateExist', () => {
describe('createIndexTemplate', () => { describe('createIndexTemplate', () => {
test('should call cluster with given template', async () => { test('should call cluster with given template', async () => {
await clusterClientAdapter.createIndexTemplate('foo', { args: true }); await clusterClientAdapter.createIndexTemplate('foo', { args: true });
expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.putTemplate', { expect(clusterClient.indices.putTemplate).toHaveBeenCalledWith({
name: 'foo', name: 'foo',
create: true, create: true,
body: { args: true }, body: { args: true },
@ -245,16 +246,16 @@ describe('createIndexTemplate', () => {
}); });
test(`should throw error if index template still doesn't exist after error is thrown`, async () => { test(`should throw error if index template still doesn't exist after error is thrown`, async () => {
clusterClient.callAsInternalUser.mockRejectedValueOnce(new Error('Fail')); clusterClient.indices.putTemplate.mockRejectedValueOnce(new Error('Fail'));
clusterClient.callAsInternalUser.mockResolvedValueOnce(false); clusterClient.indices.existsTemplate.mockResolvedValueOnce(asApiResponse(false));
await expect( await expect(
clusterClientAdapter.createIndexTemplate('foo', { args: true }) clusterClientAdapter.createIndexTemplate('foo', { args: true })
).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating index template: Fail"`); ).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating index template: Fail"`);
}); });
test('should not throw error if index template exists after error is thrown', async () => { test('should not throw error if index template exists after error is thrown', async () => {
clusterClient.callAsInternalUser.mockRejectedValueOnce(new Error('Fail')); clusterClient.indices.putTemplate.mockRejectedValueOnce(new Error('Fail'));
clusterClient.callAsInternalUser.mockResolvedValueOnce(true); clusterClient.indices.existsTemplate.mockResolvedValueOnce(asApiResponse(true));
await clusterClientAdapter.createIndexTemplate('foo', { args: true }); await clusterClientAdapter.createIndexTemplate('foo', { args: true });
}); });
}); });
@ -262,23 +263,23 @@ describe('createIndexTemplate', () => {
describe('doesAliasExist', () => { describe('doesAliasExist', () => {
test('should call cluster with proper arguments', async () => { test('should call cluster with proper arguments', async () => {
await clusterClientAdapter.doesAliasExist('foo'); await clusterClientAdapter.doesAliasExist('foo');
expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.existsAlias', { expect(clusterClient.indices.existsAlias).toHaveBeenCalledWith({
name: 'foo', name: 'foo',
}); });
}); });
test('should return true when call cluster returns true', async () => { test('should return true when call cluster returns true', async () => {
clusterClient.callAsInternalUser.mockResolvedValueOnce(true); clusterClient.indices.existsAlias.mockResolvedValueOnce(asApiResponse(true));
await expect(clusterClientAdapter.doesAliasExist('foo')).resolves.toEqual(true); await expect(clusterClientAdapter.doesAliasExist('foo')).resolves.toEqual(true);
}); });
test('should return false when call cluster returns false', async () => { test('should return false when call cluster returns false', async () => {
clusterClient.callAsInternalUser.mockResolvedValueOnce(false); clusterClient.indices.existsAlias.mockResolvedValueOnce(asApiResponse(false));
await expect(clusterClientAdapter.doesAliasExist('foo')).resolves.toEqual(false); await expect(clusterClientAdapter.doesAliasExist('foo')).resolves.toEqual(false);
}); });
test('should throw error when call cluster throws an error', async () => { test('should throw error when call cluster throws an error', async () => {
clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); clusterClient.indices.existsAlias.mockRejectedValue(new Error('Fail'));
await expect( await expect(
clusterClientAdapter.doesAliasExist('foo') clusterClientAdapter.doesAliasExist('foo')
).rejects.toThrowErrorMatchingInlineSnapshot( ).rejects.toThrowErrorMatchingInlineSnapshot(
@ -290,14 +291,14 @@ describe('doesAliasExist', () => {
describe('createIndex', () => { describe('createIndex', () => {
test('should call cluster with proper arguments', async () => { test('should call cluster with proper arguments', async () => {
await clusterClientAdapter.createIndex('foo'); await clusterClientAdapter.createIndex('foo');
expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.create', { expect(clusterClient.indices.create).toHaveBeenCalledWith({
index: 'foo', index: 'foo',
body: {}, body: {},
}); });
}); });
test('should throw error when not getting an error of type resource_already_exists_exception', async () => { test('should throw error when not getting an error of type resource_already_exists_exception', async () => {
clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); clusterClient.indices.create.mockRejectedValue(new Error('Fail'));
await expect( await expect(
clusterClientAdapter.createIndex('foo') clusterClientAdapter.createIndex('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating initial index: Fail"`); ).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating initial index: Fail"`);
@ -312,7 +313,7 @@ describe('createIndex', () => {
type: 'resource_already_exists_exception', type: 'resource_already_exists_exception',
}, },
}; };
clusterClient.callAsInternalUser.mockRejectedValue(err); clusterClient.indices.create.mockRejectedValue(err);
await clusterClientAdapter.createIndex('foo'); await clusterClientAdapter.createIndex('foo');
}); });
}); });
@ -321,12 +322,14 @@ describe('queryEventsBySavedObject', () => {
const DEFAULT_OPTIONS = findOptionsSchema.validate({}); const DEFAULT_OPTIONS = findOptionsSchema.validate({});
test('should call cluster with proper arguments with non-default namespace', async () => { test('should call cluster with proper arguments with non-default namespace', async () => {
clusterClient.callAsInternalUser.mockResolvedValue({ clusterClient.search.mockResolvedValue(
hits: { asApiResponse({
hits: [], hits: {
total: { value: 0 }, hits: [],
}, total: { value: 0 },
}); },
})
);
await clusterClientAdapter.queryEventsBySavedObjects( await clusterClientAdapter.queryEventsBySavedObjects(
'index-name', 'index-name',
'namespace', 'namespace',
@ -335,14 +338,14 @@ describe('queryEventsBySavedObject', () => {
DEFAULT_OPTIONS DEFAULT_OPTIONS
); );
const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; const [query] = clusterClient.search.mock.calls[0];
expect(method).toEqual('search');
expect(query).toMatchInlineSnapshot(` expect(query).toMatchInlineSnapshot(`
Object { Object {
"body": Object { "body": Object {
"from": 0, "from": 0,
"query": Object { "query": Object {
"bool": Object { "bool": Object {
"filter": Array [],
"must": Array [ "must": Array [
Object { Object {
"nested": Object { "nested": Object {
@ -400,12 +403,14 @@ describe('queryEventsBySavedObject', () => {
}); });
test('should call cluster with proper arguments with default namespace', async () => { test('should call cluster with proper arguments with default namespace', async () => {
clusterClient.callAsInternalUser.mockResolvedValue({ clusterClient.search.mockResolvedValue(
hits: { asApiResponse({
hits: [], hits: {
total: { value: 0 }, hits: [],
}, total: { value: 0 },
}); },
})
);
await clusterClientAdapter.queryEventsBySavedObjects( await clusterClientAdapter.queryEventsBySavedObjects(
'index-name', 'index-name',
undefined, undefined,
@ -414,14 +419,14 @@ describe('queryEventsBySavedObject', () => {
DEFAULT_OPTIONS DEFAULT_OPTIONS
); );
const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; const [query] = clusterClient.search.mock.calls[0];
expect(method).toEqual('search');
expect(query).toMatchInlineSnapshot(` expect(query).toMatchInlineSnapshot(`
Object { Object {
"body": Object { "body": Object {
"from": 0, "from": 0,
"query": Object { "query": Object {
"bool": Object { "bool": Object {
"filter": Array [],
"must": Array [ "must": Array [
Object { Object {
"nested": Object { "nested": Object {
@ -481,12 +486,14 @@ describe('queryEventsBySavedObject', () => {
}); });
test('should call cluster with sort', async () => { test('should call cluster with sort', async () => {
clusterClient.callAsInternalUser.mockResolvedValue({ clusterClient.search.mockResolvedValue(
hits: { asApiResponse({
hits: [], hits: {
total: { value: 0 }, hits: [],
}, total: { value: 0 },
}); },
})
);
await clusterClientAdapter.queryEventsBySavedObjects( await clusterClientAdapter.queryEventsBySavedObjects(
'index-name', 'index-name',
'namespace', 'namespace',
@ -495,8 +502,7 @@ describe('queryEventsBySavedObject', () => {
{ ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' } { ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' }
); );
const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; const [query] = clusterClient.search.mock.calls[0];
expect(method).toEqual('search');
expect(query).toMatchObject({ expect(query).toMatchObject({
index: 'index-name', index: 'index-name',
body: { body: {
@ -506,12 +512,14 @@ describe('queryEventsBySavedObject', () => {
}); });
test('supports open ended date', async () => { test('supports open ended date', async () => {
clusterClient.callAsInternalUser.mockResolvedValue({ clusterClient.search.mockResolvedValue(
hits: { asApiResponse({
hits: [], hits: {
total: { value: 0 }, hits: [],
}, total: { value: 0 },
}); },
})
);
const start = '2020-07-08T00:52:28.350Z'; const start = '2020-07-08T00:52:28.350Z';
@ -523,14 +531,14 @@ describe('queryEventsBySavedObject', () => {
{ ...DEFAULT_OPTIONS, start } { ...DEFAULT_OPTIONS, start }
); );
const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; const [query] = clusterClient.search.mock.calls[0];
expect(method).toEqual('search');
expect(query).toMatchInlineSnapshot(` expect(query).toMatchInlineSnapshot(`
Object { Object {
"body": Object { "body": Object {
"from": 0, "from": 0,
"query": Object { "query": Object {
"bool": Object { "bool": Object {
"filter": Array [],
"must": Array [ "must": Array [
Object { Object {
"nested": Object { "nested": Object {
@ -595,12 +603,14 @@ describe('queryEventsBySavedObject', () => {
}); });
test('supports optional date range', async () => { test('supports optional date range', async () => {
clusterClient.callAsInternalUser.mockResolvedValue({ clusterClient.search.mockResolvedValue(
hits: { asApiResponse({
hits: [], hits: {
total: { value: 0 }, hits: [],
}, total: { value: 0 },
}); },
})
);
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';
@ -613,14 +623,14 @@ describe('queryEventsBySavedObject', () => {
{ ...DEFAULT_OPTIONS, start, end } { ...DEFAULT_OPTIONS, start, end }
); );
const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; const [query] = clusterClient.search.mock.calls[0];
expect(method).toEqual('search');
expect(query).toMatchInlineSnapshot(` expect(query).toMatchInlineSnapshot(`
Object { Object {
"body": Object { "body": Object {
"from": 0, "from": 0,
"query": Object { "query": Object {
"bool": Object { "bool": Object {
"filter": Array [],
"must": Array [ "must": Array [
Object { Object {
"nested": Object { "nested": Object {
@ -697,6 +707,12 @@ type RetryableFunction = () => boolean;
const RETRY_UNTIL_DEFAULT_COUNT = 20; const RETRY_UNTIL_DEFAULT_COUNT = 20;
const RETRY_UNTIL_DEFAULT_WAIT = 1000; // milliseconds const RETRY_UNTIL_DEFAULT_WAIT = 1000; // milliseconds
function asApiResponse<T>(body: T): RequestEvent<T> {
return {
body,
} as RequestEvent<T>;
}
async function retryUntil( async function retryUntil(
label: string, label: string,
fn: RetryableFunction, fn: RetryableFunction,

View file

@ -5,20 +5,18 @@
*/ */
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { bufferTime, filter, switchMap } from 'rxjs/operators'; import { bufferTime, filter as rxFilter, switchMap } from 'rxjs/operators';
import { reject, isUndefined } from 'lodash'; import { reject, isUndefined } from 'lodash';
import { Client } from 'elasticsearch';
import type { PublicMethodsOf } from '@kbn/utility-types'; import type { PublicMethodsOf } from '@kbn/utility-types';
import { Logger, LegacyClusterClient } from 'src/core/server'; import { Logger, ElasticsearchClient } from 'src/core/server';
import { ESSearchResponse } from '../../../../typings/elasticsearch';
import { EsContext } from '.'; import { EsContext } from '.';
import { IEvent, IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; import { IEvent, IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types';
import { FindOptionsType } from '../event_log_client'; import { FindOptionsType } from '../event_log_client';
import { esKuery } from '../../../../../src/plugins/data/server';
export const EVENT_BUFFER_TIME = 1000; // milliseconds export const EVENT_BUFFER_TIME = 1000; // milliseconds
export const EVENT_BUFFER_LENGTH = 100; export const EVENT_BUFFER_LENGTH = 100;
export type EsClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>;
export type IClusterClientAdapter = PublicMethodsOf<ClusterClientAdapter>; export type IClusterClientAdapter = PublicMethodsOf<ClusterClientAdapter>;
export interface Doc { export interface Doc {
@ -28,7 +26,7 @@ export interface Doc {
export interface ConstructorOpts { export interface ConstructorOpts {
logger: Logger; logger: Logger;
clusterClientPromise: Promise<EsClusterClient>; elasticsearchClientPromise: Promise<ElasticsearchClient>;
context: EsContext; context: EsContext;
} }
@ -41,14 +39,14 @@ export interface QueryEventsBySavedObjectResult {
export class ClusterClientAdapter { export class ClusterClientAdapter {
private readonly logger: Logger; private readonly logger: Logger;
private readonly clusterClientPromise: Promise<EsClusterClient>; private readonly elasticsearchClientPromise: Promise<ElasticsearchClient>;
private readonly docBuffer$: Subject<Doc>; private readonly docBuffer$: Subject<Doc>;
private readonly context: EsContext; private readonly context: EsContext;
private readonly docsBufferedFlushed: Promise<void>; private readonly docsBufferedFlushed: Promise<void>;
constructor(opts: ConstructorOpts) { constructor(opts: ConstructorOpts) {
this.logger = opts.logger; this.logger = opts.logger;
this.clusterClientPromise = opts.clusterClientPromise; this.elasticsearchClientPromise = opts.elasticsearchClientPromise;
this.context = opts.context; this.context = opts.context;
this.docBuffer$ = new Subject<Doc>(); this.docBuffer$ = new Subject<Doc>();
@ -58,7 +56,7 @@ export class ClusterClientAdapter {
this.docsBufferedFlushed = this.docBuffer$ this.docsBufferedFlushed = this.docBuffer$
.pipe( .pipe(
bufferTime(EVENT_BUFFER_TIME, null, EVENT_BUFFER_LENGTH), bufferTime(EVENT_BUFFER_TIME, null, EVENT_BUFFER_LENGTH),
filter((docs) => docs.length > 0), rxFilter((docs) => docs.length > 0),
switchMap(async (docs) => await this.indexDocuments(docs)) switchMap(async (docs) => await this.indexDocuments(docs))
) )
.toPromise(); .toPromise();
@ -97,7 +95,8 @@ export class ClusterClientAdapter {
} }
try { try {
await this.callEs<ReturnType<Client['bulk']>>('bulk', { body: bulkBody }); const esClient = await this.elasticsearchClientPromise;
await esClient.bulk({ body: bulkBody });
} catch (err) { } catch (err) {
this.logger.error( this.logger.error(
`error writing bulk events: "${err.message}"; docs: ${JSON.stringify(bulkBody)}` `error writing bulk events: "${err.message}"; docs: ${JSON.stringify(bulkBody)}`
@ -111,7 +110,8 @@ export class ClusterClientAdapter {
path: `/_ilm/policy/${policyName}`, path: `/_ilm/policy/${policyName}`,
}; };
try { try {
await this.callEs('transport.request', request); const esClient = await this.elasticsearchClientPromise;
await esClient.transport.request(request);
} catch (err) { } catch (err) {
if (err.statusCode === 404) return false; if (err.statusCode === 404) return false;
throw new Error(`error checking existance of ilm policy: ${err.message}`); throw new Error(`error checking existance of ilm policy: ${err.message}`);
@ -119,14 +119,15 @@ export class ClusterClientAdapter {
return true; return true;
} }
public async createIlmPolicy(policyName: string, policy: unknown): Promise<void> { public async createIlmPolicy(policyName: string, policy: Record<string, unknown>): Promise<void> {
const request = { const request = {
method: 'PUT', method: 'PUT',
path: `/_ilm/policy/${policyName}`, path: `/_ilm/policy/${policyName}`,
body: policy, body: policy,
}; };
try { try {
await this.callEs('transport.request', request); const esClient = await this.elasticsearchClientPromise;
await esClient.transport.request(request);
} catch (err) { } catch (err) {
throw new Error(`error creating ilm policy: ${err.message}`); throw new Error(`error creating ilm policy: ${err.message}`);
} }
@ -135,27 +136,18 @@ export class ClusterClientAdapter {
public async doesIndexTemplateExist(name: string): Promise<boolean> { public async doesIndexTemplateExist(name: string): Promise<boolean> {
let result; let result;
try { try {
result = await this.callEs<ReturnType<Client['indices']['existsTemplate']>>( const esClient = await this.elasticsearchClientPromise;
'indices.existsTemplate', result = (await esClient.indices.existsTemplate({ name })).body;
{ name }
);
} catch (err) { } catch (err) {
throw new Error(`error checking existance of index template: ${err.message}`); throw new Error(`error checking existance of index template: ${err.message}`);
} }
return result as boolean; return result as boolean;
} }
public async createIndexTemplate(name: string, template: unknown): Promise<void> { public async createIndexTemplate(name: string, template: Record<string, unknown>): Promise<void> {
const addTemplateParams = {
name,
create: true,
body: template,
};
try { try {
await this.callEs<ReturnType<Client['indices']['putTemplate']>>( const esClient = await this.elasticsearchClientPromise;
'indices.putTemplate', await esClient.indices.putTemplate({ name, body: template, create: true });
addTemplateParams
);
} catch (err) { } catch (err) {
// The error message doesn't have a type attribute we can look to guarantee it's due // The error message doesn't have a type attribute we can look to guarantee it's due
// to the template already existing (only long message) so we'll check ourselves to see // to the template already existing (only long message) so we'll check ourselves to see
@ -171,19 +163,21 @@ export class ClusterClientAdapter {
public async doesAliasExist(name: string): Promise<boolean> { public async doesAliasExist(name: string): Promise<boolean> {
let result; let result;
try { try {
result = await this.callEs<ReturnType<Client['indices']['existsAlias']>>( const esClient = await this.elasticsearchClientPromise;
'indices.existsAlias', result = (await esClient.indices.existsAlias({ name })).body;
{ name }
);
} catch (err) { } catch (err) {
throw new Error(`error checking existance of initial index: ${err.message}`); throw new Error(`error checking existance of initial index: ${err.message}`);
} }
return result as boolean; return result as boolean;
} }
public async createIndex(name: string, body: unknown = {}): Promise<void> { public async createIndex(
name: string,
body: string | Record<string, unknown> = {}
): Promise<void> {
try { try {
await this.callEs<ReturnType<Client['indices']['create']>>('indices.create', { const esClient = await this.elasticsearchClientPromise;
await esClient.indices.create({
index: name, index: name,
body, body,
}); });
@ -200,7 +194,7 @@ export class ClusterClientAdapter {
type: string, type: string,
ids: 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, filter }: FindOptionsType
): Promise<QueryEventsBySavedObjectResult> { ): Promise<QueryEventsBySavedObjectResult> {
const defaultNamespaceQuery = { const defaultNamespaceQuery = {
bool: { bool: {
@ -220,12 +214,26 @@ export class ClusterClientAdapter {
}; };
const namespaceQuery = namespace === undefined ? defaultNamespaceQuery : namedNamespaceQuery; const namespaceQuery = namespace === undefined ? defaultNamespaceQuery : namedNamespaceQuery;
const esClient = await this.elasticsearchClientPromise;
let dslFilterQuery;
try {
dslFilterQuery = filter
? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(filter))
: [];
} catch (err) {
this.debug(`Invalid kuery syntax for the filter (${filter}) error:`, {
message: err.message,
statusCode: err.statusCode,
});
throw err;
}
const body = { const body = {
size: perPage, size: perPage,
from: (page - 1) * perPage, from: (page - 1) * perPage,
sort: { [sort_field]: { order: sort_order } }, sort: { [sort_field]: { order: sort_order } },
query: { query: {
bool: { bool: {
filter: dslFilterQuery,
must: reject( must: reject(
[ [
{ {
@ -283,8 +291,10 @@ export class ClusterClientAdapter {
try { try {
const { const {
hits: { hits, total }, body: {
}: ESSearchResponse<unknown, {}> = await this.callEs('search', { hits: { hits, total },
},
} = await esClient.search({
index, index,
track_total_hits: true, track_total_hits: true,
body, body,
@ -293,7 +303,7 @@ export class ClusterClientAdapter {
page, page,
per_page: perPage, per_page: perPage,
total: total.value, total: total.value,
data: hits.map((hit) => hit._source) as IValidatedEvent[], data: hits.map((hit: { _source: unknown }) => hit._source) as IValidatedEvent[],
}; };
} catch (err) { } catch (err) {
throw new Error( throw new Error(
@ -302,24 +312,6 @@ export class ClusterClientAdapter {
} }
} }
// We have a common problem typing ES-DSL Queries
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async callEs<ESQueryResult = unknown>(operation: string, body?: any) {
try {
this.debug(`callEs(${operation}) calls:`, body);
const clusterClient = await this.clusterClientPromise;
const result = await clusterClient.callAsInternalUser(operation, body);
this.debug(`callEs(${operation}) result:`, result);
return result as ESQueryResult;
} catch (err) {
this.debug(`callEs(${operation}) error:`, {
message: err.message,
statusCode: err.statusCode,
});
throw err;
}
}
private debug(message: string, object?: unknown) { private debug(message: string, object?: unknown) {
const objectString = object == null ? '' : JSON.stringify(object); const objectString = object == null ? '' : JSON.stringify(object);
this.logger.debug(`esContext: ${message} ${objectString}`); this.logger.debug(`esContext: ${message} ${objectString}`);

View file

@ -5,27 +5,28 @@
*/ */
import { createEsContext } from './context'; import { createEsContext } from './context';
import { LegacyClusterClient, Logger } from '../../../../../src/core/server'; import { ElasticsearchClient, Logger } from '../../../../../src/core/server';
import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks';
import { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import { RequestEvent } from '@elastic/elasticsearch';
jest.mock('../lib/../../../../package.json', () => ({ version: '1.2.3' })); jest.mock('../lib/../../../../package.json', () => ({ version: '1.2.3' }));
jest.mock('./init'); jest.mock('./init');
type EsClusterClient = Pick<jest.Mocked<LegacyClusterClient>, 'callAsInternalUser' | 'asScoped'>;
let logger: Logger; let logger: Logger;
let clusterClient: EsClusterClient; let elasticsearchClient: DeeplyMockedKeys<ElasticsearchClient>;
beforeEach(() => { beforeEach(() => {
logger = loggingSystemMock.createLogger(); logger = loggingSystemMock.createLogger();
clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
}); });
describe('createEsContext', () => { describe('createEsContext', () => {
test('should return is ready state as falsy if not initialized', () => { test('should return is ready state as falsy if not initialized', () => {
const context = createEsContext({ const context = createEsContext({
logger, logger,
clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test0', indexNameRoot: 'test0',
kibanaVersion: '1.2.3', kibanaVersion: '1.2.3',
elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
}); });
expect(context.initialized).toBeFalsy(); expect(context.initialized).toBeFalsy();
@ -37,9 +38,9 @@ describe('createEsContext', () => {
test('should return esNames', () => { test('should return esNames', () => {
const context = createEsContext({ const context = createEsContext({
logger, logger,
clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test-index', indexNameRoot: 'test-index',
kibanaVersion: '1.2.3', kibanaVersion: '1.2.3',
elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
}); });
const esNames = context.esNames; const esNames = context.esNames;
@ -57,12 +58,12 @@ describe('createEsContext', () => {
test('should return exist false for esAdapter ilm policy, index template and alias before initialize', async () => { test('should return exist false for esAdapter ilm policy, index template and alias before initialize', async () => {
const context = createEsContext({ const context = createEsContext({
logger, logger,
clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test1', indexNameRoot: 'test1',
kibanaVersion: '1.2.3', kibanaVersion: '1.2.3',
elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
}); });
clusterClient.callAsInternalUser.mockResolvedValue(false); elasticsearchClient.indices.existsTemplate.mockResolvedValue(asApiResponse(false));
elasticsearchClient.indices.existsAlias.mockResolvedValue(asApiResponse(false));
const doesAliasExist = await context.esAdapter.doesAliasExist(context.esNames.alias); const doesAliasExist = await context.esAdapter.doesAliasExist(context.esNames.alias);
expect(doesAliasExist).toBeFalsy(); expect(doesAliasExist).toBeFalsy();
@ -75,11 +76,11 @@ describe('createEsContext', () => {
test('should return exist true for esAdapter ilm policy, index template and alias after initialize', async () => { test('should return exist true for esAdapter ilm policy, index template and alias after initialize', async () => {
const context = createEsContext({ const context = createEsContext({
logger, logger,
clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test2', indexNameRoot: 'test2',
kibanaVersion: '1.2.3', kibanaVersion: '1.2.3',
elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
}); });
clusterClient.callAsInternalUser.mockResolvedValue(true); elasticsearchClient.indices.existsTemplate.mockResolvedValue(asApiResponse(true));
context.initialize(); context.initialize();
const doesIlmPolicyExist = await context.esAdapter.doesIlmPolicyExist( const doesIlmPolicyExist = await context.esAdapter.doesIlmPolicyExist(
@ -100,12 +101,18 @@ describe('createEsContext', () => {
jest.requireMock('./init').initializeEs.mockResolvedValue(false); jest.requireMock('./init').initializeEs.mockResolvedValue(false);
const context = createEsContext({ const context = createEsContext({
logger, logger,
clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test2', indexNameRoot: 'test2',
kibanaVersion: '1.2.3', kibanaVersion: '1.2.3',
elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
}); });
context.initialize(); context.initialize();
const success = await context.waitTillReady(); const success = await context.waitTillReady();
expect(success).toBe(false); expect(success).toBe(false);
}); });
}); });
function asApiResponse<T>(body: T): RequestEvent<T> {
return {
body,
} as RequestEvent<T>;
}

View file

@ -4,15 +4,13 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { Logger, LegacyClusterClient } from 'src/core/server'; import { Logger, ElasticsearchClient } from 'src/core/server';
import { EsNames, getEsNames } from './names'; import { EsNames, getEsNames } from './names';
import { initializeEs } from './init'; import { initializeEs } from './init';
import { ClusterClientAdapter, IClusterClientAdapter } from './cluster_client_adapter'; import { ClusterClientAdapter, IClusterClientAdapter } from './cluster_client_adapter';
import { createReadySignal, ReadySignal } from '../lib/ready_signal'; import { createReadySignal, ReadySignal } from '../lib/ready_signal';
export type EsClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>;
export interface EsContext { export interface EsContext {
logger: Logger; logger: Logger;
esNames: EsNames; esNames: EsNames;
@ -34,9 +32,9 @@ export function createEsContext(params: EsContextCtorParams): EsContext {
export interface EsContextCtorParams { export interface EsContextCtorParams {
logger: Logger; logger: Logger;
clusterClientPromise: Promise<EsClusterClient>;
indexNameRoot: string; indexNameRoot: string;
kibanaVersion: string; kibanaVersion: string;
elasticsearchClientPromise: Promise<ElasticsearchClient>;
} }
class EsContextImpl implements EsContext { class EsContextImpl implements EsContext {
@ -53,7 +51,7 @@ class EsContextImpl implements EsContext {
this.initialized = false; this.initialized = false;
this.esAdapter = new ClusterClientAdapter({ this.esAdapter = new ClusterClientAdapter({
logger: params.logger, logger: params.logger,
clusterClientPromise: params.clusterClientPromise, elasticsearchClientPromise: params.elasticsearchClientPromise,
context: this, context: this,
}); });
} }

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
export { EsClusterClient, EsContext, createEsContext } from './context'; export { EsContext, createEsContext } from './context';

View file

@ -6,14 +6,14 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { schema, TypeOf } from '@kbn/config-schema'; import { schema, TypeOf } from '@kbn/config-schema';
import { LegacyClusterClient, KibanaRequest } from 'src/core/server'; import { IClusterClient, KibanaRequest } from 'src/core/server';
import { SpacesServiceStart } from '../../spaces/server'; 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 { SavedObjectBulkGetterResult } from './saved_object_provider_registry'; import { SavedObjectBulkGetterResult } from './saved_object_provider_registry';
export type PluginClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>; export type PluginClusterClient = Pick<IClusterClient, 'asInternalUser'>;
export type AdminClusterClient$ = Observable<PluginClusterClient>; export type AdminClusterClient$ = Observable<PluginClusterClient>;
const optionalDateFieldSchema = schema.maybe( const optionalDateFieldSchema = schema.maybe(
@ -48,12 +48,13 @@ export const findOptionsSchema = schema.object({
sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], { sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], {
defaultValue: 'asc', defaultValue: 'asc',
}), }),
filter: schema.maybe(schema.string()),
}); });
// page & perPage are required, other fields are optional // page & perPage are required, other fields are optional
// using schema.maybe allows us to set undefined, but not to make the field optional // using schema.maybe allows us to set undefined, but not to make the field optional
export type FindOptionsType = Pick< export type FindOptionsType = Pick<
TypeOf<typeof findOptionsSchema>, TypeOf<typeof findOptionsSchema>,
'page' | 'per_page' | 'sort_field' | 'sort_order' 'page' | 'per_page' | 'sort_field' | 'sort_order' | 'filter'
> & > &
Partial<TypeOf<typeof findOptionsSchema>>; Partial<TypeOf<typeof findOptionsSchema>>;

View file

@ -5,14 +5,14 @@
*/ */
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { LegacyClusterClient } from 'src/core/server'; import { IClusterClient } from 'src/core/server';
import { Plugin } from './plugin'; import { Plugin } from './plugin';
import { EsContext } from './es'; import { EsContext } from './es';
import { IEvent, IEventLogger, IEventLogService, IEventLogConfig } from './types'; import { IEvent, IEventLogger, IEventLogService, IEventLogConfig } from './types';
import { EventLogger } from './event_logger'; import { EventLogger } from './event_logger';
import { SavedObjectProvider, SavedObjectProviderRegistry } from './saved_object_provider_registry'; import { SavedObjectProvider, SavedObjectProviderRegistry } from './saved_object_provider_registry';
export type PluginClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>; export type PluginClusterClient = Pick<IClusterClient, 'asInternalUser'>;
export type AdminClusterClient$ = Observable<PluginClusterClient>; export type AdminClusterClient$ = Observable<PluginClusterClient>;
type SystemLogger = Plugin['systemLogger']; type SystemLogger = Plugin['systemLogger'];

View file

@ -5,14 +5,14 @@
*/ */
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { LegacyClusterClient, KibanaRequest } from 'src/core/server'; import { IClusterClient, KibanaRequest } from 'src/core/server';
import { SpacesServiceStart } from '../../spaces/server'; import { SpacesServiceStart } from '../../spaces/server';
import { EsContext } from './es'; import { EsContext } from './es';
import { IEventLogClientService } from './types'; import { IEventLogClientService } from './types';
import { EventLogClient } from './event_log_client'; import { EventLogClient } from './event_log_client';
import { SavedObjectProviderRegistry } from './saved_object_provider_registry'; import { SavedObjectProviderRegistry } from './saved_object_provider_registry';
export type PluginClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>; export type PluginClusterClient = Pick<IClusterClient, 'asInternalUser'>;
export type AdminClusterClient$ = Observable<PluginClusterClient>; export type AdminClusterClient$ = Observable<PluginClusterClient>;
interface EventLogServiceCtorParams { interface EventLogServiceCtorParams {

View file

@ -12,7 +12,7 @@ import {
Logger, Logger,
Plugin as CorePlugin, Plugin as CorePlugin,
PluginInitializerContext, PluginInitializerContext,
LegacyClusterClient, IClusterClient,
SharedGlobalConfig, SharedGlobalConfig,
IContextProvider, IContextProvider,
} from 'src/core/server'; } from 'src/core/server';
@ -33,7 +33,7 @@ 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'; import { findByIdsRoute } from './routes/find_by_ids';
export type PluginClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>; export type PluginClusterClient = Pick<IClusterClient, 'asInternalUser'>;
const PROVIDER = 'eventLog'; const PROVIDER = 'eventLog';
@ -77,9 +77,9 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
logger: this.systemLogger, logger: this.systemLogger,
// TODO: get index prefix from config.get(kibana.index) // TODO: get index prefix from config.get(kibana.index)
indexNameRoot: kibanaIndex, indexNameRoot: kibanaIndex,
clusterClientPromise: core elasticsearchClientPromise: core
.getStartServices() .getStartServices()
.then(([{ elasticsearch }]) => elasticsearch.legacy.client), .then(([{ elasticsearch }]) => elasticsearch.client.asInternalUser),
kibanaVersion: this.kibanaVersion, kibanaVersion: this.kibanaVersion,
}); });

View file

@ -28,6 +28,7 @@ interface GetEventLogParams {
id: string; id: string;
provider: string; provider: string;
actions: Map<string, { gte: number } | { equal: number }>; actions: Map<string, { gte: number } | { equal: number }>;
filter?: string;
} }
// Return event log entries given the specified parameters; for the `actions` // Return event log entries given the specified parameters; for the `actions`
@ -37,7 +38,9 @@ export async function getEventLog(params: GetEventLogParams): Promise<IValidated
const supertest = getService('supertest'); const supertest = getService('supertest');
const spacePrefix = getUrlPrefix(spaceId); const spacePrefix = getUrlPrefix(spaceId);
const url = `${spacePrefix}/api/event_log/${type}/${id}/_find?per_page=5000`; const url = `${spacePrefix}/api/event_log/${type}/${id}/_find?per_page=5000${
params.filter ? `&filter=${params.filter}` : ''
}`;
const { body: result } = await supertest.get(url).expect(200); const { body: result } = await supertest.get(url).expect(200);
if (!result.total) { if (!result.total) {

View file

@ -519,6 +519,7 @@ export default function ({ getService }: FtrProviderContext) {
id: actionId, id: actionId,
provider: 'actions', provider: 'actions',
actions: new Map([['execute', { equal: 1 }]]), actions: new Map([['execute', { equal: 1 }]]),
filter: 'event.action:(execute)',
}); });
}); });

View file

@ -84,6 +84,23 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
}); });
}); });
// get the filtered events only with action 'new-instance'
const filteredEvents = await retry.try(async () => {
return await getEventLog({
getService,
spaceId: Spaces.space1.id,
type: 'alert',
id: alertId,
provider: 'alerting',
actions: new Map([['new-instance', { equal: 1 }]]),
filter: 'event.action:(new-instance)',
});
});
expect(getEventsByAction(filteredEvents, 'execute').length).equal(0);
expect(getEventsByAction(filteredEvents, 'execute-action').length).equal(0);
expect(getEventsByAction(events, 'new-instance').length).equal(1);
const executeEvents = getEventsByAction(events, 'execute'); const executeEvents = getEventsByAction(events, 'execute');
const executeActionEvents = getEventsByAction(events, 'execute-action'); const executeActionEvents = getEventsByAction(events, 'execute-action');
const newInstanceEvents = getEventsByAction(events, 'new-instance'); const newInstanceEvents = getEventsByAction(events, 'new-instance');