mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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.
<img width="500" alt="Screen Shot 2023-09-16 at 4 19 58 PM"
src="56019fb5
-ca88-46cf-a42f-86f5f51edfcc">
### 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>
This commit is contained in:
parent
45d0e32244
commit
f28445449e
29 changed files with 627 additions and 43 deletions
|
@ -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 {
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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<RawResponse = any> {
|
||||
/**
|
||||
* Some responses may contain a unique id to identify the request this response came from.
|
||||
|
@ -86,6 +93,17 @@ 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?: SanitizedConnectionRequestParams;
|
||||
}
|
||||
|
||||
export interface IEsErrorAttributes {
|
||||
error?: estypes.ErrorCause;
|
||||
rawResponse?: estypes.SearchResponseBody;
|
||||
requestParams?: SanitizedConnectionRequestParams;
|
||||
}
|
||||
|
||||
export interface IKibanaSearchRequest<Params = any> {
|
||||
|
|
|
@ -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<IEsErrorAttributes>;
|
||||
|
||||
|
|
|
@ -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 /<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: 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();
|
||||
|
|
|
@ -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<string, any>;
|
||||
constructor(message: string, public readonly statusCode: number, errBody?: Record<string, any>) {
|
||||
public requestParams?: SanitizedConnectionRequestParams;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly statusCode: number,
|
||||
errBody?: Record<string, any>,
|
||||
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,
|
||||
},
|
||||
|
|
|
@ -50,6 +50,7 @@ export function registerBsearchRoute(
|
|||
? {
|
||||
error: err.errBody.error,
|
||||
rawResponse: err.errBody.response,
|
||||
...(err.requestParams ? { requestParams: err.requestParams } : {}),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
20
src/plugins/data/server/search/sanitize_request_params.ts
Normal file
20
src/plugins/data/server/search/sanitize_request_params.ts
Normal file
|
@ -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 } : {}),
|
||||
};
|
||||
}
|
|
@ -77,7 +77,11 @@ export const eqlSearchStrategyProvider = (
|
|||
meta: true,
|
||||
});
|
||||
|
||||
return toEqlKibanaSearchResponse(response as TransportResult<EqlSearchResponse>);
|
||||
return toEqlKibanaSearchResponse(
|
||||
response as TransportResult<EqlSearchResponse>,
|
||||
// do not return requestParams on polling calls
|
||||
id ? undefined : (response as TransportResult<EqlSearchResponse>).meta?.request?.params
|
||||
);
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
|
|
|
@ -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<EqlSearchResponse>
|
||||
response: TransportResult<EqlSearchResponse>,
|
||||
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) } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 getKbnSearchError(e);
|
||||
}
|
||||
|
|
|
@ -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<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: sanitizeRequestParams(requestParams) } : {}),
|
||||
...getTotalLoaded(rawResponse),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
shimHitsTotal,
|
||||
} from '../es_search';
|
||||
import { SearchConfigSchema } from '../../../../config';
|
||||
import { sanitizeRequestParams } from '../../sanitize_request_params';
|
||||
|
||||
export const enhancedEsSearchStrategyProvider = (
|
||||
legacyConfig$: Observable<SharedGlobalConfig>,
|
||||
|
@ -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<any>;
|
||||
return {
|
||||
rawResponse: shimHitsTotal(response, options),
|
||||
...(esResponse.meta?.request?.params
|
||||
? { requestParams: sanitizeRequestParams(esResponse.meta?.request?.params) }
|
||||
: {}),
|
||||
...getTotalLoaded(response),
|
||||
};
|
||||
} catch (e) {
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -24,6 +24,7 @@ export class RequestDetailsRequest extends Component<DetailViewProps> {
|
|||
return (
|
||||
<RequestCodeViewer
|
||||
indexPattern={this.props.request.stats?.indexPattern?.value}
|
||||
requestParams={this.props.request.response?.requestParams}
|
||||
json={JSON.stringify(json, null, 2)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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