diff --git a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx index 8174f06e06b8b..2b58f30a9ec64 100644 --- a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx @@ -24,6 +24,21 @@ export interface FetcherResult { error?: IHttpFetchError; } +function getDetailsFromErrorResponse(error: IHttpFetchError) { + const message = error.body?.message ?? error.response?.statusText; + return ( + <> + {message} ({error.response?.status}) +
+ {i18n.translate('xpack.apm.fetcher.error.url', { + defaultMessage: `URL`, + })} +
+ {error.response?.url} + + ); +} + // fetcher functions can return undefined OR a promise. Previously we had a more simple type // but it led to issues when using object destructuring with default values type InferResponseType = Exclude extends Promise< @@ -82,25 +97,14 @@ export function useFetcher( if (!didCancel) { const errorDetails = - 'response' in err ? ( - <> - {err.response?.statusText} ({err.response?.status}) -
- {i18n.translate('xpack.apm.fetcher.error.url', { - defaultMessage: `URL`, - })} -
- {err.response?.url} - - ) : ( - err.message - ); + 'response' in err ? getDetailsFromErrorResponse(err) : err.message; if (showToastOnError) { - notifications.toasts.addWarning({ + notifications.toasts.addDanger({ title: i18n.translate('xpack.apm.fetcher.error.title', { defaultMessage: `Error while fetching resource`, }), + text: toMountPoint(
diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts index 8c64c37d9b7f7..e3221c17f3f2a 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -17,6 +17,8 @@ import { argv } from 'yargs'; import { Logger } from 'kibana/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { CollectTelemetryParams } from '../../server/lib/apm_telemetry/collect_data_telemetry'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { unwrapEsResponse } from '../../../observability/server/utils/unwrap_es_response'; import { downloadTelemetryTemplate } from '../shared/download-telemetry-template'; import { mergeApmTelemetryMapping } from '../../common/apm_telemetry'; import { generateSampleDocuments } from './generate-sample-documents'; @@ -80,18 +82,18 @@ async function uploadData() { apmAgentConfigurationIndex: '.apm-agent-configuration', }, search: (body) => { - return client.search(body as any).then((res) => res.body as any); + return unwrapEsResponse(client.search(body)); }, indicesStats: (body) => { - return client.indices.stats(body as any).then((res) => res.body); + return unwrapEsResponse(client.indices.stats(body)); }, transportRequest: ((params) => { - return client.transport - .request({ + return unwrapEsResponse( + client.transport.request({ method: params.method, path: params.path, }) - .then((res) => res.body); + ); }) as CollectTelemetryParams['transportRequest'], }, }); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index 730645c609cb6..90aad48fe20b9 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { merge } from 'lodash'; -import { Logger, LegacyCallAPIOptions } from 'kibana/server'; -import { IndicesStatsParams, Client } from 'elasticsearch'; +import { Logger } from 'kibana/server'; +import { RequestParams } from '@elastic/elasticsearch'; import { ESSearchRequest, ESSearchResponse, @@ -20,9 +20,17 @@ type TelemetryTaskExecutor = (params: { params: TSearchRequest ): Promise>; indicesStats( - params: IndicesStatsParams, - options?: LegacyCallAPIOptions - ): ReturnType; + params: RequestParams.IndicesStats + // promise returned by client has an abort property + // so we cannot use its ReturnType + ): Promise<{ + _all?: { + total?: { store?: { size_in_bytes?: number }; docs?: { count?: number } }; + }; + _shards?: { + total?: number; + }; + }>; transportRequest: (params: { path: string; method: 'get'; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts index 6d91e64be034d..98abff08dab5e 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts @@ -11,6 +11,7 @@ import { Logger, SavedObjectsErrorHelpers, } from '../../../../../../src/core/server'; +import { unwrapEsResponse } from '../../../../observability/server'; import { APMConfig } from '../..'; import { TaskManagerSetupContract, @@ -65,27 +66,22 @@ export async function createApmTelemetry({ const collectAndStore = async () => { const config = await config$.pipe(take(1)).toPromise(); const [{ elasticsearch }] = await core.getStartServices(); - const esClient = elasticsearch.legacy.client; + const esClient = elasticsearch.client; const indices = await getApmIndices({ config, savedObjectsClient, }); - const search = esClient.callAsInternalUser.bind( - esClient, - 'search' - ) as CollectTelemetryParams['search']; + const search: CollectTelemetryParams['search'] = (params) => + unwrapEsResponse(esClient.asInternalUser.search(params)); - const indicesStats = esClient.callAsInternalUser.bind( - esClient, - 'indices.stats' - ) as CollectTelemetryParams['indicesStats']; + const indicesStats: CollectTelemetryParams['indicesStats'] = (params) => + unwrapEsResponse(esClient.asInternalUser.indices.stats(params)); - const transportRequest = esClient.callAsInternalUser.bind( - esClient, - 'transport.request' - ) as CollectTelemetryParams['transportRequest']; + const transportRequest: CollectTelemetryParams['transportRequest'] = ( + params + ) => unwrapEsResponse(esClient.asInternalUser.transport.request(params)); const dataTelemetry = await collectDataTelemetry({ search, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts similarity index 51% rename from x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts rename to x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index 9f7aaafbefb8c..9d612d82d99bb 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -7,34 +7,31 @@ /* eslint-disable no-console */ import chalk from 'chalk'; -import { - LegacyAPICaller, - KibanaRequest, -} from '../../../../../../../src/core/server'; +import { KibanaRequest } from '../../../../../../../src/core/server'; function formatObj(obj: Record) { return JSON.stringify(obj, null, 2); } -export async function callClientWithDebug({ - apiCaller, - operationName, - params, +export async function callAsyncWithDebug({ + cb, + getDebugMessage, debug, - request, }: { - apiCaller: LegacyAPICaller; - operationName: string; - params: Record; + cb: () => Promise; + getDebugMessage: () => { body: string; title: string }; debug: boolean; - request: KibanaRequest; }) { + if (!debug) { + return cb(); + } + const startTime = process.hrtime(); let res: any; let esError = null; try { - res = await apiCaller(operationName, params); + res = await cb(); } catch (e) { // catch error and throw after outputting debug info esError = e; @@ -44,23 +41,14 @@ export async function callClientWithDebug({ const highlightColor = esError ? 'bgRed' : 'inverse'; const diff = process.hrtime(startTime); const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; - const routeInfo = `${request.route.method.toUpperCase()} ${ - request.route.path - }`; + + const { title, body } = getDebugMessage(); console.log( - chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) + chalk.bold[highlightColor](`=== Debug: ${title} (${duration}) ===`) ); - if (operationName === 'search') { - console.log(`GET ${params.index}/_${operationName}`); - console.log(formatObj(params.body)); - } else { - console.log(chalk.bold('ES operation:'), operationName); - - console.log(chalk.bold('ES query:')); - console.log(formatObj(params)); - } + console.log(body); console.log(`\n`); } @@ -70,3 +58,19 @@ export async function callClientWithDebug({ return res; } + +export const getDebugBody = ( + params: Record, + operationName: string +) => { + if (operationName === 'search') { + return `GET ${params.index}/_search\n${formatObj(params.body)}`; + } + + return `${chalk.bold('ES operation:')} ${operationName}\n${chalk.bold( + 'ES query:' + )}\n${formatObj(params)}`; +}; + +export const getDebugTitle = (request: KibanaRequest) => + `${request.route.method.toUpperCase()} ${request.route.path}`; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts new file mode 100644 index 0000000000000..e9b61a27f4380 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { KibanaRequest } from 'src/core/server'; + +export function cancelEsRequestOnAbort>( + promise: T, + request: KibanaRequest +) { + const subscription = request.events.aborted$.subscribe(() => { + promise.abort(); + }); + + // using .catch() here means unsubscribe will be called + // after it has thrown an error, so we use .then(onSuccess, onFailure) + // syntax + promise.then( + () => subscription.unsubscribe(), + () => subscription.unsubscribe() + ); + + return promise; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts new file mode 100644 index 0000000000000..f58e04061254d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { contextServiceMock } from 'src/core/server/mocks'; +import { createHttpServer } from 'src/core/server/test_utils'; +import supertest from 'supertest'; +import { createApmEventClient } from '.'; + +describe('createApmEventClient', () => { + let server: ReturnType; + + beforeEach(() => { + server = createHttpServer(); + }); + + afterEach(async () => { + await server.stop(); + }); + it('cancels a search when a request is aborted', async () => { + const { server: innerServer, createRouter } = await server.setup({ + context: contextServiceMock.createSetupContract(), + }); + const router = createRouter('/'); + + const abort = jest.fn(); + router.get( + { path: '/', validate: false }, + async (context, request, res) => { + const eventClient = createApmEventClient({ + esClient: { + search: () => { + return Object.assign( + new Promise((resolve) => setTimeout(resolve, 3000)), + { abort } + ); + }, + } as any, + debug: false, + request, + indices: {} as any, + options: { + includeFrozen: false, + }, + }); + + await eventClient.search({ + apm: { + events: [], + }, + }); + + return res.ok({ body: 'ok' }); + } + ); + + await server.start(); + + const incomingRequest = supertest(innerServer.listener) + .get('/') + // end required to send request + .end(); + + await new Promise((resolve) => { + setTimeout(() => { + incomingRequest.abort(); + setTimeout(() => { + resolve(undefined); + }, 0); + }, 50); + }); + + expect(abort).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index b7c38068eb93e..b2e25994d6fe6 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -5,10 +5,11 @@ */ import { ValuesType } from 'utility-types'; +import { unwrapEsResponse } from '../../../../../../observability/server'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { + ElasticsearchClient, KibanaRequest, - LegacyScopedClusterClient, } from '../../../../../../../../src/core/server'; import { ProcessorEvent } from '../../../../../common/processor_event'; import { @@ -17,11 +18,16 @@ import { } from '../../../../../../../typings/elasticsearch'; import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data'; -import { callClientWithDebug } from '../call_client_with_debug'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { Span } from '../../../../../typings/es_schemas/ui/span'; import { Metric } from '../../../../../typings/es_schemas/ui/metric'; import { unpackProcessorEvents } from './unpack_processor_events'; +import { + callAsyncWithDebug, + getDebugTitle, + getDebugBody, +} from '../call_async_with_debug'; +import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort'; export type APMEventESSearchRequest = Omit & { apm: { @@ -59,10 +65,7 @@ export function createApmEventClient({ indices, options: { includeFrozen } = { includeFrozen: false }, }: { - esClient: Pick< - LegacyScopedClusterClient, - 'callAsInternalUser' | 'callAsCurrentUser' - >; + esClient: ElasticsearchClient; debug: boolean; request: KibanaRequest; indices: ApmIndicesConfig; @@ -71,9 +74,9 @@ export function createApmEventClient({ }; }) { return { - search( + async search( params: TParams, - { includeLegacyData } = { includeLegacyData: false } + { includeLegacyData = false } = {} ): Promise> { const withProcessorEventFilter = unpackProcessorEvents(params, indices); @@ -81,15 +84,25 @@ export function createApmEventClient({ ? addFilterToExcludeLegacyData(withProcessorEventFilter) : withProcessorEventFilter; - return callClientWithDebug({ - apiCaller: esClient.callAsCurrentUser, - operationName: 'search', - params: { - ...withPossibleLegacyDataFilter, - ignore_throttled: !includeFrozen, - ignore_unavailable: true, + const searchParams = { + ...withPossibleLegacyDataFilter, + ignore_throttled: !includeFrozen, + ignore_unavailable: true, + }; + + return callAsyncWithDebug({ + cb: () => { + const searchPromise = cancelEsRequestOnAbort( + esClient.search(searchParams), + request + ); + + return unwrapEsResponse(searchPromise); }, - request, + getDebugMessage: () => ({ + body: getDebugBody(searchParams, 'search'), + title: getDebugTitle(request), + }), debug, }); }, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index 8e74a7992e9ea..69f596520d216 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -3,23 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { - IndexDocumentParams, - IndicesCreateParams, - DeleteDocumentResponse, - DeleteDocumentParams, -} from 'elasticsearch'; import { KibanaRequest } from 'src/core/server'; +import { RequestParams } from '@elastic/elasticsearch'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { unwrapEsResponse } from '../../../../../../observability/server'; import { APMRequestHandlerContext } from '../../../../routes/typings'; import { ESSearchResponse, ESSearchRequest, } from '../../../../../../../typings/elasticsearch'; -import { callClientWithDebug } from '../call_client_with_debug'; +import { + callAsyncWithDebug, + getDebugBody, + getDebugTitle, +} from '../call_async_with_debug'; +import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort'; -// `type` was deprecated in 7.0 -export type APMIndexDocumentParams = Omit, 'type'>; +export type APMIndexDocumentParams = RequestParams.Index; export type APMInternalClient = ReturnType; @@ -30,17 +30,26 @@ export function createInternalESClient({ context: APMRequestHandlerContext; request: KibanaRequest; }) { - const { callAsInternalUser } = context.core.elasticsearch.legacy.client; + const { asInternalUser } = context.core.elasticsearch.client; - const callEs = (operationName: string, params: Record) => { - return callClientWithDebug({ - apiCaller: callAsInternalUser, - operationName, - params, - request, + function callEs({ + cb, + operationName, + params, + }: { + operationName: string; + cb: () => TransportRequestPromise; + params: Record; + }) { + return callAsyncWithDebug({ + cb: () => unwrapEsResponse(cancelEsRequestOnAbort(cb(), request)), + getDebugMessage: () => ({ + title: getDebugTitle(request), + body: getDebugBody(params, operationName), + }), debug: context.params.query._debug, }); - }; + } return { search: async < @@ -49,18 +58,32 @@ export function createInternalESClient({ >( params: TSearchRequest ): Promise> => { - return callEs('search', params); + return callEs({ + operationName: 'search', + cb: () => asInternalUser.search(params), + params, + }); }, - index: (params: APMIndexDocumentParams) => { - return callEs('index', params); + index: (params: APMIndexDocumentParams) => { + return callEs({ + operationName: 'index', + cb: () => asInternalUser.index(params), + params, + }); }, - delete: ( - params: Omit - ): Promise => { - return callEs('delete', params); + delete: (params: RequestParams.Delete): Promise<{ result: string }> => { + return callEs({ + operationName: 'delete', + cb: () => asInternalUser.delete(params), + params, + }); }, - indicesCreate: (params: IndicesCreateParams) => { - return callEs('indices.create', params); + indicesCreate: (params: RequestParams.IndicesCreate) => { + return callEs({ + operationName: 'indices.create', + cb: () => asInternalUser.indices.create(params), + params, + }); }, }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index f2d291cd053bb..f00941d6e6800 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -31,6 +31,15 @@ jest.mock('../index_pattern/get_dynamic_index_pattern', () => ({ })); function getMockRequest() { + const esClientMock = { + asCurrentUser: { + search: jest.fn().mockResolvedValue({ body: {} }), + }, + asInternalUser: { + search: jest.fn().mockResolvedValue({ body: {} }), + }, + }; + const mockContext = ({ config: new Proxy( {}, @@ -45,12 +54,7 @@ function getMockRequest() { }, core: { elasticsearch: { - legacy: { - client: { - callAsCurrentUser: jest.fn(), - callAsInternalUser: jest.fn(), - }, - }, + client: esClientMock, }, uiSettings: { client: { @@ -69,12 +73,7 @@ function getMockRequest() { } as unknown) as APMRequestHandlerContext & { core: { elasticsearch: { - legacy: { - client: { - callAsCurrentUser: jest.Mock; - callAsInternalUser: jest.Mock; - }; - }; + client: typeof esClientMock; }; uiSettings: { client: { @@ -91,6 +90,11 @@ function getMockRequest() { const mockRequest = ({ url: '', + events: { + aborted$: { + subscribe: jest.fn(), + }, + }, } as unknown) as KibanaRequest; return { mockContext, mockRequest }; @@ -106,8 +110,8 @@ describe('setupRequest', () => { body: { foo: 'bar' }, }); expect( - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser - ).toHaveBeenCalledWith('search', { + mockContext.core.elasticsearch.client.asCurrentUser.search + ).toHaveBeenCalledWith({ index: ['apm-*'], body: { foo: 'bar', @@ -133,8 +137,8 @@ describe('setupRequest', () => { body: { foo: 'bar' }, } as any); expect( - mockContext.core.elasticsearch.legacy.client.callAsInternalUser - ).toHaveBeenCalledWith('search', { + mockContext.core.elasticsearch.client.asInternalUser.search + ).toHaveBeenCalledWith({ index: ['apm-*'], body: { foo: 'bar', @@ -154,8 +158,8 @@ describe('setupRequest', () => { body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, }); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -184,8 +188,8 @@ describe('setupRequest', () => { } ); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -214,8 +218,8 @@ describe('without a bool filter', () => { }, }); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -245,8 +249,8 @@ describe('with includeFrozen=false', () => { }); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; expect(params.ignore_throttled).toBe(true); }); }); @@ -265,8 +269,8 @@ describe('with includeFrozen=true', () => { }); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; expect(params.ignore_throttled).toBe(false); }); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 47529de1042a1..947eb68e10093 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -86,7 +86,7 @@ export async function setupRequest( const coreSetupRequest = { indices, apmEventClient: createApmEventClient({ - esClient: context.core.elasticsearch.legacy.client, + esClient: context.core.elasticsearch.client.asCurrentUser, debug: context.params.query._debug, request, indices, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 3903298415aed..55395f3a4ca4e 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, Logger } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; +import { unwrapEsResponse } from '../../../../../observability/server'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; @@ -18,14 +19,14 @@ export async function getStoredAnnotations({ setup, serviceName, environment, - apiCaller, + client, annotationsClient, logger, }: { setup: Setup & SetupTimeRange; serviceName: string; environment?: string; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; annotationsClient: ScopedAnnotationsClient; logger: Logger; }): Promise { @@ -50,10 +51,12 @@ export async function getStoredAnnotations({ const response: ESSearchResponse< ESAnnotation, { body: typeof body } - > = (await apiCaller('search', { - index: annotationsClient.index, - body, - })) as any; + > = await unwrapEsResponse( + client.search({ + index: annotationsClient.index, + body, + }) + ); return response.hits.hits.map((hit) => { return { diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.ts index 9516ed3777297..304485822be28 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, Logger } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { getDerivedServiceAnnotations } from './get_derived_service_annotations'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -15,7 +15,7 @@ export async function getServiceAnnotations({ serviceName, environment, annotationsClient, - apiCaller, + client, logger, }: { serviceName: string; @@ -23,7 +23,7 @@ export async function getServiceAnnotations({ setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; annotationsClient?: ScopedAnnotationsClient; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; logger: Logger; }) { // start fetching derived annotations (based on transactions), but don't wait on it @@ -41,7 +41,7 @@ export async function getServiceAnnotations({ serviceName, environment, annotationsClient, - apiCaller, + client, logger, }) : []; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index 83117db1167b5..190c99d0002d8 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyClusterClient, Logger } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { createOrUpdateIndex, MappingsDefinition, @@ -13,18 +13,18 @@ import { APMConfig } from '../../..'; import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; export async function createApmAgentConfigurationIndex({ - esClient, + client, config, logger, }: { - esClient: ILegacyClusterClient; + client: ElasticsearchClient; config: APMConfig; logger: Logger; }) { const index = getApmIndicesConfig(config).apmAgentConfigurationIndex; return createOrUpdateIndex({ index, - apiCaller: esClient.callAsInternalUser, + client, logger, mappings, }); diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts index 2bfe0d620e4e8..aa9e7411d1014 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyClusterClient, Logger } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { createOrUpdateIndex, MappingsDefinition, @@ -13,18 +13,18 @@ import { APMConfig } from '../../..'; import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; export const createApmCustomLinkIndex = async ({ - esClient, + client, config, logger, }: { - esClient: ILegacyClusterClient; + client: ElasticsearchClient; config: APMConfig; logger: Logger; }) => { const index = getApmIndicesConfig(config).apmCustomLinkIndex; return createOrUpdateIndex({ index, - apiCaller: esClient.callAsInternalUser, + client, logger, mappings, }); diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 09b75137e12df..3e01523aa8e31 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -173,7 +173,7 @@ export class APMPlugin implements Plugin { context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), ]); - const esClient = context.core.elasticsearch.legacy.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; return createApmEventClient({ debug: debug ?? false, @@ -195,13 +195,13 @@ export class APMPlugin implements Plugin { // create agent configuration index without blocking start lifecycle createApmAgentConfigurationIndex({ - esClient: core.elasticsearch.legacy.client, + client: core.elasticsearch.client.asInternalUser, config: this.currentConfig, logger: this.logger, }); // create custom action index without blocking start lifecycle createApmCustomLinkIndex({ - esClient: core.elasticsearch.legacy.client, + client: core.elasticsearch.client.asInternalUser, config: this.currentConfig, logger: this.logger, }); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index cfb31670bd521..721badf7fc025 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -147,7 +147,7 @@ function convertBoomToKibanaResponse( error: Boom.Boom, response: KibanaResponseFactory ) { - const opts = { body: error.message }; + const opts = { body: { message: error.message } }; switch (error.output.statusCode) { case 404: return response.notFound(opts); @@ -159,9 +159,6 @@ function convertBoomToKibanaResponse( return response.forbidden(opts); default: - return response.custom({ - statusCode: error.output.statusCode, - ...opts, - }); + throw error; } } diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index bfc2ebf062ac3..ef74437f5f0e7 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -194,7 +194,7 @@ export const serviceAnnotationsRoute = createRoute({ serviceName, environment, annotationsClient, - apiCaller: context.core.elasticsearch.legacy.client.callAsCurrentUser, + client: context.core.elasticsearch.client.asCurrentUser, logger: context.logger, }); }, diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index 78550b781b411..e88541f69d6cf 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -9,6 +9,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { ObservabilityPlugin, ObservabilityPluginSetup } from './plugin'; import { createOrUpdateIndex, MappingsDefinition } from './utils/create_or_update_index'; import { ScopedAnnotationsClient } from './lib/annotations/bootstrap_annotations'; +import { unwrapEsResponse } from './utils/unwrap_es_response'; export const config = { schema: schema.object({ @@ -30,4 +31,5 @@ export { MappingsDefinition, ObservabilityPluginSetup, ScopedAnnotationsClient, + unwrapEsResponse, }; diff --git a/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts b/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts index 6fcd780d5af29..90084611d7efc 100644 --- a/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts +++ b/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts @@ -38,7 +38,7 @@ export async function bootstrapAnnotations({ index, core, context }: Params) { ) => { return createAnnotationsClient({ index, - apiCaller: requestContext.core.elasticsearch.legacy.client.callAsCurrentUser, + esClient: requestContext.core.elasticsearch.client.asCurrentUser, logger, license: requestContext.licensing?.license, }); diff --git a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts index 41f45683d244c..76890cbd587e9 100644 --- a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts +++ b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, Logger } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import * as t from 'io-ts'; -import { Client } from 'elasticsearch'; import Boom from '@hapi/boom'; import { ILicense } from '../../../../licensing/server'; import { @@ -15,9 +14,9 @@ import { Annotation, getAnnotationByIdRt, } from '../../../common/annotations'; -import { PromiseReturnType } from '../../../typings/common'; import { createOrUpdateIndex } from '../../utils/create_or_update_index'; import { mappings } from './mappings'; +import { unwrapEsResponse } from '../../utils/unwrap_es_response'; type CreateParams = t.TypeOf; type DeleteParams = t.TypeOf; @@ -38,19 +37,25 @@ interface IndexDocumentResponse { result: string; } +interface GetResponse { + _id: string; + _index: string; + _source: Annotation; +} + export function createAnnotationsClient(params: { index: string; - apiCaller: LegacyAPICaller; + esClient: ElasticsearchClient; logger: Logger; license?: ILicense; }) { - const { index, apiCaller, logger, license } = params; + const { index, esClient, logger, license } = params; const initIndex = () => createOrUpdateIndex({ index, mappings, - apiCaller, + client: esClient, logger, }); @@ -71,9 +76,11 @@ export function createAnnotationsClient(params: { async ( createParams: CreateParams ): Promise<{ _id: string; _index: string; _source: Annotation }> => { - const indexExists = await apiCaller('indices.exists', { - index, - }); + const indexExists = await unwrapEsResponse( + esClient.indices.exists({ + index, + }) + ); if (!indexExists) { await initIndex(); @@ -86,35 +93,42 @@ export function createAnnotationsClient(params: { }, }; - const response = (await apiCaller('index', { - index, - body: annotation, - refresh: 'wait_for', - })) as IndexDocumentResponse; + const body = await unwrapEsResponse( + esClient.index({ + index, + body: annotation, + refresh: 'wait_for', + }) + ); - return apiCaller('get', { - index, - id: response._id, - }); + return ( + await esClient.get({ + index, + id: body._id, + }) + ).body; } ), getById: ensureGoldLicense(async (getByIdParams: GetByIdParams) => { const { id } = getByIdParams; - return apiCaller('get', { - id, - index, - }); + return unwrapEsResponse( + esClient.get({ + id, + index, + }) + ); }), delete: ensureGoldLicense(async (deleteParams: DeleteParams) => { const { id } = deleteParams; - const response = (await apiCaller('delete', { - index, - id, - refresh: 'wait_for', - })) as PromiseReturnType; - return response; + return unwrapEsResponse( + esClient.delete({ + index, + id, + refresh: 'wait_for', + }) + ); }), }; } diff --git a/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts b/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts index 8f0b53b5a3df2..6ae80880d22b5 100644 --- a/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts +++ b/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts @@ -55,11 +55,11 @@ export function registerAnnotationAPIs({ }); } - const apiCaller = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const esClient = context.core.elasticsearch.client.asCurrentUser; const client = createAnnotationsClient({ index, - apiCaller, + esClient, logger, license: context.licensing?.license, }); diff --git a/x-pack/plugins/observability/server/utils/create_or_update_index.ts b/x-pack/plugins/observability/server/utils/create_or_update_index.ts index 1898331451262..248488b4a5ebe 100644 --- a/x-pack/plugins/observability/server/utils/create_or_update_index.ts +++ b/x-pack/plugins/observability/server/utils/create_or_update_index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import pRetry from 'p-retry'; -import { Logger, LegacyAPICaller } from 'src/core/server'; +import { Logger, ElasticsearchClient } from 'src/core/server'; export interface MappingsObject { type: string; @@ -24,12 +24,12 @@ export interface MappingsDefinition { export async function createOrUpdateIndex({ index, mappings, - apiCaller, + client, logger, }: { index: string; mappings: MappingsDefinition; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; logger: Logger; }) { try { @@ -43,21 +43,21 @@ export async function createOrUpdateIndex({ */ await pRetry( async () => { - const indexExists = await apiCaller('indices.exists', { index }); + const indexExists = (await client.indices.exists({ index })).body; const result = indexExists ? await updateExistingIndex({ index, - apiCaller, + client, mappings, }) : await createNewIndex({ index, - apiCaller, + client, mappings, }); - if (!result.acknowledged) { - const resultError = result && result.error && JSON.stringify(result.error); + if (!result.body.acknowledged) { + const resultError = result && result.body.error && JSON.stringify(result.body.error); throw new Error(resultError); } }, @@ -75,14 +75,14 @@ export async function createOrUpdateIndex({ function createNewIndex({ index, - apiCaller, + client, mappings, }: { index: string; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; mappings: MappingsDefinition; }) { - return apiCaller('indices.create', { + return client.indices.create<{ acknowledged: boolean; error: any }>({ index, body: { // auto_expand_replicas: Allows cluster to not have replicas for this index @@ -94,14 +94,14 @@ function createNewIndex({ function updateExistingIndex({ index, - apiCaller, + client, mappings, }: { index: string; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; mappings: MappingsDefinition; }) { - return apiCaller('indices.putMapping', { + return client.indices.putMapping<{ acknowledged: boolean; error: any }>({ index, body: mappings, }); diff --git a/x-pack/plugins/observability/server/utils/unwrap_es_response.ts b/x-pack/plugins/observability/server/utils/unwrap_es_response.ts new file mode 100644 index 0000000000000..418ceeb64cc87 --- /dev/null +++ b/x-pack/plugins/observability/server/utils/unwrap_es_response.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PromiseValueType } from '../../../apm/typings/common'; + +export function unwrapEsResponse>( + responsePromise: T +): Promise['body']> { + return responsePromise.then((res) => res.body); +} diff --git a/x-pack/typings/elasticsearch/index.d.ts b/x-pack/typings/elasticsearch/index.d.ts index ff20ce39d6446..049e1e52c66d9 100644 --- a/x-pack/typings/elasticsearch/index.d.ts +++ b/x-pack/typings/elasticsearch/index.d.ts @@ -5,6 +5,7 @@ */ import { ValuesType } from 'utility-types'; import { Explanation, SearchParams, SearchResponse } from 'elasticsearch'; +import { RequestParams } from '@elastic/elasticsearch'; import { AggregationResponseMap, AggregationInputMap, SortOptions } from './aggregations'; export { AggregationInputMap, @@ -72,9 +73,7 @@ export interface ESSearchBody { _source?: ESSourceOptions; } -export type ESSearchRequest = Omit & { - body?: ESSearchBody; -}; +export type ESSearchRequest = RequestParams.Search; export interface ESSearchOptions { restTotalHitsAsInt: boolean;