diff --git a/src/plugins/profiling/server/routes/mappings.ts b/src/plugins/profiling/server/routes/mappings.ts index c52ec4d494b83..5c10bd4f8fed0 100644 --- a/src/plugins/profiling/server/routes/mappings.ts +++ b/src/plugins/profiling/server/routes/mappings.ts @@ -6,6 +6,93 @@ * Side Public License, v 1. */ +import { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/types'; + +interface ProjectTimeQuery { + bool: { + must: Array< + | { + term: { + ProjectID: { + value: string; + boost: number; + }; + }; + } + | { + range: { + '@timestamp': { + gte: string; + lt: string; + format: string; + boost: number; + }; + }; + } + >; + }; +} + +export function newProjectTimeQuery( + projectID: string, + timeFrom: string, + timeTo: string +): ProjectTimeQuery { + return { + bool: { + must: [ + { + term: { + ProjectID: { + value: projectID, + boost: 1.0, + }, + }, + }, + { + range: { + '@timestamp': { + gte: timeFrom, + lt: timeTo, + format: 'epoch_second', + boost: 1.0, + }, + }, + }, + ], + }, + } as ProjectTimeQuery; +} + +export function autoHistogramSumCountOnGroupByField( + searchField: string +): AggregationsAggregationContainer { + return { + auto_date_histogram: { + field: '@timestamp', + buckets: 100, + }, + aggs: { + group_by: { + terms: { + field: searchField, + order: { + Count: 'desc', + }, + size: 100, + }, + aggs: { + Count: { + sum: { + field: 'Count', + }, + }, + }, + }, + }, + }; +} + function getExeFileName(obj) { if (obj.ExeFileName === undefined) { return ''; diff --git a/src/plugins/profiling/server/routes/search_flamechart.ts b/src/plugins/profiling/server/routes/search_flamechart.ts index 181bb047d4c98..eb5f73495d1a6 100644 --- a/src/plugins/profiling/server/routes/search_flamechart.ts +++ b/src/plugins/profiling/server/routes/search_flamechart.ts @@ -10,58 +10,7 @@ import type { IRouter } from 'kibana/server'; import type { DataRequestHandlerContext } from '../../../data/server'; import { getRemoteRoutePaths } from '../../common'; import { FlameGraph } from './flamegraph'; - -interface FilterObject { - bool: { - must: Array< - | { - term: { - ProjectID: { - value: string; - boost: number; - }; - }; - } - | { - range: { - '@timestamp': { - gte: string; - lt: string; - format: string; - boost: number; - }; - }; - } - >; - }; -} - -function createFilterObject(projectID: string, timeFrom: string, timeTo: string): FilterObject { - return { - bool: { - must: [ - { - term: { - ProjectID: { - value: projectID, - boost: 1.0, - }, - }, - }, - { - range: { - '@timestamp': { - gte: timeFrom, - lt: timeTo, - format: 'epoch_second', - boost: 1.0, - }, - }, - }, - ], - }, - } as FilterObject; -} +import { newProjectTimeQuery } from './mappings'; function getSampledTraceEventsIndex( sampleSize: number, @@ -126,7 +75,7 @@ export function registerFlameChartSearchRoute(router: IRouter ({ + newProjectTimeQuery: (proj: string, from: string, to: string) => { + return anyQuery; + }, + autoHistogramSumCountOnGroupByField: (searchField: string): AggregationsAggregationContainer => { + return testAgg; + }, +})); + +function mockTopNData() { + return { + core: { + elasticsearch: { + client: { + asCurrentUser: { + search: jest.fn().mockResolvedValue({ + body: { + aggregations: { + histogram: { + buckets: [ + { + key_as_string: '1644506880', + key: 1644506880000, + doc_count: 700, + group_by: { + buckets: [ + { + key: 'vyHke_Kdp2c05tXV7a_Rkg==', + doc_count: 10, + Count: { + value: 100.0, + }, + }, + ], + }, + }, + ], + }, + }, + }, + }), + mget: jest.fn().mockResolvedValue({ + body: { + docs: [], + }, + }), + }, + }, + }, + }, + }; +} + +describe('TopN data from Elasticsearch', () => { + const mock = mockTopNData(); + const queryMock = mock as unknown as DataRequestHandlerContext; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('building the query', () => { + it('filters by projectID and aggregates timerange on histogram', async () => { + await topNElasticSearchQuery( + queryMock, + index, + '123', + '456', + '789', + 'field', + kibanaResponseFactory + ); + expect(mock.core.elasticsearch.client.asCurrentUser.search).toHaveBeenCalledWith({ + index, + body: { + query: anyQuery, + aggs: { + histogram: testAgg, + }, + }, + }); + }); + }); + describe('when fetching Stack Traces', () => { + it('should search first then mget', async () => { + await topNElasticSearchQuery( + queryMock, + index, + '123', + '456', + '789', + 'StackTraceID', + kibanaResponseFactory + ); + expect(mock.core.elasticsearch.client.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mock.core.elasticsearch.client.asCurrentUser.mget).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/plugins/profiling/server/routes/search_topn.ts b/src/plugins/profiling/server/routes/search_topn.ts index 9d4108a5a788a..62b1d65df6e6b 100644 --- a/src/plugins/profiling/server/routes/search_topn.ts +++ b/src/plugins/profiling/server/routes/search_topn.ts @@ -6,11 +6,65 @@ * Side Public License, v 1. */ import { schema } from '@kbn/config-schema'; -import type { IRouter } from 'kibana/server'; -import { IEsSearchRequest } from '../../../data/server'; -import { IEsSearchResponse } from '../../../data/common'; +import type { IRouter, KibanaResponseFactory } from 'kibana/server'; +import { + AggregationsHistogramBucket, + AggregationsMultiBucketAggregateBase, +} from '@elastic/elasticsearch/lib/api/types'; import type { DataRequestHandlerContext } from '../../../data/server'; import { getRemoteRoutePaths } from '../../common'; +import { newProjectTimeQuery, autoHistogramSumCountOnGroupByField } from './mappings'; + +export async function topNElasticSearchQuery( + context: DataRequestHandlerContext, + index: string, + projectID: string, + timeFrom: string, + timeTo: string, + searchField: string, + response: KibanaResponseFactory +) { + const esClient = context.core.elasticsearch.client.asCurrentUser; + const resTopNStackTraces = await esClient.search({ + index, + body: { + query: newProjectTimeQuery(projectID, timeFrom, timeTo), + aggs: { + histogram: autoHistogramSumCountOnGroupByField(searchField), + }, + }, + }); + + if (searchField === 'StackTraceID') { + const autoDateHistogram = resTopNStackTraces.body.aggregations + ?.histogram as AggregationsMultiBucketAggregateBase; + + const docIDs: string[] = []; + autoDateHistogram.buckets?.forEach((timeInterval: any) => { + timeInterval.group_by.buckets.forEach((stackTraceItem: any) => { + docIDs.push(stackTraceItem.key); + }); + }); + + const resTraceMetadata = await esClient.mget({ + index: 'profiling-stacktraces', + body: { ids: docIDs }, + }); + + return response.ok({ + body: { + topN: resTopNStackTraces.body.aggregations, + traceMetadata: resTraceMetadata.body.docs, + }, + }); + } else { + return response.ok({ + body: { + topN: resTopNStackTraces.body.aggregations, + }, + }); + } +} export function queryTopNCommon( router: IRouter, @@ -33,97 +87,15 @@ export function queryTopNCommon( const { index, projectID, timeFrom, timeTo } = request.query; try { - const resTopNStackTraces = await context - .search!.search( - { - params: { - index, - body: { - query: { - bool: { - must: [ - { - term: { - ProjectID: { - value: projectID, - boost: 1.0, - }, - }, - }, - { - range: { - '@timestamp': { - gte: timeFrom, - lt: timeTo, - format: 'epoch_second', - boost: 1.0, - }, - }, - }, - ], - }, - }, - aggs: { - histogram: { - auto_date_histogram: { - field: '@timestamp', - buckets: 100, - }, - aggs: { - group_by: { - terms: { - field: searchField, - order: { - Count: 'desc', - }, - size: 100, - }, - aggs: { - Count: { - sum: { - field: 'Count', - }, - }, - }, - }, - }, - }, - }, - }, - }, - } as IEsSearchRequest, - {} - ) - .toPromise(); - - if (searchField === 'StackTraceID') { - const docIDs: string[] = []; - resTopNStackTraces.rawResponse.aggregations.histogram.buckets.forEach((timeInterval) => { - timeInterval.group_by.buckets.forEach((stackTraceItem: any) => { - docIDs.push(stackTraceItem.key); - }); - }); - - const esClient = context.core.elasticsearch.client.asCurrentUser; - - const resTraceMetadata = await esClient.mget({ - index: 'profiling-stacktraces', - body: { ids: docIDs }, - }); - - return response.ok({ - body: { - topN: (resTopNStackTraces as IEsSearchResponse).rawResponse.aggregations, - traceMetadata: resTraceMetadata.body.docs, - }, - }); - } else { - return response.ok({ - body: { - topN: (resTopNStackTraces as IEsSearchResponse).rawResponse.aggregations, - }, - }); - } + return await topNElasticSearchQuery( + context, + index!, + projectID!, + timeFrom!, + timeTo!, + searchField, + response + ); } catch (e) { return response.customError({ statusCode: e.statusCode ?? 500,