[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:
Carlos Crespo 2024-11-14 13:31:17 +01:00 committed by GitHub
parent 671ff30516
commit 2e9926de30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1633 additions and 1633 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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