[inspector] show request method, path, and querystring (#166565)

Closes https://github.com/elastic/kibana/issues/45931

PR updates bsearch service to return 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.
<img width="500" alt="Screen Shot 2023-09-16 at 4 19 58 PM"
src="56019fb5-ca88-46cf-a42f-86f5f51edfcc">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2023-09-21 06:52:03 -06:00 committed by GitHub
parent 969bbb0a11
commit d61a5a0516
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 627 additions and 44 deletions

View file

@ -126,7 +126,7 @@ export const getEsdslFn = ({
});
try {
const { rawResponse } = await lastValueFrom(
const finalResponse = await lastValueFrom(
search(
{
params: {
@ -141,14 +141,14 @@ export const getEsdslFn = ({
const stats: RequestStatistics = {};
if (rawResponse?.took) {
if (finalResponse.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 },
values: { queryTime: finalResponse.rawResponse.took },
}),
description: i18n.translate('data.search.es_search.queryTimeDescription', {
defaultMessage:
@ -158,12 +158,12 @@ export const getEsdslFn = ({
};
}
if (rawResponse?.hits) {
if (finalResponse.rawResponse?.hits) {
stats.hitsTotal = {
label: i18n.translate('data.search.es_search.hitsTotalLabel', {
defaultMessage: 'Hits (total)',
}),
value: `${rawResponse.hits.total}`,
value: `${finalResponse.rawResponse.hits.total}`,
description: i18n.translate('data.search.es_search.hitsTotalDescription', {
defaultMessage: 'The number of documents that match the query.',
}),
@ -173,19 +173,19 @@ export const getEsdslFn = ({
label: i18n.translate('data.search.es_search.hitsLabel', {
defaultMessage: 'Hits',
}),
value: `${rawResponse.hits.hits.length}`,
value: `${finalResponse.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.stats(stats).ok({ json: finalResponse });
request.json(dsl);
return {
type: 'es_raw_response',
body: rawResponse,
body: finalResponse.rawResponse,
};
} catch (e) {
request.error({ json: e });

View file

@ -210,24 +210,24 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => {
return throwError(() => error);
}),
tap({
next({ rawResponse }) {
next(finalResponse) {
logInspectorRequest()
.stats({
hits: {
label: i18n.translate('data.search.es_search.hitsLabel', {
defaultMessage: 'Hits',
}),
value: `${rawResponse.values.length}`,
value: `${finalResponse.rawResponse.values.length}`,
description: i18n.translate('data.search.es_search.hitsDescription', {
defaultMessage: 'The number of documents returned by the query.',
}),
},
})
.json(params)
.ok({ json: rawResponse });
.ok({ json: finalResponse });
},
error(error) {
logInspectorRequest().error({ json: error });
logInspectorRequest().json(params).error({ json: error });
},
})
);

View file

@ -217,14 +217,14 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => {
return throwError(() => error);
}),
tap({
next({ rawResponse, took }) {
next(finalResponse) {
logInspectorRequest()
.stats({
hits: {
label: i18n.translate('data.search.es_search.hitsLabel', {
defaultMessage: 'Hits',
}),
value: `${rawResponse.rows.length}`,
value: `${finalResponse.rawResponse.rows.length}`,
description: i18n.translate('data.search.es_search.hitsDescription', {
defaultMessage: 'The number of documents returned by the query.',
}),
@ -235,7 +235,7 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => {
}),
value: i18n.translate('data.search.es_search.queryTimeValue', {
defaultMessage: '{queryTime}ms',
values: { queryTime: took },
values: { queryTime: finalResponse.took },
}),
description: i18n.translate('data.search.es_search.queryTimeDescription', {
defaultMessage:
@ -245,10 +245,10 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => {
},
})
.json(params)
.ok({ json: rawResponse });
.ok({ json: finalResponse });
},
error(error) {
logInspectorRequest().error({ json: error });
logInspectorRequest().json(params).error({ json: error });
},
})
);

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
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';
@ -86,6 +87,11 @@ export interface IKibanaSearchResponse<RawResponse = any> {
* 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?: ConnectionRequestParams;
}
export interface IKibanaSearchRequest<Params = any> {

View file

@ -29,6 +29,7 @@ import {
takeUntil,
tap,
} from 'rxjs/operators';
import type { ConnectionRequestParams } from '@elastic/transport';
import { PublicMethodsOf } from '@kbn/utility-types';
import type { HttpSetup, IHttpFetchError } from '@kbn/core-http-browser';
import { BfetchRequestError } from '@kbn/bfetch-plugin/public';
@ -304,18 +305,38 @@ export class SearchInterceptor {
const cancel = () => id && !isSavedToBackground && sendCancelRequest();
// Async search requires a series of requests
// 1) POST /<index pattern>/_async_search/
// 2..n) GET /_async_search/<async search identifier>
//
// First request contains useful request params for tools like Inspector.
// Preserve and project first request params into responses.
let firstRequestParams: ConnectionRequestParams;
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 (isCompleteResponse(response)) {
searchTracker?.complete();
}
}),
map((response) => {
return firstRequestParams
? {
...response,
requestParams: firstRequestParams,
}
: response;
}),
catchError((e: Error) => {
searchTracker?.error();
cancel();

View file

@ -8,6 +8,7 @@
import { firstValueFrom } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { errors } from '@elastic/elasticsearch';
import { BfetchServerSetup } from '@kbn/bfetch-plugin/server';
import type { ExecutionContextSetup } from '@kbn/core/server';
import apm from 'elastic-apm-node';
@ -47,6 +48,12 @@ export function registerBsearchRoute(
message: err.message,
statusCode: err.statusCode,
attributes: err.errBody?.error,
// TODO remove 'instanceof errors.ResponseError' check when
// eql strategy throws KbnServerError (like all of the other strategies)
requestParams:
err instanceof errors.ResponseError
? err.meta?.meta?.request?.params
: err.requestParams,
};
})
)

View file

@ -77,7 +77,10 @@ export const eqlSearchStrategyProvider = (
meta: true,
});
return toEqlKibanaSearchResponse(response as TransportResult<EqlSearchResponse>);
return toEqlKibanaSearchResponse(
response as TransportResult<EqlSearchResponse>,
(response as TransportResult<EqlSearchResponse>).meta?.request?.params
);
};
const cancel = async () => {

View file

@ -6,6 +6,7 @@
* 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';
@ -15,12 +16,14 @@ import { EqlSearchStrategyResponse } from '../../../../common';
* (EQL does not provide _shard info, so total/loaded cannot be calculated.)
*/
export function toEqlKibanaSearchResponse(
response: TransportResult<EqlSearchResponse>
response: TransportResult<EqlSearchResponse>,
requestParams?: ConnectionRequestParams
): EqlSearchStrategyResponse {
return {
id: response.body.id,
rawResponse: response,
isPartial: response.body.is_partial,
isRunning: response.body.is_running,
...(requestParams ? { requestParams } : {}),
};
}

View file

@ -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 () => {

View file

@ -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 getKbnServerError(e);
}

View file

@ -6,6 +6,7 @@
* 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';
@ -24,11 +25,15 @@ export function getTotalLoaded(response: estypes.SearchResponse<unknown>) {
* Get the Kibana representation of this response (see `IKibanaSearchResponse`).
* @internal
*/
export function toKibanaSearchResponse(rawResponse: estypes.SearchResponse<unknown>) {
export function toKibanaSearchResponse(
rawResponse: estypes.SearchResponse<unknown>,
requestParams?: ConnectionRequestParams
) {
return {
rawResponse,
isPartial: false,
isRunning: false,
...(requestParams ? { requestParams } : {}),
...getTotalLoaded(rawResponse),
};
}

View file

@ -65,7 +65,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 }
@ -78,7 +78,11 @@ export const enhancedEsSearchStrategyProvider = (
const response = shimHitsTotal(body.response, options);
return toAsyncKibanaSearchResponse({ ...body, response }, headers?.warning);
return toAsyncKibanaSearchResponse(
{ ...body, response },
headers?.warning,
meta?.request?.params
);
};
const cancel = async () => {
@ -131,8 +135,10 @@ export const enhancedEsSearchStrategyProvider = (
);
const response = esResponse.body as estypes.SearchResponse<any>;
const requestParams = esResponse.meta?.request?.params;
return {
rawResponse: shimHitsTotal(response, options),
...(requestParams ? { requestParams } : {}),
...getTotalLoaded(response),
};
} catch (e) {

View file

@ -6,19 +6,25 @@
* Side Public License, v 1.
*/
import type { ConnectionRequestParams } from '@elastic/transport';
import type { AsyncSearchResponse } from './types';
import { getTotalLoaded } from '../es_search';
/**
* 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 } : {}),
...getTotalLoaded(response.response),
};
}

View file

@ -32,7 +32,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',
@ -45,10 +45,12 @@ export const esqlSearchStrategyProvider = (
meta: true,
}
);
const transportRequestParams = meta?.request?.params;
return {
rawResponse: body,
isPartial: false,
isRunning: false,
...(transportRequestParams ? { requestParams: transportRequestParams } : {}),
warning: headers?.warning,
};
} catch (e) {

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { ConnectionRequestParams } from '@elastic/transport';
import { SqlQueryResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { SqlSearchStrategyResponse } from '../../../../common';
@ -15,7 +16,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 +26,6 @@ export function toAsyncKibanaSearchResponse(
isRunning: response.is_running,
took: Date.now() - startTime,
...(warning ? { warning } : {}),
...(requestParams ? { requestParams } : {}),
};
}

View file

@ -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 type { ISearchStrategy, SearchStrategyDependencies } from '../../types';
@ -48,9 +49,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),
@ -59,7 +61,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),
@ -79,7 +81,7 @@ export const sqlSearchStrategyProvider = (
}
}
return toAsyncKibanaSearchResponse(body, startTime, headers?.warning);
return toAsyncKibanaSearchResponse(body, startTime, headers?.warning, meta?.request?.params);
};
const cancel = async () => {

View file

@ -0,0 +1,66 @@
/*
* 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';
import { RequestStatus } from './types';
describe('moveRequestParamsToTopLevel', () => {
test('should move request meta from error response', () => {
expect(
moveRequestParamsToTopLevel(RequestStatus.ERROR, {
json: {
attributes: {},
err: {
message: 'simulated error',
requestParams: {
method: 'POST',
path: '/_query',
},
},
},
time: 1,
})
).toEqual({
json: {
attributes: {},
err: {
message: 'simulated error',
},
},
requestParams: {
method: 'POST',
path: '/_query',
},
time: 1,
});
});
test('should move request meta from ok response', () => {
expect(
moveRequestParamsToTopLevel(RequestStatus.OK, {
json: {
rawResponse: {},
requestParams: {
method: 'POST',
path: '/_query',
},
},
time: 1,
})
).toEqual({
json: {
rawResponse: {},
},
requestParams: {
method: 'POST',
path: '/_query',
},
time: 1,
});
});
});

View file

@ -0,0 +1,56 @@
/*
* 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 { RequestStatus, Response } from './types';
interface ErrorResponse {
[key: string]: unknown;
err?: {
[key: string]: unknown;
requestParams?: ConnectionRequestParams;
};
}
interface OkResponse {
[key: string]: unknown;
requestParams?: ConnectionRequestParams;
}
export function moveRequestParamsToTopLevel(status: RequestStatus, response: Response) {
if (status === RequestStatus.ERROR) {
const requestParams = (response.json as ErrorResponse)?.err?.requestParams;
if (!requestParams) {
return response;
}
const json = {
...response.json,
err: { ...(response.json as ErrorResponse).err },
};
delete json.err.requestParams;
return {
...response,
json,
requestParams,
};
}
const requestParams = (response.json as OkResponse)?.requestParams;
if (!requestParams) {
return response;
}
const json = { ...response.json } as OkResponse;
delete json.requestParams;
return {
...response,
json,
requestParams,
};
}

View file

@ -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(status, response);
this.onChange();
}

View file

@ -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;
}

View file

@ -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<InspectorPluginStartDeps>();
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
<EuiFlexItem grow={true} data-test-subj="inspectorRequestCodeViewerContainer">
<CodeEditor
languageId={XJsonLang.ID}
value={json}
value={value}
options={{
readOnly: true,
lineNumbers: 'off',

View file

@ -29,6 +29,7 @@ export class RequestDetailsRequest extends Component<RequestDetailsProps> {
return (
<RequestCodeViewer
indexPattern={this.props.request.stats?.indexPattern?.value}
requestParams={this.props.request.response?.requestParams}
json={JSON.stringify(json, null, 2)}
/>
);

View file

@ -7,14 +7,22 @@
*/
import { errors } from '@elastic/elasticsearch';
import type { ConnectionRequestParams } from '@elastic/transport';
import { KibanaResponseFactory } from '@kbn/core/server';
import { KbnError } from '../common';
export class KbnServerError extends KbnError {
public errBody?: Record<string, any>;
constructor(message: string, public readonly statusCode: number, errBody?: Record<string, any>) {
public requestParams?: ConnectionRequestParams;
constructor(
message: string,
public readonly statusCode: number,
errBody?: Record<string, any>,
requestParams?: ConnectionRequestParams
) {
super(message);
this.errBody = errBody;
this.requestParams = requestParams;
}
}
@ -28,7 +36,8 @@ export function getKbnServerError(e: Error) {
return new KbnServerError(
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
);
}
@ -43,6 +52,7 @@ export function reportServerError(res: KibanaResponseFactory, err: KbnServerErro
body: {
message: err.message,
attributes: err.errBody?.error,
...(err.requestParams ? { requestParams: err.requestParams } : {}),
},
});
}

View file

@ -232,6 +232,351 @@ 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).to.have.property('requestParams');
expect(jsonBody[0].result.requestParams.method).to.be('POST');
expect(jsonBody[0].result.requestParams.path).to.be('/.kibana/_search');
expect(jsonBody[0].result.requestParams.querystring).to.be('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).to.have.property('requestParams');
expect(jsonBody[0].error.requestParams.method).to.be('POST');
expect(jsonBody[0].error.requestParams.path).to.be('/.kibana/_search');
expect(jsonBody[0].error.requestParams.querystring).to.be('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).to.have.property('requestParams');
expect(jsonBody[0].result.requestParams.method).to.be('POST');
expect(jsonBody[0].result.requestParams.path).to.be('/.kibana/_async_search');
expect(jsonBody[0].result.requestParams.querystring).to.be(
'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).to.have.property('requestParams');
expect(jsonBody[0].error.requestParams.method).to.be('POST');
expect(jsonBody[0].error.requestParams.path).to.be('/.kibana/_async_search');
expect(jsonBody[0].error.requestParams.querystring).to.be(
'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).to.have.property('requestParams');
expect(jsonBody[0].result.requestParams.method).to.be('POST');
expect(jsonBody[0].result.requestParams.path).to.be('/_query');
expect(jsonBody[0].result.requestParams.querystring).to.be('');
});
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).to.have.property('requestParams');
expect(jsonBody[0].error.requestParams.method).to.be('POST');
expect(jsonBody[0].error.requestParams.path).to.be('/_query');
expect(jsonBody[0].error.requestParams.querystring).to.be('');
});
});
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).to.have.property('requestParams');
expect(jsonBody[0].result.requestParams.method).to.be('POST');
expect(jsonBody[0].result.requestParams.path).to.be('/_sql');
expect(jsonBody[0].result.requestParams.querystring).to.be('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).to.have.property('requestParams');
expect(jsonBody[0].error.requestParams.method).to.be('POST');
expect(jsonBody[0].error.requestParams.path).to.be('/_sql');
expect(jsonBody[0].error.requestParams.querystring).to.be('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).to.have.property('requestParams');
expect(jsonBody[0].result.requestParams.method).to.be('POST');
expect(jsonBody[0].result.requestParams.path).to.be('/.kibana/_eql/search');
expect(jsonBody[0].result.requestParams.querystring).to.be('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',
query: 'any where true',
},
},
options: {
strategy: 'eql',
},
},
],
});
const jsonBody = parseBfetchResponse(resp);
expect(resp.status).to.be(200);
expect(jsonBody[0].error).to.have.property('requestParams');
expect(jsonBody[0].error.requestParams.method).to.be('POST');
expect(jsonBody[0].error.requestParams.path).to.be('/.kibana/_eql/search');
expect(jsonBody[0].error.requestParams.querystring).to.be('ignore_unavailable=true');
});
});
});
});
});
}

View file

@ -41,11 +41,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 () => {

View file

@ -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<string, any> }> {
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<Record<string, any>> {
await (await this.getOpenRequestDetailResponseButton()).click();