[APM] add filters support to apm latency, throughput, and error rate chart apis (#181359)

## Summary

Rational:
We'd like to embed APM visualizations across the observability solution,
particularly within the SLO alert details page at this time. SLO
configuration supports unified search filters. In order to ensure that
the data accurately reflects the SLO configuration, API dependencies for
APM visualizations must support filters.

This PR adds filters support to:

1. `GET
/internal/apm/services/{serviceName}/transactions/charts/latency`
2. `GET /internal/apm/services/{serviceName}/throughput`
3. `GET
/internal/apm/services/{serviceName}/transactions/charts/error_rate`

It is expected that consumers of the filters param send a serialized
object containing a `filter` or `must_not` clause to include on the
respective ES queries.

Internally, it is expected that these objects are created using the
`buildQueryFromFilters` helper exposed by `kbn/es-query`, passing the
`Filter` object from the unified search `SearchBar` as the the
parameter.

### Testing
This feature is not yet available in the UI
To test, I've added api integration tests for each api, as well as jest
tests for any helpers introduced.
This commit is contained in:
Dominique Clarke 2024-04-24 15:33:29 -04:00 committed by GitHub
parent 7d13fbadea
commit 815718aa54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 606 additions and 8 deletions

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { BoolQuery } from '@kbn/es-query';
import { kqlQuery, rangeQuery, termQuery } from '@kbn/observability-plugin/server';
import { ApmServiceTransactionDocumentType } from '../../../common/document_type';
import { SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_TYPE } from '../../../common/es_fields/apm';
@ -22,6 +22,7 @@ import {
export async function getFailedTransactionRate({
environment,
kuery,
filters,
serviceName,
transactionTypes,
transactionName,
@ -35,6 +36,7 @@ export async function getFailedTransactionRate({
}: {
environment: string;
kuery: string;
filters?: BoolQuery;
serviceName: string;
transactionTypes: string[];
transactionName?: string;
@ -62,7 +64,9 @@ export async function getFailedTransactionRate({
...rangeQuery(startWithOffset, endWithOffset),
...environmentQuery(environment),
...kqlQuery(kuery),
...(filters?.filter || []),
];
const mustNot = filters?.must_not || [];
const outcomes = getOutcomeAggregation(documentType);
@ -73,7 +77,7 @@ export async function getFailedTransactionRate({
body: {
track_total_hits: false,
size: 0,
query: { bool: { filter } },
query: { bool: { filter, must_not: mustNot } },
aggs: {
...outcomes,
timeseries: {

View file

@ -0,0 +1,42 @@
/*
* 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 { isLeft } from 'fp-ts/lib/Either';
import { filtersRt } from './default_api_types';
describe('filtersRt', () => {
it('should decode', () => {
const filters =
'{"must_not":[{"term":{"service.name":"myService"}}],"filter":[{"range":{"@timestamp":{"gte":1617273600000,"lte":1617277200000}}}]}';
const result = filtersRt.decode(filters);
expect(result).toEqual({
_tag: 'Right',
right: {
should: [],
must: [],
must_not: [{ term: { 'service.name': 'myService' } }],
filter: [{ range: { '@timestamp': { gte: 1617273600000, lte: 1617277200000 } } }],
},
});
});
it.each(['3', 'true', '{}'])('should not decode invalid filter JSON: %s', (invalidJson) => {
const filters = `{ "filter": ${invalidJson}}`;
const result = filtersRt.decode(filters);
// @ts-ignore-next-line
expect(result.left[0].message).toEqual('filters.filter is not iterable');
expect(isLeft(result)).toEqual(true);
});
it.each(['3', 'true', '{}'])('should not decode invalid must_not JSON: %s', (invalidJson) => {
const filters = `{ "must_not": ${invalidJson}}`;
const result = filtersRt.decode(filters);
// @ts-ignore-next-line
expect(result.left[0].message).toEqual('filters.must_not is not iterable');
expect(isLeft(result)).toEqual(true);
});
});

View file

@ -7,6 +7,8 @@
import * as t from 'io-ts';
import { isoToEpochRt, toNumberRt } from '@kbn/io-ts-utils';
import { either } from 'fp-ts/lib/Either';
import { BoolQuery } from '@kbn/es-query';
import { ApmDocumentType } from '../../common/document_type';
import { RollupInterval } from '../../common/rollup';
@ -48,3 +50,31 @@ export const transactionDataSourceRt = t.type({
t.literal(RollupInterval.None),
]),
});
const BoolQueryRt = t.type({
should: t.array(t.record(t.string, t.unknown)),
must: t.array(t.record(t.string, t.unknown)),
must_not: t.array(t.record(t.string, t.unknown)),
filter: t.array(t.record(t.string, t.unknown)),
});
export const filtersRt = new t.Type<BoolQuery, string, unknown>(
'BoolQuery',
BoolQueryRt.is,
(input: unknown, context: t.Context) =>
either.chain(t.string.validate(input, context), (value: string) => {
try {
const filters = JSON.parse(value);
const decoded = {
should: [],
must: [],
must_not: filters.must_not ? [...filters.must_not] : [],
filter: filters.filter ? [...filters.filter] : [],
};
return t.success(decoded);
} catch (err) {
return t.failure(input, context, err.message);
}
}),
(filters: BoolQuery): string => JSON.stringify(filters)
);

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { BoolQuery } from '@kbn/es-query';
import { kqlQuery, rangeQuery, termQuery } from '@kbn/observability-plugin/server';
import { ApmServiceTransactionDocumentType } from '../../../common/document_type';
import { SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_TYPE } from '../../../common/es_fields/apm';
@ -17,6 +17,7 @@ import { Maybe } from '../../../typings/common';
interface Options {
environment: string;
kuery: string;
filters?: BoolQuery;
serviceName: string;
apmEventClient: APMEventClient;
transactionType: string;
@ -34,6 +35,7 @@ export type ServiceThroughputResponse = Array<{ x: number; y: Maybe<number> }>;
export async function getThroughput({
environment,
kuery,
filters,
serviceName,
apmEventClient,
transactionType,
@ -67,7 +69,9 @@ export async function getThroughput({
...environmentQuery(environment),
...kqlQuery(kuery),
...termQuery(TRANSACTION_NAME, transactionName),
...(filters?.filter ?? []),
],
must_not: [...(filters?.must_not ?? [])],
},
},
aggs: {

View file

@ -33,6 +33,7 @@ import { withApmSpan } from '../../utils/with_apm_span';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
import {
environmentRt,
filtersRt,
kueryRt,
probabilityRt,
rangeRt,
@ -495,7 +496,7 @@ const serviceThroughputRoute = createApmServerRoute({
}),
query: t.intersection([
t.type({ transactionType: t.string, bucketSizeInSeconds: toNumberRt }),
t.partial({ transactionName: t.string }),
t.partial({ transactionName: t.string, filters: filtersRt }),
t.intersection([environmentRt, kueryRt, rangeRt, offsetRt, serviceTransactionDataSourceRt]),
]),
}),
@ -512,6 +513,7 @@ const serviceThroughputRoute = createApmServerRoute({
const {
environment,
kuery,
filters,
transactionType,
transactionName,
offset,
@ -525,6 +527,7 @@ const serviceThroughputRoute = createApmServerRoute({
const commonProps = {
environment,
kuery,
filters,
serviceName,
apmEventClient,
transactionType,

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { BoolQuery } from '@kbn/es-query';
import { getFailedTransactionRate } from '../../lib/transaction_groups/get_failed_transaction_rate';
import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate';
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
@ -25,6 +26,7 @@ export interface FailedTransactionRateResponse {
export async function getFailedTransactionRatePeriods({
environment,
kuery,
filters,
serviceName,
transactionType,
transactionName,
@ -38,6 +40,7 @@ export async function getFailedTransactionRatePeriods({
}: {
environment: string;
kuery: string;
filters?: BoolQuery;
serviceName: string;
transactionType: string;
transactionName?: string;
@ -52,6 +55,7 @@ export async function getFailedTransactionRatePeriods({
const commonProps = {
environment,
kuery,
filters,
serviceName,
transactionTypes: [transactionType],
transactionName,

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { BoolQuery } from '@kbn/es-query';
import { kqlQuery, rangeQuery, termQuery } from '@kbn/observability-plugin/server';
import { ApmServiceTransactionDocumentType } from '../../../../common/document_type';
import {
@ -29,6 +29,7 @@ import { getDurationFieldForTransactions } from '../../../lib/helpers/transactio
function searchLatency({
environment,
kuery,
filters,
serviceName,
transactionType,
transactionName,
@ -45,6 +46,7 @@ function searchLatency({
}: {
environment: string;
kuery: string;
filters?: BoolQuery;
serviceName: string;
transactionType: string | undefined;
transactionName: string | undefined;
@ -87,7 +89,9 @@ function searchLatency({
...termQuery(TRANSACTION_NAME, transactionName),
...termQuery(TRANSACTION_TYPE, transactionType),
...termQuery(FAAS_ID, serverlessId),
...(filters?.filter || []),
],
must_not: filters?.must_not || [],
},
},
aggs: {
@ -111,6 +115,7 @@ function searchLatency({
export async function getLatencyTimeseries({
environment,
kuery,
filters,
serviceName,
transactionType,
transactionName,
@ -127,6 +132,7 @@ export async function getLatencyTimeseries({
}: {
environment: string;
kuery: string;
filters?: BoolQuery;
serviceName: string;
transactionType?: string;
transactionName?: string;
@ -144,6 +150,7 @@ export async function getLatencyTimeseries({
const response = await searchLatency({
environment,
kuery,
filters,
serviceName,
transactionType,
transactionName,
@ -195,6 +202,7 @@ export async function getLatencyPeriods({
apmEventClient,
latencyAggregationType,
kuery,
filters,
environment,
start,
end,
@ -210,6 +218,7 @@ export async function getLatencyPeriods({
apmEventClient: APMEventClient;
latencyAggregationType: LatencyAggregationType;
kuery: string;
filters?: BoolQuery;
environment: string;
start: number;
end: number;
@ -225,6 +234,7 @@ export async function getLatencyPeriods({
transactionName,
apmEventClient,
kuery,
filters,
environment,
documentType,
rollupInterval,

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { jsonRt, toBooleanRt, toNumberRt } from '@kbn/io-ts-utils';
import * as t from 'io-ts';
import { offsetRt } from '../../../common/comparison_rt';
@ -23,6 +22,7 @@ import {
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
import {
environmentRt,
filtersRt,
kueryRt,
rangeRt,
serviceTransactionDataSourceRt,
@ -221,7 +221,7 @@ const transactionLatencyChartsRoute = createApmServerRoute({
bucketSizeInSeconds: toNumberRt,
useDurationSummary: toBooleanRt,
}),
t.partial({ transactionName: t.string }),
t.partial({ transactionName: t.string, filters: filtersRt }),
t.intersection([environmentRt, kueryRt, rangeRt, offsetRt]),
serviceTransactionDataSourceRt,
]),
@ -235,6 +235,7 @@ const transactionLatencyChartsRoute = createApmServerRoute({
const {
environment,
kuery,
filters,
transactionType,
transactionName,
latencyAggregationType,
@ -250,6 +251,7 @@ const transactionLatencyChartsRoute = createApmServerRoute({
const options = {
environment,
kuery,
filters,
serviceName,
transactionType,
transactionName,
@ -372,7 +374,7 @@ const transactionChartsErrorRateRoute = createApmServerRoute({
}),
query: t.intersection([
t.type({ transactionType: t.string, bucketSizeInSeconds: toNumberRt }),
t.partial({ transactionName: t.string }),
t.partial({ transactionName: t.string, filters: filtersRt }),
t.intersection([environmentRt, kueryRt, rangeRt, offsetRt, serviceTransactionDataSourceRt]),
]),
}),
@ -385,6 +387,7 @@ const transactionChartsErrorRateRoute = createApmServerRoute({
const {
environment,
kuery,
filters,
transactionType,
transactionName,
start,
@ -398,6 +401,7 @@ const transactionChartsErrorRateRoute = createApmServerRoute({
return getFailedTransactionRatePeriods({
environment,
kuery,
filters,
serviceName,
transactionType,
transactionName,

View file

@ -7,6 +7,7 @@
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';
@ -285,6 +286,250 @@ export default function ApiTest({ getService }: FtrProviderContext) {
);
});
});
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

@ -6,6 +6,7 @@
*/
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import { buildQueryFromFilters } from '@kbn/es-query';
import { first, last } from 'lodash';
import moment from 'moment';
import {
@ -297,5 +298,136 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
});
});
describe('handles kuery', () => {
let txMetricsErrorRateResponse: ErrorRate;
before(async () => {
const txMetricsResponse = await fetchErrorCharts({
query: {
kuery: 'transaction.name : "GET /pear 🍎 "',
},
});
txMetricsErrorRateResponse = txMetricsResponse.body;
});
describe('has the correct calculation for average with kuery', () => {
const expectedFailureRate = config.secondTransaction.failureRate / 100;
it('for tx metrics', () => {
expect(txMetricsErrorRateResponse.currentPeriod.average).to.eql(expectedFailureRate);
});
});
});
describe('handles filters', () => {
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 /pear 🍎 ',
},
},
},
},
},
];
const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined));
let txMetricsErrorRateResponse: ErrorRate;
before(async () => {
const txMetricsResponse = await fetchErrorCharts({
query: {
filters: serializedFilters,
},
});
txMetricsErrorRateResponse = txMetricsResponse.body;
});
describe('has the correct calculation for average with filter', () => {
const expectedFailureRate = config.secondTransaction.failureRate / 100;
it('for tx metrics', () => {
expect(txMetricsErrorRateResponse.currentPeriod.average).to.eql(expectedFailureRate);
});
});
describe('has the correct calculation for average with negate filter', () => {
const expectedFailureRate = config.secondTransaction.failureRate / 100;
it('for tx metrics', () => {
expect(txMetricsErrorRateResponse.currentPeriod.average).to.eql(expectedFailureRate);
});
});
});
describe('handles negate filters', () => {
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 /pear 🍎 ',
},
},
},
},
},
];
const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined));
let txMetricsErrorRateResponse: ErrorRate;
before(async () => {
const txMetricsResponse = await fetchErrorCharts({
query: {
filters: serializedFilters,
},
});
txMetricsErrorRateResponse = txMetricsResponse.body;
});
describe('has the correct calculation for average with filter', () => {
const expectedFailureRate = config.firstTransaction.failureRate / 100;
it('for tx metrics', () => {
expect(txMetricsErrorRateResponse.currentPeriod.average).to.eql(expectedFailureRate);
});
});
});
describe('handles bad filters request', () => {
it('for tx metrics', async () => {
try {
await fetchErrorCharts({
query: {
filters: '{}}}',
},
});
} catch (e) {
expect(e.res.status).to.eql(400);
}
});
});
});
}

