mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
7d13fbadea
commit
815718aa54
11 changed files with 606 additions and 8 deletions
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue