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:
Milosz Marcinkowski 2024-11-08 18:40:55 +01:00 committed by GitHub
parent 763410c8ab
commit a947525936
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 896 additions and 894 deletions

View file

@ -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,

View file

@ -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);
});
});
});
});
}

View file

@ -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'));
});
}

View file

@ -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());
});
});
}

View file

@ -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,

View file

@ -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'));
});
}

View file

@ -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();
});
}
);
});
});
}

View file

@ -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());

View file

@ -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;

View file

@ -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
);
});
});
});
});
}

View file

@ -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());
}
);
});
});
}

View file

@ -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());

View file

@ -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'));
});
}

View file

@ -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;
}

View file

@ -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);
});
});
});
}

View file

@ -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());
});
}

View file

@ -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
);
});
});
});
}

View file

@ -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));
}