[8.17] [ObsUX][APM] Migrate Service Overview archiver test cases to synthtrace (#201407) (#201536)

# Backport

This will backport the following commits from `main` to `8.17`:
- [[ObsUX][APM] Migrate Service Overview archiver test cases to
synthtrace (#201407)](https://github.com/elastic/kibana/pull/201407)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Gonçalo Rica Pais da
Silva","email":"goncalo.rica@elastic.co"},"sourceCommit":{"committedDate":"2024-11-25T10:13:16Z","message":"[ObsUX][APM]
Migrate Service Overview archiver test cases to synthtrace
(#201407)\n\nPart of #193245\r\nRelated #200226\r\nCloses #200743
\r\n\r\n## Summary\r\n\r\nThis PR completes the migration of remaining
Service Overview tests to\r\nthe Deployment Agnostic test framework. In
this PR, one test was\r\ndeduplicated (the Dependencies test), and the
Instances Detailed\r\nStatistics cases dealing with archiver data was
migrated to make use of\r\nsynthtrace instead. Snapshots included were
redone to match the data\r\ngenerated by synthtrace, but no other cases
were changed to ensure the\r\nnew migrated tests were passing the same
assumptions as before.\r\n\r\n## How to Test\r\n\r\n###
Serverless\r\n\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt/apm.serverless.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts\r\n```\r\n\r\nIt's
recommended to be run
against\r\n[MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)\r\n\r\n###
Stateful\r\n\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts\r\n```\r\n\r\n---------\r\n\r\nCo-authored-by:
jennypavlova <jennypavlova94@gmail.com>\r\nCo-authored-by: Elastic
Machine
<elasticmachine@users.noreply.github.com>","sha":"4891c7d7f99097de649667ad0c1f48515120edbd","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services","v8.17.0"],"title":"[ObsUX][APM]
Migrate Service Overview archiver test cases to
synthtrace","number":201407,"url":"https://github.com/elastic/kibana/pull/201407","mergeCommit":{"message":"[ObsUX][APM]
Migrate Service Overview archiver test cases to synthtrace
(#201407)\n\nPart of #193245\r\nRelated #200226\r\nCloses #200743
\r\n\r\n## Summary\r\n\r\nThis PR completes the migration of remaining
Service Overview tests to\r\nthe Deployment Agnostic test framework. In
this PR, one test was\r\ndeduplicated (the Dependencies test), and the
Instances Detailed\r\nStatistics cases dealing with archiver data was
migrated to make use of\r\nsynthtrace instead. Snapshots included were
redone to match the data\r\ngenerated by synthtrace, but no other cases
were changed to ensure the\r\nnew migrated tests were passing the same
assumptions as before.\r\n\r\n## How to Test\r\n\r\n###
Serverless\r\n\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt/apm.serverless.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts\r\n```\r\n\r\nIt's
recommended to be run
against\r\n[MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)\r\n\r\n###
Stateful\r\n\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts\r\n```\r\n\r\n---------\r\n\r\nCo-authored-by:
jennypavlova <jennypavlova94@gmail.com>\r\nCo-authored-by: Elastic
Machine
<elasticmachine@users.noreply.github.com>","sha":"4891c7d7f99097de649667ad0c1f48515120edbd"}},"sourceBranch":"main","suggestedTargetBranches":["8.17"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/201407","number":201407,"mergeCommit":{"message":"[ObsUX][APM]
Migrate Service Overview archiver test cases to synthtrace
(#201407)\n\nPart of #193245\r\nRelated #200226\r\nCloses #200743
\r\n\r\n## Summary\r\n\r\nThis PR completes the migration of remaining
Service Overview tests to\r\nthe Deployment Agnostic test framework. In
this PR, one test was\r\ndeduplicated (the Dependencies test), and the
Instances Detailed\r\nStatistics cases dealing with archiver data was
migrated to make use of\r\nsynthtrace instead. Snapshots included were
redone to match the data\r\ngenerated by synthtrace, but no other cases
were changed to ensure the\r\nnew migrated tests were passing the same
assumptions as before.\r\n\r\n## How to Test\r\n\r\n###
Serverless\r\n\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt/apm.serverless.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts\r\n```\r\n\r\nIt's
recommended to be run
against\r\n[MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)\r\n\r\n###
Stateful\r\n\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts\r\n```\r\n\r\n---------\r\n\r\nCo-authored-by:
jennypavlova <jennypavlova94@gmail.com>\r\nCo-authored-by: Elastic
Machine
<elasticmachine@users.noreply.github.com>","sha":"4891c7d7f99097de649667ad0c1f48515120edbd"}},{"branch":"8.17","label":"v8.17.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Gonçalo Rica Pais da Silva <goncalo.rica@elastic.co>
This commit is contained in:
Kibana Machine 2024-12-12 02:59:21 +11:00 committed by GitHub
parent 4688d25dfa
commit fdad711331
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 961 additions and 1201 deletions

View file

@ -11,7 +11,6 @@ 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'));

View file

@ -1,72 +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 { DependencyNode } from '@kbn/apm-plugin/common/connections';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
import { generateData } from './generate_data';
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';
const serviceName = 'synth-go';
async function callApi() {
return await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services/{serviceName}/dependencies',
params: {
path: { serviceName },
query: {
environment: 'production',
numBuckets: 20,
offset: '1d',
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
},
},
});
}
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();
});
});
describe('when data is loaded', () => {
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
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);
});
});
});
}

