mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Migrate /custom_dashboards
to be deployment agnostic (#199295)
closes #198964 closes #198966 part of https://github.com/elastic/kibana/issues/193245 ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` - MKI tested against [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)
This commit is contained in:
parent
763410c8ab
commit
a947525936
19 changed files with 896 additions and 894 deletions
|
@ -5,7 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ApmApiClient } from '../../common/config';
|
||||
import { ApmApiProvider } from '../../../../services/apm_api';
|
||||
|
||||
export type ApmApiClient = ReturnType<typeof ApmApiProvider>;
|
||||
|
||||
export async function getServiceDashboardApi(
|
||||
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.
|
||||
*/
|
||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
|
||||
export const generateOperationDataConfig = {
|
||||
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.
|
||||
*/
|
||||
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';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
||||
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;
|
||||
|
@ -29,24 +29,23 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Dependency metadata when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Dependency metadata', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
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());
|
||||
|
||||
it('returns correct metadata for the dependency', async () => {
|
||||
|
@ -61,6 +60,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
await apmSynthtraceEsClient.clean();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -6,13 +6,13 @@
|
|||
*/
|
||||
import expect from '@kbn/expect';
|
||||
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';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
||||
const registry = getService('registry');
|
||||
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;
|
||||
const dependencyName = 'elasticsearch';
|
||||
|
@ -34,61 +34,21 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Dependency for service when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Dependency for service', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles empty state #1', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
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(
|
||||
'Dependency for service breakdown when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles empty state #2', async () => {
|
||||
const { status, body } = await callApi();
|
||||
describe('when data is loaded', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
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 () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
await generateData({ apmSynthtraceEsClient, start, end });
|
||||
});
|
||||
after(() => apmSynthtraceEsClient.clean());
|
|
@ -7,16 +7,16 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
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 { roundNumber } from '../../utils';
|
||||
import { roundNumber } from '../utils/common';
|
||||
|
||||
type TopDependencies = APIReturnType<'GET /internal/apm/dependencies/top_dependencies'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
||||
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;
|
||||
|
@ -37,24 +37,21 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Top dependencies when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Top dependencies', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
expect(status).to.be(200);
|
||||
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', () => {
|
||||
let topDependencies: TopDependencies;
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
await generateData({ apmSynthtraceEsClient, start, end });
|
||||
const response = await callApi();
|
||||
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 { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
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) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
||||
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;
|
||||
|
@ -50,10 +50,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Top dependency spans when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Top dependency spans', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles empty state', async () => {
|
||||
const { body, status } = await callApi({
|
||||
dependencyName: 'elasticsearch',
|
||||
|
@ -63,14 +61,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
expect(status).to.be(200);
|
||||
expect(body.spans).to.empty();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177135
|
||||
registry.when(
|
||||
'Top dependency spans when data is loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('when data is loaded', () => {
|
||||
const javaInstance = apm
|
||||
.service({ name: 'java', environment: 'production', agentName: 'java' })
|
||||
.instance('instance-a');
|
||||
|
@ -78,8 +71,11 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
const goInstance = apm
|
||||
.service({ name: 'go', environment: 'development', agentName: 'go' })
|
||||
.instance('instance-a');
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
|
||||
await apmSynthtraceEsClient.index([
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
|
@ -240,6 +236,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -6,13 +6,13 @@
|
|||
*/
|
||||
import expect from '@kbn/expect';
|
||||
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';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
||||
const registry = getService('registry');
|
||||
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;
|
||||
const dependencyName = 'elasticsearch';
|
||||
|
@ -34,23 +34,22 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Dependency upstream services when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Dependency upstream services', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
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', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
|
||||
await generateData({ apmSynthtraceEsClient, start, end });
|
||||
});
|
||||
after(() => apmSynthtraceEsClient.clean());
|
|
@ -12,5 +12,7 @@ export default function apmApiIntegrationTests({
|
|||
}: DeploymentAgnosticFtrProviderContext) {
|
||||
describe('APM', function () {
|
||||
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.
|
||||
*/
|
||||
|
||||
import { Coordinate } from '@kbn/apm-plugin/typings/timeseries';
|
||||
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;
|
||||
}
|
||||
|
||||
export function removeEmptyCoordinates(coordinates: Coordinate[]) {
|
||||
return coordinates.filter(({ y }) => isFiniteNumber(y));
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue