mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
969bbb0a11
commit
d61a5a0516
26 changed files with 627 additions and 44 deletions
|
@ -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 });
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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 } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue