Skip to content

Commit

Permalink
[APM] Migrate test apm-api-integration/tests/errors to be deploymen…
Browse files Browse the repository at this point in the history
…t agnostic api tests (#199655)

Closes #198971 
Part of #193245

## Summary

This PR migrates test apm-api-integration/tests/errors to be deployment
agnostic api tests

## Testing

Serverless:

```
node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts
node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM"
```
Stateful:

```
node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts
node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM"
```
  • Loading branch information
jennypavlova authored Nov 14, 2024
1 parent f4af267 commit 7921f56
Show file tree
Hide file tree
Showing 16 changed files with 956 additions and 931 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
* 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, last, sumBy } from 'lodash';
import {
APIClientRequestParamsOf,
APIReturnType,
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import type { RecursivePartial } from '@kbn/apm-plugin/typings/common';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client';
import { config, generateData } from './generate_data';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
import { isFiniteNumber } from '../utils/common';

type ErrorsDistribution =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>;

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}/errors/distribution'>['params']
>
) {
const response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName,
...overrides?.path,
},
query: {
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
environment: 'ENVIRONMENT_ALL',
kuery: '',
...overrides?.query,
},
},
});
return response;
}

describe('Error Distributions', () => {
describe('when 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', () => {
describe('errors distribution', () => {
const { appleTransaction, bananaTransaction } = config;
let apmSynthtraceEsClient: ApmSynthtraceEsClient;

before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
await generateData({ serviceName, start, end, apmSynthtraceEsClient });
});

after(() => apmSynthtraceEsClient.clean());

describe('without comparison', () => {
let errorsDistribution: ErrorsDistribution;
before(async () => {
const response = await callApi();
errorsDistribution = response.body;
});

it('displays combined number of occurrences', () => {
const countSum = sumBy(errorsDistribution.currentPeriod, 'y');
const numberOfBuckets = 15;
expect(countSum).to.equal(
(appleTransaction.failureRate + bananaTransaction.failureRate) * numberOfBuckets
);
});

describe('displays correct start in errors distribution chart', () => {
let errorsDistributionWithComparison: ErrorsDistribution;
before(async () => {
const responseWithComparison = await callApi({
query: {
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
offset: '15m',
},
});
errorsDistributionWithComparison = responseWithComparison.body;
});
it('has same start time when comparison is enabled', () => {
expect(first(errorsDistribution.currentPeriod)?.x).to.equal(
first(errorsDistributionWithComparison.currentPeriod)?.x
);
});
});
});

describe('displays occurrences for type "apple transaction" only', () => {
let errorsDistribution: ErrorsDistribution;
before(async () => {
const response = await callApi({
query: { kuery: `error.exception.type:"${appleTransaction.name}"` },
});
errorsDistribution = response.body;
});
it('displays combined number of occurrences', () => {
const countSum = sumBy(errorsDistribution.currentPeriod, 'y');
const numberOfBuckets = 15;
expect(countSum).to.equal(appleTransaction.failureRate * numberOfBuckets);
});
});

describe('with comparison', () => {
describe('when data is returned', () => {
let errorsDistribution: ErrorsDistribution;
before(async () => {
const fiveMinutes = 5 * 60 * 1000;
const response = await callApi({
query: {
start: new Date(end - fiveMinutes).toISOString(),
end: new Date(end).toISOString(),
offset: '5m',
},
});
errorsDistribution = response.body;
});
it('returns some data', () => {
const hasCurrentPeriodData = errorsDistribution.currentPeriod.some(({ y }) =>
isFiniteNumber(y)
);

const hasPreviousPeriodData = errorsDistribution.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(errorsDistribution.currentPeriod)?.x).to.equal(
first(errorsDistribution.previousPeriod)?.x
);
});

it('has same end time for both periods', () => {
expect(last(errorsDistribution.currentPeriod)?.x).to.equal(
last(errorsDistribution.previousPeriod)?.x
);
});

it('returns same number of buckets for both periods', () => {
expect(errorsDistribution.currentPeriod.length).to.equal(
errorsDistribution.previousPeriod.length
);
});
});

describe('when no data is returned', () => {
let errorsDistribution: ErrorsDistribution;
before(async () => {
const response = await callApi({
query: {
start: '2021-01-03T00:00:00.000Z',
end: '2021-01-03T00:15:00.000Z',
offset: '1d',
},
});
errorsDistribution = response.body;
});

it('has same start time for both periods', () => {
expect(first(errorsDistribution.currentPeriod)?.x).to.equal(
first(errorsDistribution.previousPeriod)?.x
);
});

it('has same end time for both periods', () => {
expect(last(errorsDistribution.currentPeriod)?.x).to.equal(
last(errorsDistribution.previousPeriod)?.x
);
});

it('returns same number of buckets for both periods', () => {
expect(errorsDistribution.currentPeriod.length).to.equal(
errorsDistribution.previousPeriod.length
);
});
});
});
});
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import type {
APIClientRequestParamsOf,
APIReturnType,
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import type { RecursivePartial } from '@kbn/apm-plugin/typings/common';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';

type ErrorGroups =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['errorGroups'];

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;

const callApi = async (
overrides?: RecursivePartial<
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['params']
>
) => {
return await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics',
params: {
path: { serviceName, ...overrides?.path },
query: {
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
environment: 'ENVIRONMENT_ALL',
kuery: '',
...overrides?.query,
},
},
});
};
describe('Error Group List', () => {
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();
});
});

describe('when data is loaded', () => {
describe('errors group', () => {
let apmSynthtraceEsClient: ApmSynthtraceEsClient;

const appleTransaction = {
name: 'GET /apple 🍎 ',
successRate: 75,
failureRate: 25,
};

const bananaTransaction = {
name: 'GET /banana 🍌',
successRate: 50,
failureRate: 50,
};

before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
const serviceInstance = apm
.service({ name: serviceName, environment: 'production', agentName: 'go' })
.instance('instance-a');

await apmSynthtraceEsClient.index([
timerange(start, end)
.interval('1m')
.rate(appleTransaction.successRate)
.generator((timestamp) =>
serviceInstance
.transaction({ transactionName: appleTransaction.name })
.timestamp(timestamp)
.duration(1000)
.success()
),
timerange(start, end)
.interval('1m')
.rate(appleTransaction.failureRate)
.generator((timestamp) =>
serviceInstance
.transaction({ transactionName: appleTransaction.name })
.errors(
serviceInstance.error({ message: 'error 1', type: 'foo' }).timestamp(timestamp)
)
.duration(1000)
.timestamp(timestamp)
.failure()
),
timerange(start, end)
.interval('1m')
.rate(bananaTransaction.successRate)
.generator((timestamp) =>
serviceInstance
.transaction({ transactionName: bananaTransaction.name })
.timestamp(timestamp)
.duration(1000)
.success()
),
timerange(start, end)
.interval('1m')
.rate(bananaTransaction.failureRate)
.generator((timestamp) =>
serviceInstance
.transaction({ transactionName: bananaTransaction.name })
.errors(
serviceInstance.error({ message: 'error 2', type: 'bar' }).timestamp(timestamp)
)
.duration(1000)
.timestamp(timestamp)
.failure()
),
]);
});

after(() => apmSynthtraceEsClient.clean());

describe('returns the correct data', () => {
let errorGroups: ErrorGroups;
before(async () => {
const response = await callApi();
errorGroups = response.body.errorGroups;
});

it('returns correct number of errors', () => {
expect(errorGroups.length).to.equal(2);
expect(errorGroups.map((error) => error.name).sort()).to.eql(['error 1', 'error 2']);
});

it('returns correct occurences', () => {
const numberOfBuckets = 15;
expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([
appleTransaction.failureRate * numberOfBuckets,
bananaTransaction.failureRate * numberOfBuckets,
]);
});
});
});
});
});
}
Loading

0 comments on commit 7921f56

Please sign in to comment.