mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Migrate service tests to deployment agnostic (#199812)
## Summary Closes https://github.com/elastic/kibana/issues/198988 Part of https://github.com/elastic/kibana/issues/193245 This PR contains the changes to migrate `service` test folder to Deployment-agnostic testing strategy. >[!NOTE] > `top_services.spec.ts` and `throughput.spec.ts` were partially migrated and `annotations.spec.ts` was not migrated ### 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" ``` It's recommended to be run against [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) - 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" ``` - [ ] ~(OPTIONAL, only if a test has been unskipped) Run flaky test suite~ - [x] local run for serverless - [x] local run for stateful - [x] MKI run for serverless --------- Co-authored-by: Sergi Romeu <sergi.romeu@elastic.co>
This commit is contained in:
parent
671ff30516
commit
2e9926de30
33 changed files with 1633 additions and 1633 deletions
|
@ -11,7 +11,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
|||
import expect from '@kbn/expect';
|
||||
import { omit } from 'lodash';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services';
|
||||
import type { RoleCredentials } from '../../../../services';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import {
|
||||
fetchServiceInventoryAlertCounts,
|
||||
|
@ -23,7 +23,6 @@ import {
|
|||
} from './helpers/alerting_helper';
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const roleScopedSupertest = getService('roleScopedSupertest');
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
const alertingApi = getService('alertingApi');
|
||||
|
@ -31,7 +30,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
|
||||
describe('error count threshold alert', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType;
|
||||
let roleAuthc: RoleCredentials;
|
||||
|
||||
const javaErrorMessage = 'a java error';
|
||||
|
@ -52,14 +50,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
};
|
||||
|
||||
before(async () => {
|
||||
supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope(
|
||||
'viewer',
|
||||
{
|
||||
withInternalHeaders: true,
|
||||
useCookieHeader: true,
|
||||
}
|
||||
);
|
||||
|
||||
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
|
||||
|
||||
const opbeansJava = apm
|
||||
|
@ -116,7 +106,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
|
||||
after(async () => {
|
||||
await apmSynthtraceEsClient.clean();
|
||||
await supertestViewerWithCookieCredentials.destroy();
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||
});
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
|||
import expect from '@kbn/expect';
|
||||
import { omit } from 'lodash';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services';
|
||||
import type { RoleCredentials } from '../../../../services';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import {
|
||||
fetchServiceInventoryAlertCounts,
|
||||
|
@ -24,7 +24,6 @@ import {
|
|||
} from './helpers/alerting_helper';
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const roleScopedSupertest = getService('roleScopedSupertest');
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
const alertingApi = getService('alertingApi');
|
||||
|
@ -43,18 +42,9 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
|
||||
describe('transaction duration alert', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType;
|
||||
let roleAuthc: RoleCredentials;
|
||||
|
||||
before(async () => {
|
||||
supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope(
|
||||
'viewer',
|
||||
{
|
||||
withInternalHeaders: true,
|
||||
useCookieHeader: true,
|
||||
}
|
||||
);
|
||||
|
||||
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
|
||||
|
||||
const opbeansJava = apm
|
||||
|
@ -86,7 +76,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
|
||||
after(async () => {
|
||||
await apmSynthtraceEsClient.clean();
|
||||
await supertestViewerWithCookieCredentials.destroy();
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
|||
import expect from '@kbn/expect';
|
||||
import { omit } from 'lodash';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services';
|
||||
import type { RoleCredentials } from '../../../../services';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import {
|
||||
fetchServiceInventoryAlertCounts,
|
||||
|
@ -23,7 +23,6 @@ import {
|
|||
} from './helpers/alerting_helper';
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const roleScopedSupertest = getService('roleScopedSupertest');
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
const alertingApi = getService('alertingApi');
|
||||
|
@ -31,18 +30,9 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
|
||||
describe('transaction error rate alert', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType;
|
||||
let roleAuthc: RoleCredentials;
|
||||
|
||||
before(async () => {
|
||||
supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope(
|
||||
'viewer',
|
||||
{
|
||||
withInternalHeaders: true,
|
||||
useCookieHeader: true,
|
||||
}
|
||||
);
|
||||
|
||||
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
|
||||
|
||||
const opbeansJava = apm
|
||||
|
@ -84,7 +74,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
|
||||
after(async () => {
|
||||
await apmSynthtraceEsClient.clean();
|
||||
await supertestViewerWithCookieCredentials.destroy();
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||
});
|
||||
|
||||
|
|
|
@ -7,24 +7,20 @@
|
|||
|
||||
type ArchiveName =
|
||||
| '8.0.0'
|
||||
| 'apm_8.0.0'
|
||||
| 'apm_mappings_only_8.0.0'
|
||||
| 'infra_metrics_and_apm'
|
||||
| 'metrics_8.0.0'
|
||||
| 'ml_8.0.0'
|
||||
| 'observability_overview'
|
||||
| 'rum_8.0.0'
|
||||
| 'rum_test_data';
|
||||
|
||||
export const ARCHIVER_ROUTES: { [key in ArchiveName]: string } = {
|
||||
'8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0',
|
||||
'apm_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0',
|
||||
'apm_mappings_only_8.0.0':
|
||||
'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0',
|
||||
infra_metrics_and_apm:
|
||||
'x-pack/test/apm_api_integration/common/fixtures/es_archiver/infra_metrics_and_apm',
|
||||
'metrics_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0',
|
||||
'ml_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/ml_8.0.0',
|
||||
observability_overview:
|
||||
'x-pack/test/apm_api_integration/common/fixtures/es_archiver/observability_overview',
|
||||
'rum_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_8.0.0',
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function apmApiIntegrationTests({
|
|||
loadTestFile(require.resolve('./correlations'));
|
||||
loadTestFile(require.resolve('./entities'));
|
||||
loadTestFile(require.resolve('./cold_start'));
|
||||
loadTestFile(require.resolve('./services'));
|
||||
loadTestFile(require.resolve('./historical_data'));
|
||||
loadTestFile(require.resolve('./observability_overview'));
|
||||
loadTestFile(require.resolve('./latency'));
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
|
||||
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
|
||||
const start = '2021-10-01T00:00:00.000Z';
|
||||
const end = '2021-10-01T01:00:00.000Z';
|
||||
|
||||
const serviceNames = ['opbeans-java', 'opbeans-go'];
|
||||
|
||||
describe('Services detailed statistics', () => {
|
||||
describe('Services detailed statistics when data is not loaded', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
offset: '1d',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
bucketSizeInSeconds: 60,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod).to.be.empty();
|
||||
expect(response.body.previousPeriod).to.be.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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 { RoleCredentials } from '@kbn/ftr-common-functional-services';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function annotationApiTests({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const es = getService('es');
|
||||
const samlAuth = getService('samlAuth');
|
||||
|
||||
const dates = [
|
||||
new Date('2021-02-01T00:00:00.000Z'),
|
||||
new Date('2021-02-01T01:00:00.000Z'),
|
||||
new Date('2021-02-01T02:00:00.000Z'),
|
||||
new Date('2021-02-01T03:00:00.000Z'),
|
||||
];
|
||||
|
||||
const indexName = 'apm-8.0.0-transaction';
|
||||
|
||||
describe('Derived deployment annotations', () => {
|
||||
describe('when there are multiple service versions', () => {
|
||||
let roleAuthc: RoleCredentials;
|
||||
let response: APIReturnType<'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31'>;
|
||||
|
||||
before(async () => {
|
||||
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('viewer');
|
||||
const indexExists = await es.indices.exists({ index: indexName });
|
||||
if (indexExists) {
|
||||
await es.indices.delete({
|
||||
index: indexName,
|
||||
});
|
||||
}
|
||||
|
||||
await es.indices.create({
|
||||
index: indexName,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
service: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
version: {
|
||||
type: 'keyword',
|
||||
},
|
||||
environment: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
transaction: {
|
||||
properties: {
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
duration: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
processor: {
|
||||
properties: {
|
||||
event: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const docs = dates.flatMap((date, index) => {
|
||||
const baseAnnotation = {
|
||||
transaction: {
|
||||
type: 'request',
|
||||
duration: 1000000,
|
||||
},
|
||||
|
||||
service: {
|
||||
name: 'opbeans-java',
|
||||
environment: 'production',
|
||||
version: index + 1,
|
||||
},
|
||||
processor: {
|
||||
event: 'transaction',
|
||||
},
|
||||
};
|
||||
return [
|
||||
{
|
||||
...baseAnnotation,
|
||||
'@timestamp': date.toISOString(),
|
||||
},
|
||||
{
|
||||
...baseAnnotation,
|
||||
'@timestamp': new Date(date.getTime() + 30000),
|
||||
},
|
||||
{
|
||||
...baseAnnotation,
|
||||
'@timestamp': new Date(date.getTime() + 60000),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
await es.bulk({
|
||||
index: indexName,
|
||||
body: docs.flatMap((doc) => [{ index: {} }, doc]),
|
||||
refresh: true,
|
||||
});
|
||||
|
||||
response = (
|
||||
await apmApiClient.readUser({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'opbeans-java',
|
||||
},
|
||||
query: {
|
||||
start: dates[1].toISOString(),
|
||||
end: dates[2].toISOString(),
|
||||
environment: 'production',
|
||||
},
|
||||
},
|
||||
roleAuthc,
|
||||
})
|
||||
).body;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await es.indices.delete({
|
||||
index: indexName,
|
||||
});
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||
});
|
||||
|
||||
it('annotations are displayed for the service versions in the given time range', async () => {
|
||||
expect(response.annotations.length).to.be(2);
|
||||
expect(response.annotations[0]['@timestamp']).to.be(dates[1].getTime());
|
||||
expect(response.annotations[1]['@timestamp']).to.be(dates[2].getTime());
|
||||
|
||||
expectSnapshot(response.annotations[0]).toMatchInline(`
|
||||
Object {
|
||||
"@timestamp": 1612141200000,
|
||||
"id": "2",
|
||||
"text": "2",
|
||||
"type": "version",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('annotations are not displayed for the service versions outside of the given time range', () => {
|
||||
expect(
|
||||
response.annotations.some((annotation) => {
|
||||
return (
|
||||
annotation['@timestamp'] !== dates[0].getTime() &&
|
||||
annotation['@timestamp'] !== dates[2].getTime()
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -14,17 +14,17 @@ import {
|
|||
APIReturnType,
|
||||
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context';
|
||||
import { config, generateData } from './generate_data';
|
||||
import { getErrorGroupIds } from './get_error_group_ids';
|
||||
|
||||
type ErrorGroupsDetailedStatistics =
|
||||
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
|
||||
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 serviceName = 'synth-go';
|
||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||
|
@ -52,23 +52,20 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Error groups detailed statistics when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Error groups detailed statistics', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await callApi();
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} });
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177656
|
||||
registry.when('Error groups detailed statistics', { config: 'basic', archives: [] }, () => {
|
||||
describe('when data is loaded', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE } = config;
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
await generateData({ serviceName, start, end, apmSynthtraceEsClient });
|
||||
});
|
||||
|
|
@ -11,16 +11,16 @@ import {
|
|||
APIReturnType,
|
||||
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context';
|
||||
import { generateData, config } from './generate_data';
|
||||
|
||||
type ErrorGroupsMainStatistics =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
|
||||
|
||||
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 serviceName = 'synth-go';
|
||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||
|
@ -46,24 +46,21 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Error groups main statistics when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Error groups main statistics', () => {
|
||||
describe(' when data is not loaded', () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await callApi();
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorGroups).to.empty();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177664
|
||||
registry.when('Error groups main statistics', { config: 'basic', archives: [] }, () => {
|
||||
describe('when data is loaded', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE, ERROR_NAME_1, ERROR_NAME_2 } = config;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
await generateData({ serviceName, start, end, apmSynthtraceEsClient });
|
||||
});
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { take } from 'lodash';
|
||||
import { ApmServices } from '../../../common/config';
|
||||
import type { ApmApiClient } from '../../../../../services/apm_api';
|
||||
|
||||
export async function getErrorGroupIds({
|
||||
apmApiClient,
|
||||
|
@ -14,7 +14,7 @@ export async function getErrorGroupIds({
|
|||
serviceName = 'opbeans-java',
|
||||
count = 5,
|
||||
}: {
|
||||
apmApiClient: Awaited<ReturnType<ApmServices['apmApiClient']>>;
|
||||
apmApiClient: ApmApiClient;
|
||||
start: number;
|
||||
end: number;
|
||||
serviceName?: string;
|
|
@ -9,12 +9,12 @@ import expect from '@kbn/expect';
|
|||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
|
||||
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
|
||||
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 apmApiClient = getService('apmApiClient');
|
||||
const registry = getService('registry');
|
||||
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;
|
||||
|
@ -38,10 +38,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Service node metadata when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Service node metadata', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await callApi();
|
||||
|
||||
|
@ -54,15 +52,13 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
}
|
||||
`);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177513
|
||||
registry.when(
|
||||
'Service node metadata when data is loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
const instance = apm
|
||||
.service({ name: serviceName, environment: 'production', agentName: 'go' })
|
||||
.instance(instanceName);
|
||||
|
@ -94,6 +90,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
}
|
||||
`);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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('Services', () => {
|
||||
loadTestFile(require.resolve('./error_groups/error_groups_detailed_statistics.spec.ts'));
|
||||
loadTestFile(require.resolve('./error_groups/error_groups_main_statistics.spec.ts'));
|
||||
loadTestFile(require.resolve('./service_details/service_details.spec.ts'));
|
||||
loadTestFile(require.resolve('./service_icons/service_icons.spec.ts'));
|
||||
loadTestFile(require.resolve('./archive_services_detailed_statistics.spec.ts'));
|
||||
loadTestFile(require.resolve('./derived_annotations.spec.ts'));
|
||||
loadTestFile(require.resolve('./get_service_node_metadata.spec.ts'));
|
||||
loadTestFile(require.resolve('./service_alerts.spec.ts'));
|
||||
loadTestFile(require.resolve('./services_detailed_statistics.spec.ts'));
|
||||
loadTestFile(require.resolve('./top_services.spec.ts'));
|
||||
loadTestFile(require.resolve('./transaction_types.spec.ts'));
|
||||
});
|
||||
}
|
|
@ -8,23 +8,25 @@ import expect from '@kbn/expect';
|
|||
import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
|
||||
import { ApmRuleType } from '@kbn/rule-data-utils';
|
||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { waitForAlertsForRule } from '../alerts/helpers/wait_for_alerts_for_rule';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { createApmRule, runRuleSoon, ApmAlertFields } from '../alerts/helpers/alerting_api_helper';
|
||||
import { waitForActiveRule } from '../alerts/helpers/wait_for_active_rule';
|
||||
import { cleanupRuleAndAlertState } from '../alerts/helpers/cleanup_rule_and_alert_state';
|
||||
import type { RoleCredentials } from '@kbn/ftr-common-functional-services';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import {
|
||||
APM_ACTION_VARIABLE_INDEX,
|
||||
APM_ALERTS_INDEX,
|
||||
ApmAlertFields,
|
||||
} from '../alerts/helpers/alerting_helper';
|
||||
|
||||
export default function ServiceAlerts({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const alertingApi = getService('alertingApi');
|
||||
const samlAuth = getService('samlAuth');
|
||||
const synthtrace = getService('synthtrace');
|
||||
|
||||
export default function ServiceAlerts({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const supertest = getService('supertest');
|
||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
||||
const es = getService('es');
|
||||
const dayInMs = 24 * 60 * 60 * 1000;
|
||||
const start = Date.now() - dayInMs;
|
||||
const end = Date.now() + dayInMs;
|
||||
const goService = 'synth-go';
|
||||
const logger = getService('log');
|
||||
|
||||
async function getServiceAlerts({
|
||||
serviceName,
|
||||
|
@ -46,27 +48,33 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
|
||||
function createRule() {
|
||||
return createApmRule({
|
||||
supertest,
|
||||
name: `Latency threshold | ${goService}`,
|
||||
params: {
|
||||
serviceName: goService,
|
||||
transactionType: undefined,
|
||||
windowSize: 5,
|
||||
windowUnit: 'h',
|
||||
threshold: 100,
|
||||
aggregationType: AggregationType.Avg,
|
||||
environment: 'testing',
|
||||
groupBy: ['service.name', 'service.environment', 'transaction.type', 'transaction.name'],
|
||||
},
|
||||
ruleTypeId: ApmRuleType.TransactionDuration,
|
||||
});
|
||||
}
|
||||
describe('Service alerts', () => {
|
||||
let roleAuthc: RoleCredentials;
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
function createRule() {
|
||||
return alertingApi.createRule({
|
||||
name: `Latency threshold | ${goService}`,
|
||||
params: {
|
||||
serviceName: goService,
|
||||
transactionType: undefined,
|
||||
windowSize: 5,
|
||||
windowUnit: 'h',
|
||||
threshold: 100,
|
||||
aggregationType: AggregationType.Avg,
|
||||
environment: 'testing',
|
||||
groupBy: ['service.name', 'service.environment', 'transaction.type', 'transaction.name'],
|
||||
},
|
||||
ruleTypeId: ApmRuleType.TransactionDuration,
|
||||
roleAuthc,
|
||||
consumer: 'apm',
|
||||
});
|
||||
}
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177512
|
||||
registry.when('Service alerts', { config: 'basic', archives: [] }, () => {
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
|
||||
|
||||
const synthServices = [
|
||||
apm
|
||||
.service({ name: goService, environment: 'testing', agentName: 'go' })
|
||||
|
@ -115,6 +123,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) {
|
|||
|
||||
after(async () => {
|
||||
await apmSynthtraceEsClient.clean();
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||
});
|
||||
|
||||
describe('with alerts', () => {
|
||||
|
@ -124,20 +133,35 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) {
|
|||
before(async () => {
|
||||
const createdRule = await createRule();
|
||||
ruleId = createdRule.id;
|
||||
alerts = await waitForAlertsForRule({ es, ruleId });
|
||||
alerts = (
|
||||
await alertingApi.waitForDocumentInIndex({
|
||||
indexName: APM_ALERTS_INDEX,
|
||||
ruleId,
|
||||
})
|
||||
).hits.hits.map((hit) => hit._source) as ApmAlertFields[];
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cleanupRuleAndAlertState({ es, supertest, logger });
|
||||
await alertingApi.cleanUpAlerts({
|
||||
roleAuthc,
|
||||
ruleId,
|
||||
alertIndexName: APM_ALERTS_INDEX,
|
||||
connectorIndexName: APM_ACTION_VARIABLE_INDEX,
|
||||
consumer: 'apm',
|
||||
});
|
||||
});
|
||||
|
||||
it('checks if rule is active', async () => {
|
||||
const ruleStatus = await waitForActiveRule({ ruleId, supertest });
|
||||
const ruleStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(ruleStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should successfully run the rule', async () => {
|
||||
const response = await runRuleSoon({ ruleId, supertest });
|
||||
const response = await alertingApi.runRule(roleAuthc, ruleId);
|
||||
expect(response.status).to.be(204);
|
||||
});
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { first } from 'lodash';
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context';
|
||||
import { dataConfig, generateData } from './generate_data';
|
||||
|
||||
type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>;
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
|
||||
const {
|
||||
service: { name: serviceName },
|
||||
} = dataConfig;
|
||||
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() {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
environment: 'production',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('Service details', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body).to.empty();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is generated', () => {
|
||||
let body: ServiceDetails;
|
||||
let status: number;
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
await generateData({ apmSynthtraceEsClient, start, end });
|
||||
const response = await callApi();
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns correct cloud details', () => {
|
||||
const { cloud } = dataConfig;
|
||||
const {
|
||||
provider,
|
||||
availabilityZone,
|
||||
region,
|
||||
machineType,
|
||||
projectName,
|
||||
serviceName: cloudServiceName,
|
||||
} = cloud;
|
||||
|
||||
expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone);
|
||||
expect(first(body?.cloud?.machineTypes)).to.be(machineType);
|
||||
expect(body?.cloud?.provider).to.be(provider);
|
||||
expect(body?.cloud?.projectName).to.be(projectName);
|
||||
expect(body?.cloud?.serviceName).to.be(cloudServiceName);
|
||||
expect(first(body?.cloud?.regions)).to.be(region);
|
||||
});
|
||||
|
||||
it('returns correct container details', () => {
|
||||
expect(body?.container?.totalNumberInstances).to.be(1);
|
||||
});
|
||||
|
||||
it('returns correct serverless details', () => {
|
||||
const { cloud, serverless } = dataConfig;
|
||||
const { serviceName: cloudServiceName } = cloud;
|
||||
const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless;
|
||||
|
||||
expect(body?.serverless?.type).to.be(cloudServiceName);
|
||||
expect(body?.serverless?.functionNames).to.have.length(2);
|
||||
expect(body?.serverless?.functionNames).to.contain(firstFunctionName);
|
||||
expect(body?.serverless?.functionNames).to.contain(secondFunctionName);
|
||||
expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType);
|
||||
});
|
||||
|
||||
it('returns correct service details', () => {
|
||||
const { service } = dataConfig;
|
||||
const { version, runtime, framework, agent } = service;
|
||||
const { name: runTimeName, version: runTimeVersion } = runtime;
|
||||
const { name: agentName, version: agentVersion } = agent;
|
||||
|
||||
expect(body?.service?.framework).to.be(framework);
|
||||
expect(body?.service?.agent.name).to.be(agentName);
|
||||
expect(body?.service?.agent.version).to.be(agentVersion);
|
||||
expect(body?.service?.runtime?.name).to.be(runTimeName);
|
||||
expect(body?.service?.runtime?.version).to.be(runTimeVersion);
|
||||
expect(first(body?.service?.versions)).to.be(version);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -7,25 +7,31 @@
|
|||
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 { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata';
|
||||
import archives_metadata from '../../../../../../../apm_api_integration/common/fixtures/es_archiver/archives_metadata';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context';
|
||||
import { ARCHIVER_ROUTES } from '../../constants/archiver';
|
||||
|
||||
type ServiceOverviewInstanceDetails =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>;
|
||||
|
||||
type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const archiveName = 'infra_metrics_and_apm';
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const { start, end } = archives_metadata[archiveName];
|
||||
|
||||
registry.when(
|
||||
'When data is loaded',
|
||||
{ config: 'basic', archives: ['infra_metrics_and_apm'] },
|
||||
() => {
|
||||
describe('Service infra metrics', () => {
|
||||
describe('When data is loaded', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load(ARCHIVER_ROUTES[archiveName]);
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload(ARCHIVER_ROUTES[archiveName]);
|
||||
});
|
||||
|
||||
describe('fetch service instance', () => {
|
||||
it('handles empty infra metrics data for a service node', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
|
@ -169,6 +175,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
expect(body.kubernetes?.replicasets).to.eql([]);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { getServerlessTypeFromCloudData } from '@kbn/apm-plugin/common/serverless';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context';
|
||||
import { dataConfig, generateData } from './generate_data';
|
||||
|
||||
type ServiceIconMetadata = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/icons'>;
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
|
||||
const { serviceName } = dataConfig;
|
||||
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() {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metadata/icons',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('Service icons', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body).to.empty();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is generated', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
let body: ServiceIconMetadata;
|
||||
let status: number;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
await generateData({ apmSynthtraceEsClient, start, end });
|
||||
const response = await callApi();
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns correct metadata', () => {
|
||||
const { agentName, cloud } = dataConfig;
|
||||
const { provider, serviceName: cloudServiceName, provider: cloudProvider } = cloud;
|
||||
|
||||
expect(body.agentName).to.be(agentName);
|
||||
expect(body.cloudProvider).to.be(provider);
|
||||
expect(body.containerType).to.be('Kubernetes');
|
||||
expect(body.serverlessType).to.be(
|
||||
getServerlessTypeFromCloudData(cloudProvider, cloudServiceName)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -13,55 +13,21 @@ import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
|
|||
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
|
||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { uniq, map } from 'lodash';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
type ServicesDetailedStatisticsReturn =
|
||||
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
|
||||
const synthtrace = getService('apmSynthtraceEsClient');
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
|
||||
const start = '2021-01-01T00:00:00.000Z';
|
||||
const end = '2021-01-01T00:59:59.999Z';
|
||||
|
||||
const serviceNames = ['my-service'];
|
||||
|
||||
registry.when(
|
||||
'Services detailed statistics when data is generated',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
bucketSizeInSeconds: 60,
|
||||
_inspect: true,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod).to.be.empty();
|
||||
expect(response.body.previousPeriod).to.be.empty();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
async function getStats(
|
||||
overrides?: Partial<
|
||||
APIClientRequestParamsOf<'POST /internal/apm/services/detailed_statistics'>['params']['query']
|
||||
|
@ -90,12 +56,38 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
return response.body;
|
||||
}
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177511
|
||||
registry.when(
|
||||
'Services detailed statistics when data is generated',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('Services detailed statistics', () => {
|
||||
describe('when data is not generated', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
bucketSizeInSeconds: 60,
|
||||
_inspect: true,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod).to.be.empty();
|
||||
expect(response.body.previousPeriod).to.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is generated', () => {
|
||||
let servicesDetailedStatistics: ServicesDetailedStatisticsReturn;
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
const instance = apm.service('my-service', 'production', 'java').instance('instance');
|
||||
|
||||
|
@ -103,12 +95,28 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
const EXPECTED_LATENCY = 1000;
|
||||
const EXPECTED_FAILURE_RATE = 0.25;
|
||||
|
||||
function checkStats() {
|
||||
const stats = servicesDetailedStatistics.currentPeriod['my-service'];
|
||||
|
||||
expect(stats).not.empty();
|
||||
|
||||
expect(uniq(map(stats.throughput, 'y'))).eql([EXPECTED_TPM], 'tpm');
|
||||
|
||||
expect(uniq(map(stats.latency, 'y'))).eql([EXPECTED_LATENCY * 1000], 'latency');
|
||||
|
||||
expect(uniq(map(stats.transactionErrorRate, 'y'))).eql(
|
||||
[EXPECTED_FAILURE_RATE],
|
||||
'errorRate'
|
||||
);
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval(
|
||||
'1m'
|
||||
);
|
||||
|
||||
await synthtrace.index([
|
||||
await apmSynthtraceEsClient.index([
|
||||
interval.rate(3).generator((timestamp) => {
|
||||
return instance
|
||||
.transaction('GET /api')
|
||||
|
@ -133,22 +141,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
]);
|
||||
});
|
||||
|
||||
after(() => synthtrace.clean());
|
||||
|
||||
function checkStats() {
|
||||
const stats = servicesDetailedStatistics.currentPeriod['my-service'];
|
||||
|
||||
expect(stats).not.empty();
|
||||
|
||||
expect(uniq(map(stats.throughput, 'y'))).eql([EXPECTED_TPM], 'tpm');
|
||||
|
||||
expect(uniq(map(stats.latency, 'y'))).eql([EXPECTED_LATENCY * 1000], 'latency');
|
||||
|
||||
expect(uniq(map(stats.transactionErrorRate, 'y'))).eql(
|
||||
[EXPECTED_FAILURE_RATE],
|
||||
'errorRate'
|
||||
);
|
||||
}
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
||||
describe('and transaction metrics are used', () => {
|
||||
before(async () => {
|
||||
|
@ -184,6 +177,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
checkStats();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,541 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { buildQueryFromFilters } from '@kbn/es-query';
|
||||
import { first, last, meanBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
|
||||
import {
|
||||
APIClientRequestParamsOf,
|
||||
APIReturnType,
|
||||
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
|
||||
import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
|
||||
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import { roundNumber } from '../utils/common';
|
||||
|
||||
type ThroughputReturn = APIReturnType<'GET /internal/apm/services/{serviceName}/throughput'>;
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
|
||||
const serviceName = 'synth-go';
|
||||
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(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/throughput'>['params']
|
||||
>,
|
||||
processorEvent: 'transaction' | 'metric' = 'metric'
|
||||
) {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/throughput',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'synth-go',
|
||||
...overrides?.path,
|
||||
},
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
...overrides?.query,
|
||||
...(processorEvent === 'metric'
|
||||
? {
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
bucketSizeInSeconds: 60,
|
||||
}
|
||||
: {
|
||||
documentType: ApmDocumentType.TransactionEvent,
|
||||
rollupInterval: RollupInterval.None,
|
||||
bucketSizeInSeconds: 30,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
return response;
|
||||
}
|
||||
describe('Throughput when data is not loaded', () => {
|
||||
describe('Twhen data is not loaded', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await callApi();
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod.length).to.be(0);
|
||||
expect(response.body.previousPeriod.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
describe('Throughput chart api', () => {
|
||||
const GO_PROD_RATE = 50;
|
||||
const GO_DEV_RATE = 5;
|
||||
const JAVA_PROD_RATE = 45;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
|
||||
const serviceGoProdInstance = apm
|
||||
.service({ name: serviceName, environment: 'production', agentName: 'go' })
|
||||
.instance('instance-a');
|
||||
const serviceGoDevInstance = apm
|
||||
.service({ name: serviceName, environment: 'development', agentName: 'go' })
|
||||
.instance('instance-b');
|
||||
|
||||
const serviceJavaInstance = apm
|
||||
.service({ name: 'synth-java', environment: 'development', agentName: 'java' })
|
||||
.instance('instance-c');
|
||||
|
||||
await apmSynthtraceEsClient.index([
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(GO_PROD_RATE)
|
||||
.generator((timestamp) =>
|
||||
serviceGoProdInstance
|
||||
.transaction({ transactionName: 'GET /api/product/list' })
|
||||
.duration(1000)
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(GO_DEV_RATE)
|
||||
.generator((timestamp) =>
|
||||
serviceGoDevInstance
|
||||
.transaction({ transactionName: 'GET /api/product/:id' })
|
||||
.duration(1000)
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(JAVA_PROD_RATE)
|
||||
.generator((timestamp) =>
|
||||
serviceJavaInstance
|
||||
.transaction({ transactionName: 'POST /api/product/buy' })
|
||||
.duration(1000)
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
||||
describe('compare transactions and metrics based throughput', () => {
|
||||
let throughputMetrics: ThroughputReturn;
|
||||
let throughputTransactions: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([
|
||||
callApi({}, 'metric'),
|
||||
callApi({}, 'transaction'),
|
||||
]);
|
||||
throughputMetrics = throughputMetricsResponse.body;
|
||||
throughputTransactions = throughputTransactionsResponse.body;
|
||||
});
|
||||
|
||||
it('returns some transactions data', () => {
|
||||
expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns some metrics data', () => {
|
||||
expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same mean value for metrics and transactions data', () => {
|
||||
const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y');
|
||||
const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y');
|
||||
[transactionsMean, metricsMean].forEach((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE))
|
||||
);
|
||||
});
|
||||
|
||||
it('has a bucket size of 30 seconds for transactions data', () => {
|
||||
const firstTimerange = throughputTransactions.currentPeriod[0].x;
|
||||
const secondTimerange = throughputTransactions.currentPeriod[1].x;
|
||||
const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000;
|
||||
expect(timeIntervalAsSeconds).to.equal(30);
|
||||
});
|
||||
|
||||
it('has a bucket size of 1 minute for metrics data', () => {
|
||||
const firstTimerange = throughputMetrics.currentPeriod[0].x;
|
||||
const secondTimerange = throughputMetrics.currentPeriod[1].x;
|
||||
const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60;
|
||||
expect(timeIntervalAsMinutes).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('production environment', () => {
|
||||
let throughput: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const throughputResponse = await callApi({ query: { environment: 'production' } });
|
||||
throughput = throughputResponse.body;
|
||||
});
|
||||
|
||||
it('returns some data', () => {
|
||||
expect(throughput.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns correct average throughput', () => {
|
||||
const throughputMean = meanBy(throughput.currentPeriod, 'y');
|
||||
expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when synth-java is selected', () => {
|
||||
let throughput: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } });
|
||||
throughput = throughputResponse.body;
|
||||
});
|
||||
|
||||
it('returns some data', () => {
|
||||
expect(throughput.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns throughput related to java agent', () => {
|
||||
const throughputMean = meanBy(throughput.currentPeriod, 'y');
|
||||
expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE));
|
||||
});
|
||||
});
|
||||
|
||||
describe('time comparisons', () => {
|
||||
let throughputResponse: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const response = await callApi({
|
||||
query: {
|
||||
start: moment(end).subtract(7, 'minutes').toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
offset: '7m',
|
||||
},
|
||||
});
|
||||
throughputResponse = response.body;
|
||||
});
|
||||
|
||||
it('returns some data', () => {
|
||||
expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0);
|
||||
expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0);
|
||||
|
||||
const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) =>
|
||||
isFiniteNumber(y)
|
||||
);
|
||||
const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) =>
|
||||
isFiniteNumber(y)
|
||||
);
|
||||
|
||||
expect(hasCurrentPeriodData).to.equal(true);
|
||||
expect(hasPreviousPeriodData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same start time for both periods', () => {
|
||||
expect(first(throughputResponse.currentPeriod)?.x).to.equal(
|
||||
first(throughputResponse.previousPeriod)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('has same end time for both periods', () => {
|
||||
expect(last(throughputResponse.currentPeriod)?.x).to.equal(
|
||||
last(throughputResponse.previousPeriod)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('returns same number of buckets for both periods', () => {
|
||||
expect(throughputResponse.currentPeriod.length).to.be(
|
||||
throughputResponse.previousPeriod.length
|
||||
);
|
||||
});
|
||||
|
||||
it('has same mean value for both periods', () => {
|
||||
const currentPeriodMean = meanBy(
|
||||
throughputResponse.currentPeriod.filter(
|
||||
(item) => isFiniteNumber(item.y) && item.y > 0
|
||||
),
|
||||
'y'
|
||||
);
|
||||
const previousPeriodMean = meanBy(
|
||||
throughputResponse.previousPeriod.filter(
|
||||
(item) => isFiniteNumber(item.y) && item.y > 0
|
||||
),
|
||||
'y'
|
||||
);
|
||||
const currentPeriod = throughputResponse.currentPeriod;
|
||||
const bucketSize = currentPeriod[1].x - currentPeriod[0].x;
|
||||
const durationAsMinutes = bucketSize / 1000 / 60;
|
||||
[currentPeriodMean, previousPeriodMean].every((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(
|
||||
roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes)
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles kuery', () => {
|
||||
let throughputMetrics: ThroughputReturn;
|
||||
let throughputTransactions: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
kuery: 'transaction.name : "GET /api/product/list"',
|
||||
},
|
||||
},
|
||||
'metric'
|
||||
),
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
kuery: 'transaction.name : "GET /api/product/list"',
|
||||
},
|
||||
},
|
||||
'transaction'
|
||||
),
|
||||
]);
|
||||
throughputMetrics = throughputMetricsResponse.body;
|
||||
throughputTransactions = throughputTransactionsResponse.body;
|
||||
});
|
||||
|
||||
it('returns some transactions data', () => {
|
||||
expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns some metrics data', () => {
|
||||
expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same mean value for metrics and transactions data', () => {
|
||||
const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y');
|
||||
const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y');
|
||||
[transactionsMean, metricsMean].forEach((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE))
|
||||
);
|
||||
});
|
||||
|
||||
it('has a bucket size of 30 seconds for transactions data', () => {
|
||||
const firstTimerange = throughputTransactions.currentPeriod[0].x;
|
||||
const secondTimerange = throughputTransactions.currentPeriod[1].x;
|
||||
const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000;
|
||||
expect(timeIntervalAsSeconds).to.equal(30);
|
||||
});
|
||||
|
||||
it('has a bucket size of 1 minute for metrics data', () => {
|
||||
const firstTimerange = throughputMetrics.currentPeriod[0].x;
|
||||
const secondTimerange = throughputMetrics.currentPeriod[1].x;
|
||||
const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60;
|
||||
expect(timeIntervalAsMinutes).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles filters', () => {
|
||||
let throughputMetrics: ThroughputReturn;
|
||||
let throughputTransactions: ThroughputReturn;
|
||||
const filters = [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
key: 'transaction.name',
|
||||
params: ['GET /api/product/list'],
|
||||
type: 'phrases',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: {
|
||||
match_phrase: {
|
||||
'transaction.name': 'GET /api/product/list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined));
|
||||
|
||||
before(async () => {
|
||||
const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
filters: serializedFilters,
|
||||
},
|
||||
},
|
||||
'metric'
|
||||
),
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
filters: serializedFilters,
|
||||
},
|
||||
},
|
||||
'transaction'
|
||||
),
|
||||
]);
|
||||
throughputMetrics = throughputMetricsResponse.body;
|
||||
throughputTransactions = throughputTransactionsResponse.body;
|
||||
});
|
||||
|
||||
it('returns some transactions data', () => {
|
||||
expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns some metrics data', () => {
|
||||
expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same mean value for metrics and transactions data', () => {
|
||||
const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y');
|
||||
const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y');
|
||||
[transactionsMean, metricsMean].forEach((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE))
|
||||
);
|
||||
});
|
||||
|
||||
it('has a bucket size of 30 seconds for transactions data', () => {
|
||||
const firstTimerange = throughputTransactions.currentPeriod[0].x;
|
||||
const secondTimerange = throughputTransactions.currentPeriod[1].x;
|
||||
const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000;
|
||||
expect(timeIntervalAsSeconds).to.equal(30);
|
||||
});
|
||||
|
||||
it('has a bucket size of 1 minute for metrics data', () => {
|
||||
const firstTimerange = throughputMetrics.currentPeriod[0].x;
|
||||
const secondTimerange = throughputMetrics.currentPeriod[1].x;
|
||||
const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60;
|
||||
expect(timeIntervalAsMinutes).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles negate filters', () => {
|
||||
let throughputMetrics: ThroughputReturn;
|
||||
let throughputTransactions: ThroughputReturn;
|
||||
const filters = [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: true,
|
||||
alias: null,
|
||||
key: 'transaction.name',
|
||||
params: ['GET /api/product/list'],
|
||||
type: 'phrases',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: {
|
||||
match_phrase: {
|
||||
'transaction.name': 'GET /api/product/list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined));
|
||||
|
||||
before(async () => {
|
||||
const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
filters: serializedFilters,
|
||||
},
|
||||
},
|
||||
'metric'
|
||||
),
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
filters: serializedFilters,
|
||||
},
|
||||
},
|
||||
'transaction'
|
||||
),
|
||||
]);
|
||||
throughputMetrics = throughputMetricsResponse.body;
|
||||
throughputTransactions = throughputTransactionsResponse.body;
|
||||
});
|
||||
|
||||
it('returns some transactions data', () => {
|
||||
expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns some metrics data', () => {
|
||||
expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same mean value for metrics and transactions data', () => {
|
||||
const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y');
|
||||
const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y');
|
||||
[transactionsMean, metricsMean].forEach((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(roundNumber(GO_DEV_RATE))
|
||||
);
|
||||
});
|
||||
|
||||
it('has a bucket size of 30 seconds for transactions data', () => {
|
||||
const firstTimerange = throughputTransactions.currentPeriod[0].x;
|
||||
const secondTimerange = throughputTransactions.currentPeriod[1].x;
|
||||
const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000;
|
||||
expect(timeIntervalAsSeconds).to.equal(30);
|
||||
});
|
||||
|
||||
it('has a bucket size of 1 minute for metrics data', () => {
|
||||
const firstTimerange = throughputMetrics.currentPeriod[0].x;
|
||||
const secondTimerange = throughputMetrics.currentPeriod[1].x;
|
||||
const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60;
|
||||
expect(timeIntervalAsMinutes).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles bad filters request', () => {
|
||||
it('throws bad request error', async () => {
|
||||
try {
|
||||
await callApi({
|
||||
query: { environment: 'production', filters: '{}}' },
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.res.status).to.be(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,364 @@
|
|||
/*
|
||||
* 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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||
import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
|
||||
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
|
||||
const start = '2021-10-01T00:00:00.000Z';
|
||||
const end = '2021-10-01T01:00:00.000Z';
|
||||
|
||||
describe('Top services', () => {
|
||||
describe('APM Services Overview with a basic license when data is not generated', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.items.length).to.be(0);
|
||||
expect(response.body.maxCountExceeded).to.be(false);
|
||||
expect(response.body.serviceOverflowCount).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('APM Services Overview with a basic license when data is generated', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
let response: {
|
||||
status: number;
|
||||
body: APIReturnType<'GET /internal/apm/services'>;
|
||||
};
|
||||
|
||||
const range = timerange(new Date(start).getTime(), new Date(end).getTime());
|
||||
const transactionInterval = range.interval('1s');
|
||||
const metricInterval = range.interval('30s');
|
||||
|
||||
const errorInterval = range.interval('5s');
|
||||
|
||||
const multipleEnvServiceProdInstance = apm
|
||||
.service({ name: 'multiple-env-service', environment: 'production', agentName: 'go' })
|
||||
.instance('multiple-env-service-production');
|
||||
|
||||
const multipleEnvServiceDevInstance = apm
|
||||
.service({ name: 'multiple-env-service', environment: 'development', agentName: 'go' })
|
||||
.instance('multiple-env-service-development');
|
||||
|
||||
const metricOnlyInstance = apm
|
||||
.service({ name: 'metric-only-service', environment: 'production', agentName: 'java' })
|
||||
.instance('metric-only-production');
|
||||
|
||||
const errorOnlyInstance = apm
|
||||
.service({ name: 'error-only-service', environment: 'production', agentName: 'java' })
|
||||
.instance('error-only-production');
|
||||
|
||||
const config = {
|
||||
multiple: {
|
||||
prod: {
|
||||
rps: 4,
|
||||
duration: 1000,
|
||||
},
|
||||
dev: {
|
||||
rps: 1,
|
||||
duration: 500,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function checkStats() {
|
||||
const multipleEnvService = response.body.items.find(
|
||||
(item) => item.serviceName === 'multiple-env-service'
|
||||
);
|
||||
|
||||
const totalRps = config.multiple.prod.rps + config.multiple.dev.rps;
|
||||
|
||||
expect(multipleEnvService).to.eql({
|
||||
serviceName: 'multiple-env-service',
|
||||
transactionType: 'request',
|
||||
environments: ['production', 'development'],
|
||||
agentName: 'go',
|
||||
latency:
|
||||
1000 *
|
||||
((config.multiple.prod.duration * config.multiple.prod.rps +
|
||||
config.multiple.dev.duration * config.multiple.dev.rps) /
|
||||
totalRps),
|
||||
throughput: totalRps * 60,
|
||||
transactionErrorRate:
|
||||
config.multiple.dev.rps / (config.multiple.prod.rps + config.multiple.dev.rps),
|
||||
});
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
return apmSynthtraceEsClient.index([
|
||||
transactionInterval
|
||||
.rate(config.multiple.prod.rps)
|
||||
.generator((timestamp) =>
|
||||
multipleEnvServiceProdInstance
|
||||
.transaction({ transactionName: 'GET /api' })
|
||||
.timestamp(timestamp)
|
||||
.duration(config.multiple.prod.duration)
|
||||
.success()
|
||||
),
|
||||
transactionInterval
|
||||
.rate(config.multiple.dev.rps)
|
||||
.generator((timestamp) =>
|
||||
multipleEnvServiceDevInstance
|
||||
.transaction({ transactionName: 'GET /api' })
|
||||
.timestamp(timestamp)
|
||||
.duration(config.multiple.dev.duration)
|
||||
.failure()
|
||||
),
|
||||
transactionInterval
|
||||
.rate(config.multiple.prod.rps)
|
||||
.generator((timestamp) =>
|
||||
multipleEnvServiceDevInstance
|
||||
.transaction({ transactionName: 'non-request', transactionType: 'rpc' })
|
||||
.timestamp(timestamp)
|
||||
.duration(config.multiple.prod.duration)
|
||||
.success()
|
||||
),
|
||||
metricInterval.rate(1).generator((timestamp) =>
|
||||
metricOnlyInstance
|
||||
.appMetrics({
|
||||
'system.memory.actual.free': 1,
|
||||
'system.cpu.total.norm.pct': 1,
|
||||
'system.memory.total': 1,
|
||||
'system.process.cpu.total.norm.pct': 1,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
errorInterval
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
errorOnlyInstance.error({ message: 'Foo' }).timestamp(timestamp)
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return apmSynthtraceEsClient.clean();
|
||||
});
|
||||
|
||||
describe('when no additional filters are applied', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a successful response', () => {
|
||||
expect(response.status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns the correct statistics', () => {
|
||||
checkStats();
|
||||
});
|
||||
|
||||
it('returns services without transaction data', () => {
|
||||
const serviceNames = response.body.items.map((item) => item.serviceName);
|
||||
|
||||
expect(serviceNames).to.contain('metric-only-service');
|
||||
|
||||
expect(serviceNames).to.contain('error-only-service');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when applying an environment filter', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: 'production',
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns data only for that environment', () => {
|
||||
const multipleEnvService = response.body.items.find(
|
||||
(item) => item.serviceName === 'multiple-env-service'
|
||||
);
|
||||
|
||||
const totalRps = config.multiple.prod.rps;
|
||||
|
||||
expect(multipleEnvService).to.eql({
|
||||
serviceName: 'multiple-env-service',
|
||||
transactionType: 'request',
|
||||
environments: ['production'],
|
||||
agentName: 'go',
|
||||
latency: 1000 * ((config.multiple.prod.duration * config.multiple.prod.rps) / totalRps),
|
||||
throughput: totalRps * 60,
|
||||
transactionErrorRate: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when applying a kuery filter', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: 'service.node.name:"multiple-env-service-development"',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns data for that kuery filter only', () => {
|
||||
const multipleEnvService = response.body.items.find(
|
||||
(item) => item.serviceName === 'multiple-env-service'
|
||||
);
|
||||
|
||||
const totalRps = config.multiple.dev.rps;
|
||||
|
||||
expect(multipleEnvService).to.eql({
|
||||
serviceName: 'multiple-env-service',
|
||||
transactionType: 'request',
|
||||
environments: ['development'],
|
||||
agentName: 'go',
|
||||
latency: 1000 * ((config.multiple.dev.duration * config.multiple.dev.rps) / totalRps),
|
||||
throughput: totalRps * 60,
|
||||
transactionErrorRate: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when excluding default transaction types', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: 'not (transaction.type:request)',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns data for the top transaction type that is not a default', () => {
|
||||
const multipleEnvService = response.body.items.find(
|
||||
(item) => item.serviceName === 'multiple-env-service'
|
||||
);
|
||||
|
||||
expect(multipleEnvService?.transactionType).to.eql('rpc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using service transaction metrics', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.ServiceTransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns services without transaction data', () => {
|
||||
const serviceNames = response.body.items.map((item) => item.serviceName);
|
||||
|
||||
expect(serviceNames).to.contain('metric-only-service');
|
||||
|
||||
expect(serviceNames).to.contain('error-only-service');
|
||||
});
|
||||
|
||||
it('returns the correct statistics', () => {
|
||||
checkStats();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using rolled up data', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.TenMinutes,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct statistics', () => {
|
||||
checkStats();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
|
||||
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
|
||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
|
||||
const start = '2023-10-28T00:00:00.000Z';
|
||||
const end = '2023-10-28T00:14:59.999Z';
|
||||
|
||||
const serviceName = 'opbeans-node';
|
||||
|
||||
async function getTransactionTypes() {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
describe('Transaction types', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await getTransactionTypes();
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expect(response.body.transactionTypes.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval(
|
||||
'1m'
|
||||
);
|
||||
|
||||
const instance = apm.service(serviceName, 'production', 'node').instance('instance');
|
||||
|
||||
await apmSynthtraceEsClient.index([
|
||||
interval.rate(3).generator((timestamp) => {
|
||||
return instance
|
||||
.transaction({ transactionName: 'GET /api', transactionType: 'request' })
|
||||
.duration(1000)
|
||||
.outcome('success')
|
||||
.timestamp(timestamp);
|
||||
}),
|
||||
interval.rate(1).generator((timestamp) => {
|
||||
return instance
|
||||
.transaction({ transactionName: 'rm -rf *', transactionType: 'worker' })
|
||||
.duration(100)
|
||||
.outcome('failure')
|
||||
.timestamp(timestamp);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
it('displays available tx types', async () => {
|
||||
const response = await getTransactionTypes();
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.transactionTypes.length).to.be.greaterThan(0);
|
||||
|
||||
expect(response.body.transactionTypes).to.eql(['request', 'worker']);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1099,6 +1099,25 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
|
|||
return body;
|
||||
},
|
||||
|
||||
async runRule(roleAuthc: RoleCredentials, ruleId: string) {
|
||||
return await retry.tryForTime(retryTimeout, async () => {
|
||||
try {
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`/internal/alerting/rule/${ruleId}/_run_soon`)
|
||||
.set(samlAuth.getInternalRequestHeader())
|
||||
.set(roleAuthc.apiKeyHeader)
|
||||
.expect(204);
|
||||
|
||||
if (response.status !== 204) {
|
||||
throw new Error(`runRuleSoon got ${response.status} status`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`[Rule] Running a rule ${ruleId} failed: ${error}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async findInRules(roleAuthc: RoleCredentials, ruleId: string) {
|
||||
const response = await supertestWithoutAuth
|
||||
.get('/api/alerting/rules/_find')
|
||||
|
|
|
@ -1,59 +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 { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import archives from '../../common/fixtures/es_archiver/archives_metadata';
|
||||
|
||||
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];
|
||||
|
||||
registry.when('Agent name when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/agent',
|
||||
params: {
|
||||
path: { serviceName: 'opbeans-node' },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.eql({});
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(
|
||||
'Agent name when data is loaded',
|
||||
{ config: 'basic', archives: [archiveName] },
|
||||
() => {
|
||||
it('returns the agent name', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/agent',
|
||||
params: {
|
||||
path: { serviceName: 'opbeans-node' },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expect(response.body).to.eql({ agentName: 'nodejs', runtimeName: 'node' });
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -27,38 +27,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
const { start, end } = metadata;
|
||||
const serviceNames = ['opbeans-java', 'opbeans-go'];
|
||||
|
||||
registry.when(
|
||||
'Services detailed statistics when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
offset: '1d',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
bucketSizeInSeconds: 60,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod).to.be.empty();
|
||||
expect(response.body.previousPeriod).to.be.empty();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when(
|
||||
'Services detailed statistics when data is loaded',
|
||||
{ config: 'basic', archives: [archiveName] },
|
||||
|
|
|
@ -1,171 +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 { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export default function annotationApiTests({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const es = getService('es');
|
||||
|
||||
const dates = [
|
||||
new Date('2021-02-01T00:00:00.000Z'),
|
||||
new Date('2021-02-01T01:00:00.000Z'),
|
||||
new Date('2021-02-01T02:00:00.000Z'),
|
||||
new Date('2021-02-01T03:00:00.000Z'),
|
||||
];
|
||||
|
||||
const indexName = 'apm-8.0.0-transaction';
|
||||
|
||||
registry.when(
|
||||
'Derived deployment annotations with a basic license',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('when there are multiple service versions', () => {
|
||||
let response: APIReturnType<'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31'>;
|
||||
|
||||
before(async () => {
|
||||
const indexExists = await es.indices.exists({ index: indexName });
|
||||
if (indexExists) {
|
||||
await es.indices.delete({
|
||||
index: indexName,
|
||||
});
|
||||
}
|
||||
|
||||
await es.indices.create({
|
||||
index: indexName,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
service: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
version: {
|
||||
type: 'keyword',
|
||||
},
|
||||
environment: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
transaction: {
|
||||
properties: {
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
duration: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
processor: {
|
||||
properties: {
|
||||
event: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const docs = dates.flatMap((date, index) => {
|
||||
const baseAnnotation = {
|
||||
transaction: {
|
||||
type: 'request',
|
||||
duration: 1000000,
|
||||
},
|
||||
|
||||
service: {
|
||||
name: 'opbeans-java',
|
||||
environment: 'production',
|
||||
version: index + 1,
|
||||
},
|
||||
processor: {
|
||||
event: 'transaction',
|
||||
},
|
||||
};
|
||||
return [
|
||||
{
|
||||
...baseAnnotation,
|
||||
'@timestamp': date.toISOString(),
|
||||
},
|
||||
{
|
||||
...baseAnnotation,
|
||||
'@timestamp': new Date(date.getTime() + 30000),
|
||||
},
|
||||
{
|
||||
...baseAnnotation,
|
||||
'@timestamp': new Date(date.getTime() + 60000),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
await es.bulk({
|
||||
index: indexName,
|
||||
body: docs.flatMap((doc) => [{ index: {} }, doc]),
|
||||
refresh: true,
|
||||
});
|
||||
|
||||
response = (
|
||||
await apmApiClient.readUser({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'opbeans-java',
|
||||
},
|
||||
query: {
|
||||
start: dates[1].toISOString(),
|
||||
end: dates[2].toISOString(),
|
||||
environment: 'production',
|
||||
},
|
||||
},
|
||||
})
|
||||
).body;
|
||||
});
|
||||
|
||||
it('annotations are displayed for the service versions in the given time range', async () => {
|
||||
expect(response.annotations.length).to.be(2);
|
||||
expect(response.annotations[0]['@timestamp']).to.be(dates[1].getTime());
|
||||
expect(response.annotations[1]['@timestamp']).to.be(dates[2].getTime());
|
||||
|
||||
expectSnapshot(response.annotations[0]).toMatchInline(`
|
||||
Object {
|
||||
"@timestamp": 1612141200000,
|
||||
"id": "2",
|
||||
"text": "2",
|
||||
"type": "version",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('annotations are not displayed for the service versions outside of the given time range', () => {
|
||||
expect(
|
||||
response.annotations.some((annotation) => {
|
||||
return (
|
||||
annotation['@timestamp'] !== dates[0].getTime() &&
|
||||
annotation['@timestamp'] !== dates[2].getTime()
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await es.indices.delete({
|
||||
index: indexName,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,121 +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 { first } from 'lodash';
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { dataConfig, generateData } from './generate_data';
|
||||
|
||||
type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
||||
|
||||
const {
|
||||
service: { name: serviceName },
|
||||
} = dataConfig;
|
||||
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() {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
environment: 'production',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Service details when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body).to.empty();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177663
|
||||
registry.when('Service details when data is generated', { config: 'basic', archives: [] }, () => {
|
||||
let body: ServiceDetails;
|
||||
let status: number;
|
||||
|
||||
before(async () => {
|
||||
await generateData({ apmSynthtraceEsClient, start, end });
|
||||
const response = await callApi();
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns correct cloud details', () => {
|
||||
const { cloud } = dataConfig;
|
||||
const {
|
||||
provider,
|
||||
availabilityZone,
|
||||
region,
|
||||
machineType,
|
||||
projectName,
|
||||
serviceName: cloudServiceName,
|
||||
} = cloud;
|
||||
|
||||
expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone);
|
||||
expect(first(body?.cloud?.machineTypes)).to.be(machineType);
|
||||
expect(body?.cloud?.provider).to.be(provider);
|
||||
expect(body?.cloud?.projectName).to.be(projectName);
|
||||
expect(body?.cloud?.serviceName).to.be(cloudServiceName);
|
||||
expect(first(body?.cloud?.regions)).to.be(region);
|
||||
});
|
||||
|
||||
it('returns correct container details', () => {
|
||||
expect(body?.container?.totalNumberInstances).to.be(1);
|
||||
});
|
||||
|
||||
it('returns correct serverless details', () => {
|
||||
const { cloud, serverless } = dataConfig;
|
||||
const { serviceName: cloudServiceName } = cloud;
|
||||
const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless;
|
||||
|
||||
expect(body?.serverless?.type).to.be(cloudServiceName);
|
||||
expect(body?.serverless?.functionNames).to.have.length(2);
|
||||
expect(body?.serverless?.functionNames).to.contain(firstFunctionName);
|
||||
expect(body?.serverless?.functionNames).to.contain(secondFunctionName);
|
||||
expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType);
|
||||
});
|
||||
|
||||
it('returns correct service details', () => {
|
||||
const { service } = dataConfig;
|
||||
const { version, runtime, framework, agent } = service;
|
||||
const { name: runTimeName, version: runTimeVersion } = runtime;
|
||||
const { name: agentName, version: agentVersion } = agent;
|
||||
|
||||
expect(body?.service?.framework).to.be(framework);
|
||||
expect(body?.service?.agent.name).to.be(agentName);
|
||||
expect(body?.service?.agent.version).to.be(agentVersion);
|
||||
expect(body?.service?.runtime?.name).to.be(runTimeName);
|
||||
expect(body?.service?.runtime?.version).to.be(runTimeVersion);
|
||||
expect(first(body?.service?.versions)).to.be(version);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,77 +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 { getServerlessTypeFromCloudData } from '@kbn/apm-plugin/common/serverless';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { dataConfig, generateData } from './generate_data';
|
||||
|
||||
type ServiceIconMetadata = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/icons'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
||||
|
||||
const { serviceName } = dataConfig;
|
||||
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() {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metadata/icons',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when('Service icons when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body).to.empty();
|
||||
});
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177662
|
||||
registry.when('Service icons when data is generated', { config: 'basic', archives: [] }, () => {
|
||||
let body: ServiceIconMetadata;
|
||||
let status: number;
|
||||
|
||||
before(async () => {
|
||||
await generateData({ apmSynthtraceEsClient, start, end });
|
||||
const response = await callApi();
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns correct metadata', () => {
|
||||
const { agentName, cloud } = dataConfig;
|
||||
const { provider, serviceName: cloudServiceName, provider: cloudProvider } = cloud;
|
||||
|
||||
expect(body.agentName).to.be(agentName);
|
||||
expect(body.cloudProvider).to.be(provider);
|
||||
expect(body.containerType).to.be('Kubernetes');
|
||||
expect(body.serverlessType).to.be(
|
||||
getServerlessTypeFromCloudData(cloudProvider, cloudServiceName)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,535 +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 { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import expect from '@kbn/expect';
|
||||
import { buildQueryFromFilters } from '@kbn/es-query';
|
||||
import { first, last, meanBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
|
||||
import {
|
||||
APIClientRequestParamsOf,
|
||||
APIReturnType,
|
||||
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
|
||||
import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
|
||||
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { roundNumber } from '../../utils';
|
||||
|
||||
type ThroughputReturn = APIReturnType<'GET /internal/apm/services/{serviceName}/throughput'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
|
||||
|
||||
const serviceName = 'synth-go';
|
||||
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(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/throughput'>['params']
|
||||
>,
|
||||
processorEvent: 'transaction' | 'metric' = 'metric'
|
||||
) {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/throughput',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'synth-go',
|
||||
...overrides?.path,
|
||||
},
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
...overrides?.query,
|
||||
...(processorEvent === 'metric'
|
||||
? {
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
bucketSizeInSeconds: 60,
|
||||
}
|
||||
: {
|
||||
documentType: ApmDocumentType.TransactionEvent,
|
||||
rollupInterval: RollupInterval.None,
|
||||
bucketSizeInSeconds: 30,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
registry.when('Throughput when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await callApi();
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.currentPeriod.length).to.be(0);
|
||||
expect(response.body.previousPeriod.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177510
|
||||
registry.when('Throughput when data is loaded', { config: 'basic', archives: [] }, () => {
|
||||
describe('Throughput chart api', () => {
|
||||
const GO_PROD_RATE = 50;
|
||||
const GO_DEV_RATE = 5;
|
||||
const JAVA_PROD_RATE = 45;
|
||||
|
||||
before(async () => {
|
||||
const serviceGoProdInstance = apm
|
||||
.service({ name: serviceName, environment: 'production', agentName: 'go' })
|
||||
.instance('instance-a');
|
||||
const serviceGoDevInstance = apm
|
||||
.service({ name: serviceName, environment: 'development', agentName: 'go' })
|
||||
.instance('instance-b');
|
||||
|
||||
const serviceJavaInstance = apm
|
||||
.service({ name: 'synth-java', environment: 'development', agentName: 'java' })
|
||||
.instance('instance-c');
|
||||
|
||||
await apmSynthtraceEsClient.index([
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(GO_PROD_RATE)
|
||||
.generator((timestamp) =>
|
||||
serviceGoProdInstance
|
||||
.transaction({ transactionName: 'GET /api/product/list' })
|
||||
.duration(1000)
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(GO_DEV_RATE)
|
||||
.generator((timestamp) =>
|
||||
serviceGoDevInstance
|
||||
.transaction({ transactionName: 'GET /api/product/:id' })
|
||||
.duration(1000)
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(JAVA_PROD_RATE)
|
||||
.generator((timestamp) =>
|
||||
serviceJavaInstance
|
||||
.transaction({ transactionName: 'POST /api/product/buy' })
|
||||
.duration(1000)
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
||||
describe('compare transactions and metrics based throughput', () => {
|
||||
let throughputMetrics: ThroughputReturn;
|
||||
let throughputTransactions: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([
|
||||
callApi({}, 'metric'),
|
||||
callApi({}, 'transaction'),
|
||||
]);
|
||||
throughputMetrics = throughputMetricsResponse.body;
|
||||
throughputTransactions = throughputTransactionsResponse.body;
|
||||
});
|
||||
|
||||
it('returns some transactions data', () => {
|
||||
expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns some metrics data', () => {
|
||||
expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same mean value for metrics and transactions data', () => {
|
||||
const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y');
|
||||
const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y');
|
||||
[transactionsMean, metricsMean].forEach((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE))
|
||||
);
|
||||
});
|
||||
|
||||
it('has a bucket size of 30 seconds for transactions data', () => {
|
||||
const firstTimerange = throughputTransactions.currentPeriod[0].x;
|
||||
const secondTimerange = throughputTransactions.currentPeriod[1].x;
|
||||
const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000;
|
||||
expect(timeIntervalAsSeconds).to.equal(30);
|
||||
});
|
||||
|
||||
it('has a bucket size of 1 minute for metrics data', () => {
|
||||
const firstTimerange = throughputMetrics.currentPeriod[0].x;
|
||||
const secondTimerange = throughputMetrics.currentPeriod[1].x;
|
||||
const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60;
|
||||
expect(timeIntervalAsMinutes).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('production environment', () => {
|
||||
let throughput: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const throughputResponse = await callApi({ query: { environment: 'production' } });
|
||||
throughput = throughputResponse.body;
|
||||
});
|
||||
|
||||
it('returns some data', () => {
|
||||
expect(throughput.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns correct average throughput', () => {
|
||||
const throughputMean = meanBy(throughput.currentPeriod, 'y');
|
||||
expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when synth-java is selected', () => {
|
||||
let throughput: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } });
|
||||
throughput = throughputResponse.body;
|
||||
});
|
||||
|
||||
it('returns some data', () => {
|
||||
expect(throughput.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns throughput related to java agent', () => {
|
||||
const throughputMean = meanBy(throughput.currentPeriod, 'y');
|
||||
expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE));
|
||||
});
|
||||
});
|
||||
|
||||
describe('time comparisons', () => {
|
||||
let throughputResponse: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const response = await callApi({
|
||||
query: {
|
||||
start: moment(end).subtract(7, 'minutes').toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
offset: '7m',
|
||||
},
|
||||
});
|
||||
throughputResponse = response.body;
|
||||
});
|
||||
|
||||
it('returns some data', () => {
|
||||
expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0);
|
||||
expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0);
|
||||
|
||||
const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) =>
|
||||
isFiniteNumber(y)
|
||||
);
|
||||
const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) =>
|
||||
isFiniteNumber(y)
|
||||
);
|
||||
|
||||
expect(hasCurrentPeriodData).to.equal(true);
|
||||
expect(hasPreviousPeriodData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same start time for both periods', () => {
|
||||
expect(first(throughputResponse.currentPeriod)?.x).to.equal(
|
||||
first(throughputResponse.previousPeriod)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('has same end time for both periods', () => {
|
||||
expect(last(throughputResponse.currentPeriod)?.x).to.equal(
|
||||
last(throughputResponse.previousPeriod)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('returns same number of buckets for both periods', () => {
|
||||
expect(throughputResponse.currentPeriod.length).to.be(
|
||||
throughputResponse.previousPeriod.length
|
||||
);
|
||||
});
|
||||
|
||||
it('has same mean value for both periods', () => {
|
||||
const currentPeriodMean = meanBy(
|
||||
throughputResponse.currentPeriod.filter((item) => isFiniteNumber(item.y) && item.y > 0),
|
||||
'y'
|
||||
);
|
||||
const previousPeriodMean = meanBy(
|
||||
throughputResponse.previousPeriod.filter(
|
||||
(item) => isFiniteNumber(item.y) && item.y > 0
|
||||
),
|
||||
'y'
|
||||
);
|
||||
const currentPeriod = throughputResponse.currentPeriod;
|
||||
const bucketSize = currentPeriod[1].x - currentPeriod[0].x;
|
||||
const durationAsMinutes = bucketSize / 1000 / 60;
|
||||
[currentPeriodMean, previousPeriodMean].every((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(
|
||||
roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes)
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles kuery', () => {
|
||||
let throughputMetrics: ThroughputReturn;
|
||||
let throughputTransactions: ThroughputReturn;
|
||||
|
||||
before(async () => {
|
||||
const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
kuery: 'transaction.name : "GET /api/product/list"',
|
||||
},
|
||||
},
|
||||
'metric'
|
||||
),
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
kuery: 'transaction.name : "GET /api/product/list"',
|
||||
},
|
||||
},
|
||||
'transaction'
|
||||
),
|
||||
]);
|
||||
throughputMetrics = throughputMetricsResponse.body;
|
||||
throughputTransactions = throughputTransactionsResponse.body;
|
||||
});
|
||||
|
||||
it('returns some transactions data', () => {
|
||||
expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns some metrics data', () => {
|
||||
expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same mean value for metrics and transactions data', () => {
|
||||
const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y');
|
||||
const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y');
|
||||
[transactionsMean, metricsMean].forEach((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE))
|
||||
);
|
||||
});
|
||||
|
||||
it('has a bucket size of 30 seconds for transactions data', () => {
|
||||
const firstTimerange = throughputTransactions.currentPeriod[0].x;
|
||||
const secondTimerange = throughputTransactions.currentPeriod[1].x;
|
||||
const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000;
|
||||
expect(timeIntervalAsSeconds).to.equal(30);
|
||||
});
|
||||
|
||||
it('has a bucket size of 1 minute for metrics data', () => {
|
||||
const firstTimerange = throughputMetrics.currentPeriod[0].x;
|
||||
const secondTimerange = throughputMetrics.currentPeriod[1].x;
|
||||
const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60;
|
||||
expect(timeIntervalAsMinutes).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles filters', () => {
|
||||
let throughputMetrics: ThroughputReturn;
|
||||
let throughputTransactions: ThroughputReturn;
|
||||
const filters = [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
key: 'transaction.name',
|
||||
params: ['GET /api/product/list'],
|
||||
type: 'phrases',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: {
|
||||
match_phrase: {
|
||||
'transaction.name': 'GET /api/product/list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined));
|
||||
|
||||
before(async () => {
|
||||
const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
filters: serializedFilters,
|
||||
},
|
||||
},
|
||||
'metric'
|
||||
),
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
filters: serializedFilters,
|
||||
},
|
||||
},
|
||||
'transaction'
|
||||
),
|
||||
]);
|
||||
throughputMetrics = throughputMetricsResponse.body;
|
||||
throughputTransactions = throughputTransactionsResponse.body;
|
||||
});
|
||||
|
||||
it('returns some transactions data', () => {
|
||||
expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns some metrics data', () => {
|
||||
expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same mean value for metrics and transactions data', () => {
|
||||
const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y');
|
||||
const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y');
|
||||
[transactionsMean, metricsMean].forEach((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE))
|
||||
);
|
||||
});
|
||||
|
||||
it('has a bucket size of 30 seconds for transactions data', () => {
|
||||
const firstTimerange = throughputTransactions.currentPeriod[0].x;
|
||||
const secondTimerange = throughputTransactions.currentPeriod[1].x;
|
||||
const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000;
|
||||
expect(timeIntervalAsSeconds).to.equal(30);
|
||||
});
|
||||
|
||||
it('has a bucket size of 1 minute for metrics data', () => {
|
||||
const firstTimerange = throughputMetrics.currentPeriod[0].x;
|
||||
const secondTimerange = throughputMetrics.currentPeriod[1].x;
|
||||
const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60;
|
||||
expect(timeIntervalAsMinutes).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles negate filters', () => {
|
||||
let throughputMetrics: ThroughputReturn;
|
||||
let throughputTransactions: ThroughputReturn;
|
||||
const filters = [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: true,
|
||||
alias: null,
|
||||
key: 'transaction.name',
|
||||
params: ['GET /api/product/list'],
|
||||
type: 'phrases',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: {
|
||||
match_phrase: {
|
||||
'transaction.name': 'GET /api/product/list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined));
|
||||
|
||||
before(async () => {
|
||||
const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
filters: serializedFilters,
|
||||
},
|
||||
},
|
||||
'metric'
|
||||
),
|
||||
callApi(
|
||||
{
|
||||
query: {
|
||||
filters: serializedFilters,
|
||||
},
|
||||
},
|
||||
'transaction'
|
||||
),
|
||||
]);
|
||||
throughputMetrics = throughputMetricsResponse.body;
|
||||
throughputTransactions = throughputTransactionsResponse.body;
|
||||
});
|
||||
|
||||
it('returns some transactions data', () => {
|
||||
expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns some metrics data', () => {
|
||||
expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0);
|
||||
const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y));
|
||||
expect(hasData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same mean value for metrics and transactions data', () => {
|
||||
const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y');
|
||||
const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y');
|
||||
[transactionsMean, metricsMean].forEach((value) =>
|
||||
expect(roundNumber(value)).to.be.equal(roundNumber(GO_DEV_RATE))
|
||||
);
|
||||
});
|
||||
|
||||
it('has a bucket size of 30 seconds for transactions data', () => {
|
||||
const firstTimerange = throughputTransactions.currentPeriod[0].x;
|
||||
const secondTimerange = throughputTransactions.currentPeriod[1].x;
|
||||
const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000;
|
||||
expect(timeIntervalAsSeconds).to.equal(30);
|
||||
});
|
||||
|
||||
it('has a bucket size of 1 minute for metrics data', () => {
|
||||
const firstTimerange = throughputMetrics.currentPeriod[0].x;
|
||||
const secondTimerange = throughputMetrics.currentPeriod[1].x;
|
||||
const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60;
|
||||
expect(timeIntervalAsMinutes).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles bad filters request', () => {
|
||||
it('throws bad request error', async () => {
|
||||
try {
|
||||
await callApi({
|
||||
query: { environment: 'production', filters: '{}}' },
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.res.status).to.be(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { sortBy } from 'lodash';
|
||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
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 { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
|
||||
|
@ -20,7 +19,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
const registry = getService('registry');
|
||||
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtrace = getService('apmSynthtraceEsClient');
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
|
||||
|
@ -30,355 +28,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
const archiveStart = archiveRange.start;
|
||||
const archiveEnd = archiveRange.end;
|
||||
|
||||
const start = '2021-10-01T00:00:00.000Z';
|
||||
const end = '2021-10-01T01:00:00.000Z';
|
||||
|
||||
registry.when(
|
||||
'APM Services Overview with a basic license when data is not generated',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.items.length).to.be(0);
|
||||
expect(response.body.maxCountExceeded).to.be(false);
|
||||
expect(response.body.serviceOverflowCount).to.be(0);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177509
|
||||
registry.when(
|
||||
'APM Services Overview with a basic license when data is generated',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
let response: {
|
||||
status: number;
|
||||
body: APIReturnType<'GET /internal/apm/services'>;
|
||||
};
|
||||
|
||||
const range = timerange(new Date(start).getTime(), new Date(end).getTime());
|
||||
const transactionInterval = range.interval('1s');
|
||||
const metricInterval = range.interval('30s');
|
||||
|
||||
const errorInterval = range.interval('5s');
|
||||
|
||||
const multipleEnvServiceProdInstance = apm
|
||||
.service({ name: 'multiple-env-service', environment: 'production', agentName: 'go' })
|
||||
.instance('multiple-env-service-production');
|
||||
|
||||
const multipleEnvServiceDevInstance = apm
|
||||
.service({ name: 'multiple-env-service', environment: 'development', agentName: 'go' })
|
||||
.instance('multiple-env-service-development');
|
||||
|
||||
const metricOnlyInstance = apm
|
||||
.service({ name: 'metric-only-service', environment: 'production', agentName: 'java' })
|
||||
.instance('metric-only-production');
|
||||
|
||||
const errorOnlyInstance = apm
|
||||
.service({ name: 'error-only-service', environment: 'production', agentName: 'java' })
|
||||
.instance('error-only-production');
|
||||
|
||||
const config = {
|
||||
multiple: {
|
||||
prod: {
|
||||
rps: 4,
|
||||
duration: 1000,
|
||||
},
|
||||
dev: {
|
||||
rps: 1,
|
||||
duration: 500,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function checkStats() {
|
||||
const multipleEnvService = response.body.items.find(
|
||||
(item) => item.serviceName === 'multiple-env-service'
|
||||
);
|
||||
|
||||
const totalRps = config.multiple.prod.rps + config.multiple.dev.rps;
|
||||
|
||||
expect(multipleEnvService).to.eql({
|
||||
serviceName: 'multiple-env-service',
|
||||
transactionType: 'request',
|
||||
environments: ['production', 'development'],
|
||||
agentName: 'go',
|
||||
latency:
|
||||
1000 *
|
||||
((config.multiple.prod.duration * config.multiple.prod.rps +
|
||||
config.multiple.dev.duration * config.multiple.dev.rps) /
|
||||
totalRps),
|
||||
throughput: totalRps * 60,
|
||||
transactionErrorRate:
|
||||
config.multiple.dev.rps / (config.multiple.prod.rps + config.multiple.dev.rps),
|
||||
});
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
return synthtrace.index([
|
||||
transactionInterval
|
||||
.rate(config.multiple.prod.rps)
|
||||
.generator((timestamp) =>
|
||||
multipleEnvServiceProdInstance
|
||||
.transaction({ transactionName: 'GET /api' })
|
||||
.timestamp(timestamp)
|
||||
.duration(config.multiple.prod.duration)
|
||||
.success()
|
||||
),
|
||||
transactionInterval
|
||||
.rate(config.multiple.dev.rps)
|
||||
.generator((timestamp) =>
|
||||
multipleEnvServiceDevInstance
|
||||
.transaction({ transactionName: 'GET /api' })
|
||||
.timestamp(timestamp)
|
||||
.duration(config.multiple.dev.duration)
|
||||
.failure()
|
||||
),
|
||||
transactionInterval
|
||||
.rate(config.multiple.prod.rps)
|
||||
.generator((timestamp) =>
|
||||
multipleEnvServiceDevInstance
|
||||
.transaction({ transactionName: 'non-request', transactionType: 'rpc' })
|
||||
.timestamp(timestamp)
|
||||
.duration(config.multiple.prod.duration)
|
||||
.success()
|
||||
),
|
||||
metricInterval.rate(1).generator((timestamp) =>
|
||||
metricOnlyInstance
|
||||
.appMetrics({
|
||||
'system.memory.actual.free': 1,
|
||||
'system.cpu.total.norm.pct': 1,
|
||||
'system.memory.total': 1,
|
||||
'system.process.cpu.total.norm.pct': 1,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
errorInterval
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
errorOnlyInstance.error({ message: 'Foo' }).timestamp(timestamp)
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return synthtrace.clean();
|
||||
});
|
||||
|
||||
describe('when no additional filters are applied', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a successful response', () => {
|
||||
expect(response.status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns the correct statistics', () => {
|
||||
checkStats();
|
||||
});
|
||||
|
||||
it('returns services without transaction data', () => {
|
||||
const serviceNames = response.body.items.map((item) => item.serviceName);
|
||||
|
||||
expect(serviceNames).to.contain('metric-only-service');
|
||||
|
||||
expect(serviceNames).to.contain('error-only-service');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when applying an environment filter', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: 'production',
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns data only for that environment', () => {
|
||||
const multipleEnvService = response.body.items.find(
|
||||
(item) => item.serviceName === 'multiple-env-service'
|
||||
);
|
||||
|
||||
const totalRps = config.multiple.prod.rps;
|
||||
|
||||
expect(multipleEnvService).to.eql({
|
||||
serviceName: 'multiple-env-service',
|
||||
transactionType: 'request',
|
||||
environments: ['production'],
|
||||
agentName: 'go',
|
||||
latency: 1000 * ((config.multiple.prod.duration * config.multiple.prod.rps) / totalRps),
|
||||
throughput: totalRps * 60,
|
||||
transactionErrorRate: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when applying a kuery filter', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: 'service.node.name:"multiple-env-service-development"',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns data for that kuery filter only', () => {
|
||||
const multipleEnvService = response.body.items.find(
|
||||
(item) => item.serviceName === 'multiple-env-service'
|
||||
);
|
||||
|
||||
const totalRps = config.multiple.dev.rps;
|
||||
|
||||
expect(multipleEnvService).to.eql({
|
||||
serviceName: 'multiple-env-service',
|
||||
transactionType: 'request',
|
||||
environments: ['development'],
|
||||
agentName: 'go',
|
||||
latency: 1000 * ((config.multiple.dev.duration * config.multiple.dev.rps) / totalRps),
|
||||
throughput: totalRps * 60,
|
||||
transactionErrorRate: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when excluding default transaction types', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: 'not (transaction.type:request)',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns data for the top transaction type that is not a default', () => {
|
||||
const multipleEnvService = response.body.items.find(
|
||||
(item) => item.serviceName === 'multiple-env-service'
|
||||
);
|
||||
|
||||
expect(multipleEnvService?.transactionType).to.eql('rpc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using service transaction metrics', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.ServiceTransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns services without transaction data', () => {
|
||||
const serviceNames = response.body.items.map((item) => item.serviceName);
|
||||
|
||||
expect(serviceNames).to.contain('metric-only-service');
|
||||
|
||||
expect(serviceNames).to.contain('error-only-service');
|
||||
});
|
||||
|
||||
it('returns the correct statistics', () => {
|
||||
checkStats();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using rolled up data', () => {
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.TenMinutes,
|
||||
useDurationSummary: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct statistics', () => {
|
||||
checkStats();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when(
|
||||
'APM Services Overview with a trial license when data is loaded',
|
||||
{ config: 'trial', archives: [archiveName] },
|
||||
|
|
|
@ -1,92 +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 { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
|
||||
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
|
||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtrace = getService('apmSynthtraceEsClient');
|
||||
|
||||
const start = '2023-10-28T00:00:00.000Z';
|
||||
const end = '2023-10-28T00:14:59.999Z';
|
||||
|
||||
const serviceName = 'opbeans-node';
|
||||
|
||||
async function getTransactionTypes() {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Transaction types when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await getTransactionTypes();
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expect(response.body.transactionTypes.length).to.be(0);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/177521
|
||||
registry.when('Transaction types when data is loaded', { config: 'basic', archives: [] }, () => {
|
||||
before(async () => {
|
||||
const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval(
|
||||
'1m'
|
||||
);
|
||||
|
||||
const instance = apm.service(serviceName, 'production', 'node').instance('instance');
|
||||
|
||||
await synthtrace.index([
|
||||
interval.rate(3).generator((timestamp) => {
|
||||
return instance
|
||||
.transaction({ transactionName: 'GET /api', transactionType: 'request' })
|
||||
.duration(1000)
|
||||
.outcome('success')
|
||||
.timestamp(timestamp);
|
||||
}),
|
||||
interval.rate(1).generator((timestamp) => {
|
||||
return instance
|
||||
.transaction({ transactionName: 'rm -rf *', transactionType: 'worker' })
|
||||
.duration(100)
|
||||
.outcome('failure')
|
||||
.timestamp(timestamp);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => synthtrace.clean());
|
||||
it('displays available tx types', async () => {
|
||||
const response = await getTransactionTypes();
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.transactionTypes.length).to.be.greaterThan(0);
|
||||
|
||||
expect(response.body.transactionTypes).to.eql(['request', 'worker']);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue