mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [Migrate `/custom_dashboards` to be deployment agnostic (#199295)](https://github.com/elastic/kibana/pull/199295) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Milosz Marcinkowski","email":"38698566+miloszmarcinkowski@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-11-08T17:40:55Z","message":"Migrate `/custom_dashboards` to be deployment agnostic (#199295)\n\ncloses #198964 \r\ncloses #198966\r\npart of https://github.com/elastic/kibana/issues/193245\r\n\r\n### How to test\r\n\r\n- Serverless\r\n\r\n```\r\nnode scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts\r\nnode scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep=\"APM\"\r\n```\r\n\r\n- Stateful\r\n```\r\nnode scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts\r\nnode scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep=\"APM\"\r\n```\r\n\r\n- MKI\r\n\r\ntested against\r\n[MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)","sha":"a9475259368c992ac3761eb3b20fd52e6ed1c1d8","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","apm","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services"],"number":199295,"url":"https://github.com/elastic/kibana/pull/199295","mergeCommit":{"message":"Migrate `/custom_dashboards` to be deployment agnostic (#199295)\n\ncloses #198964 \r\ncloses #198966\r\npart of https://github.com/elastic/kibana/issues/193245\r\n\r\n### How to test\r\n\r\n- Serverless\r\n\r\n```\r\nnode scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts\r\nnode scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep=\"APM\"\r\n```\r\n\r\n- Stateful\r\n```\r\nnode scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts\r\nnode scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep=\"APM\"\r\n```\r\n\r\n- MKI\r\n\r\ntested against\r\n[MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)","sha":"a9475259368c992ac3761eb3b20fd52e6ed1c1d8"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/199295","number":199295,"mergeCommit":{"message":"Migrate `/custom_dashboards` to be deployment agnostic (#199295)\n\ncloses #198964 \r\ncloses #198966\r\npart of https://github.com/elastic/kibana/issues/193245\r\n\r\n### How to test\r\n\r\n- Serverless\r\n\r\n```\r\nnode scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts\r\nnode scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep=\"APM\"\r\n```\r\n\r\n- Stateful\r\n```\r\nnode scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts\r\nnode scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep=\"APM\"\r\n```\r\n\r\n- MKI\r\n\r\ntested against\r\n[MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)","sha":"a9475259368c992ac3761eb3b20fd52e6ed1c1d8"}}]}] BACKPORT-->
This commit is contained in:
parent
3b033b4116
commit
f75f8ba966
19 changed files with 896 additions and 894 deletions
|
@ -5,7 +5,9 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ApmApiClient } from '../../common/config';
|
import { ApmApiProvider } from '../../../../services/apm_api';
|
||||||
|
|
||||||
|
export type ApmApiClient = ReturnType<typeof ApmApiProvider>;
|
||||||
|
|
||||||
export async function getServiceDashboardApi(
|
export async function getServiceDashboardApi(
|
||||||
apmApiClient: ApmApiClient,
|
apmApiClient: ApmApiClient,
|
|
@ -0,0 +1,193 @@
|
||||||
|
/*
|
||||||
|
* 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 { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||||
|
|
||||||
|
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||||
|
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
|
import {
|
||||||
|
getServiceDashboardApi,
|
||||||
|
getLinkServiceDashboardApi,
|
||||||
|
deleteAllServiceDashboard,
|
||||||
|
} from './api_helper';
|
||||||
|
|
||||||
|
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||||
|
const apmApiClient = getService('apmApi');
|
||||||
|
const synthtrace = getService('synthtrace');
|
||||||
|
|
||||||
|
const start = '2023-08-22T00:00:00.000Z';
|
||||||
|
const end = '2023-08-22T00:15:00.000Z';
|
||||||
|
|
||||||
|
describe('Service dashboards', () => {
|
||||||
|
describe('when data is not loaded', () => {
|
||||||
|
it('handles empty state', async () => {
|
||||||
|
const response = await getServiceDashboardApi(apmApiClient, 'synth-go', start, end);
|
||||||
|
expect(response.status).to.be(200);
|
||||||
|
expect(response.body.serviceDashboards).to.eql([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when data is loaded', () => {
|
||||||
|
const range = timerange(new Date(start).getTime(), new Date(end).getTime());
|
||||||
|
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||||
|
|
||||||
|
const goInstance = apm
|
||||||
|
.service({
|
||||||
|
name: 'synth-go',
|
||||||
|
environment: 'production',
|
||||||
|
agentName: 'go',
|
||||||
|
})
|
||||||
|
.instance('go-instance');
|
||||||
|
|
||||||
|
const javaInstance = apm
|
||||||
|
.service({
|
||||||
|
name: 'synth-java',
|
||||||
|
environment: 'production',
|
||||||
|
agentName: 'java',
|
||||||
|
})
|
||||||
|
.instance('java-instance');
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||||
|
|
||||||
|
return apmSynthtraceEsClient.index([
|
||||||
|
range
|
||||||
|
.interval('1s')
|
||||||
|
.rate(4)
|
||||||
|
.generator((timestamp) =>
|
||||||
|
goInstance
|
||||||
|
.transaction({ transactionName: 'GET /api' })
|
||||||
|
.timestamp(timestamp)
|
||||||
|
.duration(1000)
|
||||||
|
.success()
|
||||||
|
),
|
||||||
|
range
|
||||||
|
.interval('1s')
|
||||||
|
.rate(4)
|
||||||
|
.generator((timestamp) =>
|
||||||
|
javaInstance
|
||||||
|
.transaction({ transactionName: 'GET /api' })
|
||||||
|
.timestamp(timestamp)
|
||||||
|
.duration(1000)
|
||||||
|
.success()
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
return apmSynthtraceEsClient.clean();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await deleteAllServiceDashboard(apmApiClient, 'synth-go', start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and when data is not loaded', () => {
|
||||||
|
it('creates a new service dashboard', async () => {
|
||||||
|
const serviceDashboard = {
|
||||||
|
dashboardSavedObjectId: 'dashboard-saved-object-id',
|
||||||
|
serviceFiltersEnabled: true,
|
||||||
|
kuery: 'service.name: synth-go',
|
||||||
|
};
|
||||||
|
const createResponse = await getLinkServiceDashboardApi({
|
||||||
|
apmApiClient,
|
||||||
|
...serviceDashboard,
|
||||||
|
});
|
||||||
|
expect(createResponse.status).to.be(200);
|
||||||
|
expect(createResponse.body).to.have.property('id');
|
||||||
|
expect(createResponse.body).to.have.property('updatedAt');
|
||||||
|
|
||||||
|
expect(createResponse.body).to.have.property(
|
||||||
|
'dashboardSavedObjectId',
|
||||||
|
serviceDashboard.dashboardSavedObjectId
|
||||||
|
);
|
||||||
|
expect(createResponse.body).to.have.property('kuery', serviceDashboard.kuery);
|
||||||
|
expect(createResponse.body).to.have.property(
|
||||||
|
'serviceEnvironmentFilterEnabled',
|
||||||
|
serviceDashboard.serviceFiltersEnabled
|
||||||
|
);
|
||||||
|
expect(createResponse.body).to.have.property(
|
||||||
|
'serviceNameFilterEnabled',
|
||||||
|
serviceDashboard.serviceFiltersEnabled
|
||||||
|
);
|
||||||
|
|
||||||
|
const dasboardForGoService = await getServiceDashboardApi(
|
||||||
|
apmApiClient,
|
||||||
|
'synth-go',
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
const dashboardForJavaService = await getServiceDashboardApi(
|
||||||
|
apmApiClient,
|
||||||
|
'synth-java',
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
expect(dashboardForJavaService.body.serviceDashboards.length).to.be(0);
|
||||||
|
expect(dasboardForGoService.body.serviceDashboards.length).to.be(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the existing linked service dashboard', async () => {
|
||||||
|
const serviceDashboard = {
|
||||||
|
dashboardSavedObjectId: 'dashboard-saved-object-id',
|
||||||
|
serviceFiltersEnabled: true,
|
||||||
|
kuery: 'service.name: synth-go or agent.name: java',
|
||||||
|
};
|
||||||
|
|
||||||
|
await getLinkServiceDashboardApi({
|
||||||
|
apmApiClient,
|
||||||
|
...serviceDashboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dasboardForGoService = await getServiceDashboardApi(
|
||||||
|
apmApiClient,
|
||||||
|
'synth-go',
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateResponse = await getLinkServiceDashboardApi({
|
||||||
|
apmApiClient,
|
||||||
|
customDashboardId: dasboardForGoService.body.serviceDashboards[0].id,
|
||||||
|
...serviceDashboard,
|
||||||
|
serviceFiltersEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateResponse.status).to.be(200);
|
||||||
|
|
||||||
|
const updateddasboardForGoService = await getServiceDashboardApi(
|
||||||
|
apmApiClient,
|
||||||
|
'synth-go',
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
expect(updateddasboardForGoService.body.serviceDashboards.length).to.be(1);
|
||||||
|
expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property(
|
||||||
|
'serviceEnvironmentFilterEnabled',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property(
|
||||||
|
'serviceNameFilterEnabled',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property(
|
||||||
|
'kuery',
|
||||||
|
'service.name: synth-go or agent.name: java'
|
||||||
|
);
|
||||||
|
|
||||||
|
const dashboardForJavaService = await getServiceDashboardApi(
|
||||||
|
apmApiClient,
|
||||||
|
'synth-java',
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
expect(dashboardForJavaService.body.serviceDashboards.length).to.be(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
|
|
||||||
|
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
|
||||||
|
describe('custom_dashboards', () => {
|
||||||
|
loadTestFile(require.resolve('./custom_dashboards.spec.ts'));
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,306 @@
|
||||||
|
/*
|
||||||
|
* 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 { sum } from 'lodash';
|
||||||
|
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
|
||||||
|
import { Coordinate } from '@kbn/apm-plugin/typings/timeseries';
|
||||||
|
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||||
|
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||||
|
import { SupertestReturnType } from '../../../../services/apm_api';
|
||||||
|
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
|
import { roundNumber } from '../utils/common';
|
||||||
|
import { generateOperationData, generateOperationDataConfig } from './generate_operation_data';
|
||||||
|
|
||||||
|
const {
|
||||||
|
ES_BULK_DURATION,
|
||||||
|
ES_BULK_RATE,
|
||||||
|
ES_SEARCH_DURATION,
|
||||||
|
ES_SEARCH_FAILURE_RATE,
|
||||||
|
ES_SEARCH_SUCCESS_RATE,
|
||||||
|
ES_SEARCH_UNKNOWN_RATE,
|
||||||
|
REDIS_SET_RATE,
|
||||||
|
} = generateOperationDataConfig;
|
||||||
|
|
||||||
|
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||||
|
const apmApiClient = getService('apmApi');
|
||||||
|
const synthtrace = getService('synthtrace');
|
||||||
|
|
||||||
|
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||||
|
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||||
|
|
||||||
|
async function callApi<TMetricName extends 'latency' | 'throughput' | 'error_rate'>({
|
||||||
|
dependencyName,
|
||||||
|
searchServiceDestinationMetrics,
|
||||||
|
spanName = '',
|
||||||
|
metric,
|
||||||
|
kuery = '',
|
||||||
|
environment = ENVIRONMENT_ALL.value,
|
||||||
|
}: {
|
||||||
|
dependencyName: string;
|
||||||
|
searchServiceDestinationMetrics: boolean;
|
||||||
|
spanName?: string;
|
||||||
|
metric: TMetricName;
|
||||||
|
kuery?: string;
|
||||||
|
environment?: string;
|
||||||
|
}): Promise<SupertestReturnType<`GET /internal/apm/dependencies/charts/${TMetricName}`>> {
|
||||||
|
return await apmApiClient.readUser({
|
||||||
|
endpoint: `GET /internal/apm/dependencies/charts/${
|
||||||
|
metric as 'latency' | 'throughput' | 'error_rate'
|
||||||
|
}`,
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
dependencyName,
|
||||||
|
start: new Date(start).toISOString(),
|
||||||
|
end: new Date(end).toISOString(),
|
||||||
|
environment,
|
||||||
|
kuery,
|
||||||
|
offset: '',
|
||||||
|
spanName,
|
||||||
|
searchServiceDestinationMetrics,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function avg(coordinates: Coordinate[]) {
|
||||||
|
const values = coordinates
|
||||||
|
.filter((coord): coord is { x: number; y: number } => isFiniteNumber(coord.y))
|
||||||
|
.map((coord) => coord.y);
|
||||||
|
|
||||||
|
return roundNumber(sum(values) / values.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Dependency metrics', () => {
|
||||||
|
describe('when data is not loaded', () => {
|
||||||
|
it('handles empty state', async () => {
|
||||||
|
const { body, status } = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
metric: 'latency',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).to.be(200);
|
||||||
|
expect(body.currentTimeseries.filter((val) => isFiniteNumber(val.y))).to.empty();
|
||||||
|
expect(
|
||||||
|
(body.comparisonTimeseries || [])?.filter((val) => isFiniteNumber(val.y))
|
||||||
|
).to.empty();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when data is loaded', () => {
|
||||||
|
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||||
|
|
||||||
|
await generateOperationData({
|
||||||
|
apmSynthtraceEsClient,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('without spanName', () => {
|
||||||
|
describe('without a kuery or environment', () => {
|
||||||
|
it('returns the correct latency', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
spanName: '',
|
||||||
|
metric: 'latency',
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchRate =
|
||||||
|
ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE;
|
||||||
|
const bulkRate = ES_BULK_RATE;
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(
|
||||||
|
roundNumber(
|
||||||
|
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
|
||||||
|
(searchRate + bulkRate)) *
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct throughput', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'redis',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
spanName: '',
|
||||||
|
metric: 'throughput',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct failure rate', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
spanName: '',
|
||||||
|
metric: 'error_rate',
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedErrorRate =
|
||||||
|
ES_SEARCH_FAILURE_RATE / (ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE);
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(expectedErrorRate);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a kuery', () => {
|
||||||
|
it('returns the correct latency', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
spanName: '',
|
||||||
|
metric: 'latency',
|
||||||
|
kuery: `event.outcome:unknown`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchRate = ES_SEARCH_UNKNOWN_RATE;
|
||||||
|
const bulkRate = ES_BULK_RATE;
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(
|
||||||
|
roundNumber(
|
||||||
|
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
|
||||||
|
(searchRate + bulkRate)) *
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct throughput', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
spanName: '',
|
||||||
|
metric: 'throughput',
|
||||||
|
kuery: `event.outcome:unknown`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchRate = ES_SEARCH_UNKNOWN_RATE;
|
||||||
|
const bulkRate = ES_BULK_RATE;
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct failure rate', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
spanName: '',
|
||||||
|
metric: 'error_rate',
|
||||||
|
kuery: 'event.outcome:success',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with an environment', () => {
|
||||||
|
it('returns the correct latency', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
spanName: '',
|
||||||
|
metric: 'latency',
|
||||||
|
environment: 'production',
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchRate = ES_SEARCH_UNKNOWN_RATE;
|
||||||
|
const bulkRate = 0;
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(
|
||||||
|
roundNumber(
|
||||||
|
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
|
||||||
|
(searchRate + bulkRate)) *
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct throughput', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
spanName: '',
|
||||||
|
metric: 'throughput',
|
||||||
|
environment: 'production',
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchRate =
|
||||||
|
ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE;
|
||||||
|
const bulkRate = 0;
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct failure rate', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: true,
|
||||||
|
spanName: '',
|
||||||
|
metric: 'error_rate',
|
||||||
|
environment: 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with spanName', () => {
|
||||||
|
it('returns the correct latency', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: false,
|
||||||
|
spanName: '/_search',
|
||||||
|
metric: 'latency',
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchRate =
|
||||||
|
ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE;
|
||||||
|
const bulkRate = 0;
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(
|
||||||
|
roundNumber(
|
||||||
|
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
|
||||||
|
(searchRate + bulkRate)) *
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct throughput', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'redis',
|
||||||
|
searchServiceDestinationMetrics: false,
|
||||||
|
spanName: 'SET',
|
||||||
|
metric: 'throughput',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct failure rate', async () => {
|
||||||
|
const response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
searchServiceDestinationMetrics: false,
|
||||||
|
spanName: '/_bulk',
|
||||||
|
metric: 'error_rate',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(avg(response.body.currentTimeseries)).to.eql(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => apmSynthtraceEsClient.clean());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||||
|
|
||||||
export const generateOperationDataConfig = {
|
export const generateOperationDataConfig = {
|
||||||
ES_SEARCH_DURATION: 100,
|
ES_SEARCH_DURATION: 100,
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
|
|
||||||
|
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
|
||||||
|
describe('custom_dashboards', () => {
|
||||||
|
loadTestFile(require.resolve('./dependency_metrics.spec.ts'));
|
||||||
|
loadTestFile(require.resolve('./metadata.spec.ts'));
|
||||||
|
loadTestFile(require.resolve('./service_dependencies.spec.ts'));
|
||||||
|
loadTestFile(require.resolve('./top_dependencies.spec.ts'));
|
||||||
|
loadTestFile(require.resolve('./top_operations.spec.ts'));
|
||||||
|
loadTestFile(require.resolve('./top_spans.spec.ts'));
|
||||||
|
loadTestFile(require.resolve('./upstream_services.spec.ts'));
|
||||||
|
});
|
||||||
|
}
|
|
@ -5,13 +5,13 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import expect from '@kbn/expect';
|
import expect from '@kbn/expect';
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||||
|
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
import { dataConfig, generateData } from './generate_data';
|
import { dataConfig, generateData } from './generate_data';
|
||||||
|
|
||||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||||
const registry = getService('registry');
|
const apmApiClient = getService('apmApi');
|
||||||
const apmApiClient = getService('apmApiClient');
|
const synthtrace = getService('synthtrace');
|
||||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
|
||||||
|
|
||||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||||
|
@ -29,24 +29,23 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.when(
|
describe('Dependency metadata', () => {
|
||||||
'Dependency metadata when data is not loaded',
|
describe('when data is not loaded', () => {
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
it('handles empty state', async () => {
|
it('handles empty state', async () => {
|
||||||
const { status, body } = await callApi();
|
const { status, body } = await callApi();
|
||||||
|
|
||||||
expect(status).to.be(200);
|
expect(status).to.be(200);
|
||||||
expect(body.metadata).to.empty();
|
expect(body.metadata).to.empty();
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
describe('when data is generated', () => {
|
||||||
|
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||||
|
});
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/177122
|
|
||||||
registry.when(
|
|
||||||
'Dependency metadata when data is generated',
|
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
after(() => apmSynthtraceEsClient.clean());
|
after(() => apmSynthtraceEsClient.clean());
|
||||||
|
|
||||||
it('returns correct metadata for the dependency', async () => {
|
it('returns correct metadata for the dependency', async () => {
|
||||||
|
@ -61,6 +60,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
|
|
||||||
await apmSynthtraceEsClient.clean();
|
await apmSynthtraceEsClient.clean();
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
});
|
||||||
}
|
}
|
|
@ -6,13 +6,13 @@
|
||||||
*/
|
*/
|
||||||
import expect from '@kbn/expect';
|
import expect from '@kbn/expect';
|
||||||
import { DependencyNode } from '@kbn/apm-plugin/common/connections';
|
import { DependencyNode } from '@kbn/apm-plugin/common/connections';
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||||
|
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
import { generateData } from './generate_data';
|
import { generateData } from './generate_data';
|
||||||
|
|
||||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||||
const apmApiClient = getService('apmApiClient');
|
const apmApiClient = getService('apmApi');
|
||||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
const synthtrace = getService('synthtrace');
|
||||||
const registry = getService('registry');
|
|
||||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||||
const dependencyName = 'elasticsearch';
|
const dependencyName = 'elasticsearch';
|
||||||
|
@ -34,61 +34,21 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.when(
|
describe('Dependency for service', () => {
|
||||||
'Dependency for service when data is not loaded',
|
describe('when data is not loaded', () => {
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
it('handles empty state #1', async () => {
|
it('handles empty state #1', async () => {
|
||||||
const { status, body } = await callApi();
|
const { status, body } = await callApi();
|
||||||
|
|
||||||
expect(status).to.be(200);
|
expect(status).to.be(200);
|
||||||
expect(body.serviceDependencies).to.empty();
|
expect(body.serviceDependencies).to.empty();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/177123
|
|
||||||
registry.when('Dependency for services', { config: 'basic', archives: [] }, () => {
|
|
||||||
describe('when data is loaded', () => {
|
|
||||||
before(async () => {
|
|
||||||
await generateData({ apmSynthtraceEsClient, start, end });
|
|
||||||
});
|
|
||||||
after(() => apmSynthtraceEsClient.clean());
|
|
||||||
|
|
||||||
it('returns a list of dependencies for a service', async () => {
|
|
||||||
const { status, body } = await callApi();
|
|
||||||
|
|
||||||
expect(status).to.be(200);
|
|
||||||
expect(
|
|
||||||
body.serviceDependencies.map(
|
|
||||||
({ location }) => (location as DependencyNode).dependencyName
|
|
||||||
)
|
|
||||||
).to.eql([dependencyName]);
|
|
||||||
|
|
||||||
const currentStatsLatencyValues =
|
|
||||||
body.serviceDependencies[0].currentStats.latency.timeseries;
|
|
||||||
expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
registry.when(
|
describe('when data is loaded', () => {
|
||||||
'Dependency for service breakdown when data is not loaded',
|
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
it('handles empty state #2', async () => {
|
|
||||||
const { status, body } = await callApi();
|
|
||||||
|
|
||||||
expect(status).to.be(200);
|
|
||||||
expect(body.serviceDependencies).to.empty();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/177125
|
|
||||||
registry.when('Dependency for services breakdown', { config: 'basic', archives: [] }, () => {
|
|
||||||
describe('when data is loaded - breakdown', () => {
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||||
await generateData({ apmSynthtraceEsClient, start, end });
|
await generateData({ apmSynthtraceEsClient, start, end });
|
||||||
});
|
});
|
||||||
after(() => apmSynthtraceEsClient.clean());
|
after(() => apmSynthtraceEsClient.clean());
|
|
@ -7,16 +7,16 @@
|
||||||
import expect from '@kbn/expect';
|
import expect from '@kbn/expect';
|
||||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||||
import { NodeType, DependencyNode } from '@kbn/apm-plugin/common/connections';
|
import { NodeType, DependencyNode } from '@kbn/apm-plugin/common/connections';
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||||
|
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
import { dataConfig, generateData } from './generate_data';
|
import { dataConfig, generateData } from './generate_data';
|
||||||
import { roundNumber } from '../../utils';
|
import { roundNumber } from '../utils/common';
|
||||||
|
|
||||||
type TopDependencies = APIReturnType<'GET /internal/apm/dependencies/top_dependencies'>;
|
type TopDependencies = APIReturnType<'GET /internal/apm/dependencies/top_dependencies'>;
|
||||||
|
|
||||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||||
const registry = getService('registry');
|
const apmApiClient = getService('apmApi');
|
||||||
const apmApiClient = getService('apmApiClient');
|
const synthtrace = getService('synthtrace');
|
||||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
|
||||||
|
|
||||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||||
|
@ -37,24 +37,21 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.when(
|
describe('Top dependencies', () => {
|
||||||
'Top dependencies when data is not loaded',
|
describe('when data is not loaded', () => {
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
it('handles empty state', async () => {
|
it('handles empty state', async () => {
|
||||||
const { status, body } = await callApi();
|
const { status, body } = await callApi();
|
||||||
expect(status).to.be(200);
|
expect(status).to.be(200);
|
||||||
expect(body.dependencies).to.empty();
|
expect(body.dependencies).to.empty();
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/177126
|
|
||||||
registry.when('Top dependencies', { config: 'basic', archives: [] }, () => {
|
|
||||||
describe('when data is generated', () => {
|
describe('when data is generated', () => {
|
||||||
let topDependencies: TopDependencies;
|
let topDependencies: TopDependencies;
|
||||||
|
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||||
await generateData({ apmSynthtraceEsClient, start, end });
|
await generateData({ apmSynthtraceEsClient, start, end });
|
||||||
const response = await callApi();
|
const response = await callApi();
|
||||||
topDependencies = response.body;
|
topDependencies = response.body;
|
|
@ -0,0 +1,280 @@
|
||||||
|
/*
|
||||||
|
* 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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||||
|
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||||
|
import { ValuesType } from 'utility-types';
|
||||||
|
import { DependencyOperation } from '@kbn/apm-plugin/server/routes/dependencies/get_top_dependency_operations';
|
||||||
|
import { meanBy } from 'lodash';
|
||||||
|
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||||
|
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
|
import { roundNumber } from '../utils/common';
|
||||||
|
import { generateOperationData, generateOperationDataConfig } from './generate_operation_data';
|
||||||
|
|
||||||
|
type TopOperations = APIReturnType<'GET /internal/apm/dependencies/operations'>['operations'];
|
||||||
|
|
||||||
|
const {
|
||||||
|
ES_BULK_DURATION,
|
||||||
|
ES_BULK_RATE,
|
||||||
|
ES_SEARCH_DURATION,
|
||||||
|
ES_SEARCH_FAILURE_RATE,
|
||||||
|
ES_SEARCH_SUCCESS_RATE,
|
||||||
|
ES_SEARCH_UNKNOWN_RATE,
|
||||||
|
REDIS_SET_DURATION,
|
||||||
|
REDIS_SET_RATE,
|
||||||
|
} = generateOperationDataConfig;
|
||||||
|
|
||||||
|
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||||
|
const apmApiClient = getService('apmApi');
|
||||||
|
const synthtrace = getService('synthtrace');
|
||||||
|
|
||||||
|
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||||
|
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||||
|
|
||||||
|
async function callApi({
|
||||||
|
dependencyName,
|
||||||
|
environment = ENVIRONMENT_ALL.value,
|
||||||
|
kuery = '',
|
||||||
|
searchServiceDestinationMetrics = false,
|
||||||
|
}: {
|
||||||
|
dependencyName: string;
|
||||||
|
environment?: string;
|
||||||
|
kuery?: string;
|
||||||
|
searchServiceDestinationMetrics?: boolean;
|
||||||
|
}) {
|
||||||
|
return await apmApiClient
|
||||||
|
.readUser({
|
||||||
|
endpoint: 'GET /internal/apm/dependencies/operations',
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
start: new Date(start).toISOString(),
|
||||||
|
end: new Date(end).toISOString(),
|
||||||
|
environment,
|
||||||
|
kuery,
|
||||||
|
dependencyName,
|
||||||
|
searchServiceDestinationMetrics,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ body }) => body.operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Top operations', () => {
|
||||||
|
describe('when data is not loaded', () => {
|
||||||
|
it('handles empty state', async () => {
|
||||||
|
const operations = await callApi({ dependencyName: 'elasticsearch' });
|
||||||
|
expect(operations).to.empty();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when data is generated', () => {
|
||||||
|
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||||
|
|
||||||
|
return generateOperationData({
|
||||||
|
apmSynthtraceEsClient,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => apmSynthtraceEsClient.clean());
|
||||||
|
|
||||||
|
describe('requested for elasticsearch', () => {
|
||||||
|
let response: TopOperations;
|
||||||
|
let searchOperation: ValuesType<TopOperations>;
|
||||||
|
let bulkOperation: ValuesType<TopOperations>;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
response = await callApi({ dependencyName: 'elasticsearch' });
|
||||||
|
searchOperation = response.find((op) => op.spanName === '/_search')!;
|
||||||
|
bulkOperation = response.find((op) => op.spanName === '/_bulk')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct operations', () => {
|
||||||
|
expect(response.length).to.eql(2);
|
||||||
|
|
||||||
|
expect(searchOperation).to.be.ok();
|
||||||
|
expect(bulkOperation).to.be.ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct latency', () => {
|
||||||
|
expect(searchOperation.latency).to.eql(ES_SEARCH_DURATION * 1000);
|
||||||
|
expect(bulkOperation.latency).to.eql(ES_BULK_DURATION * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct throughput', () => {
|
||||||
|
const expectedSearchThroughput = roundNumber(
|
||||||
|
ES_SEARCH_UNKNOWN_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE
|
||||||
|
);
|
||||||
|
const expectedBulkThroughput = ES_BULK_RATE;
|
||||||
|
|
||||||
|
expect(roundNumber(searchOperation.throughput)).to.eql(expectedSearchThroughput);
|
||||||
|
expect(roundNumber(bulkOperation.throughput)).to.eql(expectedBulkThroughput);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
searchOperation.timeseries.throughput
|
||||||
|
.map((bucket) => bucket.y)
|
||||||
|
.every((val) => val === expectedSearchThroughput)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct failure rate', () => {
|
||||||
|
const expectedSearchFailureRate =
|
||||||
|
ES_SEARCH_FAILURE_RATE / (ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE);
|
||||||
|
const expectedBulkFailureRate = null;
|
||||||
|
|
||||||
|
expect(searchOperation.failureRate).to.be(expectedSearchFailureRate);
|
||||||
|
|
||||||
|
expect(bulkOperation.failureRate).to.be(expectedBulkFailureRate);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
searchOperation.timeseries.failureRate
|
||||||
|
.map((bucket) => bucket.y)
|
||||||
|
.every((val) => val === expectedSearchFailureRate)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
bulkOperation.timeseries.failureRate
|
||||||
|
.map((bucket) => bucket.y)
|
||||||
|
.every((val) => val === expectedBulkFailureRate)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct impact', () => {
|
||||||
|
expect(searchOperation.impact).to.eql(0);
|
||||||
|
expect(bulkOperation.impact).to.eql(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('requested for redis', () => {
|
||||||
|
let response: TopOperations;
|
||||||
|
let setOperation: ValuesType<TopOperations>;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
response = await callApi({ dependencyName: 'redis' });
|
||||||
|
setOperation = response.find((op) => op.spanName === 'SET')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct operations', () => {
|
||||||
|
expect(response.length).to.eql(1);
|
||||||
|
|
||||||
|
expect(setOperation).to.be.ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct latency', () => {
|
||||||
|
expect(setOperation.latency).to.eql(REDIS_SET_DURATION * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct throughput', () => {
|
||||||
|
expect(roundNumber(setOperation.throughput)).to.eql(roundNumber(REDIS_SET_RATE));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('requested for a specific service', () => {
|
||||||
|
let response: TopOperations;
|
||||||
|
let searchOperation: ValuesType<TopOperations>;
|
||||||
|
let bulkOperation: ValuesType<TopOperations> | undefined;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
kuery: `service.name:"synth-go"`,
|
||||||
|
});
|
||||||
|
searchOperation = response.find((op) => op.spanName === '/_search')!;
|
||||||
|
bulkOperation = response.find((op) => op.spanName === '/_bulk');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct operations', () => {
|
||||||
|
expect(response.length).to.eql(1);
|
||||||
|
|
||||||
|
expect(searchOperation).to.be.ok();
|
||||||
|
expect(bulkOperation).not.to.be.ok();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('requested for a specific environment', () => {
|
||||||
|
let response: TopOperations;
|
||||||
|
let searchOperation: ValuesType<TopOperations> | undefined;
|
||||||
|
let bulkOperation: ValuesType<TopOperations>;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
response = await callApi({
|
||||||
|
dependencyName: 'elasticsearch',
|
||||||
|
environment: 'development',
|
||||||
|
});
|
||||||
|
searchOperation = response.find((op) => op.spanName === '/_search');
|
||||||
|
bulkOperation = response.find((op) => op.spanName === '/_bulk')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct operations', () => {
|
||||||
|
expect(response.length).to.eql(1);
|
||||||
|
|
||||||
|
expect(searchOperation).not.to.be.ok();
|
||||||
|
expect(bulkOperation).to.be.ok();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Compare span metrics and span events', () => {
|
||||||
|
let bulkOperationSpanEventsResponse: ValuesType<TopOperations>;
|
||||||
|
let bulkOperationSpanMetricsResponse: ValuesType<TopOperations>;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
const [spanEventsResponse, spanMetricsResponse] = await Promise.all([
|
||||||
|
callApi({ dependencyName: 'elasticsearch', searchServiceDestinationMetrics: false }),
|
||||||
|
callApi({ dependencyName: 'elasticsearch', searchServiceDestinationMetrics: true }),
|
||||||
|
]);
|
||||||
|
function findBulkOperation(op: DependencyOperation) {
|
||||||
|
return op.spanName === '/_bulk';
|
||||||
|
}
|
||||||
|
bulkOperationSpanEventsResponse = spanEventsResponse.find(findBulkOperation)!;
|
||||||
|
bulkOperationSpanMetricsResponse = spanMetricsResponse.find(findBulkOperation)!;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns same latency', () => {
|
||||||
|
expect(bulkOperationSpanEventsResponse.latency).to.eql(
|
||||||
|
bulkOperationSpanMetricsResponse.latency
|
||||||
|
);
|
||||||
|
|
||||||
|
const meanSpanMetrics = meanBy(
|
||||||
|
bulkOperationSpanEventsResponse.timeseries.latency.filter(({ y }) => y !== null),
|
||||||
|
'y'
|
||||||
|
);
|
||||||
|
const meanSpanEvents = meanBy(
|
||||||
|
bulkOperationSpanMetricsResponse.timeseries.latency.filter(({ y }) => y !== null),
|
||||||
|
'y'
|
||||||
|
);
|
||||||
|
expect(meanSpanMetrics).to.eql(meanSpanEvents);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns same throughput', () => {
|
||||||
|
expect(bulkOperationSpanEventsResponse.throughput).to.eql(
|
||||||
|
bulkOperationSpanMetricsResponse.throughput
|
||||||
|
);
|
||||||
|
|
||||||
|
const meanSpanMetrics = meanBy(
|
||||||
|
bulkOperationSpanEventsResponse.timeseries.throughput.filter(({ y }) => y !== 0),
|
||||||
|
'y'
|
||||||
|
);
|
||||||
|
const meanSpanEvents = meanBy(
|
||||||
|
bulkOperationSpanMetricsResponse.timeseries.throughput.filter(({ y }) => y !== 0),
|
||||||
|
'y'
|
||||||
|
);
|
||||||
|
expect(meanSpanMetrics).to.eql(meanSpanEvents);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns same impact', () => {
|
||||||
|
expect(bulkOperationSpanEventsResponse.impact).to.eql(
|
||||||
|
bulkOperationSpanMetricsResponse.impact
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -8,12 +8,12 @@ import expect from '@kbn/expect';
|
||||||
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||||
import { omit, uniq } from 'lodash';
|
import { omit, uniq } from 'lodash';
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||||
|
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
|
|
||||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||||
const registry = getService('registry');
|
const apmApiClient = getService('apmApi');
|
||||||
const apmApiClient = getService('apmApiClient');
|
const synthtrace = getService('synthtrace');
|
||||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
|
||||||
|
|
||||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||||
|
@ -50,10 +50,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.when(
|
describe('Top dependency spans', () => {
|
||||||
'Top dependency spans when data is not loaded',
|
describe('when data is not loaded', () => {
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
it('handles empty state', async () => {
|
it('handles empty state', async () => {
|
||||||
const { body, status } = await callApi({
|
const { body, status } = await callApi({
|
||||||
dependencyName: 'elasticsearch',
|
dependencyName: 'elasticsearch',
|
||||||
|
@ -63,14 +61,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
expect(status).to.be(200);
|
expect(status).to.be(200);
|
||||||
expect(body.spans).to.empty();
|
expect(body.spans).to.empty();
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/177135
|
describe('when data is loaded', () => {
|
||||||
registry.when(
|
|
||||||
'Top dependency spans when data is loaded',
|
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
const javaInstance = apm
|
const javaInstance = apm
|
||||||
.service({ name: 'java', environment: 'production', agentName: 'java' })
|
.service({ name: 'java', environment: 'production', agentName: 'java' })
|
||||||
.instance('instance-a');
|
.instance('instance-a');
|
||||||
|
@ -78,8 +71,11 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
const goInstance = apm
|
const goInstance = apm
|
||||||
.service({ name: 'go', environment: 'development', agentName: 'go' })
|
.service({ name: 'go', environment: 'development', agentName: 'go' })
|
||||||
.instance('instance-a');
|
.instance('instance-a');
|
||||||
|
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||||
|
|
||||||
await apmSynthtraceEsClient.index([
|
await apmSynthtraceEsClient.index([
|
||||||
timerange(start, end)
|
timerange(start, end)
|
||||||
.interval('1m')
|
.interval('1m')
|
||||||
|
@ -240,6 +236,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => apmSynthtraceEsClient.clean());
|
after(() => apmSynthtraceEsClient.clean());
|
||||||
}
|
});
|
||||||
);
|
});
|
||||||
}
|
}
|
|
@ -6,13 +6,13 @@
|
||||||
*/
|
*/
|
||||||
import expect from '@kbn/expect';
|
import expect from '@kbn/expect';
|
||||||
import { ServiceNode } from '@kbn/apm-plugin/common/connections';
|
import { ServiceNode } from '@kbn/apm-plugin/common/connections';
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||||
|
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||||
import { generateData } from './generate_data';
|
import { generateData } from './generate_data';
|
||||||
|
|
||||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||||
const apmApiClient = getService('apmApiClient');
|
const apmApiClient = getService('apmApi');
|
||||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
const synthtrace = getService('synthtrace');
|
||||||
const registry = getService('registry');
|
|
||||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||||
const dependencyName = 'elasticsearch';
|
const dependencyName = 'elasticsearch';
|
||||||
|
@ -34,23 +34,22 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.when(
|
describe('Dependency upstream services', () => {
|
||||||
'Dependency upstream services when data is not loaded',
|
describe('when data is not loaded', () => {
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
it('handles empty state', async () => {
|
it('handles empty state', async () => {
|
||||||
const { status, body } = await callApi();
|
const { status, body } = await callApi();
|
||||||
|
|
||||||
expect(status).to.be(200);
|
expect(status).to.be(200);
|
||||||
expect(body.services).to.empty();
|
expect(body.services).to.empty();
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/177137
|
|
||||||
registry.when('Dependency upstream services', { config: 'basic', archives: [] }, () => {
|
|
||||||
describe('when data is loaded', () => {
|
describe('when data is loaded', () => {
|
||||||
|
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||||
|
|
||||||
await generateData({ apmSynthtraceEsClient, start, end });
|
await generateData({ apmSynthtraceEsClient, start, end });
|
||||||
});
|
});
|
||||||
after(() => apmSynthtraceEsClient.clean());
|
after(() => apmSynthtraceEsClient.clean());
|
|
@ -12,5 +12,7 @@ export default function apmApiIntegrationTests({
|
||||||
}: DeploymentAgnosticFtrProviderContext) {
|
}: DeploymentAgnosticFtrProviderContext) {
|
||||||
describe('APM', function () {
|
describe('APM', function () {
|
||||||
loadTestFile(require.resolve('./agent_explorer'));
|
loadTestFile(require.resolve('./agent_explorer'));
|
||||||
|
loadTestFile(require.resolve('./custom_dashboards'));
|
||||||
|
loadTestFile(require.resolve('./dependencies'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
|
||||||
|
import { Maybe } from '@kbn/apm-plugin/typings/common';
|
||||||
|
|
||||||
|
export function roundNumber(num: Maybe<number>) {
|
||||||
|
return isFiniteNumber(num) ? Number(num.toPrecision(4)) : null;
|
||||||
|
}
|
|
@ -1,195 +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 { apm, timerange } from '@kbn/apm-synthtrace-client';
|
|
||||||
|
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
|
||||||
import {
|
|
||||||
getServiceDashboardApi,
|
|
||||||
getLinkServiceDashboardApi,
|
|
||||||
deleteAllServiceDashboard,
|
|
||||||
} from './api_helper';
|
|
||||||
|
|
||||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
|
||||||
const registry = getService('registry');
|
|
||||||
const apmApiClient = getService('apmApiClient');
|
|
||||||
const synthtrace = getService('apmSynthtraceEsClient');
|
|
||||||
|
|
||||||
const start = '2023-08-22T00:00:00.000Z';
|
|
||||||
const end = '2023-08-22T00:15:00.000Z';
|
|
||||||
|
|
||||||
registry.when(
|
|
||||||
'Service dashboards when data is not loaded',
|
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
describe('when data is not loaded', () => {
|
|
||||||
it('handles empty state', async () => {
|
|
||||||
const response = await getServiceDashboardApi(apmApiClient, 'synth-go', start, end);
|
|
||||||
expect(response.status).to.be(200);
|
|
||||||
expect(response.body.serviceDashboards).to.eql([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/177119
|
|
||||||
registry.when('Service dashboards when data is loaded', { config: 'basic', archives: [] }, () => {
|
|
||||||
const range = timerange(new Date(start).getTime(), new Date(end).getTime());
|
|
||||||
|
|
||||||
const goInstance = apm
|
|
||||||
.service({
|
|
||||||
name: 'synth-go',
|
|
||||||
environment: 'production',
|
|
||||||
agentName: 'go',
|
|
||||||
})
|
|
||||||
.instance('go-instance');
|
|
||||||
|
|
||||||
const javaInstance = apm
|
|
||||||
.service({
|
|
||||||
name: 'synth-java',
|
|
||||||
environment: 'production',
|
|
||||||
agentName: 'java',
|
|
||||||
})
|
|
||||||
.instance('java-instance');
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
return synthtrace.index([
|
|
||||||
range
|
|
||||||
.interval('1s')
|
|
||||||
.rate(4)
|
|
||||||
.generator((timestamp) =>
|
|
||||||
goInstance
|
|
||||||
.transaction({ transactionName: 'GET /api' })
|
|
||||||
.timestamp(timestamp)
|
|
||||||
.duration(1000)
|
|
||||||
.success()
|
|
||||||
),
|
|
||||||
range
|
|
||||||
.interval('1s')
|
|
||||||
.rate(4)
|
|
||||||
.generator((timestamp) =>
|
|
||||||
javaInstance
|
|
||||||
.transaction({ transactionName: 'GET /api' })
|
|
||||||
.timestamp(timestamp)
|
|
||||||
.duration(1000)
|
|
||||||
.success()
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
return synthtrace.clean();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await deleteAllServiceDashboard(apmApiClient, 'synth-go', start, end);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and when data is not loaded', () => {
|
|
||||||
it('creates a new service dashboard', async () => {
|
|
||||||
const serviceDashboard = {
|
|
||||||
dashboardSavedObjectId: 'dashboard-saved-object-id',
|
|
||||||
serviceFiltersEnabled: true,
|
|
||||||
kuery: 'service.name: synth-go',
|
|
||||||
};
|
|
||||||
const createResponse = await getLinkServiceDashboardApi({
|
|
||||||
apmApiClient,
|
|
||||||
...serviceDashboard,
|
|
||||||
});
|
|
||||||
expect(createResponse.status).to.be(200);
|
|
||||||
expect(createResponse.body).to.have.property('id');
|
|
||||||
expect(createResponse.body).to.have.property('updatedAt');
|
|
||||||
|
|
||||||
expect(createResponse.body).to.have.property(
|
|
||||||
'dashboardSavedObjectId',
|
|
||||||
serviceDashboard.dashboardSavedObjectId
|
|
||||||
);
|
|
||||||
expect(createResponse.body).to.have.property('kuery', serviceDashboard.kuery);
|
|
||||||
expect(createResponse.body).to.have.property(
|
|
||||||
'serviceEnvironmentFilterEnabled',
|
|
||||||
serviceDashboard.serviceFiltersEnabled
|
|
||||||
);
|
|
||||||
expect(createResponse.body).to.have.property(
|
|
||||||
'serviceNameFilterEnabled',
|
|
||||||
serviceDashboard.serviceFiltersEnabled
|
|
||||||
);
|
|
||||||
|
|
||||||
const dasboardForGoService = await getServiceDashboardApi(
|
|
||||||
apmApiClient,
|
|
||||||
'synth-go',
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
);
|
|
||||||
const dashboardForJavaService = await getServiceDashboardApi(
|
|
||||||
apmApiClient,
|
|
||||||
'synth-java',
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
);
|
|
||||||
expect(dashboardForJavaService.body.serviceDashboards.length).to.be(0);
|
|
||||||
expect(dasboardForGoService.body.serviceDashboards.length).to.be(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates the existing linked service dashboard', async () => {
|
|
||||||
const serviceDashboard = {
|
|
||||||
dashboardSavedObjectId: 'dashboard-saved-object-id',
|
|
||||||
serviceFiltersEnabled: true,
|
|
||||||
kuery: 'service.name: synth-go or agent.name: java',
|
|
||||||
};
|
|
||||||
|
|
||||||
await getLinkServiceDashboardApi({
|
|
||||||
apmApiClient,
|
|
||||||
...serviceDashboard,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dasboardForGoService = await getServiceDashboardApi(
|
|
||||||
apmApiClient,
|
|
||||||
'synth-go',
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateResponse = await getLinkServiceDashboardApi({
|
|
||||||
apmApiClient,
|
|
||||||
customDashboardId: dasboardForGoService.body.serviceDashboards[0].id,
|
|
||||||
...serviceDashboard,
|
|
||||||
serviceFiltersEnabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(updateResponse.status).to.be(200);
|
|
||||||
|
|
||||||
const updateddasboardForGoService = await getServiceDashboardApi(
|
|
||||||
apmApiClient,
|
|
||||||
'synth-go',
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
);
|
|
||||||
expect(updateddasboardForGoService.body.serviceDashboards.length).to.be(1);
|
|
||||||
expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property(
|
|
||||||
'serviceEnvironmentFilterEnabled',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property(
|
|
||||||
'serviceNameFilterEnabled',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property(
|
|
||||||
'kuery',
|
|
||||||
'service.name: synth-go or agent.name: java'
|
|
||||||
);
|
|
||||||
|
|
||||||
const dashboardForJavaService = await getServiceDashboardApi(
|
|
||||||
apmApiClient,
|
|
||||||
'synth-java',
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
);
|
|
||||||
expect(dashboardForJavaService.body.serviceDashboards.length).to.be(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,304 +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 { sum } from 'lodash';
|
|
||||||
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
|
|
||||||
import { Coordinate } from '@kbn/apm-plugin/typings/timeseries';
|
|
||||||
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
|
||||||
import { roundNumber } from '../../utils';
|
|
||||||
import { generateOperationData, generateOperationDataConfig } from './generate_operation_data';
|
|
||||||
import { SupertestReturnType } from '../../common/apm_api_supertest';
|
|
||||||
|
|
||||||
const {
|
|
||||||
ES_BULK_DURATION,
|
|
||||||
ES_BULK_RATE,
|
|
||||||
ES_SEARCH_DURATION,
|
|
||||||
ES_SEARCH_FAILURE_RATE,
|
|
||||||
ES_SEARCH_SUCCESS_RATE,
|
|
||||||
ES_SEARCH_UNKNOWN_RATE,
|
|
||||||
REDIS_SET_RATE,
|
|
||||||
} = generateOperationDataConfig;
|
|
||||||
|
|
||||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
|
||||||
const registry = getService('registry');
|
|
||||||
const apmApiClient = getService('apmApiClient');
|
|
||||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
|
||||||
|
|
||||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
|
||||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
|
||||||
|
|
||||||
async function callApi<TMetricName extends 'latency' | 'throughput' | 'error_rate'>({
|
|
||||||
dependencyName,
|
|
||||||
searchServiceDestinationMetrics,
|
|
||||||
spanName = '',
|
|
||||||
metric,
|
|
||||||
kuery = '',
|
|
||||||
environment = ENVIRONMENT_ALL.value,
|
|
||||||
}: {
|
|
||||||
dependencyName: string;
|
|
||||||
searchServiceDestinationMetrics: boolean;
|
|
||||||
spanName?: string;
|
|
||||||
metric: TMetricName;
|
|
||||||
kuery?: string;
|
|
||||||
environment?: string;
|
|
||||||
}): Promise<SupertestReturnType<`GET /internal/apm/dependencies/charts/${TMetricName}`>> {
|
|
||||||
return await apmApiClient.readUser({
|
|
||||||
endpoint: `GET /internal/apm/dependencies/charts/${
|
|
||||||
metric as 'latency' | 'throughput' | 'error_rate'
|
|
||||||
}`,
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
dependencyName,
|
|
||||||
start: new Date(start).toISOString(),
|
|
||||||
end: new Date(end).toISOString(),
|
|
||||||
environment,
|
|
||||||
kuery,
|
|
||||||
offset: '',
|
|
||||||
spanName,
|
|
||||||
searchServiceDestinationMetrics,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function avg(coordinates: Coordinate[]) {
|
|
||||||
const values = coordinates
|
|
||||||
.filter((coord): coord is { x: number; y: number } => isFiniteNumber(coord.y))
|
|
||||||
.map((coord) => coord.y);
|
|
||||||
|
|
||||||
return roundNumber(sum(values) / values.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.when(
|
|
||||||
'Dependency metrics when data is not loaded',
|
|
||||||
{ config: 'basic', archives: [] },
|
|
||||||
() => {
|
|
||||||
it('handles empty state', async () => {
|
|
||||||
const { body, status } = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
metric: 'latency',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(status).to.be(200);
|
|
||||||
expect(body.currentTimeseries.filter((val) => isFiniteNumber(val.y))).to.empty();
|
|
||||||
expect(
|
|
||||||
(body.comparisonTimeseries || [])?.filter((val) => isFiniteNumber(val.y))
|
|
||||||
).to.empty();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/177121
|
|
||||||
registry.when('Dependency metrics when data is loaded', { config: 'basic', archives: [] }, () => {
|
|
||||||
before(async () => {
|
|
||||||
await generateOperationData({
|
|
||||||
apmSynthtraceEsClient,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('without spanName', () => {
|
|
||||||
describe('without a kuery or environment', () => {
|
|
||||||
it('returns the correct latency', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
spanName: '',
|
|
||||||
metric: 'latency',
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchRate =
|
|
||||||
ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE;
|
|
||||||
const bulkRate = ES_BULK_RATE;
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(
|
|
||||||
roundNumber(
|
|
||||||
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
|
|
||||||
(searchRate + bulkRate)) *
|
|
||||||
1000
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct throughput', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'redis',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
spanName: '',
|
|
||||||
metric: 'throughput',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct failure rate', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
spanName: '',
|
|
||||||
metric: 'error_rate',
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedErrorRate =
|
|
||||||
ES_SEARCH_FAILURE_RATE / (ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE);
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(expectedErrorRate);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a kuery', () => {
|
|
||||||
it('returns the correct latency', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
spanName: '',
|
|
||||||
metric: 'latency',
|
|
||||||
kuery: `event.outcome:unknown`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchRate = ES_SEARCH_UNKNOWN_RATE;
|
|
||||||
const bulkRate = ES_BULK_RATE;
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(
|
|
||||||
roundNumber(
|
|
||||||
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
|
|
||||||
(searchRate + bulkRate)) *
|
|
||||||
1000
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct throughput', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
spanName: '',
|
|
||||||
metric: 'throughput',
|
|
||||||
kuery: `event.outcome:unknown`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchRate = ES_SEARCH_UNKNOWN_RATE;
|
|
||||||
const bulkRate = ES_BULK_RATE;
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct failure rate', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
spanName: '',
|
|
||||||
metric: 'error_rate',
|
|
||||||
kuery: 'event.outcome:success',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with an environment', () => {
|
|
||||||
it('returns the correct latency', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
spanName: '',
|
|
||||||
metric: 'latency',
|
|
||||||
environment: 'production',
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchRate = ES_SEARCH_UNKNOWN_RATE;
|
|
||||||
const bulkRate = 0;
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(
|
|
||||||
roundNumber(
|
|
||||||
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
|
|
||||||
(searchRate + bulkRate)) *
|
|
||||||
1000
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct throughput', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
spanName: '',
|
|
||||||
metric: 'throughput',
|
|
||||||
environment: 'production',
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchRate =
|
|
||||||
ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE;
|
|
||||||
const bulkRate = 0;
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct failure rate', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: true,
|
|
||||||
spanName: '',
|
|
||||||
metric: 'error_rate',
|
|
||||||
environment: 'development',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with spanName', () => {
|
|
||||||
it('returns the correct latency', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: false,
|
|
||||||
spanName: '/_search',
|
|
||||||
metric: 'latency',
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchRate = ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE;
|
|
||||||
const bulkRate = 0;
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(
|
|
||||||
roundNumber(
|
|
||||||
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
|
|
||||||
(searchRate + bulkRate)) *
|
|
||||||
1000
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct throughput', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'redis',
|
|
||||||
searchServiceDestinationMetrics: false,
|
|
||||||
spanName: 'SET',
|
|
||||||
metric: 'throughput',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct failure rate', async () => {
|
|
||||||
const response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
searchServiceDestinationMetrics: false,
|
|
||||||
spanName: '/_bulk',
|
|
||||||
metric: 'error_rate',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(avg(response.body.currentTimeseries)).to.eql(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => apmSynthtraceEsClient.clean());
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,275 +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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
|
||||||
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
|
||||||
import { ValuesType } from 'utility-types';
|
|
||||||
import { DependencyOperation } from '@kbn/apm-plugin/server/routes/dependencies/get_top_dependency_operations';
|
|
||||||
import { meanBy } from 'lodash';
|
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
|
||||||
import { roundNumber } from '../../utils';
|
|
||||||
import { generateOperationData, generateOperationDataConfig } from './generate_operation_data';
|
|
||||||
|
|
||||||
type TopOperations = APIReturnType<'GET /internal/apm/dependencies/operations'>['operations'];
|
|
||||||
|
|
||||||
const {
|
|
||||||
ES_BULK_DURATION,
|
|
||||||
ES_BULK_RATE,
|
|
||||||
ES_SEARCH_DURATION,
|
|
||||||
ES_SEARCH_FAILURE_RATE,
|
|
||||||
ES_SEARCH_SUCCESS_RATE,
|
|
||||||
ES_SEARCH_UNKNOWN_RATE,
|
|
||||||
REDIS_SET_DURATION,
|
|
||||||
REDIS_SET_RATE,
|
|
||||||
} = generateOperationDataConfig;
|
|
||||||
|
|
||||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
|
||||||
const registry = getService('registry');
|
|
||||||
const apmApiClient = getService('apmApiClient');
|
|
||||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
|
||||||
|
|
||||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
|
||||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
|
||||||
|
|
||||||
async function callApi({
|
|
||||||
dependencyName,
|
|
||||||
environment = ENVIRONMENT_ALL.value,
|
|
||||||
kuery = '',
|
|
||||||
searchServiceDestinationMetrics = false,
|
|
||||||
}: {
|
|
||||||
dependencyName: string;
|
|
||||||
environment?: string;
|
|
||||||
kuery?: string;
|
|
||||||
searchServiceDestinationMetrics?: boolean;
|
|
||||||
}) {
|
|
||||||
return await apmApiClient
|
|
||||||
.readUser({
|
|
||||||
endpoint: 'GET /internal/apm/dependencies/operations',
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
start: new Date(start).toISOString(),
|
|
||||||
end: new Date(end).toISOString(),
|
|
||||||
environment,
|
|
||||||
kuery,
|
|
||||||
dependencyName,
|
|
||||||
searchServiceDestinationMetrics,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(({ body }) => body.operations);
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.when('Top operations when data is not loaded', { config: 'basic', archives: [] }, () => {
|
|
||||||
it('handles empty state', async () => {
|
|
||||||
const operations = await callApi({ dependencyName: 'elasticsearch' });
|
|
||||||
expect(operations).to.empty();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/177128
|
|
||||||
registry.when('Top operations when data is generated', { config: 'basic', archives: [] }, () => {
|
|
||||||
before(() =>
|
|
||||||
generateOperationData({
|
|
||||||
apmSynthtraceEsClient,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
after(() => apmSynthtraceEsClient.clean());
|
|
||||||
|
|
||||||
describe('requested for elasticsearch', () => {
|
|
||||||
let response: TopOperations;
|
|
||||||
let searchOperation: ValuesType<TopOperations>;
|
|
||||||
let bulkOperation: ValuesType<TopOperations>;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
response = await callApi({ dependencyName: 'elasticsearch' });
|
|
||||||
searchOperation = response.find((op) => op.spanName === '/_search')!;
|
|
||||||
bulkOperation = response.find((op) => op.spanName === '/_bulk')!;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct operations', () => {
|
|
||||||
expect(response.length).to.eql(2);
|
|
||||||
|
|
||||||
expect(searchOperation).to.be.ok();
|
|
||||||
expect(bulkOperation).to.be.ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct latency', () => {
|
|
||||||
expect(searchOperation.latency).to.eql(ES_SEARCH_DURATION * 1000);
|
|
||||||
expect(bulkOperation.latency).to.eql(ES_BULK_DURATION * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct throughput', () => {
|
|
||||||
const expectedSearchThroughput = roundNumber(
|
|
||||||
ES_SEARCH_UNKNOWN_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE
|
|
||||||
);
|
|
||||||
const expectedBulkThroughput = ES_BULK_RATE;
|
|
||||||
|
|
||||||
expect(roundNumber(searchOperation.throughput)).to.eql(expectedSearchThroughput);
|
|
||||||
expect(roundNumber(bulkOperation.throughput)).to.eql(expectedBulkThroughput);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
searchOperation.timeseries.throughput
|
|
||||||
.map((bucket) => bucket.y)
|
|
||||||
.every((val) => val === expectedSearchThroughput)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct failure rate', () => {
|
|
||||||
const expectedSearchFailureRate =
|
|
||||||
ES_SEARCH_FAILURE_RATE / (ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE);
|
|
||||||
const expectedBulkFailureRate = null;
|
|
||||||
|
|
||||||
expect(searchOperation.failureRate).to.be(expectedSearchFailureRate);
|
|
||||||
|
|
||||||
expect(bulkOperation.failureRate).to.be(expectedBulkFailureRate);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
searchOperation.timeseries.failureRate
|
|
||||||
.map((bucket) => bucket.y)
|
|
||||||
.every((val) => val === expectedSearchFailureRate)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
bulkOperation.timeseries.failureRate
|
|
||||||
.map((bucket) => bucket.y)
|
|
||||||
.every((val) => val === expectedBulkFailureRate)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct impact', () => {
|
|
||||||
expect(searchOperation.impact).to.eql(0);
|
|
||||||
expect(bulkOperation.impact).to.eql(100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('requested for redis', () => {
|
|
||||||
let response: TopOperations;
|
|
||||||
let setOperation: ValuesType<TopOperations>;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
response = await callApi({ dependencyName: 'redis' });
|
|
||||||
setOperation = response.find((op) => op.spanName === 'SET')!;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct operations', () => {
|
|
||||||
expect(response.length).to.eql(1);
|
|
||||||
|
|
||||||
expect(setOperation).to.be.ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct latency', () => {
|
|
||||||
expect(setOperation.latency).to.eql(REDIS_SET_DURATION * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct throughput', () => {
|
|
||||||
expect(roundNumber(setOperation.throughput)).to.eql(roundNumber(REDIS_SET_RATE));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('requested for a specific service', () => {
|
|
||||||
let response: TopOperations;
|
|
||||||
let searchOperation: ValuesType<TopOperations>;
|
|
||||||
let bulkOperation: ValuesType<TopOperations> | undefined;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
kuery: `service.name:"synth-go"`,
|
|
||||||
});
|
|
||||||
searchOperation = response.find((op) => op.spanName === '/_search')!;
|
|
||||||
bulkOperation = response.find((op) => op.spanName === '/_bulk');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct operations', () => {
|
|
||||||
expect(response.length).to.eql(1);
|
|
||||||
|
|
||||||
expect(searchOperation).to.be.ok();
|
|
||||||
expect(bulkOperation).not.to.be.ok();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('requested for a specific environment', () => {
|
|
||||||
let response: TopOperations;
|
|
||||||
let searchOperation: ValuesType<TopOperations> | undefined;
|
|
||||||
let bulkOperation: ValuesType<TopOperations>;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
response = await callApi({
|
|
||||||
dependencyName: 'elasticsearch',
|
|
||||||
environment: 'development',
|
|
||||||
});
|
|
||||||
searchOperation = response.find((op) => op.spanName === '/_search');
|
|
||||||
bulkOperation = response.find((op) => op.spanName === '/_bulk')!;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct operations', () => {
|
|
||||||
expect(response.length).to.eql(1);
|
|
||||||
|
|
||||||
expect(searchOperation).not.to.be.ok();
|
|
||||||
expect(bulkOperation).to.be.ok();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Compare span metrics and span events', () => {
|
|
||||||
let bulkOperationSpanEventsResponse: ValuesType<TopOperations>;
|
|
||||||
let bulkOperationSpanMetricsResponse: ValuesType<TopOperations>;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
const [spanEventsResponse, spanMetricsResponse] = await Promise.all([
|
|
||||||
callApi({ dependencyName: 'elasticsearch', searchServiceDestinationMetrics: false }),
|
|
||||||
callApi({ dependencyName: 'elasticsearch', searchServiceDestinationMetrics: true }),
|
|
||||||
]);
|
|
||||||
function findBulkOperation(op: DependencyOperation) {
|
|
||||||
return op.spanName === '/_bulk';
|
|
||||||
}
|
|
||||||
bulkOperationSpanEventsResponse = spanEventsResponse.find(findBulkOperation)!;
|
|
||||||
bulkOperationSpanMetricsResponse = spanMetricsResponse.find(findBulkOperation)!;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns same latency', () => {
|
|
||||||
expect(bulkOperationSpanEventsResponse.latency).to.eql(
|
|
||||||
bulkOperationSpanMetricsResponse.latency
|
|
||||||
);
|
|
||||||
|
|
||||||
const meanSpanMetrics = meanBy(
|
|
||||||
bulkOperationSpanEventsResponse.timeseries.latency.filter(({ y }) => y !== null),
|
|
||||||
'y'
|
|
||||||
);
|
|
||||||
const meanSpanEvents = meanBy(
|
|
||||||
bulkOperationSpanMetricsResponse.timeseries.latency.filter(({ y }) => y !== null),
|
|
||||||
'y'
|
|
||||||
);
|
|
||||||
expect(meanSpanMetrics).to.eql(meanSpanEvents);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns same throughput', () => {
|
|
||||||
expect(bulkOperationSpanEventsResponse.throughput).to.eql(
|
|
||||||
bulkOperationSpanMetricsResponse.throughput
|
|
||||||
);
|
|
||||||
|
|
||||||
const meanSpanMetrics = meanBy(
|
|
||||||
bulkOperationSpanEventsResponse.timeseries.throughput.filter(({ y }) => y !== 0),
|
|
||||||
'y'
|
|
||||||
);
|
|
||||||
const meanSpanEvents = meanBy(
|
|
||||||
bulkOperationSpanMetricsResponse.timeseries.throughput.filter(({ y }) => y !== 0),
|
|
||||||
'y'
|
|
||||||
);
|
|
||||||
expect(meanSpanMetrics).to.eql(meanSpanEvents);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns same impact', () => {
|
|
||||||
expect(bulkOperationSpanEventsResponse.impact).to.eql(
|
|
||||||
bulkOperationSpanMetricsResponse.impact
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -5,14 +5,9 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Coordinate } from '@kbn/apm-plugin/typings/timeseries';
|
|
||||||
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
|
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
|
||||||
import { Maybe } from '@kbn/apm-plugin/typings/common';
|
import { Maybe } from '@kbn/apm-plugin/typings/common';
|
||||||
|
|
||||||
export function roundNumber(num: Maybe<number>) {
|
export function roundNumber(num: Maybe<number>) {
|
||||||
return isFiniteNumber(num) ? Number(num.toPrecision(4)) : null;
|
return isFiniteNumber(num) ? Number(num.toPrecision(4)) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeEmptyCoordinates(coordinates: Coordinate[]) {
|
|
||||||
return coordinates.filter(({ y }) => isFiniteNumber(y));
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue