[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>
This commit is contained in:
parent
5f53d4f34b
commit
951034cf0c
|
@ -214,11 +214,13 @@ export {
|
|||
ISearchSetup,
|
||||
ISearchStart,
|
||||
toSnakeCase,
|
||||
getAsyncOptions,
|
||||
getDefaultSearchParams,
|
||||
getShardTimeout,
|
||||
getTotalLoaded,
|
||||
shimHitsTotal,
|
||||
usageProvider,
|
||||
shimAbortSignal,
|
||||
SearchUsage,
|
||||
} from './search';
|
||||
|
||||
|
|
|
@ -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<SharedGlobalConfig>,
|
||||
|
@ -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<SearchResponse<any>>;
|
||||
|
||||
if (usage) usage.trackSuccess(rawResponse.took);
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 = <T extends TransportRequestPromise<unknown>>(
|
||||
promise: T,
|
||||
signal: AbortSignal | undefined
|
||||
): T => {
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', () => promise.abort());
|
||||
}
|
||||
return promise;
|
||||
};
|
|
@ -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<SearchResponse<any>> }>;
|
||||
|
||||
return {
|
||||
|
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
// @internal
|
||||
export const shimAbortSignal: <T extends TransportRequestPromise<unknown>>(promise: T, signal: AbortSignal | undefined) => T;
|
||||
|
||||
// @internal
|
||||
export function shimHitsTotal(response: SearchResponse<any>): {
|
||||
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
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
*/
|
||||
|
||||
export {
|
||||
IEnhancedEsSearchRequest,
|
||||
IAsyncSearchRequest,
|
||||
ENHANCED_ES_SEARCH_STRATEGY,
|
||||
EQL_SEARCH_STRATEGY,
|
||||
EqlRequestParams,
|
||||
EqlSearchStrategyRequest,
|
||||
EqlSearchStrategyResponse,
|
||||
IAsyncSearchRequest,
|
||||
IEnhancedEsSearchRequest,
|
||||
} from './search';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<Record<string, unknown>>;
|
||||
|
||||
export interface EqlSearchStrategyRequest extends IKibanaSearchRequest<EqlRequestParams> {
|
||||
options?: TransportRequestOptions;
|
||||
}
|
||||
|
||||
export type EqlSearchStrategyResponse<T = unknown> = IKibanaSearchResponse<ApiResponse<T>>;
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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<void, void, SetupDepende
|
|||
)
|
||||
);
|
||||
|
||||
deps.data.search.registerSearchStrategy(
|
||||
EQL_SEARCH_STRATEGY,
|
||||
eqlSearchStrategyProvider(this.logger)
|
||||
);
|
||||
|
||||
deps.data.__enhance({
|
||||
search: {
|
||||
defaultStrategy: ENHANCED_ES_SEARCH_STRATEGY,
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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, RequestHandlerContext } from 'src/core/server';
|
||||
import { EqlSearchStrategyRequest } from '../../common/search/types';
|
||||
import { eqlSearchStrategyProvider } from './eql_search_strategy';
|
||||
|
||||
const getMockEqlResponse = () => ({
|
||||
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<EqlSearchStrategyRequest>['params'];
|
||||
let options: Required<EqlSearchStrategyRequest>['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],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<EqlSearchStrategyRequest, EqlSearchStrategyResponse> => {
|
||||
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<ApiResponse>;
|
||||
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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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<any>;
|
||||
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<any>;
|
||||
return {
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
*/
|
||||
|
||||
export { enhancedEsSearchStrategyProvider } from './es_search_strategy';
|
||||
export { eqlSearchStrategyProvider } from './eql_search_strategy';
|
||||
|
|
Loading…
Reference in a new issue