View file

@ -4,301 +4,328 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { last, pick } from 'lodash';
import { DependencyNode } from '@kbn/apm-plugin/common/connections';
import type { ValuesType } from 'utility-types';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { type Node, NodeType } from '@kbn/apm-plugin/common/connections';
import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED,
} from '@kbn/apm-plugin/common/environment_filter_values';
import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { roundNumber } from '../../utils/common';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context';
import { roundNumber } from '../../utils/common';
import { generateDependencyData } from '../generate_data';
import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils';
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
const es = getService('es');
const { start, end } = {
start: '2021-08-03T06:50:15.910Z',
end: '2021-08-03T07:20:15.910Z',
};
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';
const serviceName = 'synth-go';
function getName(node: Node) {
return node.type === NodeType.service ? node.serviceName : node.dependencyName;
}
describe('Service Overview', () => {
describe('Dependencies', () => {
describe('when data is not loaded', () => {
it('handles the empty state', async () => {
const response = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/dependencies`,
params: {
path: { serviceName: 'opbeans-java' },
query: {
start,
end,
numBuckets: 20,
environment: ENVIRONMENT_ALL.value,
},
},
});
async function callApi() {
return await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services/{serviceName}/dependencies',
params: {
path: { serviceName },
query: {
environment: 'production',
numBuckets: 20,
offset: '1d',
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
},
},
});
}
expect(response.status).to.be(200);
expect(response.body.serviceDependencies).to.eql([]);
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.be.empty();
});
});
describe('when specific data is loaded', () => {
let response: {
status: number;
body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>;
};
const indices = {
metric: 'apm-dependencies-metric',
transaction: 'apm-dependencies-transaction',
span: 'apm-dependencies-span',
};
const startTime = new Date(start).getTime();
const endTime = new Date(end).getTime();
after(async () => {
const allIndices = Object.values(indices).join(',');
const indexExists = await es.indices.exists({ index: allIndices });
if (indexExists) {
await es.indices.delete({
index: allIndices,
});
}
});
before(async () => {
await es.indices.create({
index: indices.metric,
body: {
mappings: apmDependenciesMapping,
},
});
await es.indices.create({
index: indices.transaction,
body: {
mappings: apmDependenciesMapping,
},
});
await es.indices.create({
index: indices.span,
body: {
mappings: apmDependenciesMapping,
},
});
const docs = [
...createServiceDependencyDocs({
service: {
name: 'opbeans-java',
environment: 'production',
},
agentName: 'java',
span: {
type: 'external',
subtype: 'http',
},
resource: 'opbeans-node:3000',
outcome: 'success',
responseTime: {
count: 2,
sum: 10,
},
time: startTime,
to: {
service: {
name: 'opbeans-node',
},
agentName: 'nodejs',
},
}),
...createServiceDependencyDocs({
service: {
name: 'opbeans-java',
environment: 'production',
},
agentName: 'java',
span: {
type: 'external',
subtype: 'http',
},
resource: 'opbeans-node:3000',
outcome: 'failure',
responseTime: {
count: 1,
sum: 10,
},
time: startTime,
}),
...createServiceDependencyDocs({
service: {
name: 'opbeans-java',
environment: 'production',
},
agentName: 'java',
span: {
type: 'external',
subtype: 'http',
},
resource: 'postgres',
outcome: 'success',
responseTime: {
count: 1,
sum: 3,
},
time: startTime,
}),
...createServiceDependencyDocs({
service: {
name: 'opbeans-java',
environment: 'production',
},
agentName: 'java',
span: {
type: 'external',
subtype: 'http',
},
resource: 'opbeans-node-via-proxy',
outcome: 'success',
responseTime: {
count: 1,
sum: 1,
},
time: endTime - 1,
to: {
service: {
name: 'opbeans-node',
},
agentName: 'nodejs',
},
}),
];
const bulkActions = docs.reduce(
(prev, doc) => {
return [...prev, { index: { _index: indices[doc.processor.event] } }, doc];
},
[] as Array<
| {
index: {
_index: string;
};
}
| ValuesType<typeof docs>
>
);
await es.bulk({
body: bulkActions,
refresh: 'wait_for',
});
response = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/dependencies`,
params: {
path: { serviceName: 'opbeans-java' },
query: {
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
numBuckets: 20,
environment: ENVIRONMENT_ALL.value,
},
},
});
});
describe('when specific data is loaded', () => {
let response: {
status: number;
body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>;
};
const indices = {
metric: 'apm-dependencies-metric',
transaction: 'apm-dependencies-transaction',
span: 'apm-dependencies-span',
};
const startTime = new Date(start).getTime();
const endTime = new Date(end).getTime();
after(async () => {
const allIndices = Object.values(indices).join(',');
const indexExists = await es.indices.exists({ index: allIndices });
if (indexExists) {
await es.indices.delete({
index: allIndices,
});
}
});
before(async () => {
await es.indices.create({
index: indices.metric,
body: {
mappings: apmDependenciesMapping,
},
});
await es.indices.create({
index: indices.transaction,
body: {
mappings: apmDependenciesMapping,
},
});
await es.indices.create({
index: indices.span,
body: {
mappings: apmDependenciesMapping,
},
});
const docs = [
...createServiceDependencyDocs({
service: {
name: 'opbeans-java',
environment: 'production',
},
agentName: 'java',
span: {
type: 'external',
subtype: 'http',
},
resource: 'opbeans-node:3000',
outcome: 'success',
responseTime: {
count: 2,
sum: 10,
},
time: startTime,
to: {
service: {
name: 'opbeans-node',
},
agentName: 'nodejs',
},
}),
...createServiceDependencyDocs({
service: {
name: 'opbeans-java',
environment: 'production',
},
agentName: 'java',
span: {
type: 'external',
subtype: 'http',
},
resource: 'opbeans-node:3000',
outcome: 'failure',
responseTime: {
count: 1,
sum: 10,
},
time: startTime,
}),
...createServiceDependencyDocs({
service: {
name: 'opbeans-java',
environment: 'production',
},
agentName: 'java',
span: {
type: 'external',
subtype: 'http',
},
resource: 'postgres',
outcome: 'success',
responseTime: {
count: 1,
sum: 3,
},
time: startTime,
}),
...createServiceDependencyDocs({
service: {
name: 'opbeans-java',
environment: 'production',
},
agentName: 'java',
span: {
type: 'external',
subtype: 'http',
},
resource: 'opbeans-node-via-proxy',
outcome: 'success',
responseTime: {
count: 1,
sum: 1,
},
time: endTime - 1,
to: {
service: {
name: 'opbeans-node',
},
agentName: 'nodejs',
},
}),
];
const bulkActions = docs.reduce(
(prev, doc) => {
return [...prev, { index: { _index: indices[doc.processor.event] } }, doc];
},
[] as Array<
| {
index: {
_index: string;
};
}
| ValuesType<typeof docs>
>
);
await es.bulk({
body: bulkActions,
refresh: 'wait_for',
});
response = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/dependencies`,
params: {
path: { serviceName: 'opbeans-java' },
query: {
start,
end,
numBuckets: 20,
environment: ENVIRONMENT_ALL.value,
},
},
});
});
it('returns a 200', () => {
expect(response.status).to.be(200);
});
it('returns two dependencies', () => {
expect(response.body.serviceDependencies.length).to.be(2);
});
it('returns opbeans-node as a dependency', () => {
const opbeansNode = response.body.serviceDependencies.find(
(item) => getName(item.location) === 'opbeans-node'
);
expect(opbeansNode !== undefined).to.be(true);
const values = {
latency: roundNumber(opbeansNode?.currentStats.latency.value),
throughput: roundNumber(opbeansNode?.currentStats.throughput.value),
errorRate: roundNumber(opbeansNode?.currentStats.errorRate.value),
impact: opbeansNode?.currentStats.impact,
...pick(opbeansNode?.location, 'serviceName', 'type', 'agentName', 'environment'),
};
const count = 4;
const sum = 21;
const errors = 1;
expect(values).to.eql({
agentName: 'nodejs',
environment: ENVIRONMENT_NOT_DEFINED.value,
serviceName: 'opbeans-node',
type: 'service',
errorRate: roundNumber(errors / count),
latency: roundNumber(sum / count),
throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)),
impact: 100,
});
const firstValue = roundNumber(opbeansNode?.currentStats.latency.timeseries[0].y);
const lastValue = roundNumber(last(opbeansNode?.currentStats.latency.timeseries)?.y);
expect(firstValue).to.be(roundNumber(20 / 3));
expect(lastValue).to.be(1);
});
it('returns postgres as an external dependency', () => {
const postgres = response.body.serviceDependencies.find(
(item) => getName(item.location) === 'postgres'
);
expect(postgres !== undefined).to.be(true);
const values = {
latency: roundNumber(postgres?.currentStats.latency.value),
throughput: roundNumber(postgres?.currentStats.throughput.value),
errorRate: roundNumber(postgres?.currentStats.errorRate.value),
impact: postgres?.currentStats.impact,
...pick(postgres?.location, 'spanType', 'spanSubtype', 'dependencyName', 'type'),
};
const count = 1;
const sum = 3;
const errors = 0;
expect(values).to.eql({
spanType: 'external',
spanSubtype: 'http',
dependencyName: 'postgres',
type: 'dependency',
errorRate: roundNumber(errors / count),
latency: roundNumber(sum / count),
throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)),
impact: 0,
});
});
it('returns a 200', () => {
expect(response.status).to.be(200);
});
// UNSUPPORTED TEST CASES - when data is loaded
// TODO: These tests should be migrated to use synthtrace: https://github.com/elastic/kibana/issues/200743
it('returns two dependencies', () => {
expect(response.body.serviceDependencies.length).to.be(2);
});
it('returns opbeans-node as a dependency', () => {
const opbeansNode = response.body.serviceDependencies.find(
(item) => getName(item.location) === 'opbeans-node'
);
expect(opbeansNode !== undefined).to.be(true);
const values = {
latency: roundNumber(opbeansNode?.currentStats.latency.value),
throughput: roundNumber(opbeansNode?.currentStats.throughput.value),
errorRate: roundNumber(opbeansNode?.currentStats.errorRate.value),
impact: opbeansNode?.currentStats.impact,
...pick(opbeansNode?.location, 'serviceName', 'type', 'agentName', 'environment'),
};
const count = 4;
const sum = 21;
const errors = 1;
expect(values).to.eql({
agentName: 'nodejs',
environment: ENVIRONMENT_NOT_DEFINED.value,
serviceName: 'opbeans-node',
type: 'service',
errorRate: roundNumber(errors / count),
latency: roundNumber(sum / count),
throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)),
impact: 100,
});
const firstValue = roundNumber(opbeansNode?.currentStats.latency.timeseries[0].y);
const lastValue = roundNumber(last(opbeansNode?.currentStats.latency.timeseries)?.y);
expect(firstValue).to.be(roundNumber(20 / 3));
expect(lastValue).to.be(1);
});
it('returns postgres as an external dependency', () => {
const postgres = response.body.serviceDependencies.find(
(item) => getName(item.location) === 'postgres'
);
expect(postgres !== undefined).to.be(true);
const values = {
latency: roundNumber(postgres?.currentStats.latency.value),
throughput: roundNumber(postgres?.currentStats.throughput.value),
errorRate: roundNumber(postgres?.currentStats.errorRate.value),
impact: postgres?.currentStats.impact,
...pick(postgres?.location, 'spanType', 'spanSubtype', 'dependencyName', 'type'),
};
const count = 1;
const sum = 3;
const errors = 0;
expect(values).to.eql({
spanType: 'external',
spanSubtype: 'http',
dependencyName: 'postgres',
type: 'dependency',
errorRate: roundNumber(errors / count),
latency: roundNumber(sum / count),
throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)),
impact: 0,
});
});
});
describe('when data is loaded', () => {
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
await generateDependencyData({ 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);
});
});
});
}

View file

@ -0,0 +1,110 @@
/*
* 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 { apm, timerange } from '@kbn/apm-synthtrace-client';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
export const dataConfig = {
rate: 20,
transaction: {
name: 'GET /api/product/list',
duration: 1000,
},
span: {
name: 'GET apm-*/_search',
type: 'db',
subType: 'elasticsearch',
destination: 'elasticsearch',
},
} as const;
export async function generateServiceData({
apmSynthtraceEsClient,
start,
end,
name,
environment,
agentName,
}: {
apmSynthtraceEsClient: ApmSynthtraceEsClient;
start: number;
end: number;
name: string;
environment: string;
agentName: string;
}) {
const instance = apm.service({ name, environment, agentName }).instance('instance-a');
const { rate, transaction, span } = dataConfig;
await apmSynthtraceEsClient.index([
timerange(start, end)
.interval('1m')
.rate(rate)
.generator((timestamp) =>
instance
.transaction({ transactionName: transaction.name })
.timestamp(timestamp)
.duration(transaction.duration)
.success()
.children(
instance
.span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType })
.duration(transaction.duration)
.success()
.destination(span.destination)
.timestamp(timestamp)
)
),
timerange(start, end)
.interval('1m')
.rate(rate)
.generator((timestamp) =>
instance
.appMetrics({
'system.process.cpu.total.norm.pct': 0.5,
'system.memory.total': 120000,
'system.process.cgroup.memory.mem.usage.bytes': 50000,
})
.timestamp(timestamp)
),
]);
}
export async function generateDependencyData({
apmSynthtraceEsClient,
start,
end,
}: {
apmSynthtraceEsClient: ApmSynthtraceEsClient;
start: number;
end: number;
}) {
const instance = apm
.service({ name: 'synth-go', environment: 'production', agentName: 'go' })
.instance('instance-a');
const { rate, transaction, span } = dataConfig;
await apmSynthtraceEsClient.index(
timerange(start, end)
.interval('1m')
.rate(rate)
.generator((timestamp) =>
instance
.transaction({ transactionName: transaction.name })
.timestamp(timestamp)
.duration(transaction.duration)
.success()
.children(
instance
.span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType })
.duration(transaction.duration)
.success()
.destination(span.destination)
.timestamp(timestamp)
)
)
);
}

View file

@ -6,12 +6,19 @@
*/
import expect from '@kbn/expect';
import moment from 'moment';
import type { Coordinate } from '@kbn/apm-plugin/typings/timeseries';
import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types';
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
import { getServiceNodeIds } from './get_service_node_ids';
import { generateServiceData } from './generate_data';
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
const serviceName = 'opbeans-java';
@ -20,6 +27,11 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
end: '2021-08-03T07:20:15.910Z',
};
interface Response {
status: number;
body: APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>;
}
describe('Service Overview', () => {
describe('Instances detailed statistics', () => {
describe('when data is not loaded', () => {
@ -49,8 +61,166 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
});
});
// UNSUPPORTED TEST CASES - when data is loaded
// TODO: These tests should be migrated to use synthtrace: https://github.com/elastic/kibana/issues/200743
describe('when data is loaded', () => {
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
await generateServiceData({
apmSynthtraceEsClient,
start: new Date(start).getTime(),
end: new Date(end).getTime(),
name: serviceName,
environment: 'ENVIRONMENT_ALL',
agentName: 'java',
});
});
after(() => apmSynthtraceEsClient.clean());
describe('fetching data without comparison', () => {
let response: Response;
let serviceNodeIds: string[];
beforeEach(async () => {
serviceNodeIds = await getServiceNodeIds({
apmApiClient,
start,
end,
});
response = await apmApiClient.readUser({
endpoint:
'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics',
params: {
path: { serviceName },
query: {
latencyAggregationType: LatencyAggregationType.avg,
start,
end,
numBuckets: 20,
transactionType: 'request',
serviceNodeIds: JSON.stringify(serviceNodeIds),
environment: 'ENVIRONMENT_ALL',
kuery: '',
},
},
});
});
it('returns a service node item', () => {
expect(Object.values(response.body.currentPeriod).length).to.be.greaterThan(0);
expect(Object.values(response.body.previousPeriod)).to.eql(0);
});
it('returns statistics for each service node', () => {
const item = response.body.currentPeriod[serviceNodeIds[0]];
expect(item?.cpuUsage?.some((point) => isFiniteNumber(point.y))).to.be(true);
expect(item?.memoryUsage?.some((point) => isFiniteNumber(point.y))).to.be(true);
expect(item?.errorRate?.some((point) => isFiniteNumber(point.y))).to.be(true);
expect(item?.throughput?.some((point) => isFiniteNumber(point.y))).to.be(true);
expect(item?.latency?.some((point) => isFiniteNumber(point.y))).to.be(true);
});
it('returns the right data', () => {
expectSnapshot(Object.values(response.body.currentPeriod).length).toMatchInline(`1`);
expectSnapshot(Object.keys(response.body.currentPeriod)).toMatchInline(`
Array [
"instance-a",
]
`);
expectSnapshot(response.body).toMatch();
});
});
describe('fetching data with comparison', () => {
let response: Response;
let serviceNodeIds: string[];
beforeEach(async () => {
serviceNodeIds = await getServiceNodeIds({
apmApiClient,
start,
end,
});
response = await apmApiClient.readUser({
endpoint:
'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics',
params: {
path: { serviceName },
query: {
latencyAggregationType: LatencyAggregationType.avg,
numBuckets: 20,
transactionType: 'request',
serviceNodeIds: JSON.stringify(serviceNodeIds),
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
offset: '15m',
environment: 'ENVIRONMENT_ALL',
kuery: '',
},
},
});
});
it('returns a service node item for current and previous periods', () => {
expect(Object.values(response.body.currentPeriod).length).to.be.greaterThan(0);
expect(Object.values(response.body.previousPeriod).length).to.be.greaterThan(0);
});
it('returns statistics for current and previous periods', () => {
const currentPeriodItem = response.body.currentPeriod[serviceNodeIds[0]];
function hasValidYCoordinate(point: Coordinate) {
return isFiniteNumber(point.y);
}
expect(currentPeriodItem?.cpuUsage?.some(hasValidYCoordinate)).to.be(true);
expect(currentPeriodItem?.memoryUsage?.some(hasValidYCoordinate)).to.be(true);
expect(currentPeriodItem?.errorRate?.some(hasValidYCoordinate)).to.be(true);
expect(currentPeriodItem?.throughput?.some(hasValidYCoordinate)).to.be(true);
expect(currentPeriodItem?.latency?.some(hasValidYCoordinate)).to.be(true);
const previousPeriodItem = response.body.previousPeriod[serviceNodeIds[0]];
expect(previousPeriodItem?.cpuUsage?.some(hasValidYCoordinate)).to.be(true);
expect(previousPeriodItem?.memoryUsage?.some(hasValidYCoordinate)).to.be(true);
expect(previousPeriodItem?.errorRate?.some(hasValidYCoordinate)).to.be(true);
expect(previousPeriodItem?.throughput?.some(hasValidYCoordinate)).to.be(true);
expect(previousPeriodItem?.latency?.some(hasValidYCoordinate)).to.be(true);
});
it('returns the right data for current and previous periods', () => {
expectSnapshot(Object.values(response.body.currentPeriod).length).toMatchInline(`1`);
expectSnapshot(Object.values(response.body.previousPeriod).length).toMatchInline(`1`);
expectSnapshot(Object.keys(response.body.currentPeriod)).toMatchInline(`
Array [
"instance-a",
]
`);
expectSnapshot(Object.keys(response.body.previousPeriod)).toMatchInline(`
Array [
"instance-a",
]
`);
expectSnapshot(response.body).toMatch();
});
it('matches x-axis on current period and previous period', () => {
const currentLatencyItems = response.body.currentPeriod[serviceNodeIds[0]]?.latency;
const previousLatencyItems = response.body.previousPeriod[serviceNodeIds[0]]?.latency;
expect(currentLatencyItems?.map(({ x }) => x)).to.be.eql(
previousLatencyItems?.map(({ x }) => x)
);
});
});
});
});
});
}

View file

@ -1,253 +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 { omit, sortBy } from 'lodash';
import { type Node, NodeType } from '@kbn/apm-plugin/common/connections';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import archives from '../../../common/fixtures/es_archiver/archives_metadata';
import type { FtrProviderContext } from '../../../common/ftr_provider_context';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const archiveName = 'apm_8.0.0';
const { start, end } = archives[archiveName];
function getName(node: Node) {
return node.type === NodeType.service ? node.serviceName : node.dependencyName;
}
registry.when(
'Service overview dependencies when data is loaded',
{ config: 'basic', archives: [archiveName] },
() => {
let response: {
status: number;
body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>;
};
before(async () => {
response = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/dependencies`,
params: {
path: { serviceName: 'opbeans-python' },
query: {
start,
end,
numBuckets: 20,
environment: ENVIRONMENT_ALL.value,
},
},
});
});
it('returns a successful response', () => {
expect(response.status).to.be(200);
});
it('returns at least one item', () => {
expect(response.body.serviceDependencies.length).to.be.greaterThan(0);
expectSnapshot(response.body.serviceDependencies.length).toMatchInline(`4`);
const { currentStats, ...firstItem } = sortBy(
response.body.serviceDependencies,
'currentStats.impact'
).reverse()[0];
expectSnapshot(firstItem.location).toMatchInline(`
Object {
"agentName": "dotnet",
"dependencyName": "opbeans:3000",
"environment": "production",
"id": "5948c153c2d8989f92a9c75ef45bb845f53e200d",
"serviceName": "opbeans-dotnet",
"type": "service",
}
`);
expectSnapshot(
omit(currentStats, [
'errorRate.timeseries',
'throughput.timeseries',
'latency.timeseries',
'totalTime.timeseries',
])
).toMatchInline(`
Object {
"errorRate": Object {
"value": 0.163636363636364,
},
"impact": 100,
"latency": Object {
"value": 1117085.74545455,
},
"throughput": Object {
"value": 1.83333333333333,
},
"totalTime": Object {
"value": 61439716,
},
}
`);
});
it('returns the right names', () => {
const names = response.body.serviceDependencies.map((item) => getName(item.location));
expectSnapshot(names.sort()).toMatchInline(`
Array [
"elasticsearch",
"opbeans-dotnet",
"postgresql",
"redis",
]
`);
});
it('returns the right service names', () => {
const serviceNames = response.body.serviceDependencies
.map((item) =>
item.location.type === NodeType.service ? getName(item.location) : undefined
)
.filter(Boolean);
expectSnapshot(serviceNames.sort()).toMatchInline(`
Array [
"opbeans-dotnet",
]
`);
});
it('returns the right latency values', () => {
const latencyValues = sortBy(
response.body.serviceDependencies.map((item) => ({
name: getName(item.location),
latency: item.currentStats.latency.value,
})),
'name'
);
expectSnapshot(latencyValues).toMatchInline(`
Array [
Object {
"latency": 9496.32291666667,
"name": "elasticsearch",
},
Object {
"latency": 1117085.74545455,
"name": "opbeans-dotnet",
},
Object {
"latency": 27826.9968314322,
"name": "postgresql",
},
Object {
"latency": 1468.27242524917,
"name": "redis",
},
]
`);
});
it('returns the right throughput values', () => {
const throughputValues = sortBy(
response.body.serviceDependencies.map((item) => ({
name: getName(item.location),
throughput: item.currentStats.throughput.value,
})),
'name'
);
expectSnapshot(throughputValues).toMatchInline(`
Array [
Object {
"name": "elasticsearch",
"throughput": 3.2,
},
Object {
"name": "opbeans-dotnet",
"throughput": 1.83333333333333,
},
Object {
"name": "postgresql",
"throughput": 52.6,
},
Object {
"name": "redis",
"throughput": 40.1333333333333,
},
]
`);
});
it('returns the right impact values', () => {
const impactValues = sortBy(
response.body.serviceDependencies.map((item) => ({
name: getName(item.location),
impact: item.currentStats.impact,
})),
'name'
);
expectSnapshot(impactValues).toMatchInline(`
Array [
Object {
"impact": 0,
"name": "elasticsearch",
},
Object {
"impact": 100,
"name": "opbeans-dotnet",
},
Object {
"impact": 71.0403531954737,
"name": "postgresql",
},
Object {
"impact": 1.41447268043525,
"name": "redis",
},
]
`);
});
it('returns the right totalTime values', () => {
const totalTimeValues = sortBy(
response.body.serviceDependencies.map((item) => ({
name: getName(item.location),
totalTime: item.currentStats.totalTime.value,
})),
'name'
);
expectSnapshot(totalTimeValues).toMatchInline(`
Array [
Object {
"name": "elasticsearch",
"totalTime": 911647,
},
Object {
"name": "opbeans-dotnet",
"totalTime": 61439716,
},
Object {
"name": "postgresql",
"totalTime": 43911001,
},
Object {
"name": "redis",
"totalTime": 1767800,
},
]
`);
});
}
);
}

View file

@ -1,42 +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 { take } from 'lodash';
import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types';
import type { ApmServices } from '../../common/config';
export async function getServiceNodeIds({
apmApiClient,
start,
end,
serviceName = 'opbeans-java',
count = 1,
}: {
apmApiClient: Awaited<ReturnType<ApmServices['apmApiClient']>>;
start: string;
end: string;
serviceName?: string;
count?: number;
}) {
const { body } = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`,
params: {
path: { serviceName },
query: {
latencyAggregationType: LatencyAggregationType.avg,
start,
end,
transactionType: 'request',
environment: 'ENVIRONMENT_ALL',
kuery: '',
sortField: 'throughput',
sortDirection: 'desc',
},
},
});
return take(body.currentPeriod.map((item) => item.serviceNodeName).sort(), count);
}

View file

@ -1,179 +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 moment from 'moment';
import type { Coordinate } from '@kbn/apm-plugin/typings/timeseries';
import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types';
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import type { FtrProviderContext } from '../../common/ftr_provider_context';
import archives from '../../common/fixtures/es_archiver/archives_metadata';
import { getServiceNodeIds } from './get_service_node_ids';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const serviceName = 'opbeans-java';
const archiveName = 'apm_8.0.0';
const { start, end } = archives[archiveName];
interface Response {
status: number;
body: APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>;
}
registry.when(
'Service overview instances detailed statistics when data is loaded',
{ config: 'basic', archives: [archiveName] },
() => {
describe('fetching data without comparison', () => {
let response: Response;
let serviceNodeIds: string[];
beforeEach(async () => {
serviceNodeIds = await getServiceNodeIds({
apmApiClient,
start,
end,
});
response = await apmApiClient.readUser({
endpoint:
'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics',
params: {
path: { serviceName },
query: {
latencyAggregationType: LatencyAggregationType.avg,
start,
end,
numBuckets: 20,
transactionType: 'request',
serviceNodeIds: JSON.stringify(serviceNodeIds),
environment: 'ENVIRONMENT_ALL',
kuery: '',
},
},
});
});
it('returns a service node item', () => {
expect(Object.values(response.body.currentPeriod).length).to.be.greaterThan(0);
expect(Object.values(response.body.previousPeriod)).to.eql(0);
});
it('returns statistics for each service node', async () => {
const item = response.body.currentPeriod[serviceNodeIds[0]];
expect(item?.cpuUsage?.some((point) => isFiniteNumber(point.y))).to.be(true);
expect(item?.memoryUsage?.some((point) => isFiniteNumber(point.y))).to.be(true);
expect(item?.errorRate?.some((point) => isFiniteNumber(point.y))).to.be(true);
expect(item?.throughput?.some((point) => isFiniteNumber(point.y))).to.be(true);
expect(item?.latency?.some((point) => isFiniteNumber(point.y))).to.be(true);
});
it('returns the right data', () => {
expectSnapshot(Object.values(response.body.currentPeriod).length).toMatchInline(`1`);
expectSnapshot(Object.keys(response.body.currentPeriod)).toMatchInline(`
Array [
"31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad",
]
`);
expectSnapshot(response.body).toMatch();
});
});
describe('fetching data with comparison', () => {
let response: Response;
let serviceNodeIds: string[];
beforeEach(async () => {
serviceNodeIds = await getServiceNodeIds({
apmApiClient,
start,
end,
});
response = await apmApiClient.readUser({
endpoint:
'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics',
params: {
path: { serviceName },
query: {
latencyAggregationType: LatencyAggregationType.avg,
numBuckets: 20,
transactionType: 'request',
serviceNodeIds: JSON.stringify(serviceNodeIds),
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
offset: '15m',
environment: 'ENVIRONMENT_ALL',
kuery: '',
},
},
});
});
it('returns a service node item for current and previous periods', () => {
expect(Object.values(response.body.currentPeriod).length).to.be.greaterThan(0);
expect(Object.values(response.body.previousPeriod).length).to.be.greaterThan(0);
});
it('returns statistics for current and previous periods', () => {
const currentPeriodItem = response.body.currentPeriod[serviceNodeIds[0]];
function hasValidYCoordinate(point: Coordinate) {
return isFiniteNumber(point.y);
}
expect(currentPeriodItem?.cpuUsage?.some(hasValidYCoordinate)).to.be(true);
expect(currentPeriodItem?.memoryUsage?.some(hasValidYCoordinate)).to.be(true);
expect(currentPeriodItem?.errorRate?.some(hasValidYCoordinate)).to.be(true);
expect(currentPeriodItem?.throughput?.some(hasValidYCoordinate)).to.be(true);
expect(currentPeriodItem?.latency?.some(hasValidYCoordinate)).to.be(true);
const previousPeriodItem = response.body.previousPeriod[serviceNodeIds[0]];
expect(previousPeriodItem?.cpuUsage?.some(hasValidYCoordinate)).to.be(true);
expect(previousPeriodItem?.memoryUsage?.some(hasValidYCoordinate)).to.be(true);
expect(previousPeriodItem?.errorRate?.some(hasValidYCoordinate)).to.be(true);
expect(previousPeriodItem?.throughput?.some(hasValidYCoordinate)).to.be(true);
expect(previousPeriodItem?.latency?.some(hasValidYCoordinate)).to.be(true);
});
it('returns the right data for current and previous periods', () => {
expectSnapshot(Object.values(response.body.currentPeriod).length).toMatchInline(`1`);
expectSnapshot(Object.values(response.body.previousPeriod).length).toMatchInline(`1`);
expectSnapshot(Object.keys(response.body.currentPeriod)).toMatchInline(`
Array [
"31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad",
]
`);
expectSnapshot(Object.keys(response.body.previousPeriod)).toMatchInline(`
Array [
"31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad",
]
`);
expectSnapshot(response.body).toMatch();
});
it('matches x-axis on current period and previous period', () => {
const currentLatencyItems = response.body.currentPeriod[serviceNodeIds[0]]?.latency;
const previousLatencyItems = response.body.previousPeriod[serviceNodeIds[0]]?.latency;
expect(currentLatencyItems?.map(({ x }) => x)).to.be.eql(
previousLatencyItems?.map(({ x }) => x)
);
});
});
}
);
}