Skip to content

Commit

Permalink
[APM]Create API to return data to be used on the Overview page (#69137)
Browse files Browse the repository at this point in the history
* Adding apm data fetcher

* removing error rate

* chaging observability dashboard routes

* APM observability fetch data

* fixing imports

* adding unit test

* addressing PR comments

* adding processor event in the query, and refactoring theme

* fixing ts issues

* fixing unit tests
  • Loading branch information
cauemarcondes authored Jun 26, 2020
1 parent 8448ae8 commit 41ecf39
Show file tree
Hide file tree
Showing 10 changed files with 409 additions and 2 deletions.
18 changes: 18 additions & 0 deletions x-pack/plugins/apm/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ import { setHelpExtension } from './setHelpExtension';
import { toggleAppLinkInNav } from './toggleAppLinkInNav';
import { setReadonlyBadge } from './updateBadge';
import { createStaticIndexPattern } from './services/rest/index_pattern';
import {
fetchLandingPageData,
hasData,
} from './services/rest/observability_dashboard';
import { getTheme } from './utils/get_theme';

export type ApmPluginSetup = void;
export type ApmPluginStart = void;
Expand Down Expand Up @@ -73,6 +78,19 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
pluginSetupDeps.home.environment.update({ apmUi: true });
pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry);

if (plugins.observability) {
const theme = getTheme({
isDarkMode: core.uiSettings.get('theme:darkMode'),
});
plugins.observability.dashboard.register({
appName: 'apm',
fetchData: async (params) => {
return fetchLandingPageData(params, { theme });
},
hasData,
});
}

