From f28445449ec725852a0063fa23d842b35ddd4f08 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 3 Nov 2023 15:46:55 -0600 Subject: [PATCH] [inspector] show request method, path, and querystring (#169970) Closes https://github.com/elastic/kibana/issues/45931 PR updates data plugin `search` and `bsearch` endpoints to return method, path, and querystring request params from elasticsearch-js client requests. This provides inspector with the exact details used to fetch data from elasticsearch, ensuring inspector displays request exactly as used by elasticsearch-js client. **ESQL** This PR makes it possible to open ESQL searches in console. Screen Shot 2023-09-16 at 4 19 58 PM ### background If you are thinking to yourself, "haven't I reviewed this before?", you are right. This functionality has been through several iterations. 1) Original PR https://github.com/elastic/kibana/pull/166565 was reverted for exposing `headers`. 2) [Fix to only expose method, path, and querystring keys from request parameters](https://github.com/elastic/kibana/pull/167544) was rejected because it applied changes to `kibana_utils/server/report_server_error.ts`, which is used extensively throughout Kibana. 3) This iteration moves logic into the data plugin to be as narrow as possible. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../data/common/search/expressions/esdsl.ts | 4 +- .../data/common/search/expressions/esql.ts | 12 +- .../data/common/search/expressions/essql.ts | 4 +- src/plugins/data/common/search/types.ts | 18 + .../data/public/search/errors/types.ts | 7 +- .../search_interceptor/search_interceptor.ts | 21 ++ .../data/server/search/report_search_error.ts | 17 +- .../data/server/search/routes/bsearch.ts | 1 + .../search/sanitize_request_params.test.ts | 41 +++ .../server/search/sanitize_request_params.ts | 20 ++ .../eql_search/eql_search_strategy.ts | 6 +- .../strategies/eql_search/response_utils.ts | 6 +- .../es_search/es_search_strategy.test.ts | 7 +- .../es_search/es_search_strategy.ts | 5 +- .../strategies/es_search/response_utils.ts | 8 +- .../ese_search/ese_search_strategy.ts | 13 +- .../strategies/ese_search/response_utils.ts | 9 +- .../esql_search/esql_search_strategy.ts | 6 +- .../strategies/sql_search/response_utils.ts | 6 +- .../sql_search/sql_search_strategy.ts | 14 +- .../move_request_params_to_top_level.test.ts | 35 ++ .../move_request_params_to_top_level.ts | 30 ++ .../adapters/request/request_responder.ts | 3 +- .../common/adapters/request/types.ts | 4 + .../components/details/req_code_viewer.tsx | 28 +- .../details/req_details_request.tsx | 1 + test/api_integration/apis/search/bsearch.ts | 321 ++++++++++++++++++ .../apps/visualize/group2/_inspector.ts | 8 +- test/functional/services/inspector.ts | 15 + 29 files changed, 627 insertions(+), 43 deletions(-) create mode 100644 src/plugins/data/server/search/sanitize_request_params.test.ts create mode 100644 src/plugins/data/server/search/sanitize_request_params.ts create mode 100644 src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.test.ts create mode 100644 src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.ts diff --git a/src/plugins/data/common/search/expressions/esdsl.ts b/src/plugins/data/common/search/expressions/esdsl.ts index a18e1e3240050..bf5aacef2113d 100644 --- a/src/plugins/data/common/search/expressions/esdsl.ts +++ b/src/plugins/data/common/search/expressions/esdsl.ts @@ -126,7 +126,7 @@ export const getEsdslFn = ({ }); try { - const { rawResponse } = await lastValueFrom( + const { rawResponse, requestParams } = await lastValueFrom( search( { params: { @@ -180,7 +180,7 @@ export const getEsdslFn = ({ }; } - request.stats(stats).ok({ json: rawResponse }); + request.stats(stats).ok({ json: rawResponse, requestParams }); request.json(dsl); return { diff --git a/src/plugins/data/common/search/expressions/esql.ts b/src/plugins/data/common/search/expressions/esql.ts index cfdbeb39b860e..ec30a92bae996 100644 --- a/src/plugins/data/common/search/expressions/esql.ts +++ b/src/plugins/data/common/search/expressions/esql.ts @@ -220,7 +220,7 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => { return throwError(() => error); }), tap({ - next({ rawResponse }) { + next({ rawResponse, requestParams }) { logInspectorRequest() .stats({ hits: { @@ -234,12 +234,14 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => { }, }) .json(params) - .ok({ json: rawResponse }); + .ok({ json: rawResponse, requestParams }); }, error(error) { - logInspectorRequest().error({ - json: 'attributes' in error ? error.attributes : { message: error.message }, - }); + logInspectorRequest() + .json(params) + .error({ + json: 'attributes' in error ? error.attributes : { message: error.message }, + }); }, }) ); diff --git a/src/plugins/data/common/search/expressions/essql.ts b/src/plugins/data/common/search/expressions/essql.ts index d943b406ff7f5..5012f01a749ac 100644 --- a/src/plugins/data/common/search/expressions/essql.ts +++ b/src/plugins/data/common/search/expressions/essql.ts @@ -217,7 +217,7 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => { return throwError(() => error); }), tap({ - next({ rawResponse, took }) { + next({ rawResponse, requestParams, took }) { logInspectorRequest() .stats({ hits: { @@ -245,7 +245,7 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => { }, }) .json(params) - .ok({ json: rawResponse }); + .ok({ json: rawResponse, requestParams }); }, error(error) { logInspectorRequest().error({ diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index cedfa3ee02274..7187ac7b9748d 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { estypes } from '@elastic/elasticsearch'; +import type { ConnectionRequestParams } from '@elastic/transport'; import type { TransportRequestOptions } from '@elastic/elasticsearch'; import type { KibanaExecutionContext } from '@kbn/core/public'; import type { DataView } from '@kbn/data-views-plugin/common'; @@ -39,6 +41,11 @@ export interface ISearchClient { extend: ISearchExtendGeneric; } +export type SanitizedConnectionRequestParams = Pick< + ConnectionRequestParams, + 'method' | 'path' | 'querystring' +>; + export interface IKibanaSearchResponse { /** * Some responses may contain a unique id to identify the request this response came from. @@ -86,6 +93,17 @@ export interface IKibanaSearchResponse { * The raw response returned by the internal search method (usually the raw ES response) */ rawResponse: RawResponse; + + /** + * HTTP request parameters from elasticsearch transport client t + */ + requestParams?: SanitizedConnectionRequestParams; +} + +export interface IEsErrorAttributes { + error?: estypes.ErrorCause; + rawResponse?: estypes.SearchResponseBody; + requestParams?: SanitizedConnectionRequestParams; } export interface IKibanaSearchRequest { diff --git a/src/plugins/data/public/search/errors/types.ts b/src/plugins/data/public/search/errors/types.ts index de03350a6d41c..14477327e83bb 100644 --- a/src/plugins/data/public/search/errors/types.ts +++ b/src/plugins/data/public/search/errors/types.ts @@ -6,13 +6,8 @@ * Side Public License, v 1. */ -import { estypes } from '@elastic/elasticsearch'; import { KibanaServerError } from '@kbn/kibana-utils-plugin/common'; - -interface IEsErrorAttributes { - rawResponse?: estypes.SearchResponseBody; - error?: estypes.ErrorCause; -} +import { IEsErrorAttributes } from '../../../common'; export type IEsError = KibanaServerError; diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index 24b2e1c41216a..5aebc455ad5b6 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -56,6 +56,7 @@ import { ISearchOptionsSerializable, pollSearch, UI_SETTINGS, + type SanitizedConnectionRequestParams, } from '../../../common'; import { SearchUsageCollector } from '../collectors'; import { @@ -304,18 +305,38 @@ export class SearchInterceptor { const cancel = () => id && !isSavedToBackground && sendCancelRequest(); + // Async search requires a series of requests + // 1) POST //_async_search/ + // 2..n) GET /_async_search/ + // + // First request contains useful request params for tools like Inspector. + // Preserve and project first request params into responses. + let firstRequestParams: SanitizedConnectionRequestParams; + return pollSearch(search, cancel, { pollInterval: this.deps.searchConfig.asyncSearch.pollInterval, ...options, abortSignal: searchAbortController.getSignal(), }).pipe( tap((response) => { + if (!firstRequestParams && response.requestParams) { + firstRequestParams = response.requestParams; + } + id = response.id; if (!isRunningResponse(response)) { searchTracker?.complete(); } }), + map((response) => { + return firstRequestParams + ? { + ...response, + requestParams: firstRequestParams, + } + : response; + }), catchError((e: Error) => { searchTracker?.error(); cancel(); diff --git a/src/plugins/data/server/search/report_search_error.ts b/src/plugins/data/server/search/report_search_error.ts index dc6bf2399abf6..4f95ce8fd1ec2 100644 --- a/src/plugins/data/server/search/report_search_error.ts +++ b/src/plugins/data/server/search/report_search_error.ts @@ -6,9 +6,12 @@ * Side Public License, v 1. */ +import type { ConnectionRequestParams } from '@elastic/transport'; import { errors } from '@elastic/elasticsearch'; import { KibanaResponseFactory } from '@kbn/core/server'; import { KbnError } from '@kbn/kibana-utils-plugin/common'; +import type { SanitizedConnectionRequestParams } from '../../common'; +import { sanitizeRequestParams } from './sanitize_request_params'; // Why not use just use kibana-utils-plugin KbnServerError and reportServerError? // @@ -19,9 +22,17 @@ import { KbnError } from '@kbn/kibana-utils-plugin/common'; // non-search usages of KbnServerError and reportServerError with extra information. export class KbnSearchError extends KbnError { public errBody?: Record; - constructor(message: string, public readonly statusCode: number, errBody?: Record) { + public requestParams?: SanitizedConnectionRequestParams; + + constructor( + message: string, + public readonly statusCode: number, + errBody?: Record, + requestParams?: ConnectionRequestParams + ) { super(message); this.errBody = errBody; + this.requestParams = requestParams ? sanitizeRequestParams(requestParams) : undefined; } } @@ -35,7 +46,8 @@ export function getKbnSearchError(e: Error) { return new KbnSearchError( e.message ?? 'Unknown error', e instanceof errors.ResponseError ? e.statusCode! : 500, - e instanceof errors.ResponseError ? e.body : undefined + e instanceof errors.ResponseError ? e.body : undefined, + e instanceof errors.ResponseError ? e.meta?.meta?.request?.params : undefined ); } @@ -53,6 +65,7 @@ export function reportSearchError(res: KibanaResponseFactory, err: KbnSearchErro ? { error: err.errBody.error, rawResponse: err.errBody.response, + ...(err.requestParams ? { requestParams: err.requestParams } : {}), } : undefined, }, diff --git a/src/plugins/data/server/search/routes/bsearch.ts b/src/plugins/data/server/search/routes/bsearch.ts index cbbe5dedb03cf..8b65c5d8eb1dc 100644 --- a/src/plugins/data/server/search/routes/bsearch.ts +++ b/src/plugins/data/server/search/routes/bsearch.ts @@ -50,6 +50,7 @@ export function registerBsearchRoute( ? { error: err.errBody.error, rawResponse: err.errBody.response, + ...(err.requestParams ? { requestParams: err.requestParams } : {}), } : undefined, }; diff --git a/src/plugins/data/server/search/sanitize_request_params.test.ts b/src/plugins/data/server/search/sanitize_request_params.test.ts new file mode 100644 index 0000000000000..de3796b20455d --- /dev/null +++ b/src/plugins/data/server/search/sanitize_request_params.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { sanitizeRequestParams } from './sanitize_request_params'; + +describe('sanitizeRequestParams', () => { + test('should remove headers and body', () => { + expect( + sanitizeRequestParams({ + method: 'POST', + path: '/endpoint', + querystring: 'param1=value', + headers: { + Connection: 'Keep-Alive', + }, + body: 'response', + }) + ).toEqual({ + method: 'POST', + path: '/endpoint', + querystring: 'param1=value', + }); + }); + + test('should not include querystring key when its not provided', () => { + expect( + sanitizeRequestParams({ + method: 'POST', + path: '/endpoint', + }) + ).toEqual({ + method: 'POST', + path: '/endpoint', + }); + }); +}); diff --git a/src/plugins/data/server/search/sanitize_request_params.ts b/src/plugins/data/server/search/sanitize_request_params.ts new file mode 100644 index 0000000000000..34e4e721cc872 --- /dev/null +++ b/src/plugins/data/server/search/sanitize_request_params.ts @@ -0,0 +1,20 @@ +/* + * 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 type { ConnectionRequestParams } from '@elastic/transport'; +import type { SanitizedConnectionRequestParams } from '../../common'; + +export function sanitizeRequestParams( + requestParams: ConnectionRequestParams +): SanitizedConnectionRequestParams { + return { + method: requestParams.method, + path: requestParams.path, + ...(requestParams.querystring ? { querystring: requestParams.querystring } : {}), + }; +} diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts index d6f5d948c784a..9dd24e6791719 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts @@ -77,7 +77,11 @@ export const eqlSearchStrategyProvider = ( meta: true, }); - return toEqlKibanaSearchResponse(response as TransportResult); + return toEqlKibanaSearchResponse( + response as TransportResult, + // do not return requestParams on polling calls + id ? undefined : (response as TransportResult).meta?.request?.params + ); }; const cancel = async () => { diff --git a/src/plugins/data/server/search/strategies/eql_search/response_utils.ts b/src/plugins/data/server/search/strategies/eql_search/response_utils.ts index 3d913455fb440..b00d5cf53097b 100644 --- a/src/plugins/data/server/search/strategies/eql_search/response_utils.ts +++ b/src/plugins/data/server/search/strategies/eql_search/response_utils.ts @@ -6,21 +6,25 @@ * Side Public License, v 1. */ +import type { ConnectionRequestParams } from '@elastic/transport'; import type { TransportResult } from '@elastic/elasticsearch'; import { EqlSearchResponse } from './types'; import { EqlSearchStrategyResponse } from '../../../../common'; +import { sanitizeRequestParams } from '../../sanitize_request_params'; /** * Get the Kibana representation of an EQL search response (see `IKibanaSearchResponse`). * (EQL does not provide _shard info, so total/loaded cannot be calculated.) */ export function toEqlKibanaSearchResponse( - response: TransportResult + response: TransportResult, + requestParams?: ConnectionRequestParams ): EqlSearchStrategyResponse { return { id: response.body.id, rawResponse: response.body, isPartial: response.body.is_partial, isRunning: response.body.is_running, + ...(requestParams ? { requestParams: sanitizeRequestParams(requestParams) } : {}), }; } diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts index 9db6b00200bcc..215d89c607889 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts @@ -113,7 +113,7 @@ describe('ES search strategy', () => { ) ); const [, searchOptions] = esClient.search.mock.calls[0]; - expect(searchOptions).toEqual({ signal: undefined, maxRetries: 5 }); + expect(searchOptions).toEqual({ signal: undefined, maxRetries: 5, meta: true }); }); it('can be aborted', async () => { @@ -131,7 +131,10 @@ describe('ES search strategy', () => { ...params, track_total_hits: true, }); - expect(esClient.search.mock.calls[0][1]).toEqual({ signal: expect.any(AbortSignal) }); + expect(esClient.search.mock.calls[0][1]).toEqual({ + signal: expect.any(AbortSignal), + meta: true, + }); }); it('throws normalized error if ResponseError is thrown', async () => { diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts index 64b2234a573c8..319af08750689 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts @@ -50,12 +50,13 @@ export const esSearchStrategyProvider = ( ...(terminateAfter ? { terminate_after: terminateAfter } : {}), ...requestParams, }; - const body = await esClient.asCurrentUser.search(params, { + const { body, meta } = await esClient.asCurrentUser.search(params, { signal: abortSignal, ...transport, + meta: true, }); const response = shimHitsTotal(body, options); - return toKibanaSearchResponse(response); + return toKibanaSearchResponse(response, meta?.request?.params); } catch (e) { throw getKbnSearchError(e); } diff --git a/src/plugins/data/server/search/strategies/es_search/response_utils.ts b/src/plugins/data/server/search/strategies/es_search/response_utils.ts index 4773b6df3bbaf..9bbf9544791cf 100644 --- a/src/plugins/data/server/search/strategies/es_search/response_utils.ts +++ b/src/plugins/data/server/search/strategies/es_search/response_utils.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ +import type { ConnectionRequestParams } from '@elastic/transport'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ISearchOptions } from '../../../../common'; +import { sanitizeRequestParams } from '../../sanitize_request_params'; /** * Get the `total`/`loaded` for this response (see `IKibanaSearchResponse`). Note that `skipped` is @@ -24,11 +26,15 @@ export function getTotalLoaded(response: estypes.SearchResponse) { * Get the Kibana representation of this response (see `IKibanaSearchResponse`). * @internal */ -export function toKibanaSearchResponse(rawResponse: estypes.SearchResponse) { +export function toKibanaSearchResponse( + rawResponse: estypes.SearchResponse, + requestParams?: ConnectionRequestParams +) { return { rawResponse, isPartial: false, isRunning: false, + ...(requestParams ? { requestParams: sanitizeRequestParams(requestParams) } : {}), ...getTotalLoaded(rawResponse), }; } diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index c4b8933f2c7c3..89699d7d58611 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -35,6 +35,7 @@ import { shimHitsTotal, } from '../es_search'; import { SearchConfigSchema } from '../../../../config'; +import { sanitizeRequestParams } from '../../sanitize_request_params'; export const enhancedEsSearchStrategyProvider = ( legacyConfig$: Observable, @@ -66,7 +67,7 @@ export const enhancedEsSearchStrategyProvider = ( ...(await getDefaultAsyncSubmitParams(uiSettingsClient, searchConfig, options)), ...request.params, }; - const { body, headers } = id + const { body, headers, meta } = id ? await client.asyncSearch.get( { ...params, id }, { ...options.transport, signal: options.abortSignal, meta: true } @@ -79,7 +80,12 @@ export const enhancedEsSearchStrategyProvider = ( const response = shimHitsTotal(body.response, options); - return toAsyncKibanaSearchResponse({ ...body, response }, headers?.warning); + return toAsyncKibanaSearchResponse( + { ...body, response }, + headers?.warning, + // do not return requestParams on polling calls + id ? undefined : meta?.request?.params + ); }; const cancel = async () => { @@ -134,6 +140,9 @@ export const enhancedEsSearchStrategyProvider = ( const response = esResponse.body as estypes.SearchResponse; return { rawResponse: shimHitsTotal(response, options), + ...(esResponse.meta?.request?.params + ? { requestParams: sanitizeRequestParams(esResponse.meta?.request?.params) } + : {}), ...getTotalLoaded(response), }; } catch (e) { diff --git a/src/plugins/data/server/search/strategies/ese_search/response_utils.ts b/src/plugins/data/server/search/strategies/ese_search/response_utils.ts index c9390a1b381d5..c9cb4acf3f3a9 100644 --- a/src/plugins/data/server/search/strategies/ese_search/response_utils.ts +++ b/src/plugins/data/server/search/strategies/ese_search/response_utils.ts @@ -6,19 +6,26 @@ * Side Public License, v 1. */ +import type { ConnectionRequestParams } from '@elastic/transport'; import type { AsyncSearchResponse } from './types'; import { getTotalLoaded } from '../es_search'; +import { sanitizeRequestParams } from '../../sanitize_request_params'; /** * Get the Kibana representation of an async search response (see `IKibanaSearchResponse`). */ -export function toAsyncKibanaSearchResponse(response: AsyncSearchResponse, warning?: string) { +export function toAsyncKibanaSearchResponse( + response: AsyncSearchResponse, + warning?: string, + requestParams?: ConnectionRequestParams +) { return { id: response.id, rawResponse: response.response, isPartial: response.is_partial, isRunning: response.is_running, ...(warning ? { warning } : {}), + ...(requestParams ? { requestParams: sanitizeRequestParams(requestParams) } : {}), ...getTotalLoaded(response.response), }; } diff --git a/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts b/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts index 460755a74df8f..dbe5624675e83 100644 --- a/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts @@ -10,6 +10,7 @@ import { from } from 'rxjs'; import type { Logger } from '@kbn/core/server'; import { getKbnSearchError, KbnSearchError } from '../../report_search_error'; import type { ISearchStrategy } from '../../types'; +import { sanitizeRequestParams } from '../../sanitize_request_params'; const ES_TIMEOUT_IN_MS = 120000; @@ -45,7 +46,7 @@ export const esqlSearchStrategyProvider = ( const search = async () => { try { const { terminateAfter, ...requestParams } = request.params ?? {}; - const { headers, body } = await esClient.asCurrentUser.transport.request( + const { headers, body, meta } = await esClient.asCurrentUser.transport.request( { method: 'POST', path: '/_query', @@ -64,6 +65,9 @@ export const esqlSearchStrategyProvider = ( rawResponse: body, isPartial: false, isRunning: false, + ...(meta?.request?.params + ? { requestParams: sanitizeRequestParams(meta?.request?.params) } + : {}), warning: headers?.warning, }; } catch (e) { diff --git a/src/plugins/data/server/search/strategies/sql_search/response_utils.ts b/src/plugins/data/server/search/strategies/sql_search/response_utils.ts index b859df9db4237..85f63d8d75724 100644 --- a/src/plugins/data/server/search/strategies/sql_search/response_utils.ts +++ b/src/plugins/data/server/search/strategies/sql_search/response_utils.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ +import type { ConnectionRequestParams } from '@elastic/transport'; import { SqlQueryResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { SqlSearchStrategyResponse } from '../../../../common'; +import { sanitizeRequestParams } from '../../sanitize_request_params'; /** * Get the Kibana representation of an async search response @@ -15,7 +17,8 @@ import { SqlSearchStrategyResponse } from '../../../../common'; export function toAsyncKibanaSearchResponse( response: SqlQueryResponse, startTime: number, - warning?: string + warning?: string, + requestParams?: ConnectionRequestParams ): SqlSearchStrategyResponse { return { id: response.id, @@ -24,5 +27,6 @@ export function toAsyncKibanaSearchResponse( isRunning: response.is_running, took: Date.now() - startTime, ...(warning ? { warning } : {}), + ...(requestParams ? { requestParams: sanitizeRequestParams(requestParams) } : {}), }; } diff --git a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts index 34134a1491cd0..87b29f5438efb 100644 --- a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts @@ -9,6 +9,7 @@ import type { IncomingHttpHeaders } from 'http'; import type { IScopedClusterClient, Logger } from '@kbn/core/server'; import { catchError, tap } from 'rxjs/operators'; +import type { DiagnosticResult } from '@elastic/transport'; import { SqlQueryResponse } from '@elastic/elasticsearch/lib/api/types'; import { getKbnServerError } from '@kbn/kibana-utils-plugin/server'; import { getKbnSearchError } from '../../report_search_error'; @@ -49,9 +50,10 @@ export const sqlSearchStrategyProvider = ( const { keep_cursor: keepCursor, ...params } = request.params ?? {}; let body: SqlQueryResponse; let headers: IncomingHttpHeaders; + let meta: DiagnosticResult['meta']; if (id) { - ({ body, headers } = await client.sql.getAsync( + ({ body, headers, meta } = await client.sql.getAsync( { format: params?.format ?? 'json', ...getDefaultAsyncGetParams(searchConfig, options), @@ -60,7 +62,7 @@ export const sqlSearchStrategyProvider = ( { ...options.transport, signal: options.abortSignal, meta: true } )); } else { - ({ headers, body } = await client.sql.query( + ({ headers, body, meta } = await client.sql.query( { format: params.format ?? 'json', ...getDefaultAsyncSubmitParams(searchConfig, options), @@ -80,7 +82,13 @@ export const sqlSearchStrategyProvider = ( } } - return toAsyncKibanaSearchResponse(body, startTime, headers?.warning); + return toAsyncKibanaSearchResponse( + body, + startTime, + headers?.warning, + // do not return requestParams on polling calls + id ? undefined : meta?.request?.params + ); }; const cancel = async () => { diff --git a/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.test.ts b/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.test.ts new file mode 100644 index 0000000000000..e08b489dc414d --- /dev/null +++ b/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { moveRequestParamsToTopLevel } from './move_request_params_to_top_level'; + +describe('moveRequestParamsToTopLevel', () => { + test('should move request meta to top level', () => { + expect( + moveRequestParamsToTopLevel({ + json: { + rawResponse: {}, + requestParams: { + method: 'POST', + path: '/_query', + }, + }, + time: 1, + }) + ).toEqual({ + json: { + rawResponse: {}, + }, + requestParams: { + method: 'POST', + path: '/_query', + }, + time: 1, + }); + }); +}); diff --git a/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.ts b/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.ts new file mode 100644 index 0000000000000..36589172d518f --- /dev/null +++ b/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.ts @@ -0,0 +1,30 @@ +/* + * 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 type { ConnectionRequestParams } from '@elastic/transport'; +import { Response } from './types'; + +interface SearchResponse { + [key: string]: unknown; + requestParams?: ConnectionRequestParams; +} + +export function moveRequestParamsToTopLevel(response: Response) { + const requestParams = (response.json as SearchResponse)?.requestParams; + if (!requestParams) { + return response; + } + + const json = { ...response.json } as SearchResponse; + delete json.requestParams; + return { + ...response, + json, + requestParams, + }; +} diff --git a/src/plugins/inspector/common/adapters/request/request_responder.ts b/src/plugins/inspector/common/adapters/request/request_responder.ts index 1d3a999e4834d..8766a6f5deeff 100644 --- a/src/plugins/inspector/common/adapters/request/request_responder.ts +++ b/src/plugins/inspector/common/adapters/request/request_responder.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Request, RequestStatistics, RequestStatus, Response } from './types'; +import { moveRequestParamsToTopLevel } from './move_request_params_to_top_level'; /** * An API to specify information about a specific request that will be logged. @@ -53,7 +54,7 @@ export class RequestResponder { public finish(status: RequestStatus, response: Response): void { this.request.time = response.time ?? Date.now() - this.request.startTime; this.request.status = status; - this.request.response = response; + this.request.response = moveRequestParamsToTopLevel(response); this.onChange(); } diff --git a/src/plugins/inspector/common/adapters/request/types.ts b/src/plugins/inspector/common/adapters/request/types.ts index 4e6a8d324559f..d00e1304f74f5 100644 --- a/src/plugins/inspector/common/adapters/request/types.ts +++ b/src/plugins/inspector/common/adapters/request/types.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import type { ConnectionRequestParams } from '@elastic/transport'; + /** * The status a request can have. */ @@ -52,6 +54,8 @@ export interface RequestStatistic { } export interface Response { + // TODO replace object with IKibanaSearchResponse once IKibanaSearchResponse is seperated from data plugin. json?: object; + requestParams?: ConnectionRequestParams; time?: number; } diff --git a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx index 5ab50ba33a514..58f5dd44f3f11 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx @@ -12,6 +12,7 @@ /* eslint-disable @elastic/eui/href-or-on-click */ import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import type { ConnectionRequestParams } from '@elastic/transport'; import { i18n } from '@kbn/i18n'; import { XJsonLang } from '@kbn/monaco'; import { compressToEncodedURIComponent } from 'lz-string'; @@ -21,6 +22,7 @@ import { InspectorPluginStartDeps } from '../../../../plugin'; interface RequestCodeViewerProps { indexPattern?: string; + requestParams?: ConnectionRequestParams; json: string; } @@ -39,19 +41,37 @@ const openInSearchProfilerLabel = i18n.translate('inspector.requests.openInSearc /** * @internal */ -export const RequestCodeViewer = ({ indexPattern, json }: RequestCodeViewerProps) => { +export const RequestCodeViewer = ({ + indexPattern, + requestParams, + json, +}: RequestCodeViewerProps) => { const { services } = useKibana(); const navigateToUrl = services.application?.navigateToUrl; - const devToolsDataUri = compressToEncodedURIComponent(`GET ${indexPattern}/_search\n${json}`); + function getValue() { + if (!requestParams) { + return json; + } + + const fullPath = requestParams.querystring + ? `${requestParams.path}?${requestParams.querystring}` + : requestParams.path; + + return `${requestParams.method} ${fullPath}\n${json}`; + } + + const value = getValue(); + + const devToolsDataUri = compressToEncodedURIComponent(value); const consoleHref = services.share.url.locators .get('CONSOLE_APP_LOCATOR') ?.useUrl({ loadFrom: `data:text/plain,${devToolsDataUri}` }); // Check if both the Dev Tools UI and the Console UI are enabled. const canShowDevTools = services.application?.capabilities?.dev_tools.show && consoleHref !== undefined; - const shouldShowDevToolsLink = !!(indexPattern && canShowDevTools); + const shouldShowDevToolsLink = !!(requestParams && canShowDevTools); const handleDevToolsLinkClick = useCallback( () => consoleHref && navigateToUrl && navigateToUrl(consoleHref), [consoleHref, navigateToUrl] @@ -135,7 +155,7 @@ export const RequestCodeViewer = ({ indexPattern, json }: RequestCodeViewerProps { return ( ); diff --git a/test/api_integration/apis/search/bsearch.ts b/test/api_integration/apis/search/bsearch.ts index 9ce10dc38a643..96b4bbbf622cf 100644 --- a/test/api_integration/apis/search/bsearch.ts +++ b/test/api_integration/apis/search/bsearch.ts @@ -232,6 +232,327 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); + + describe('request meta', () => { + describe('es', () => { + it(`should return request meta`, async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, + }, + }, + }, + options: { + strategy: 'es', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].result.requestParams).to.eql({ + method: 'POST', + path: '/.kibana/_search', + querystring: 'ignore_unavailable=true', + }); + }); + + it(`should return request meta when request fails`, async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + bool: { + filter: [ + { + error_query: { + indices: [ + { + error_type: 'exception', + message: 'simulated failure', + name: '.kibana', + }, + ], + }, + }, + ], + }, + }, + }, + }, + }, + options: { + strategy: 'es', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].error.attributes.requestParams).to.eql({ + method: 'POST', + path: '/.kibana/_search', + querystring: 'ignore_unavailable=true', + }); + }); + }); + + describe('ese', () => { + it(`should return request meta`, async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, + }, + }, + }, + options: { + strategy: 'ese', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].result.requestParams).to.eql({ + method: 'POST', + path: '/.kibana/_async_search', + querystring: + 'batched_reduce_size=64&ccs_minimize_roundtrips=true&wait_for_completion_timeout=200ms&keep_on_completion=false&keep_alive=60000ms&ignore_unavailable=true', + }); + }); + + it(`should return request meta when request fails`, async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + bool: { + filter: [ + { + error_query: { + indices: [ + { + error_type: 'exception', + message: 'simulated failure', + name: '.kibana', + }, + ], + }, + }, + ], + }, + }, + }, + }, + options: { + strategy: 'ese', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].error.attributes.requestParams).to.eql({ + method: 'POST', + path: '/.kibana/_async_search', + querystring: + 'batched_reduce_size=64&ccs_minimize_roundtrips=true&wait_for_completion_timeout=200ms&keep_on_completion=false&keep_alive=60000ms&ignore_unavailable=true', + }); + }); + }); + + describe('esql', () => { + it(`should return request meta`, async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + .send({ + batch: [ + { + request: { + params: { + query: 'from .kibana | limit 1', + }, + }, + options: { + strategy: 'esql', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].result.requestParams).to.eql({ + method: 'POST', + path: '/_query', + }); + }); + + it(`should return request meta when request fails`, async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + .send({ + batch: [ + { + request: { + params: { + query: 'fro .kibana | limit 1', + }, + }, + options: { + strategy: 'esql', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].error.attributes.requestParams).to.eql({ + method: 'POST', + path: '/_query', + }); + }); + }); + + describe('sql', () => { + it(`should return request meta`, async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + .send({ + batch: [ + { + request: { + params: { + query: 'SELECT * FROM ".kibana" LIMIT 1', + }, + }, + options: { + strategy: 'sql', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].result.requestParams).to.eql({ + method: 'POST', + path: '/_sql', + querystring: 'format=json', + }); + }); + + it(`should return request meta when request fails`, async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + .send({ + batch: [ + { + request: { + params: { + query: 'SELEC * FROM ".kibana" LIMIT 1', + }, + }, + options: { + strategy: 'sql', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].error.attributes.requestParams).to.eql({ + method: 'POST', + path: '/_sql', + querystring: 'format=json', + }); + }); + }); + + describe('eql', () => { + it(`should return request meta`, async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + query: 'any where true', + timestamp_field: 'created_at', + }, + }, + options: { + strategy: 'eql', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].result.requestParams).to.eql({ + method: 'POST', + path: '/.kibana/_eql/search', + querystring: 'ignore_unavailable=true', + }); + }); + }); + }); }); }); } diff --git a/test/functional/apps/visualize/group2/_inspector.ts b/test/functional/apps/visualize/group2/_inspector.ts index 80cfc42ab3cd6..077a37a90c06c 100644 --- a/test/functional/apps/visualize/group2/_inspector.ts +++ b/test/functional/apps/visualize/group2/_inspector.ts @@ -14,7 +14,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); - const monacoEditor = getService('monacoEditor'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('inspector', function describeIndexTests() { @@ -41,11 +40,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await inspector.open(); await inspector.openInspectorRequestsView(); - const requestTab = await inspector.getOpenRequestDetailRequestButton(); - await requestTab.click(); - const requestJSON = JSON.parse(await monacoEditor.getCodeEditorValue(1)); - - expect(requestJSON.aggs['2'].max).property('missing', 10); + const { body } = await inspector.getRequest(1); + expect(body.aggs['2'].max).property('missing', 10); }); after(async () => { diff --git a/test/functional/services/inspector.ts b/test/functional/services/inspector.ts index 6222405aa6dae..7313187047a18 100644 --- a/test/functional/services/inspector.ts +++ b/test/functional/services/inspector.ts @@ -299,6 +299,21 @@ export class InspectorService extends FtrService { return this.testSubjects.find('inspectorRequestDetailResponse'); } + public async getRequest( + codeEditorIndex: number = 0 + ): Promise<{ command: string; body: Record }> { + await (await this.getOpenRequestDetailRequestButton()).click(); + + await this.monacoEditor.waitCodeEditorReady('inspectorRequestCodeViewerContainer'); + const requestString = await this.monacoEditor.getCodeEditorValue(codeEditorIndex); + this.log.debug('Request string from inspector:', requestString); + const openBraceIndex = requestString.indexOf('{'); + return { + command: openBraceIndex >= 0 ? requestString.substring(0, openBraceIndex).trim() : '', + body: openBraceIndex >= 0 ? JSON.parse(requestString.substring(openBraceIndex)) : {}, + }; + } + public async getResponse(): Promise> { await (await this.getOpenRequestDetailResponseButton()).click();