From 0860b34dcc13a9aef0f5828c92da371c106cfb6d Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 17 Sep 2024 12:19:37 +0200 Subject: [PATCH] Add tests --- .../lib/helpers/get_apm_event_client.ts | 2 +- .../routes/alerts/alerting_es_client.test.ts | 78 +++++ .../routes/alerts/alerting_es_client.ts | 2 +- .../server/routes/alerts/test_utils/index.ts | 3 + .../create_apm_event_client/index.test.ts | 267 ++++++++++++++---- .../create_apm_event_client/index.ts | 8 +- .../server/lib/helpers/tier_filter.ts | 4 +- .../profiling/common/storage_explorer.ts | 2 +- 8 files changed, 306 insertions(+), 60 deletions(-) create mode 100644 x-pack/plugins/observability_solution/apm/server/routes/alerts/alerting_es_client.test.ts diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts index ab48d13cce237..7c20db7b1bdff 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts @@ -30,7 +30,7 @@ export async function getApmEventClient({ const includeFrozen = await coreContext.uiSettings.client.get( UI_SETTINGS.SEARCH_INCLUDE_FROZEN ); - const excludedDataTiers = await coreContext.uiSettings.client.get( + const excludedDataTiers = await coreContext.uiSettings.client.get( searchExcludedDataTiers ); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/alerts/alerting_es_client.test.ts b/x-pack/plugins/observability_solution/apm/server/routes/alerts/alerting_es_client.test.ts new file mode 100644 index 0000000000000..d655a68445a0e --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/server/routes/alerts/alerting_es_client.test.ts @@ -0,0 +1,78 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { APMEventESSearchRequestParams, alertingEsClient } from './alerting_es_client'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server'; +import { ESSearchResponse } from '@kbn/es-types'; + +describe('alertingEsClient', () => { + let scopedClusterClientMock: jest.Mocked<{ + asCurrentUser: jest.Mocked; + }>; + + let uiSettingsClientMock: jest.Mocked; + + beforeEach(() => { + scopedClusterClientMock = { + asCurrentUser: { + search: jest.fn(), + } as unknown as jest.Mocked, + }; + + uiSettingsClientMock = { + get: jest.fn(), + } as unknown as jest.Mocked; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should call search with filters containing excluded data tiers', async () => { + const excludedDataTiers = ['data_warm', 'data_cold']; + uiSettingsClientMock.get.mockResolvedValue(excludedDataTiers); + + const params = { + body: { + size: 10, + track_total_hits: true, + query: { + match: { field: 'value' }, + }, + }, + }; + + scopedClusterClientMock.asCurrentUser.search.mockResolvedValue({ + hits: { + total: { value: 1, relation: 'eq' }, + hits: [{ _source: {}, _index: '' }], + max_score: 1, + }, + took: 1, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + timed_out: false, + } as unknown as ESSearchResponse); + + await alertingEsClient({ + scopedClusterClient: scopedClusterClientMock as unknown as RuleExecutorServices< + never, + never, + never + >['scopedClusterClient'], + uiSettingsClient: uiSettingsClientMock, + params, + }); + + const searchParams = scopedClusterClientMock.asCurrentUser.search.mock + .calls[0][0] as APMEventESSearchRequestParams; + expect(searchParams?.body?.query?.bool).toEqual({ + filter: { bool: { must_not: [{ terms: { _tier: ['data_warm', 'data_cold'] } }] } }, + must: [{ match: { field: 'value' } }], + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/alerts/alerting_es_client.ts b/x-pack/plugins/observability_solution/apm/server/routes/alerts/alerting_es_client.ts index 34e517354989a..5bf18703ba0f5 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/alerts/alerting_es_client.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/alerts/alerting_es_client.ts @@ -26,7 +26,7 @@ export async function alertingEsClient> { - const excludedDataTiers = await uiSettingsClient.get( + const excludedDataTiers = await uiSettingsClient.get( searchExcludedDataTiers ); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/alerts/test_utils/index.ts index 1f8ddeaff4620..8db29408d4752 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/alerts/test_utils/index.ts @@ -40,6 +40,9 @@ export const createRuleTypeMocks = () => { savedObjectsClient: { get: () => ({ attributes: { consumer: APM_SERVER_FEATURE_ID } }), }, + uiSettingsClient: { + get: jest.fn(), + }, alertFactory: { create: jest.fn(() => ({ scheduleActions, getUuid })), done: {}, diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts index 2239f6d8d8fb0..a349c7c48f687 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts @@ -7,80 +7,245 @@ import { setTimeout as setTimeoutPromise } from 'timers/promises'; import { contextServiceMock, executionContextServiceMock } from '@kbn/core/server/mocks'; import { createHttpService } from '@kbn/core-http-server-mocks'; +import type { ElasticsearchClient, KibanaRequest } from '@kbn/core/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { + TermsEnumRequest, + MsearchMultisearchBody, +} from '@elastic/elasticsearch/lib/api/types'; import supertest from 'supertest'; -import { APMEventClient } from '.'; +import { APMEventClient, type APMEventESSearchRequest, type APMEventFieldCapsRequest } from '.'; +import { APMIndices } from '../../../..'; -describe('APMEventClient', () => { - let server: ReturnType; +import * as cancelEsRequestOnAbortModule from '../cancel_es_request_on_abort'; +import * as observabilityPluginModule from '@kbn/observability-plugin/server'; - beforeEach(() => { - server = createHttpService(); - }); +jest.mock('@kbn/observability-plugin/server', () => ({ + __esModule: true, + ...jest.requireActual('@kbn/observability-plugin/server'), +})); - afterEach(async () => { - await server.stop(); - }); - it('cancels a search when a request is aborted', async () => { - await server.preboot({ - context: contextServiceMock.createPrebootContract(), +describe('APMEventClient', () => { + describe('Abort controller', () => { + let server: ReturnType; + beforeEach(() => { + server = createHttpService(); }); - const { server: innerServer, createRouter } = await server.setup({ - context: contextServiceMock.createSetupContract(), - executionContext: executionContextServiceMock.createInternalSetupContract(), + + afterEach(async () => { + await server.stop(); }); - const router = createRouter('/'); - - let abortSignal: AbortSignal | undefined; - router.get({ path: '/', validate: false }, async (context, request, res) => { - const eventClient = new APMEventClient({ - esClient: { - search: async (params: any, { signal }: { signal: AbortSignal }) => { - abortSignal = signal; - await setTimeoutPromise(3_000, undefined, { - signal: abortSignal, - }); - return {}; + + it('cancels a search when a request is aborted', async () => { + await server.preboot({ + context: contextServiceMock.createPrebootContract(), + }); + const { server: innerServer, createRouter } = await server.setup({ + context: contextServiceMock.createSetupContract(), + executionContext: executionContextServiceMock.createInternalSetupContract(), + }); + const router = createRouter('/'); + + let abortSignal: AbortSignal | undefined; + router.get({ path: '/', validate: false }, async (context, request, res) => { + const eventClient = new APMEventClient({ + esClient: { + search: async (params: any, { signal }: { signal: AbortSignal }) => { + abortSignal = signal; + await setTimeoutPromise(3_000, undefined, { + signal: abortSignal, + }); + return {}; + }, + } as any, + debug: false, + request, + indices: {} as APMIndices, + options: { + includeFrozen: false, }, - } as any, + }); + + await eventClient.search('foo', { + apm: { + events: [], + }, + body: { size: 0, track_total_hits: false }, + }); + + return res.ok({ body: 'ok' }); + }); + + await server.start(); + + expect(abortSignal?.aborted).toBeFalsy(); + + const incomingRequest = supertest(innerServer.listener) + .get('/') + // end required to send request + .end(); + + await new Promise((resolve) => { + setTimeout(() => { + void incomingRequest.on('abort', () => { + setTimeout(() => { + resolve(undefined); + }, 100); + }); + + void incomingRequest.abort(); + }, 200); + }); + + expect(abortSignal?.aborted).toBe(true); + }); + }); + + describe('excludedDataTiers filter', () => { + let esClientMock: jest.Mocked; + let apmEventClient: APMEventClient; + let cancelEsRequestOnAbortSpy: jest.SpyInstance; + let unwrapEsResponseSpy: jest.SpyInstance; + + const esResponse: estypes.SearchResponse = { + hits: { + total: { value: 1, relation: 'eq' }, + hits: [{ _source: {}, _index: '' }], + max_score: 1, + }, + took: 1, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + timed_out: false, + }; + + beforeAll(() => { + jest.resetModules(); + }); + + beforeEach(() => { + cancelEsRequestOnAbortSpy = jest + .spyOn(cancelEsRequestOnAbortModule, 'cancelEsRequestOnAbort') + .mockImplementation(jest.fn()); + + unwrapEsResponseSpy = jest + .spyOn(observabilityPluginModule, 'unwrapEsResponse') + .mockImplementation(jest.fn()); + + esClientMock = { + search: jest.fn(), + msearch: jest.fn(), + eql: { search: jest.fn() }, + fieldCaps: jest.fn(), + termsEnum: jest.fn(), + } as unknown as jest.Mocked; + + apmEventClient = new APMEventClient({ + esClient: esClientMock, debug: false, - request, - indices: {} as any, + request: {} as KibanaRequest, + indices: {} as APMIndices, options: { includeFrozen: false, + excludedDataTiers: ['data_warm', 'data_cold'], }, }); + }); + + afterAll(() => { + cancelEsRequestOnAbortSpy.mockReset(); + unwrapEsResponseSpy.mockReset(); + }); - await eventClient.search('foo', { - apm: { - events: [], + it('includes excludedDataTiers filter in search params', async () => { + esClientMock.search.mockResolvedValue(esResponse); + + await apmEventClient.search('testOperation', { + apm: { events: [] }, + body: { + size: 0, + track_total_hits: false, + query: { bool: { filter: [{ match_all: {} }] } }, }, - body: { size: 0, track_total_hits: false }, }); - return res.ok({ body: 'ok' }); + const searchParams = esClientMock.search.mock.calls[0][0] as APMEventESSearchRequest; + + expect(searchParams.body.query?.bool).toEqual({ + filter: [ + { terms: { 'processor.event': [] } }, + { bool: { must_not: [{ terms: { _tier: ['data_warm', 'data_cold'] } }] } }, + ], + must: [{ bool: { filter: [{ match_all: {} }] } }], + }); }); - await server.start(); + it('includes excludedDataTiers filter in msearch params', async () => { + esClientMock.msearch.mockResolvedValue({ responses: [esResponse], took: 1 }); - expect(abortSignal?.aborted).toBeFalsy(); + await apmEventClient.msearch('testOperation', { + apm: { events: [] }, + body: { + size: 0, + track_total_hits: false, + query: { bool: { filter: [{ match_all: {} }] } }, + }, + }); - const incomingRequest = supertest(innerServer.listener) - .get('/') - // end required to send request - .end(); + const msearchParams = esClientMock.msearch.mock.calls[0][0] as { + searches: MsearchMultisearchBody[]; + }; - await new Promise((resolve) => { - setTimeout(() => { - void incomingRequest.on('abort', () => { - setTimeout(() => { - resolve(undefined); - }, 100); - }); + expect(msearchParams.searches[1].query?.bool).toEqual({ + filter: [ + { bool: { filter: [{ match_all: {} }] } }, + { terms: { 'processor.event': [] } }, + { bool: { must_not: [{ terms: { _tier: ['data_warm', 'data_cold'] } }] } }, + ], + }); + }); + + it('includes excludedDataTiers filter in fieldCaps params', async () => { + esClientMock.fieldCaps.mockResolvedValue({ + fields: {}, + indices: '', + }); - void incomingRequest.abort(); - }, 200); + await apmEventClient.fieldCaps('testOperation', { + apm: { events: [] }, + fields: ['field1'], + index_filter: { bool: { filter: [{ match_all: {} }] } }, + }); + + const fieldCapsParams = esClientMock.fieldCaps.mock.calls[0][0] as APMEventFieldCapsRequest; + expect(fieldCapsParams?.index_filter?.bool).toEqual({ + must: [ + { bool: { filter: [{ match_all: {} }] } }, + { bool: { must_not: [{ terms: { _tier: ['data_warm', 'data_cold'] } }] } }, + ], + }); }); - expect(abortSignal?.aborted).toBe(true); + it('includes excludedDataTiers filter in termsEnum params', async () => { + esClientMock.termsEnum.mockResolvedValue({ + terms: [''], + _shards: { total: 1, successful: 1, failed: 0 }, + complete: true, + }); + + await apmEventClient.termsEnum('testOperation', { + apm: { events: [] }, + field: 'field1', + index_filter: { bool: { filter: [{ match_all: {} }] } }, + }); + + const termsEnumParams = esClientMock.termsEnum.mock.calls[0][0] as TermsEnumRequest; + + expect(termsEnumParams.index_filter?.bool).toEqual({ + must: [ + { bool: { filter: [{ match_all: {} }] } }, + { bool: { must_not: [{ terms: { _tier: ['data_warm', 'data_cold'] } }] } }, + ], + }); + }); }); }); diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index d1066207696ef..3d359041df369 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -53,9 +53,9 @@ type APMEventWrapper = Omit & { apm: { events: ProcessorEvent[] }; }; -type APMEventTermsEnumRequest = APMEventWrapper; +export type APMEventTermsEnumRequest = APMEventWrapper; type APMEventEqlSearchRequest = APMEventWrapper; -type APMEventFieldCapsRequest = APMEventWrapper; +export type APMEventFieldCapsRequest = APMEventWrapper; type TypeOfProcessorEvent = { [ProcessorEvent.error]: APMError; @@ -90,7 +90,7 @@ export interface APMEventClientConfig { options: { includeFrozen: boolean; inspectableEsQueriesMap?: WeakMap; - excludedDataTiers?: IndexLifeCycleDataTier; + excludedDataTiers?: IndexLifeCycleDataTier[]; }; } @@ -102,7 +102,7 @@ export class APMEventClient { /** @deprecated Use {@link excludedDataTiers} instead. * See https://www.elastic.co/guide/en/kibana/current/advanced-options.html **/ private readonly includeFrozen: boolean; - private readonly excludedDataTiers?: IndexLifeCycleDataTier; + private readonly excludedDataTiers?: IndexLifeCycleDataTier[]; private readonly inspectableEsQueriesMap?: WeakMap; constructor(config: APMEventClientConfig) { diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/tier_filter.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/tier_filter.ts index e4f4e881d3465..a57fdd2f504ad 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/tier_filter.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/tier_filter.ts @@ -8,7 +8,7 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type import type { IndexLifeCycleDataTier } from '@kbn/observability-shared-plugin/common'; export function getExcludedDataTiersFilter( - excludedDataTiers: IndexLifeCycleDataTier + excludedDataTiers: IndexLifeCycleDataTier[] ): QueryDslQueryContainer { return { bool: { @@ -28,7 +28,7 @@ export function getIndexFilter({ excludedDataTiers, }: { indexFilter?: QueryDslQueryContainer; - excludedDataTiers?: IndexLifeCycleDataTier; + excludedDataTiers?: IndexLifeCycleDataTier[]; }): QueryDslQueryContainer | undefined { if (!indexFilter) { return excludedDataTiers ? getExcludedDataTiersFilter(excludedDataTiers) : undefined; diff --git a/x-pack/plugins/observability_solution/profiling/common/storage_explorer.ts b/x-pack/plugins/observability_solution/profiling/common/storage_explorer.ts index f55f2da1b37a0..7705988274c41 100644 --- a/x-pack/plugins/observability_solution/profiling/common/storage_explorer.ts +++ b/x-pack/plugins/observability_solution/profiling/common/storage_explorer.ts @@ -10,7 +10,7 @@ import { } from '@kbn/observability-shared-plugin/common'; import * as t from 'io-ts'; -export { IndexLifecyclePhaseSelectOption, type indexLifeCyclePhaseToDataTier }; +export { IndexLifecyclePhaseSelectOption, indexLifeCyclePhaseToDataTier }; export const indexLifecyclePhaseRt = t.type({ indexLifecyclePhase: t.union([ t.literal(IndexLifecyclePhaseSelectOption.All),