View file

@ -6,6 +6,7 @@
*/
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import { buildQueryFromFilters } from '@kbn/es-query';
import moment from 'moment';
import {
APIClientRequestParamsOf,
@ -115,6 +116,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
((GO_PROD_RATE * GO_PROD_DURATION + GO_DEV_RATE * GO_DEV_DURATION) /
(GO_PROD_RATE + GO_DEV_RATE)) *
1000;
const expectedLatencyAvgValueProdMs =
((GO_PROD_RATE * GO_PROD_DURATION) / GO_PROD_RATE) * 1000;
const expectedLatencyAvgValueDevMs = ((GO_DEV_RATE * GO_DEV_DURATION) / GO_DEV_RATE) * 1000;
describe('average latency type', () => {
it('returns average duration and timeseries', async () => {
@ -319,6 +323,122 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
});
});
describe('handles kuery', () => {
it('should return the appropriate latency values when a kuery is applied', async () => {
const response = await fetchLatencyCharts({
query: {
latencyAggregationType: LatencyAggregationType.p95,
useDurationSummary: false,
kuery: 'transaction.name : "GET /api/product/list"',
},
});
expect(response.status).to.be(200);
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn.currentPeriod.overallAvgDuration).to.be(
expectedLatencyAvgValueProdMs
);
expect(latencyChartReturn.currentPeriod.latencyTimeseries.length).to.be.eql(15);
});
});
describe('handles filters', () => {
it('should return the appropriate latency values when filters are applied', async () => {
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));
const response = await fetchLatencyCharts({
query: {
latencyAggregationType: LatencyAggregationType.p95,
useDurationSummary: false,
filters: serializedFilters,
},
});
expect(response.status).to.be(200);
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn.currentPeriod.overallAvgDuration).to.be(
expectedLatencyAvgValueProdMs
);
expect(latencyChartReturn.currentPeriod.latencyTimeseries.length).to.be.eql(15);
});
it('should return the appropriate latency values when negate filters are applied', async () => {
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));
const response = await fetchLatencyCharts({
query: {
latencyAggregationType: LatencyAggregationType.p95,
useDurationSummary: false,
filters: serializedFilters,
},
});
expect(response.status).to.be(200);
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn.currentPeriod.overallAvgDuration).to.be(
expectedLatencyAvgValueDevMs
);
expect(latencyChartReturn.currentPeriod.latencyTimeseries.length).to.be.eql(15);
});
});
describe('handles bad filters request', () => {
it('throws bad request error', async () => {
try {
await fetchLatencyCharts({
query: { environment: 'production', filters: '{}}' },
});
} catch (error) {
expect(error.res.status).to.be(400);
}
});
});
}
);
}