core.application.register({
id: 'apm',
title: 'APM',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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 { fetchLandingPageData, hasData } from './observability_dashboard';
import * as createCallApmApi from './createCallApmApi';
import { getTheme } from '../../utils/get_theme';

const theme = getTheme({ isDarkMode: false });

describe('Observability dashboard data', () => {
const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi');
afterEach(() => {
callApmApiMock.mockClear();
});
describe('hasData', () => {
it('returns false when no data is available', async () => {
callApmApiMock.mockImplementation(() => Promise.resolve(false));
const response = await hasData();
expect(response).toBeFalsy();
});
it('returns true when data is available', async () => {
callApmApiMock.mockImplementation(() => Promise.resolve(true));
const response = await hasData();
expect(response).toBeTruthy();
});
});

describe('fetchLandingPageData', () => {
it('returns APM data with series and stats', async () => {
callApmApiMock.mockImplementation(() =>
Promise.resolve({
serviceCount: 10,
transactionCoordinates: [
{ x: 1, y: 1 },
{ x: 2, y: 2 },
{ x: 3, y: 3 },
],
})
);
const response = await fetchLandingPageData(
{
startTime: '1',
endTime: '2',
bucketSize: '3',
},
{ theme }
);
expect(response).toEqual({
title: 'APM',
appLink: '/app/apm',
stats: {
services: {
type: 'number',
label: 'Services',
value: 10,
},
transactions: {
type: 'number',
label: 'Transactions',
value: 6,
color: '#6092c0',
},
},
series: {
transactions: {
label: 'Transactions',
coordinates: [
{ x: 1, y: 1 },
{ x: 2, y: 2 },
{ x: 3, y: 3 },
],
color: '#6092c0',
},
},
});
});
it('returns empty transaction coordinates', async () => {
callApmApiMock.mockImplementation(() =>
Promise.resolve({
serviceCount: 0,
transactionCoordinates: [],
})
);
const response = await fetchLandingPageData(
{
startTime: '1',
endTime: '2',
bucketSize: '3',
},
{ theme }
);
expect(response).toEqual({
title: 'APM',
appLink: '/app/apm',
stats: {
services: {
type: 'number',
label: 'Services',
value: 0,
},
transactions: {
type: 'number',
label: 'Transactions',
value: 0,
color: '#6092c0',
},
},
series: {
transactions: {
label: 'Transactions',
coordinates: [],
color: '#6092c0',
},
},
});
});
});
});
71 changes: 71 additions & 0 deletions x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { sum } from 'lodash';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { FetchDataParams } from '../../../../observability/public/data_handler';
import { ApmFetchDataResponse } from '../../../../observability/public/typings/fetch_data_response';
import { callApmApi } from './createCallApmApi';
import { Theme } from '../../utils/get_theme';

interface Options {
theme: Theme;
}

export const fetchLandingPageData = async (
{ startTime, endTime, bucketSize }: FetchDataParams,
{ theme }: Options
): Promise<ApmFetchDataResponse> => {
const data = await callApmApi({
pathname: '/api/apm/observability_dashboard',
params: { query: { start: startTime, end: endTime, bucketSize } },
});

const { serviceCount, transactionCoordinates } = data;

return {
title: i18n.translate('xpack.apm.observabilityDashboard.title', {
defaultMessage: 'APM',
}),
appLink: '/app/apm',
stats: {
services: {
type: 'number',
label: i18n.translate(
'xpack.apm.observabilityDashboard.stats.services',
{ defaultMessage: 'Services' }
),
value: serviceCount,
},
transactions: {
type: 'number',
label: i18n.translate(
'xpack.apm.observabilityDashboard.stats.transactions',
{ defaultMessage: 'Transactions' }
),
value: sum(transactionCoordinates.map((coordinates) => coordinates.y)),
color: theme.euiColorVis1,
},
},
series: {
transactions: {
label: i18n.translate(
'xpack.apm.observabilityDashboard.chart.transactions',
{ defaultMessage: 'Transactions' }
),
color: theme.euiColorVis1,
coordinates: transactionCoordinates,
},
},
};
};

export async function hasData() {
return await callApmApi({
pathname: '/api/apm/observability_dashboard/has_data',
});
}
13 changes: 13 additions & 0 deletions x-pack/plugins/apm/public/utils/get_theme.ts
Original file line number Diff line number Diff line change
@@ -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 lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';

export type Theme = ReturnType<typeof getTheme>;

export function getTheme({ isDarkMode }: { isDarkMode: boolean }) {
return isDarkMode ? darkTheme : lightTheme;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 { ProcessorEvent } from '../../../common/processor_event';
import { rangeFilter } from '../../../common/utils/range_filter';
import {
SERVICE_NAME,
PROCESSOR_EVENT,
} from '../../../common/elasticsearch_fieldnames';
import { Setup, SetupTimeRange } from '../helpers/setup_request';

export async function getServiceCount({
setup,
}: {
setup: Setup & SetupTimeRange;
}) {
const { client, indices, start, end } = setup;

const params = {
index: [
indices['apm_oss.transactionIndices'],
indices['apm_oss.errorIndices'],
indices['apm_oss.metricsIndices'],
],
body: {
size: 0,
query: {
bool: {
filter: [
{ range: rangeFilter(start, end) },
{
terms: {
[PROCESSOR_EVENT]: [
ProcessorEvent.error,
ProcessorEvent.transaction,
ProcessorEvent.metric,
],
},
},
],
},
},
aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } },
},
};

const { aggregations } = await client.search(params);
return aggregations?.serviceCount.value || 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.
*/
/*
* 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 { rangeFilter } from '../../../common/utils/range_filter';
import { Coordinates } from '../../../../observability/public/typings/fetch_data_response';
import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { ProcessorEvent } from '../../../common/processor_event';

export async function getTransactionCoordinates({
setup,
bucketSize,
}: {
setup: Setup & SetupTimeRange;
bucketSize: string;
}): Promise<Coordinates[]> {
const { client, indices, start, end } = setup;

const { aggregations } = await client.search({
index: indices['apm_oss.transactionIndices'],
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
{ range: rangeFilter(start, end) },
],
},
},
aggs: {
distribution: {
date_histogram: {
field: '@timestamp',
fixed_interval: bucketSize,
min_doc_count: 0,
extended_bounds: { min: start, max: end },
},
},
},
},
});

return (
aggregations?.distribution.buckets.map((bucket) => ({
x: bucket.key,
y: bucket.doc_count,
})) || []
);
}
26 changes: 26 additions & 0 deletions x-pack/plugins/apm/server/lib/observability_dashboard/has_data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 { Setup } from '../helpers/setup_request';

export async function hasData({ setup }: { setup: Setup }) {
const { client, indices } = setup;
try {
const params = {
index: [
indices['apm_oss.transactionIndices'],
indices['apm_oss.errorIndices'],
indices['apm_oss.metricsIndices'],
],
terminateAfter: 1,
size: 0,
};

const response = await client.search(params);
return response.hits.total.value > 0;
} catch (e) {
return false;
}
}
Loading

0 comments on commit 41ecf39

Please sign in to comment.