mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[UX Dashboard] Migrate UX hasData
function out of APM server (#134524)
* Remove dependence on APM route for UX `hasData` call. * Simplify type import. * Migrate UX `hasRumData` hook away from APM server call. * Delete `hasRumData` route from APM server. * remove * update query Co-authored-by: Shahzad <shahzad.muhammad@elastic.co>
This commit is contained in:
parent
d9c0f14779
commit
212eef1808
8 changed files with 150 additions and 225 deletions
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import { SetupUX } from './route';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types';
|
||||
|
||||
export async function hasRumData({
|
||||
setup,
|
||||
start = moment().subtract(24, 'h').valueOf(),
|
||||
end = moment().valueOf(),
|
||||
}: {
|
||||
setup: SetupUX;
|
||||
start?: number;
|
||||
end?: number;
|
||||
}) {
|
||||
try {
|
||||
const params = {
|
||||
apm: {
|
||||
events: [ProcessorEvent.transaction],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
services: {
|
||||
filter: rangeQuery(start, end)[0],
|
||||
aggs: {
|
||||
mostTraffic: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const response = await apmEventClient.search('has_rum_data', params);
|
||||
return {
|
||||
indices: setup.indices.transaction,
|
||||
hasData: response.hits.total.value > 0,
|
||||
serviceName:
|
||||
response.aggregations?.services?.mostTraffic?.buckets?.[0]?.key,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
hasData: false,
|
||||
serviceName: undefined,
|
||||
indices: setup.indices.transaction,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { isoToEpochRt } from '@kbn/io-ts-utils';
|
||||
import { setupRequest, Setup } from '../../lib/helpers/setup_request';
|
||||
import { getClientMetrics } from './get_client_metrics';
|
||||
import { getLongTaskMetrics } from './get_long_task_metrics';
|
||||
|
@ -14,7 +13,6 @@ import { getPageLoadDistribution } from './get_page_load_distribution';
|
|||
import { getPageViewTrends } from './get_page_view_trends';
|
||||
import { getPageLoadDistBreakdown } from './get_pl_dist_breakdown';
|
||||
import { getVisitorBreakdown } from './get_visitor_breakdown';
|
||||
import { hasRumData } from './has_rum_data';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { rangeRt } from '../default_api_types';
|
||||
import { APMRouteHandlerResources } from '../typings';
|
||||
|
@ -242,31 +240,6 @@ const rumLongTaskMetrics = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const rumHasDataRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
uiFilters: t.string,
|
||||
start: isoToEpochRt,
|
||||
end: isoToEpochRt,
|
||||
}),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
indices: string;
|
||||
hasData: boolean;
|
||||
serviceName: string | number | undefined;
|
||||
}> => {
|
||||
const setup = await setupUXRequest(resources);
|
||||
const {
|
||||
query: { start, end },
|
||||
} = resources.params;
|
||||
return await hasRumData({ setup, start, end });
|
||||
},
|
||||
});
|
||||
|
||||
function decodeUiFilters(
|
||||
logger: Logger,
|
||||
uiFiltersEncoded?: string
|
||||
|
@ -303,5 +276,4 @@ export const rumRouteRepository = {
|
|||
...rumPageViewsTrendRoute,
|
||||
...rumVisitorsBreakdownRoute,
|
||||
...rumLongTaskMetrics,
|
||||
...rumHasDataRoute,
|
||||
};
|
||||
|
|
|
@ -5,10 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useEsSearch } from '@kbn/observability-plugin/public';
|
||||
import {
|
||||
formatHasRumResult,
|
||||
hasRumDataQuery,
|
||||
} from '../../../../services/data/has_rum_data_query';
|
||||
import { useDataView } from '../local_uifilters/use_data_view';
|
||||
|
||||
export function useHasRumData() {
|
||||
return useFetcher((callApmApi) => {
|
||||
return callApmApi('GET /api/apm/observability_overview/has_rum_data', {});
|
||||
}, []);
|
||||
const { dataViewTitle } = useDataView();
|
||||
const { data: response, loading } = useEsSearch(
|
||||
{
|
||||
index: dataViewTitle,
|
||||
...hasRumDataQuery({}),
|
||||
},
|
||||
[dataViewTitle],
|
||||
{
|
||||
name: 'UXHasRumData',
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
data: formatHasRumResult(response, dataViewTitle),
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export function RumHome() {
|
|||
|
||||
const PageTemplateComponent = observability.navigation.PageTemplate;
|
||||
|
||||
const { data: rumHasData, status } = useHasRumData();
|
||||
const { data: rumHasData, loading: isLoading } = useHasRumData();
|
||||
|
||||
const noDataConfig: KibanaPageTemplateProps['noDataConfig'] =
|
||||
!rumHasData?.hasData
|
||||
|
@ -56,8 +56,6 @@ export function RumHome() {
|
|||
}
|
||||
: undefined;
|
||||
|
||||
const isLoading = status === 'loading';
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CsmSharedContextProvider>
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
DataPublicPluginStart,
|
||||
isCompleteResponse,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { IKibanaSearchRequest } from '@kbn/data-plugin/common';
|
||||
import {
|
||||
FetchDataParams,
|
||||
HasDataParams,
|
||||
|
@ -23,49 +24,30 @@ import {
|
|||
DEFAULT_RANKS,
|
||||
} from '../../../services/data/core_web_vitals_query';
|
||||
import { callApmApi } from '../../../services/rest/create_call_apm_api';
|
||||
import {
|
||||
formatHasRumResult,
|
||||
hasRumDataQuery,
|
||||
} from '../../../services/data/has_rum_data_query';
|
||||
|
||||
export { createCallApmApi } from '../../../services/rest/create_call_apm_api';
|
||||
|
||||
type FetchUxOverviewDateParams = FetchDataParams & {
|
||||
dataStartPlugin: DataPublicPluginStart;
|
||||
};
|
||||
type WithDataPlugin<T> = T & { dataStartPlugin: DataPublicPluginStart };
|
||||
|
||||
async function getCoreWebVitalsResponse({
|
||||
absoluteTime,
|
||||
serviceName,
|
||||
dataStartPlugin,
|
||||
}: FetchUxOverviewDateParams) {
|
||||
}: WithDataPlugin<FetchDataParams>) {
|
||||
const dataView = await callApmApi('GET /internal/apm/data_view/dynamic', {
|
||||
signal: null,
|
||||
});
|
||||
return new Promise<
|
||||
ESSearchResponse<{}, ReturnType<typeof coreWebVitalsQuery>>
|
||||
>((resolve) => {
|
||||
const search$ = dataStartPlugin.search
|
||||
.search(
|
||||
{
|
||||
params: {
|
||||
index: dataView.dynamicDataView?.title,
|
||||
...coreWebVitalsQuery(
|
||||
absoluteTime.start,
|
||||
absoluteTime.end,
|
||||
undefined,
|
||||
{
|
||||
serviceName: serviceName ? [serviceName] : undefined,
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
{}
|
||||
)
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
if (isCompleteResponse(result)) {
|
||||
resolve(result.rawResponse as any);
|
||||
search$.unsubscribe();
|
||||
}
|
||||
},
|
||||
});
|
||||
return await esQuery<ReturnType<typeof coreWebVitalsQuery>>(dataStartPlugin, {
|
||||
params: {
|
||||
index: dataView.dynamicDataView?.title,
|
||||
...coreWebVitalsQuery(absoluteTime.start, absoluteTime.end, undefined, {
|
||||
serviceName: serviceName ? [serviceName] : undefined,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -82,7 +64,7 @@ const CORE_WEB_VITALS_DEFAULTS: UXMetrics = {
|
|||
};
|
||||
|
||||
export const fetchUxOverviewDate = async (
|
||||
params: FetchUxOverviewDateParams
|
||||
params: WithDataPlugin<FetchDataParams>
|
||||
): Promise<UxFetchDataResponse> => {
|
||||
const coreWebVitalsResponse = await getCoreWebVitalsResponse(params);
|
||||
return {
|
||||
|
@ -94,18 +76,42 @@ export const fetchUxOverviewDate = async (
|
|||
};
|
||||
|
||||
export async function hasRumData(
|
||||
params: HasDataParams
|
||||
params: WithDataPlugin<HasDataParams>
|
||||
): Promise<UXHasDataResponse> {
|
||||
return await callApmApi('GET /api/apm/observability_overview/has_rum_data', {
|
||||
const dataView = await callApmApi('GET /internal/apm/data_view/dynamic', {
|
||||
signal: null,
|
||||
params: {
|
||||
query: params?.absoluteTime
|
||||
? {
|
||||
start: new Date(params.absoluteTime.start).toISOString(),
|
||||
end: new Date(params.absoluteTime.end).toISOString(),
|
||||
uiFilters: '',
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
const esQueryResponse = await esQuery<ReturnType<typeof hasRumDataQuery>>(
|
||||
params.dataStartPlugin,
|
||||
{
|
||||
params: {
|
||||
index: dataView.dynamicDataView?.title,
|
||||
...hasRumDataQuery({
|
||||
start: params?.absoluteTime?.start,
|
||||
end: params?.absoluteTime?.end,
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return formatHasRumResult(esQueryResponse, dataView.dynamicDataView?.title);
|
||||
}
|
||||
|
||||
async function esQuery<T>(
|
||||
dataStartPlugin: DataPublicPluginStart,
|
||||
query: IKibanaSearchRequest<T> & { params: { index?: string } }
|
||||
) {
|
||||
return new Promise<ESSearchResponse<{}, T>>((resolve, reject) => {
|
||||
const search$ = dataStartPlugin.search.search(query).subscribe({
|
||||
next: (result) => {
|
||||
if (isCompleteResponse(result)) {
|
||||
resolve(result.rawResponse as any);
|
||||
search$.unsubscribe();
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -56,6 +56,11 @@ export interface ApmPluginStartDeps {
|
|||
dataViews: DataViewsPublicPluginStart;
|
||||
}
|
||||
|
||||
async function getDataStartPlugin(core: CoreSetup) {
|
||||
const [_, startPlugins] = await core.getStartServices();
|
||||
return (startPlugins as ApmPluginStartDeps).data;
|
||||
}
|
||||
|
||||
export class UxPlugin implements Plugin<UxPluginSetup, UxPluginStart> {
|
||||
constructor() {}
|
||||
|
||||
|
@ -76,14 +81,16 @@ export class UxPlugin implements Plugin<UxPluginSetup, UxPluginStart> {
|
|||
appName: 'ux',
|
||||
hasData: async (params?: HasDataParams) => {
|
||||
const dataHelper = await getUxDataHelper();
|
||||
return await dataHelper.hasRumData(params!);
|
||||
const dataStartPlugin = await getDataStartPlugin(core);
|
||||
return dataHelper.hasRumData({
|
||||
...params!,
|
||||
dataStartPlugin,
|
||||
});
|
||||
},
|
||||
fetchData: async (params: FetchDataParams) => {
|
||||
const [_, startPlugins] = await core.getStartServices();
|
||||
|
||||
const { data: dataStartPlugin } = startPlugins as ApmPluginStartDeps;
|
||||
const dataStartPlugin = await getDataStartPlugin(core);
|
||||
const dataHelper = await getUxDataHelper();
|
||||
return await dataHelper.fetchUxOverviewDate({
|
||||
return dataHelper.fetchUxOverviewDate({
|
||||
...params,
|
||||
dataStartPlugin,
|
||||
});
|
||||
|
|
65
x-pack/plugins/ux/public/services/data/has_rum_data_query.ts
Normal file
65
x-pack/plugins/ux/public/services/data/has_rum_data_query.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { ESSearchResponse } from '@kbn/core/types/elasticsearch';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
PROCESSOR_EVENT,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types';
|
||||
import { rangeQuery } from './range_query';
|
||||
|
||||
export function formatHasRumResult<T>(
|
||||
esResult: ESSearchResponse<T, ReturnType<typeof hasRumDataQuery>>,
|
||||
indices?: string
|
||||
) {
|
||||
if (!esResult) return esResult;
|
||||
return {
|
||||
indices,
|
||||
// @ts-ignore total.value is undefined by the returned type, total is a `number`
|
||||
hasData: esResult.hits.total > 0,
|
||||
serviceName:
|
||||
esResult.aggregations?.services?.mostTraffic?.buckets?.[0]?.key,
|
||||
};
|
||||
}
|
||||
|
||||
export function hasRumDataQuery({
|
||||
start = moment().subtract(24, 'h').valueOf(),
|
||||
end = moment().valueOf(),
|
||||
}: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
}) {
|
||||
return {
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } },
|
||||
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
services: {
|
||||
filter: rangeQuery(start, end)[0],
|
||||
aggs: {
|
||||
mostTraffic: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export default function rumHasDataApiTests({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
|
||||
registry.when('has_rum_data without data', { config: 'trial', archives: [] }, () => {
|
||||
it('returns empty list', async () => {
|
||||
const start = new Date('2020-09-07T00:00:00.000Z').getTime();
|
||||
const end = new Date('2020-09-14T00:00:00.000Z').getTime() - 1;
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"hasData": false,
|
||||
"indices": "traces-apm*,apm-*",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(
|
||||
'has RUM data with data',
|
||||
{ config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] },
|
||||
() => {
|
||||
it('returns that it has data and service name with most traffic', async () => {
|
||||
const start = new Date('2020-09-07T00:00:00.000Z').getTime();
|
||||
const end = new Date('2020-09-16T00:00:00.000Z').getTime() - 1;
|
||||
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"hasData": true,
|
||||
"indices": "traces-apm*,apm-*",
|
||||
"serviceName": "client",
|
||||
}
|
||||
`);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue