[APM] Fix for no-data state for fallback from aggregated transactions (#109995)

* [APM] Fix for no-data state for fallback from aggregated transactions (#109609)

* PR feedback and unit tests

* fixes lint error

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Oliver Gupte 2021-08-31 06:57:10 -07:00 committed by GitHub
parent d3774519c0
commit e22c46bcc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 503 additions and 45 deletions

View file

@ -0,0 +1,145 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getIsUsingTransactionEvents with config xpack.apm.searchAggregatedTransactions: always should query for data when kuery is set 1`] = `
Object {
"apm": Object {
"events": Array [
"metric",
],
},
"body": Object {
"query": Object {
"bool": Object {
"filter": Array [
Object {
"exists": Object {
"field": "transaction.duration.histogram",
},
},
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
Object {
"bool": Object {
"minimum_should_match": 1,
"should": Array [
Object {
"match_phrase": Object {
"proccessor.event": "transaction",
},
},
],
},
},
],
},
},
},
"terminateAfter": 1,
}
`;
exports[`getIsUsingTransactionEvents with config xpack.apm.searchAggregatedTransactions: auto should query for data once if metrics data found 1`] = `
Object {
"apm": Object {
"events": Array [
"metric",
],
},
"body": Object {
"query": Object {
"bool": Object {
"filter": Array [
Object {
"exists": Object {
"field": "transaction.duration.histogram",
},
},
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
],
},
},
},
"terminateAfter": 1,
}
`;
exports[`getIsUsingTransactionEvents with config xpack.apm.searchAggregatedTransactions: auto should query for data twice if metrics data not found 1`] = `
Array [
Array [
"get_has_aggregated_transactions",
Object {
"apm": Object {
"events": Array [
"metric",
],
},
"body": Object {
"query": Object {
"bool": Object {
"filter": Array [
Object {
"exists": Object {
"field": "transaction.duration.histogram",
},
},
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
],
},
},
},
"terminateAfter": 1,
},
],
Array [
"get_has_transactions",
Object {
"apm": Object {
"events": Array [
"transaction",
],
},
"body": Object {
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
],
},
},
},
"terminateAfter": 1,
},
],
]
`;

View file

@ -1,36 +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 { getSearchAggregatedTransactions } from '.';
import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions';
import { Setup, SetupTimeRange } from '../setup_request';
export async function getFallbackToTransactions({
setup: { config, start, end, apmEventClient },
kuery,
}: {
setup: Setup & Partial<SetupTimeRange>;
kuery: string;
}): Promise<boolean> {
const searchAggregatedTransactions =
config['xpack.apm.searchAggregatedTransactions'];
const neverSearchAggregatedTransactions =
searchAggregatedTransactions === SearchAggregatedTransactionSetting.never;
if (neverSearchAggregatedTransactions) {
return false;
}
const searchesAggregatedTransactions = await getSearchAggregatedTransactions({
config,
start,
end,
apmEventClient,
kuery,
});
return !searchesAggregatedTransactions;
}

View file

@ -0,0 +1,257 @@
/*
* 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 { getIsUsingTransactionEvents } from './get_is_using_transaction_events';
import {
SearchParamsMock,
inspectSearchParams,
} from '../../../utils/test_helpers';
import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions';
const mockResponseNoHits = {
took: 398,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 0,
relation: 'gte' as const,
max_score: 0,
},
hits: [],
},
};
const mockResponseSomeHits = {
took: 398,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 3,
relation: 'gte' as const,
},
hits: [],
},
};
describe('getIsUsingTransactionEvents', () => {
let mock: SearchParamsMock;
afterEach(() => {
mock.teardown();
});
describe('with config xpack.apm.searchAggregatedTransactions: never', () => {
const config = {
'xpack.apm.searchAggregatedTransactions':
SearchAggregatedTransactionSetting.never,
};
it('should be false', async () => {
mock = await inspectSearchParams(
(setup) => getIsUsingTransactionEvents({ setup, kuery: '' }),
{ config }
);
expect(mock.response).toBe(false);
});
it('should not query for data', async () => {
mock = await inspectSearchParams(
(setup) => getIsUsingTransactionEvents({ setup, kuery: '' }),
{ config }
);
expect(mock.spy).toHaveBeenCalledTimes(0);
});
});
describe('with config xpack.apm.searchAggregatedTransactions: always', () => {
const config = {
'xpack.apm.searchAggregatedTransactions':
SearchAggregatedTransactionSetting.always,
};
it('should be false when kuery is empty', async () => {
mock = await inspectSearchParams(
(setup) => getIsUsingTransactionEvents({ setup, kuery: '' }),
{ config }
);
expect(mock.response).toBe(false);
});
it('should be false when kuery is set and metrics data found', async () => {
mock = await inspectSearchParams(
(setup) =>
getIsUsingTransactionEvents({
setup,
kuery: 'proccessor.event: "transaction"',
}),
{
config,
mockResponse: (request) => {
if (request === 'get_has_aggregated_transactions') {
return mockResponseSomeHits;
}
if (request === 'get_has_transactions') {
return mockResponseNoHits;
}
return mockResponseNoHits;
},
}
);
expect(mock.spy).toHaveBeenCalledTimes(1);
expect(mock.response).toBe(false);
});
it('should be true when kuery is set and metrics data are not found', async () => {
mock = await inspectSearchParams(
(setup) =>
getIsUsingTransactionEvents({
setup,
kuery: 'proccessor.event: "transaction"',
}),
{
config,
mockResponse: (request) => {
if (request === 'get_has_aggregated_transactions') {
return mockResponseNoHits;
}
if (request === 'get_has_transactions') {
return mockResponseSomeHits;
}
return mockResponseNoHits;
},
}
);
expect(mock.spy).toHaveBeenCalledTimes(2);
expect(mock.response).toBe(true);
});
it('should not query for data when kuery is empty', async () => {
mock = await inspectSearchParams(
(setup) => getIsUsingTransactionEvents({ setup, kuery: '' }),
{ config }
);
expect(mock.spy).toHaveBeenCalledTimes(0);
});
it('should query for data when kuery is set', async () => {
mock = await inspectSearchParams(
(setup) =>
getIsUsingTransactionEvents({
setup,
kuery: 'proccessor.event: "transaction"',
}),
{ config }
);
expect(mock.spy).toHaveBeenCalledTimes(1);
expect(mock.params).toMatchSnapshot();
});
});
describe('with config xpack.apm.searchAggregatedTransactions: auto', () => {
const config = {
'xpack.apm.searchAggregatedTransactions':
SearchAggregatedTransactionSetting.auto,
};
it('should query for data once if metrics data found', async () => {
mock = await inspectSearchParams(
(setup) => getIsUsingTransactionEvents({ setup, kuery: '' }),
{
config,
mockResponse: (request) => {
if (request === 'get_has_aggregated_transactions') {
return mockResponseSomeHits;
}
if (request === 'get_has_transactions') {
return mockResponseNoHits;
}
return mockResponseNoHits;
},
}
);
expect(mock.spy).toHaveBeenCalledTimes(1);
expect(mock.params).toMatchSnapshot();
});
it('should query for data twice if metrics data not found', async () => {
mock = await inspectSearchParams(
(setup) => getIsUsingTransactionEvents({ setup, kuery: '' }),
{
config,
mockResponse: (request) => {
if (request === 'get_has_aggregated_transactions') {
return mockResponseNoHits;
}
if (request === 'get_has_transactions') {
return mockResponseSomeHits;
}
return mockResponseNoHits;
},
}
);
expect(mock.spy).toHaveBeenCalledTimes(2);
expect(mock.spy.mock.calls).toMatchSnapshot();
});
it('should be false if metrics data are found', async () => {
mock = await inspectSearchParams(
(setup) => getIsUsingTransactionEvents({ setup, kuery: '' }),
{
config,
mockResponse: (request) => {
if (request === 'get_has_aggregated_transactions') {
return mockResponseSomeHits;
}
if (request === 'get_has_transactions') {
return mockResponseNoHits;
}
return mockResponseNoHits;
},
}
);
expect(mock.response).toBe(false);
});
it('should be true if no metrics data are found', async () => {
mock = await inspectSearchParams(
(setup) => getIsUsingTransactionEvents({ setup, kuery: '' }),
{
config,
mockResponse: (request) => {
if (request === 'get_has_aggregated_transactions') {
return mockResponseNoHits;
}
if (request === 'get_has_transactions') {
return mockResponseSomeHits;
}
return mockResponseNoHits;
},
}
);
expect(mock.response).toBe(true);
});
it('should be false if no metrics or transactions data are found', async () => {
mock = await inspectSearchParams(
(setup) => getIsUsingTransactionEvents({ setup, kuery: '' }),
{ config, mockResponse: () => mockResponseNoHits }
);
expect(mock.response).toBe(false);
});
});
});

View file

@ -0,0 +1,87 @@
/*
* 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 { getSearchAggregatedTransactions } from '.';
import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions';
import { Setup, SetupTimeRange } from '../setup_request';
import { kqlQuery, rangeQuery } from '../../../../../observability/server';
import { ProcessorEvent } from '../../../../common/processor_event';
import { APMEventClient } from '../create_es_client/create_apm_event_client';
export async function getIsUsingTransactionEvents({
setup: { config, start, end, apmEventClient },
kuery,
}: {
setup: Setup & Partial<SetupTimeRange>;
kuery: string;
}): Promise<boolean> {
const searchAggregatedTransactions =
config['xpack.apm.searchAggregatedTransactions'];
if (
searchAggregatedTransactions === SearchAggregatedTransactionSetting.never
) {
return false;
}
if (
!kuery &&
searchAggregatedTransactions === SearchAggregatedTransactionSetting.always
) {
return false;
}
const searchesAggregatedTransactions = await getSearchAggregatedTransactions({
config,
start,
end,
apmEventClient,
kuery,
});
if (!searchesAggregatedTransactions) {
// if no aggregrated transactions, check if any transactions at all
return await getHasTransactions({
start,
end,
apmEventClient,
kuery,
});
}
return false;
}
async function getHasTransactions({
start,
end,
apmEventClient,
kuery,
}: {
start?: number;
end?: number;
apmEventClient: APMEventClient;
kuery: string;
}) {
const response = await apmEventClient.search('get_has_transactions', {
apm: {
events: [ProcessorEvent.transaction],
},
body: {
query: {
bool: {
filter: [
...(start && end ? rangeQuery(start, end) : []),
...kqlQuery(kuery),
],
},
},
},
terminateAfter: 1,
});
return response.hits.total.value > 0;
}

View file

@ -47,11 +47,7 @@ export async function getHasAggregatedTransactions({
}
);
if (response.hits.total.value > 0) {
return true;
}
return false;
return response.hits.total.value > 0;
}
export async function getSearchAggregatedTransactions({

View file

@ -6,7 +6,7 @@
*/
import * as t from 'io-ts';
import { getFallbackToTransactions } from '../lib/helpers/aggregated_transactions/get_fallback_to_transactions';
import { getIsUsingTransactionEvents } from '../lib/helpers/aggregated_transactions/get_is_using_transaction_events';
import { setupRequest } from '../lib/helpers/setup_request';
import { createApmServerRoute } from './create_apm_server_route';
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
@ -26,7 +26,10 @@ const fallbackToTransactionsRoute = createApmServerRoute({
},
} = resources;
return {
fallbackToTransactions: await getFallbackToTransactions({ setup, kuery }),
fallbackToTransactions: await getIsUsingTransactionEvents({
setup,
kuery,
}),
};
},
});

View file

@ -18,6 +18,7 @@ interface Options {
request: ESSearchRequest
) => ESSearchResponse<unknown, ESSearchRequest>;
uiFilters?: Record<string, string>;
config?: Partial<APMConfig>;
}
interface MockSetup {
@ -70,7 +71,12 @@ export async function inspectSearchParams(
config: new Proxy(
{},
{
get: (_, key) => {
get: (_, key: keyof APMConfig) => {
const { config } = options;
if (config?.[key]) {
return config?.[key];
}
switch (key) {
default:
return 'myIndex';
@ -110,7 +116,7 @@ export async function inspectSearchParams(
}
return {
params: spy.mock.calls[0][1],
params: spy.mock.calls[0]?.[1],
response,
error,
spy,