From 951034cf0caa09b7e1914d8e16b8197f343eca25 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 5 Oct 2020 11:30:32 -0500 Subject: [PATCH] [Search]Add EQL search strategy (#78645) * Add EQL search strategy Since EQL is an x-pack feature, this strategy will live in the x-pack plugin data_enhanced. * Refactor our test setup to minimize shared state * Ensures that the same variable is not used for both test setup and test assertions * Ensures that mocks are reinstantiated on every test * Use explicit top-level exports * Move async search options to a helper function * Move our workaround to a helper function This was repeated in five places, time to consolidate. * Commit documentation changes We export a few new helper functions. * Mark our internal methods as such Updates documentation accordingly. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/data/server/index.ts | 2 + .../search/es_search/es_search_strategy.ts | 16 +- .../es_search/get_default_search_params.ts | 8 + .../data/server/search/es_search/index.ts | 1 + .../es_search/shim_abort_signal.test.ts | 55 ++++++ .../search/es_search/shim_abort_signal.ts | 41 +++++ .../data/server/search/routes/call_msearch.ts | 23 ++- src/plugins/data/server/server.api.md | 36 ++-- x-pack/plugins/data_enhanced/common/index.ts | 8 +- .../data_enhanced/common/search/index.ts | 6 +- .../data_enhanced/common/search/types.ts | 19 +- x-pack/plugins/data_enhanced/server/index.ts | 2 +- x-pack/plugins/data_enhanced/server/plugin.ts | 9 +- .../server/search/eql_search_strategy.test.ts | 168 ++++++++++++++++++ .../server/search/eql_search_strategy.ts | 70 ++++++++ .../server/search/es_search_strategy.ts | 16 +- .../data_enhanced/server/search/index.ts | 1 + 17 files changed, 429 insertions(+), 52 deletions(-) create mode 100644 src/plugins/data/server/search/es_search/shim_abort_signal.test.ts create mode 100644 src/plugins/data/server/search/es_search/shim_abort_signal.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index aac1fe1fde21..11dcbb01bf4a 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -214,11 +214,13 @@ export { ISearchSetup, ISearchStart, toSnakeCase, + getAsyncOptions, getDefaultSearchParams, getShardTimeout, getTotalLoaded, shimHitsTotal, usageProvider, + shimAbortSignal, SearchUsage, } from './search'; diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index e2ed500689cf..6e185d30ad56 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -23,7 +23,13 @@ import { Observable } from 'rxjs'; import { ApiResponse } from '@elastic/elasticsearch'; import { SearchUsage } from '../collectors/usage'; import { toSnakeCase } from './to_snake_case'; -import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded, getShardTimeout } from '..'; +import { + ISearchStrategy, + getDefaultSearchParams, + getTotalLoaded, + getShardTimeout, + shimAbortSignal, +} from '..'; export const esSearchStrategyProvider = ( config$: Observable, @@ -52,10 +58,10 @@ export const esSearchStrategyProvider = ( }); try { - // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 - const promise = context.core.elasticsearch.client.asCurrentUser.search(params); - if (options?.abortSignal) - options.abortSignal.addEventListener('abort', () => promise.abort()); + const promise = shimAbortSignal( + context.core.elasticsearch.client.asCurrentUser.search(params), + options?.abortSignal + ); const { body: rawResponse } = (await promise) as ApiResponse>; if (usage) usage.trackSuccess(rawResponse.took); diff --git a/src/plugins/data/server/search/es_search/get_default_search_params.ts b/src/plugins/data/server/search/es_search/get_default_search_params.ts index 13607fce5167..b51293b88fce 100644 --- a/src/plugins/data/server/search/es_search/get_default_search_params.ts +++ b/src/plugins/data/server/search/es_search/get_default_search_params.ts @@ -42,3 +42,11 @@ export async function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient trackTotalHits: true, }; } + +/** + @internal + */ +export const getAsyncOptions = () => ({ + waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return + keepAlive: '1m', // Extend the TTL for this search request by one minute +}); diff --git a/src/plugins/data/server/search/es_search/index.ts b/src/plugins/data/server/search/es_search/index.ts index 1bd17fc98616..63ab7a025ee5 100644 --- a/src/plugins/data/server/search/es_search/index.ts +++ b/src/plugins/data/server/search/es_search/index.ts @@ -21,5 +21,6 @@ export { esSearchStrategyProvider } from './es_search_strategy'; export * from './get_default_search_params'; export { getTotalLoaded } from './get_total_loaded'; export * from './to_snake_case'; +export { shimAbortSignal } from './shim_abort_signal'; export { ES_SEARCH_STRATEGY, IEsSearchRequest, IEsSearchResponse } from '../../../common'; diff --git a/src/plugins/data/server/search/es_search/shim_abort_signal.test.ts b/src/plugins/data/server/search/es_search/shim_abort_signal.test.ts new file mode 100644 index 000000000000..794b6535cc18 --- /dev/null +++ b/src/plugins/data/server/search/es_search/shim_abort_signal.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { elasticsearchServiceMock } from '../../../../../core/server/mocks'; +import { shimAbortSignal } from '.'; + +describe('shimAbortSignal', () => { + it('aborts the promise if the signal is aborted', () => { + const promise = elasticsearchServiceMock.createSuccessTransportRequestPromise({ + success: true, + }); + const controller = new AbortController(); + shimAbortSignal(promise, controller.signal); + controller.abort(); + + expect(promise.abort).toHaveBeenCalled(); + }); + + it('returns the original promise', async () => { + const promise = elasticsearchServiceMock.createSuccessTransportRequestPromise({ + success: true, + }); + const controller = new AbortController(); + const response = await shimAbortSignal(promise, controller.signal); + + expect(response).toEqual(expect.objectContaining({ body: { success: true } })); + }); + + it('allows the promise to be aborted manually', () => { + const promise = elasticsearchServiceMock.createSuccessTransportRequestPromise({ + success: true, + }); + const controller = new AbortController(); + const enhancedPromise = shimAbortSignal(promise, controller.signal); + + enhancedPromise.abort(); + expect(promise.abort).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/data/server/search/es_search/shim_abort_signal.ts b/src/plugins/data/server/search/es_search/shim_abort_signal.ts new file mode 100644 index 000000000000..14a4a6919c5a --- /dev/null +++ b/src/plugins/data/server/search/es_search/shim_abort_signal.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; + +/** + * + * @internal + * NOTE: Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 + * is resolved + * + * @param promise a TransportRequestPromise + * @param signal optional AbortSignal + * + * @returns a TransportRequestPromise that will be aborted if the signal is aborted + */ +export const shimAbortSignal = >( + promise: T, + signal: AbortSignal | undefined +): T => { + if (signal) { + signal.addEventListener('abort', () => promise.abort()); + } + return promise; +}; diff --git a/src/plugins/data/server/search/routes/call_msearch.ts b/src/plugins/data/server/search/routes/call_msearch.ts index 764dcd189f8d..8103b680c6bb 100644 --- a/src/plugins/data/server/search/routes/call_msearch.ts +++ b/src/plugins/data/server/search/routes/call_msearch.ts @@ -25,7 +25,7 @@ import { IUiSettingsClient, IScopedClusterClient, SharedGlobalConfig } from 'src import { MsearchRequestBody, MsearchResponse } from '../../../common/search/search_source'; import { shimHitsTotal } from './shim_hits_total'; -import { getShardTimeout, getDefaultSearchParams, toSnakeCase } from '..'; +import { getShardTimeout, getDefaultSearchParams, toSnakeCase, shimAbortSignal } from '..'; /** @internal */ export function convertRequestBody( @@ -74,18 +74,17 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) { const body = convertRequestBody(params.body, timeout); - // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 - const promise = esClient.asCurrentUser.msearch( - { - body, - }, - { - querystring: toSnakeCase(defaultParams), - } + const promise = shimAbortSignal( + esClient.asCurrentUser.msearch( + { + body, + }, + { + querystring: toSnakeCase(defaultParams), + } + ), + params.signal ); - if (params.signal) { - params.signal.addEventListener('abort', () => promise.abort()); - } const response = (await promise) as ApiResponse<{ responses: Array> }>; return { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index fed0c1a02297..45dbdee0f846 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -47,6 +47,7 @@ import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; import { ShardsResponse } from 'elasticsearch'; import { ToastInputFields } from 'src/core/public/notifications'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Unit } from '@elastic/datemath'; @@ -354,6 +355,12 @@ export type Filter = { query?: any; }; +// @internal (undocumented) +export const getAsyncOptions: () => { + waitForCompletionTimeout: string; + keepAlive: string; +}; + // Warning: (ae-forgotten-export) The symbol "IUiSettingsClient" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getDefaultSearchParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -980,6 +987,9 @@ export interface SearchUsage { trackSuccess(duration: number): Promise; } +// @internal +export const shimAbortSignal: >(promise: T, signal: AbortSignal | undefined) => T; + // @internal export function shimHitsTotal(response: SearchResponse): { hits: { @@ -1115,19 +1125,19 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:228:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:229:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:238:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:240:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:245:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:249:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:230:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:231:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:240:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:242:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:78:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts index 012f1204da46..bcea85556d42 100644 --- a/x-pack/plugins/data_enhanced/common/index.ts +++ b/x-pack/plugins/data_enhanced/common/index.ts @@ -5,7 +5,11 @@ */ export { - IEnhancedEsSearchRequest, - IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY, + EQL_SEARCH_STRATEGY, + EqlRequestParams, + EqlSearchStrategyRequest, + EqlSearchStrategyResponse, + IAsyncSearchRequest, + IEnhancedEsSearchRequest, } from './search'; diff --git a/x-pack/plugins/data_enhanced/common/search/index.ts b/x-pack/plugins/data_enhanced/common/search/index.ts index 696938a403e8..9f4141dbcae7 100644 --- a/x-pack/plugins/data_enhanced/common/search/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/index.ts @@ -4,8 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - IEnhancedEsSearchRequest, - IAsyncSearchRequest, - ENHANCED_ES_SEARCH_STRATEGY, -} from './types'; +export * from './types'; diff --git a/x-pack/plugins/data_enhanced/common/search/types.ts b/x-pack/plugins/data_enhanced/common/search/types.ts index 24d459ade4bf..235fcdc325bc 100644 --- a/x-pack/plugins/data_enhanced/common/search/types.ts +++ b/x-pack/plugins/data_enhanced/common/search/types.ts @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IEsSearchRequest } from '../../../../../src/plugins/data/common'; +import { EqlSearch } from '@elastic/elasticsearch/api/requestParams'; +import { ApiResponse, TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; + +import { + IEsSearchRequest, + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../src/plugins/data/common'; export const ENHANCED_ES_SEARCH_STRATEGY = 'ese'; @@ -21,3 +28,13 @@ export interface IEnhancedEsSearchRequest extends IEsSearchRequest { */ isRollup?: boolean; } + +export const EQL_SEARCH_STRATEGY = 'eql'; + +export type EqlRequestParams = EqlSearch>; + +export interface EqlSearchStrategyRequest extends IKibanaSearchRequest { + options?: TransportRequestOptions; +} + +export type EqlSearchStrategyResponse = IKibanaSearchResponse>; diff --git a/x-pack/plugins/data_enhanced/server/index.ts b/x-pack/plugins/data_enhanced/server/index.ts index 3c5d5d1e99d1..a0edd2e26ebe 100644 --- a/x-pack/plugins/data_enhanced/server/index.ts +++ b/x-pack/plugins/data_enhanced/server/index.ts @@ -11,6 +11,6 @@ export function plugin(initializerContext: PluginInitializerContext) { return new EnhancedDataServerPlugin(initializerContext); } -export { ENHANCED_ES_SEARCH_STRATEGY } from '../common'; +export { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; export { EnhancedDataServerPlugin as Plugin }; diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index a1dff00ddfdd..ad21216bb703 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -16,10 +16,10 @@ import { PluginStart as DataPluginStart, usageProvider, } from '../../../../src/plugins/data/server'; -import { enhancedEsSearchStrategyProvider } from './search'; +import { enhancedEsSearchStrategyProvider, eqlSearchStrategyProvider } from './search'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { getUiSettings } from './ui_settings'; -import { ENHANCED_ES_SEARCH_STRATEGY } from '../common'; +import { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; interface SetupDependencies { data: DataPluginSetup; @@ -47,6 +47,11 @@ export class EnhancedDataServerPlugin implements Plugin ({ + body: { + is_partial: false, + is_running: false, + took: 162, + timed_out: false, + hits: { + total: { + value: 1, + relation: 'eq', + }, + sequences: [], + }, + }, +}); + +describe('EQL search strategy', () => { + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = ({ debug: jest.fn() } as unknown) as Logger; + }); + + describe('strategy interface', () => { + it('returns a strategy with a `search` function', async () => { + const eqlSearch = await eqlSearchStrategyProvider(mockLogger); + expect(typeof eqlSearch.search).toBe('function'); + }); + + it('returns a strategy with a `cancel` function', async () => { + const eqlSearch = await eqlSearchStrategyProvider(mockLogger); + expect(typeof eqlSearch.cancel).toBe('function'); + }); + }); + + describe('search()', () => { + let mockEqlSearch: jest.Mock; + let mockEqlGet: jest.Mock; + let mockContext: RequestHandlerContext; + let params: Required['params']; + let options: Required['options']; + + beforeEach(() => { + mockEqlSearch = jest.fn().mockResolvedValueOnce(getMockEqlResponse()); + mockEqlGet = jest.fn().mockResolvedValueOnce(getMockEqlResponse()); + mockContext = ({ + core: { + uiSettings: { + client: { + get: jest.fn(), + }, + }, + elasticsearch: { + client: { + asCurrentUser: { + eql: { + get: mockEqlGet, + search: mockEqlSearch, + }, + }, + }, + }, + }, + } as unknown) as RequestHandlerContext; + params = { + index: 'logstash-*', + body: { query: 'process where 1 == 1' }, + }; + options = { ignore: [400] }; + }); + + describe('async functionality', () => { + it('performs an eql client search with params when no ID is provided', async () => { + const eqlSearch = await eqlSearchStrategyProvider(mockLogger); + await eqlSearch.search(mockContext, { options, params }); + const [[request, requestOptions]] = mockEqlSearch.mock.calls; + + expect(request.index).toEqual('logstash-*'); + expect(request.body).toEqual(expect.objectContaining({ query: 'process where 1 == 1' })); + expect(requestOptions).toEqual(expect.objectContaining({ ignore: [400] })); + }); + + it('retrieves the current request if an id is provided', async () => { + const eqlSearch = await eqlSearchStrategyProvider(mockLogger); + await eqlSearch.search(mockContext, { id: 'my-search-id' }); + const [[requestParams]] = mockEqlGet.mock.calls; + + expect(mockEqlSearch).not.toHaveBeenCalled(); + expect(requestParams).toEqual(expect.objectContaining({ id: 'my-search-id' })); + }); + }); + + describe('arguments', () => { + it('sends along async search options', async () => { + const eqlSearch = await eqlSearchStrategyProvider(mockLogger); + await eqlSearch.search(mockContext, { options, params }); + const [[request]] = mockEqlSearch.mock.calls; + + expect(request).toEqual( + expect.objectContaining({ + wait_for_completion_timeout: '100ms', + keep_alive: '1m', + }) + ); + }); + + it('sends along default search parameters', async () => { + const eqlSearch = await eqlSearchStrategyProvider(mockLogger); + await eqlSearch.search(mockContext, { options, params }); + const [[request]] = mockEqlSearch.mock.calls; + + expect(request).toEqual( + expect.objectContaining({ + ignore_unavailable: true, + ignore_throttled: true, + }) + ); + }); + + it('allows search parameters to be overridden', async () => { + const eqlSearch = await eqlSearchStrategyProvider(mockLogger); + await eqlSearch.search(mockContext, { + options, + params: { + ...params, + wait_for_completion_timeout: '5ms', + keep_on_completion: false, + }, + }); + const [[request]] = mockEqlSearch.mock.calls; + + expect(request).toEqual( + expect.objectContaining({ + wait_for_completion_timeout: '5ms', + keep_alive: '1m', + keep_on_completion: false, + }) + ); + }); + + it('allows search options to be overridden', async () => { + const eqlSearch = await eqlSearchStrategyProvider(mockLogger); + await eqlSearch.search(mockContext, { + options: { ...options, maxRetries: 2, ignore: [300] }, + params, + }); + const [[, requestOptions]] = mockEqlSearch.mock.calls; + + expect(requestOptions).toEqual( + expect.objectContaining({ + max_retries: 2, + ignore: [300], + }) + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts new file mode 100644 index 000000000000..5fd3b8df8727 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts @@ -0,0 +1,70 @@ +/* + * 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 { Logger } from 'kibana/server'; +import { ApiResponse, TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; + +import { + getAsyncOptions, + getDefaultSearchParams, + ISearchStrategy, + toSnakeCase, + shimAbortSignal, +} from '../../../../../src/plugins/data/server'; +import { EqlSearchStrategyRequest, EqlSearchStrategyResponse } from '../../common/search/types'; + +export const eqlSearchStrategyProvider = ( + logger: Logger +): ISearchStrategy => { + return { + cancel: async (context, id) => { + logger.debug(`_eql/delete ${id}`); + await context.core.elasticsearch.client.asCurrentUser.eql.delete({ + id, + }); + }, + search: async (context, request, options) => { + logger.debug(`_eql/search ${JSON.stringify(request.params) || request.id}`); + let promise: TransportRequestPromise; + const eqlClient = context.core.elasticsearch.client.asCurrentUser.eql; + const uiSettingsClient = await context.core.uiSettings.client; + const asyncOptions = getAsyncOptions(); + + if (request.id) { + promise = eqlClient.get({ + id: request.id, + ...toSnakeCase(asyncOptions), + }); + } else { + const { ignoreThrottled, ignoreUnavailable } = await getDefaultSearchParams( + uiSettingsClient + ); + const searchParams = toSnakeCase({ + ignoreThrottled, + ignoreUnavailable, + ...asyncOptions, + ...request.params, + }); + const searchOptions = toSnakeCase({ ...request.options }); + + promise = eqlClient.search( + searchParams as EqlSearchStrategyRequest['params'], + searchOptions as EqlSearchStrategyRequest['options'] + ); + } + + const rawResponse = await shimAbortSignal(promise, options?.abortSignal); + const { id, is_partial: isPartial, is_running: isRunning } = rawResponse.body; + + return { + id, + isPartial, + isRunning, + rawResponse, + }; + }, + }; +}; diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index f3cf67a487a6..747522872438 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -17,6 +17,8 @@ import { getShardTimeout, toSnakeCase, shimHitsTotal, + getAsyncOptions, + shimAbortSignal, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; import { @@ -79,11 +81,7 @@ export const enhancedEsSearchStrategyProvider = ( let promise: TransportRequestPromise; const esClient = context.core.elasticsearch.client.asCurrentUser; const uiSettingsClient = await context.core.uiSettings.client; - - const asyncOptions = { - waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return - keepAlive: '1m', // Extend the TTL for this search request by one minute - }; + const asyncOptions = getAsyncOptions(); // If we have an ID, then just poll for that ID, otherwise send the entire request body if (!request.id) { @@ -102,9 +100,7 @@ export const enhancedEsSearchStrategyProvider = ( }); } - // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 - if (options?.abortSignal) options.abortSignal.addEventListener('abort', () => promise.abort()); - const esResponse = await promise; + const esResponse = await shimAbortSignal(promise, options?.abortSignal); const { id, response, is_partial: isPartial, is_running: isRunning } = esResponse.body; return { id, @@ -139,9 +135,7 @@ export const enhancedEsSearchStrategyProvider = ( querystring, }); - // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 - if (options?.abortSignal) options.abortSignal.addEventListener('abort', () => promise.abort()); - const esResponse = await promise; + const esResponse = await shimAbortSignal(promise, options?.abortSignal); const response = esResponse.body as SearchResponse; return { diff --git a/x-pack/plugins/data_enhanced/server/search/index.ts b/x-pack/plugins/data_enhanced/server/search/index.ts index f914326f30d3..64a28cea358e 100644 --- a/x-pack/plugins/data_enhanced/server/search/index.ts +++ b/x-pack/plugins/data_enhanced/server/search/index.ts @@ -5,3 +5,4 @@ */ export { enhancedEsSearchStrategyProvider } from './es_search_strategy'; +export { eqlSearchStrategyProvider } from './eql_search_strategy';