enabling esdsl function in expressions on server side (#95304)

This commit is contained in:
Peter Pisljar 2021-03-31 13:32:42 +02:00 committed by GitHub
parent 44a46358c2
commit a62d69d7ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 327 additions and 205 deletions

View file

@ -0,0 +1,194 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { EsRawResponse } from './es_raw_response';
import { RequestStatistics, RequestAdapter } from '../../../../inspector/common';
import { ISearchGeneric, KibanaContext } from '..';
import { buildEsQuery, getEsQueryConfig } from '../../es_query/es_query';
import { UiSettingsCommon } from '../../index_patterns';
const name = 'esdsl';
type Input = KibanaContext | null;
type Output = Promise<EsRawResponse>;
interface Arguments {
dsl: string;
index: string;
size: number;
}
export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof name,
Input,
Arguments,
Output
>;
/** @internal */
export interface EsdslStartDependencies {
search: ISearchGeneric;
uiSettingsClient: UiSettingsCommon;
}
export const getEsdslFn = ({
getStartDependencies,
}: {
getStartDependencies: (getKibanaRequest: any) => Promise<EsdslStartDependencies>;
}) => {
const esdsl: EsdslExpressionFunctionDefinition = {
name,
type: 'es_raw_response',
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('data.search.esdsl.help', {
defaultMessage: 'Run Elasticsearch request',
}),
args: {
dsl: {
types: ['string'],
aliases: ['_', 'q', 'query'],
help: i18n.translate('data.search.esdsl.q.help', {
defaultMessage: 'Query DSL',
}),
required: true,
},
index: {
types: ['string'],
help: i18n.translate('data.search.esdsl.index.help', {
defaultMessage: 'ElasticSearch index to query',
}),
required: true,
},
size: {
types: ['number'],
help: i18n.translate('data.search.esdsl.size.help', {
defaultMessage: 'ElasticSearch searchAPI size parameter',
}),
default: 10,
},
},
async fn(input, args, { inspectorAdapters, abortSignal, getKibanaRequest }) {
const { search, uiSettingsClient } = await getStartDependencies(getKibanaRequest);
const dsl = JSON.parse(args.dsl);
if (input) {
const esQueryConfigs = getEsQueryConfig(uiSettingsClient as any);
const query = buildEsQuery(
undefined, // args.index,
input.query || [],
input.filters || [],
esQueryConfigs
);
if (dsl.query) {
query.bool.must.push(dsl.query);
}
dsl.query = query;
}
if (!inspectorAdapters.requests) {
inspectorAdapters.requests = new RequestAdapter();
}
const request = inspectorAdapters.requests.start(
i18n.translate('data.search.dataRequest.title', {
defaultMessage: 'Data',
}),
{
description: i18n.translate('data.search.es_search.dataRequest.description', {
defaultMessage:
'This request queries Elasticsearch to fetch the data for the visualization.',
}),
}
);
request.stats({
indexPattern: {
label: i18n.translate('data.search.es_search.indexPatternLabel', {
defaultMessage: 'Index pattern',
}),
value: args.index,
description: i18n.translate('data.search.es_search.indexPatternDescription', {
defaultMessage: 'The index pattern that connected to the Elasticsearch indices.',
}),
},
});
try {
const { rawResponse } = await search(
{
params: {
index: args.index,
size: args.size,
body: dsl,
},
},
{ abortSignal }
).toPromise();
const stats: RequestStatistics = {};
if (rawResponse?.took) {
stats.queryTime = {
label: i18n.translate('data.search.es_search.queryTimeLabel', {
defaultMessage: 'Query time',
}),
value: i18n.translate('data.search.es_search.queryTimeValue', {
defaultMessage: '{queryTime}ms',
values: { queryTime: rawResponse.took },
}),
description: i18n.translate('data.search.es_search.queryTimeDescription', {
defaultMessage:
'The time it took to process the query. ' +
'Does not include the time to send the request or parse it in the browser.',
}),
};
}
if (rawResponse?.hits) {
stats.hitsTotal = {
label: i18n.translate('data.search.es_search.hitsTotalLabel', {
defaultMessage: 'Hits (total)',
}),
value: `${rawResponse.hits.total}`,
description: i18n.translate('data.search.es_search.hitsTotalDescription', {
defaultMessage: 'The number of documents that match the query.',
}),
};
stats.hits = {
label: i18n.translate('data.search.es_search.hitsLabel', {
defaultMessage: 'Hits',
}),
value: `${rawResponse.hits.hits.length}`,
description: i18n.translate('data.search.es_search.hitsDescription', {
defaultMessage: 'The number of documents returned by the query.',
}),
};
}
request.stats(stats).ok({ json: rawResponse });
request.json(dsl);
return {
type: 'es_raw_response',
body: rawResponse,
};
} catch (e) {
request.error({ json: e });
throw e;
}
},
};
return esdsl;
};

View file

@ -23,3 +23,5 @@ export * from './range_filter';
export * from './kibana_filter';
export * from './filters_to_ast';
export * from './timerange';
export * from './es_raw_response';
export * from './esdsl';

View file

@ -40,7 +40,6 @@ import { EventEmitter } from 'events';
import { ExecutionContext } from 'src/plugins/expressions/common';
import { ExpressionAstExpression } from 'src/plugins/expressions/common';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { ExpressionFunctionDefinition as ExpressionFunctionDefinition_2 } from 'src/plugins/expressions/public';
import { ExpressionsSetup } from 'src/plugins/expressions/public';
import { ExpressionValueBoxed } from 'src/plugins/expressions/common';
import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils';
@ -727,7 +726,7 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'e
// Warning: (ae-missing-release-tag) "EsdslExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition_2<typeof name_3, Input_36, Arguments_23, Output_36>;
export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition<typeof name_3, Input_36, Arguments_23, Output_36>;
// Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//

View file

@ -6,7 +6,12 @@
* Side Public License, v 1.
*/
import { esdsl } from './esdsl';
import { getEsdsl } from './esdsl';
import { MockedKeys } from '@kbn/utility-types/target/jest';
import { EsdslExpressionFunctionDefinition } from '../../../common/search/expressions';
import { StartServicesAccessor } from 'kibana/public';
import { DataPublicPluginStart, DataStartDependencies } from '../../types';
import { of } from 'rxjs';
jest.mock('@kbn/i18n', () => {
return {
@ -16,26 +21,38 @@ jest.mock('@kbn/i18n', () => {
};
});
jest.mock('../../services', () => ({
getUiSettings: () => ({
get: () => true,
}),
getSearchService: () => ({
search: jest.fn((params: any) => {
return {
toPromise: async () => {
return { rawResponse: params };
},
};
}),
}),
}));
describe('esdsl', () => {
let getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>;
let startDependencies: MockedKeys<
StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>
>;
let esdsl: EsdslExpressionFunctionDefinition;
beforeEach(() => {
jest.clearAllMocks();
startDependencies = [
{
uiSettings: {
get: jest.fn().mockReturnValue(true),
},
},
{},
{
search: {
search: jest.fn((params: any) => of({ rawResponse: params })),
},
},
];
getStartServices = jest
.fn()
.mockResolvedValue(new Promise((resolve) => resolve(startDependencies)));
esdsl = getEsdsl({ getStartServices });
});
describe('correctly handles input', () => {
test('throws on invalid json input', async () => {
const fn = async function () {
await esdsl().fn(null, { dsl: 'invalid json', index: 'test', size: 0 }, {
await esdsl.fn(null, { dsl: 'invalid json', index: 'test', size: 0 }, {
inspectorAdapters: {},
} as any);
};
@ -50,7 +67,7 @@ describe('esdsl', () => {
});
test('adds filters', async () => {
const result = await esdsl().fn(
const result = await esdsl.fn(
{
type: 'kibana_context',
filters: [
@ -68,7 +85,7 @@ describe('esdsl', () => {
});
test('adds filters to query with filters', async () => {
const result = await esdsl().fn(
const result = await esdsl.fn(
{
type: 'kibana_context',
filters: [
@ -90,7 +107,7 @@ describe('esdsl', () => {
});
test('adds query', async () => {
const result = await esdsl().fn(
const result = await esdsl.fn(
{
type: 'kibana_context',
query: { language: 'lucene', query: '*' },
@ -103,7 +120,7 @@ describe('esdsl', () => {
});
test('adds query to a query with filters', async () => {
const result = await esdsl().fn(
const result = await esdsl.fn(
{
type: 'kibana_context',
query: { language: 'lucene', query: '*' },
@ -120,7 +137,7 @@ describe('esdsl', () => {
});
test('ignores timerange', async () => {
const result = await esdsl().fn(
const result = await esdsl.fn(
{
type: 'kibana_context',
timeRange: { from: 'now-15m', to: 'now' },
@ -134,7 +151,7 @@ describe('esdsl', () => {
});
test('correctly handles filter, query and timerange on context', async () => {
const result = await esdsl().fn(
const result = await esdsl.fn(
{
type: 'kibana_context',
query: { language: 'lucene', query: '*' },

View file

@ -6,182 +6,37 @@
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public';
import { getSearchService, getUiSettings } from '../../services';
import { EsRawResponse } from './es_raw_response';
import { RequestStatistics, RequestAdapter } from '../../../../inspector/common';
import { IEsSearchResponse, KibanaContext } from '../../../common/search';
import { buildEsQuery, getEsQueryConfig } from '../../../common/es_query/es_query';
import { DataPublicPluginStart } from '../../types';
const name = 'esdsl';
type Input = KibanaContext | null;
type Output = Promise<EsRawResponse>;
interface Arguments {
dsl: string;
index: string;
size: number;
}
export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof name,
Input,
Arguments,
Output
>;
export const esdsl = (): EsdslExpressionFunctionDefinition => ({
name,
type: 'es_raw_response',
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('data.search.esdsl.help', {
defaultMessage: 'Run Elasticsearch request',
}),
args: {
dsl: {
types: ['string'],
aliases: ['_', 'q', 'query'],
help: i18n.translate('data.search.esdsl.q.help', {
defaultMessage: 'Query DSL',
}),
required: true,
},
index: {
types: ['string'],
help: i18n.translate('data.search.esdsl.index.help', {
defaultMessage: 'ElasticSearch index to query',
}),
required: true,
},
size: {
types: ['number'],
help: i18n.translate('data.search.esdsl.size.help', {
defaultMessage: 'ElasticSearch searchAPI size parameter',
}),
default: 10,
},
},
async fn(input, args, { inspectorAdapters, abortSignal }) {
const searchService: DataPublicPluginStart['search'] = getSearchService();
const dsl = JSON.parse(args.dsl);
if (input) {
const esQueryConfigs = getEsQueryConfig(getUiSettings());
const query = buildEsQuery(
undefined, // args.index,
input.query || [],
input.filters || [],
esQueryConfigs
);
if (!dsl.query) {
dsl.query = query;
} else {
query.bool.must.push(dsl.query);
dsl.query = query;
}
}
if (!inspectorAdapters.requests) {
inspectorAdapters.requests = new RequestAdapter();
}
const request = inspectorAdapters.requests.start(
i18n.translate('data.search.dataRequest.title', {
defaultMessage: 'Data',
}),
{
description: i18n.translate('data.search.es_search.dataRequest.description', {
defaultMessage:
'This request queries Elasticsearch to fetch the data for the visualization.',
}),
}
);
request.stats({
indexPattern: {
label: i18n.translate('data.search.es_search.indexPatternLabel', {
defaultMessage: 'Index pattern',
}),
value: args.index,
description: i18n.translate('data.search.es_search.indexPatternDescription', {
defaultMessage: 'The index pattern that connected to the Elasticsearch indices.',
}),
},
});
let res: IEsSearchResponse;
try {
res = await searchService
.search(
{
params: {
index: args.index,
size: args.size,
body: dsl,
},
},
{ abortSignal }
)
.toPromise();
const stats: RequestStatistics = {};
const resp = res.rawResponse;
if (resp && resp.took) {
stats.queryTime = {
label: i18n.translate('data.search.es_search.queryTimeLabel', {
defaultMessage: 'Query time',
}),
value: i18n.translate('data.search.es_search.queryTimeValue', {
defaultMessage: '{queryTime}ms',
values: { queryTime: resp.took },
}),
description: i18n.translate('data.search.es_search.queryTimeDescription', {
defaultMessage:
'The time it took to process the query. ' +
'Does not include the time to send the request or parse it in the browser.',
}),
};
}
if (resp && resp.hits) {
stats.hitsTotal = {
label: i18n.translate('data.search.es_search.hitsTotalLabel', {
defaultMessage: 'Hits (total)',
}),
value: `${resp.hits.total}`,
description: i18n.translate('data.search.es_search.hitsTotalDescription', {
defaultMessage: 'The number of documents that match the query.',
}),
};
stats.hits = {
label: i18n.translate('data.search.es_search.hitsLabel', {
defaultMessage: 'Hits',
}),
value: `${resp.hits.hits.length}`,
description: i18n.translate('data.search.es_search.hitsDescription', {
defaultMessage: 'The number of documents returned by the query.',
}),
};
}
request.stats(stats).ok({ json: resp });
request.json(dsl);
import { StartServicesAccessor } from 'src/core/public';
import { DataPublicPluginStart, DataStartDependencies } from '../../types';
import { getEsdslFn } from '../../../common/search/expressions/esdsl';
import { UiSettingsCommon } from '../../../common/index_patterns';
/**
* This is some glue code that takes in `core.getStartServices`, extracts the dependencies
* needed for this function, and wraps them behind a `getStartDependencies` function that
* is then called at runtime.
*
* We do this so that we can be explicit about exactly which dependencies the function
* requires, without cluttering up the top-level `plugin.ts` with this logic. It also
* makes testing the expression function a bit easier since `getStartDependencies` is
* the only thing you should need to mock.
*
* @param getStartServices - core's StartServicesAccessor for this plugin
*
* @internal
*/
export function getEsdsl({
getStartServices,
}: {
getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>;
}) {
return getEsdslFn({
getStartDependencies: async () => {
const [core, , { search }] = await getStartServices();
return {
type: 'es_raw_response',
body: resp,
uiSettingsClient: (core.uiSettings as any) as UiSettingsCommon,
search: search.search,
};
} catch (e) {
request.error({ json: e });
throw e;
}
},
});
},
});
}

View file

@ -6,6 +6,6 @@
* Side Public License, v 1.
*/
export * from './es_raw_response';
export * from './esaggs';
export * from './esdsl';
export * from '../../../common/search/expressions';

View file

@ -33,6 +33,7 @@ import {
rangeFilterFunction,
kibanaFilterFunction,
phraseFilterFunction,
esRawResponse,
} from '../../common/search';
import { getCallMsearch } from './legacy';
import { AggsService, AggsStartDependencies } from './aggs';
@ -40,7 +41,7 @@ import { IndexPatternsContract } from '../index_patterns/index_patterns';
import { ISearchInterceptor, SearchInterceptor } from './search_interceptor';
import { SearchUsageCollector, createUsageCollector } from './collectors';
import { UsageCollectionSetup } from '../../../usage_collection/public';
import { esdsl, esRawResponse, getEsaggs } from './expressions';
import { getEsaggs, getEsdsl } from './expressions';
import { ExpressionsSetup } from '../../../expressions/public';
import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session';
import { ConfigSchema } from '../../config';
@ -126,7 +127,11 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
expressions.registerFunction(phraseFilterFunction);
expressions.registerType(kibanaContext);
expressions.registerFunction(esdsl);
expressions.registerFunction(
getEsdsl({ getStartServices } as {
getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>;
})
);
expressions.registerType(esRawResponse);
const aggs = this.aggsService.setup({

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { StartServicesAccessor } from 'src/core/server';
import { DataPluginStart, DataPluginStartDependencies } from '../../plugin';
import { getEsdslFn } from '../../../common/search/expressions/esdsl';
/**
* This is some glue code that takes in `core.getStartServices`, extracts the dependencies
* needed for this function, and wraps them behind a `getStartDependencies` function that
* is then called at runtime.
*
* We do this so that we can be explicit about exactly which dependencies the function
* requires, without cluttering up the top-level `plugin.ts` with this logic. It also
* makes testing the expression function a bit easier since `getStartDependencies` is
* the only thing you should need to mock.
*
* @param getStartServices - core's StartServicesAccessor for this plugin
*
* @internal
*/
export function getEsdsl({
getStartServices,
}: {
getStartServices: StartServicesAccessor<DataPluginStartDependencies, DataPluginStart>;
}) {
return getEsdslFn({
getStartDependencies: async (getKibanaRequest: any) => {
const [core, , { search }] = await getStartServices();
if (!getKibanaRequest || !getKibanaRequest()) {
throw new Error('TODO: add text');
}
const request = getKibanaRequest();
const savedObjectsClient = core.savedObjects.getScopedClient(request);
return {
uiSettingsClient: core.uiSettings.asScopedToClient(savedObjectsClient),
search: search.asScoped(request).search,
};
},
});
}

View file

@ -7,3 +7,4 @@
*/
export * from './esaggs';
export * from './esdsl';

View file

@ -63,8 +63,9 @@ import {
searchSourceRequiredUiSettings,
SearchSourceService,
phraseFilterFunction,
esRawResponse,
} from '../../common/search';
import { getEsaggs } from './expressions';
import { getEsaggs, getEsdsl } from './expressions';
import {
getShardDelayBucketAgg,
SHARD_DELAY_AGG_NAME,
@ -150,6 +151,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
}
expressions.registerFunction(getEsaggs({ getStartServices: core.getStartServices }));
expressions.registerFunction(getEsdsl({ getStartServices: core.getStartServices }));
expressions.registerFunction(kibana);
expressions.registerFunction(luceneFunction);
expressions.registerFunction(kqlFunction);
@ -162,6 +164,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
expressions.registerFunction(rangeFilterFunction);
expressions.registerFunction(phraseFilterFunction);
expressions.registerType(kibanaContext);
expressions.registerType(esRawResponse);
const aggs = this.aggsService.setup({ registerFunction: expressions.registerFunction });