[APM] Remove confusing transaction group abstraction (#41886)

This commit is contained in:
Søren Louv-Jansen 2019-07-26 21:01:23 +02:00 committed by GitHub
parent 25b17d4960
commit 9885200c3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 260 additions and 146 deletions

View file

@ -5,11 +5,11 @@
*/
import { useMemo } from 'react';
import { TransactionListAPIResponse } from '../../server/lib/transactions/get_top_transactions';
import { loadTransactionList } from '../services/rest/apm/transaction_groups';
import { IUrlParams } from '../context/UrlParamsContext/types';
import { useUiFilters } from '../context/UrlParamsContext';
import { useFetcher } from './useFetcher';
import { TransactionGroupListAPIResponse } from '../../server/lib/transaction_groups';
const getRelativeImpact = (
impact: number,
@ -21,7 +21,7 @@ const getRelativeImpact = (
1
);
function getWithRelativeImpact(items: TransactionListAPIResponse) {
function getWithRelativeImpact(items: TransactionGroupListAPIResponse) {
const impacts = items
.map(({ impact }) => impact)
.filter(impact => impact !== null) as number[];

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TraceListAPIResponse } from '../../../../server/lib/traces/get_top_traces';
import { TraceAPIResponse } from '../../../../server/lib/traces/get_trace';
import { callApi } from '../callApi';
import { getUiFiltersES } from '../../ui_filters/get_ui_filters_es';
import { UIFilters } from '../../../../typings/ui-filters';
import { TransactionGroupListAPIResponse } from '../../../../server/lib/transaction_groups';
export async function loadTrace({
traceId,
@ -37,7 +37,7 @@ export async function loadTraceList({
end: string;
uiFilters: UIFilters;
}) {
return callApi<TraceListAPIResponse>({
return callApi<TransactionGroupListAPIResponse>({
pathname: '/api/apm/traces',
query: {
start,

View file

@ -7,10 +7,10 @@
import { TransactionBreakdownAPIResponse } from '../../../../server/lib/transactions/breakdown';
import { TimeSeriesAPIResponse } from '../../../../server/lib/transactions/charts';
import { ITransactionDistributionAPIResponse } from '../../../../server/lib/transactions/distribution';
import { TransactionListAPIResponse } from '../../../../server/lib/transactions/get_top_transactions';
import { callApi } from '../callApi';
import { getUiFiltersES } from '../../ui_filters/get_ui_filters_es';
import { UIFilters } from '../../../../typings/ui-filters';
import { TransactionGroupListAPIResponse } from '../../../../server/lib/transaction_groups';
export async function loadTransactionList({
serviceName,
@ -25,7 +25,7 @@ export async function loadTransactionList({
transactionType: string;
uiFilters: UIFilters;
}) {
return await callApi<TransactionListAPIResponse>({
return await callApi<TransactionGroupListAPIResponse>({
pathname: `/api/apm/services/${serviceName}/transaction_groups`,
query: {
start,

View file

@ -1,35 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import {
PARENT_ID,
PROCESSOR_EVENT,
TRANSACTION_SAMPLED
} from '../../../common/elasticsearch_fieldnames';
import { PromiseReturnType } from '../../../typings/common';
import { rangeFilter } from '../helpers/range_filter';
import { Setup } from '../helpers/setup_request';
import { getTransactionGroups } from '../transaction_groups';
export type TraceListAPIResponse = PromiseReturnType<typeof getTopTraces>;
export async function getTopTraces(setup: Setup) {
const { start, end, uiFiltersES } = setup;
const bodyQuery = {
bool: {
// no parent ID means this transaction is a "root" transaction, i.e. a trace
must_not: { exists: { field: PARENT_ID } },
filter: [
{ range: rangeFilter(start, end) },
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
...uiFiltersES
],
should: [{ term: { [TRANSACTION_SAMPLED]: true } }]
}
};
return getTransactionGroups(setup, bodyQuery);
}

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transactionGroupsFetcher should call client with correct query 1`] = `
exports[`transactionGroupsFetcher type: top_traces should call client.search with correct query 1`] = `
Array [
Array [
Object {
@ -52,7 +52,145 @@ Array [
},
},
"query": Object {
"my": "bodyQuery",
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
Object {
"term": Object {
"processor.event": "transaction",
},
},
Object {
"term": Object {
"service.environment": "test",
},
},
],
"must_not": Array [
Object {
"exists": Object {
"field": "parent.id",
},
},
],
"should": Array [
Object {
"term": Object {
"transaction.sampled": true,
},
},
],
},
},
"size": 0,
},
"index": "myIndex",
},
],
]
`;
exports[`transactionGroupsFetcher type: top_transactions should call client.search with correct query 1`] = `
Array [
Array [
Object {
"body": Object {
"aggs": Object {
"transactions": Object {
"aggs": Object {
"avg": Object {
"avg": Object {
"field": "transaction.duration.us",
},
},
"p95": Object {
"percentiles": Object {
"field": "transaction.duration.us",
"percents": Array [
95,
],
},
},
"sample": Object {
"top_hits": Object {
"size": 1,
"sort": Array [
Object {
"_score": "desc",
},
Object {
"@timestamp": Object {
"order": "desc",
},
},
],
},
},
"sum": Object {
"sum": Object {
"field": "transaction.duration.us",
},
},
},
"terms": Object {
"field": "transaction.name",
"order": Object {
"sum": "desc",
},
"size": 100,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
Object {
"term": Object {
"processor.event": "transaction",
},
},
Object {
"term": Object {
"service.environment": "test",
},
},
Object {
"term": Object {
"service.name": "opbeans-node",
},
},
Object {
"term": Object {
"transaction.type": "request",
},
},
],
"must_not": Array [],
"should": Array [
Object {
"term": Object {
"transaction.sampled": true,
},
},
],
},
},
"size": 0,
},

View file

@ -4,42 +4,51 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ESResponse, transactionGroupsFetcher } from './fetcher';
import { transactionGroupsFetcher } from './fetcher';
function getSetup() {
return {
start: 1528113600000,
end: 1528977600000,
client: {
search: jest.fn()
} as any,
config: {
get: jest.fn<any, string[]>((key: string) => {
switch (key) {
case 'apm_oss.transactionIndices':
return 'myIndex';
case 'xpack.apm.ui.transactionGroupBucketSize':
return 100;
}
}),
has: () => true
},
uiFiltersES: [{ term: { 'service.environment': 'test' } }]
};
}
describe('transactionGroupsFetcher', () => {
let res: ESResponse;
let clientSpy: jest.Mock;
beforeEach(async () => {
clientSpy = jest.fn().mockResolvedValue('ES response');
const setup = {
start: 1528113600000,
end: 1528977600000,
client: {
search: clientSpy
} as any,
config: {
get: jest.fn<any, string[]>((key: string) => {
switch (key) {
case 'apm_oss.transactionIndices':
return 'myIndex';
case 'xpack.apm.ui.transactionGroupBucketSize':
return 100;
}
}),
has: () => true
},
uiFiltersES: [{ term: { 'service.environment': 'test' } }]
};
const bodyQuery = { my: 'bodyQuery' };
res = await transactionGroupsFetcher(setup, bodyQuery);
describe('type: top_traces', () => {
it('should call client.search with correct query', async () => {
const setup = getSetup();
await transactionGroupsFetcher({ type: 'top_traces' }, setup);
expect(setup.client.search.mock.calls).toMatchSnapshot();
});
});
it('should call client with correct query', () => {
expect(clientSpy.mock.calls).toMatchSnapshot();
});
it('should return correct response', () => {
expect(res).toBe('ES response');
describe('type: top_transactions', () => {
it('should call client.search with correct query', async () => {
const setup = getSetup();
await transactionGroupsFetcher(
{
type: 'top_transactions',
serviceName: 'opbeans-node',
transactionType: 'request'
},
setup
);
expect(setup.client.search.mock.calls).toMatchSnapshot();
});
});
});

View file

@ -6,19 +6,60 @@
import {
TRANSACTION_DURATION,
TRANSACTION_NAME
TRANSACTION_NAME,
PROCESSOR_EVENT,
PARENT_ID,
TRANSACTION_SAMPLED,
SERVICE_NAME,
TRANSACTION_TYPE
} from '../../../common/elasticsearch_fieldnames';
import { PromiseReturnType, StringMap } from '../../../typings/common';
import { PromiseReturnType } from '../../../typings/common';
import { Setup } from '../helpers/setup_request';
import { rangeFilter } from '../helpers/range_filter';
import { BoolQuery } from '../../../typings/elasticsearch';
interface TopTransactionOptions {
type: 'top_transactions';
serviceName: string;
transactionType: string;
}
interface TopTraceOptions {
type: 'top_traces';
}
export type Options = TopTransactionOptions | TopTraceOptions;
export type ESResponse = PromiseReturnType<typeof transactionGroupsFetcher>;
export function transactionGroupsFetcher(setup: Setup, bodyQuery: StringMap) {
const { client, config } = setup;
export function transactionGroupsFetcher(options: Options, setup: Setup) {
const { client, config, start, end, uiFiltersES } = setup;
const bool: BoolQuery = {
must_not: [],
// prefer sampled transactions
should: [{ term: { [TRANSACTION_SAMPLED]: true } }],
filter: [
{ range: rangeFilter(start, end) },
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
...uiFiltersES
]
};
if (options.type === 'top_traces') {
// A transaction without `parent.id` is considered a "root" transaction, i.e. a trace
bool.must_not.push({ exists: { field: PARENT_ID } });
} else {
bool.filter.push({ term: { [SERVICE_NAME]: options.serviceName } });
bool.filter.push({ term: { [TRANSACTION_TYPE]: options.transactionType } });
}
const params = {
index: config.get<string>('apm_oss.transactionIndices'),
body: {
size: 0,
query: bodyQuery,
query: {
bool
},
aggs: {
transactions: {
terms: {

View file

@ -4,14 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { StringMap } from '../../../typings/common';
import { Setup } from '../helpers/setup_request';
import { transactionGroupsFetcher } from './fetcher';
import { transactionGroupsFetcher, Options } from './fetcher';
import { transactionGroupsTransformer } from './transform';
import { PromiseReturnType } from '../../../typings/common';
export async function getTransactionGroups(setup: Setup, bodyQuery: StringMap) {
export type TransactionGroupListAPIResponse = PromiseReturnType<
typeof getTransactionGroupList
>;
export async function getTransactionGroupList(options: Options, setup: Setup) {
const { start, end } = setup;
const response = await transactionGroupsFetcher(setup, bodyQuery);
const response = await transactionGroupsFetcher(options, setup);
return transactionGroupsTransformer({
response,
start,

View file

@ -1,51 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import {
PROCESSOR_EVENT,
SERVICE_NAME,
TRANSACTION_TYPE
} from '../../../../common/elasticsearch_fieldnames';
import { PromiseReturnType } from '../../../../typings/common';
import { rangeFilter } from '../../helpers/range_filter';
import { Setup } from '../../helpers/setup_request';
import { getTransactionGroups } from '../../transaction_groups';
export interface IOptions {
setup: Setup;
transactionType?: string;
serviceName: string;
}
export type TransactionListAPIResponse = PromiseReturnType<
typeof getTopTransactions
>;
export async function getTopTransactions({
setup,
transactionType,
serviceName
}: IOptions) {
const { start, end, uiFiltersES } = setup;
const bodyQuery = {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
{ range: rangeFilter(start, end) },
...uiFiltersES
]
}
};
if (transactionType) {
bodyQuery.bool.filter.push({
term: { [TRANSACTION_TYPE]: transactionType }
});
}
return getTransactionGroups(setup, bodyQuery);
}

View file

@ -5,12 +5,11 @@
*/
import Boom from 'boom';
import { InternalCoreSetup } from 'src/core/server';
import { withDefaultValidators } from '../lib/helpers/input_validation';
import { setupRequest } from '../lib/helpers/setup_request';
import { getTopTraces } from '../lib/traces/get_top_traces';
import { getTrace } from '../lib/traces/get_trace';
import { getTransactionGroupList } from '../lib/transaction_groups';
const ROOT = '/api/apm/traces';
const defaultErrorHandler = (err: Error) => {
@ -34,8 +33,9 @@ export function initTracesApi(core: InternalCoreSetup) {
},
handler: req => {
const setup = setupRequest(req);
return getTopTraces(setup).catch(defaultErrorHandler);
return getTransactionGroupList({ type: 'top_traces' }, setup).catch(
defaultErrorHandler
);
}
});

View file

@ -11,8 +11,8 @@ import { withDefaultValidators } from '../lib/helpers/input_validation';
import { setupRequest } from '../lib/helpers/setup_request';
import { getTransactionCharts } from '../lib/transactions/charts';
import { getTransactionDistribution } from '../lib/transactions/distribution';
import { getTopTransactions } from '../lib/transactions/get_top_transactions';
import { getTransactionBreakdown } from '../lib/transactions/breakdown';
import { getTransactionGroupList } from '../lib/transaction_groups';
const defaultErrorHandler = (err: Error) => {
// eslint-disable-next-line
@ -36,14 +36,17 @@ export function initTransactionGroupsApi(core: InternalCoreSetup) {
},
handler: req => {
const { serviceName } = req.params;
const { transactionType } = req.query as { transactionType?: string };
const { transactionType } = req.query as { transactionType: string };
const setup = setupRequest(req);
return getTopTransactions({
serviceName,
transactionType,
return getTransactionGroupList(
{
type: 'top_transactions',
serviceName,
transactionType
},
setup
}).catch(defaultErrorHandler);
).catch(defaultErrorHandler);
}
});

View file

@ -6,6 +6,12 @@
import { StringMap, IndexAsString } from './common';
export interface BoolQuery {
must_not: Array<Record<string, any>>;
should: Array<Record<string, any>>;
filter: Array<Record<string, any>>;
}
declare module 'elasticsearch' {
// extending SearchResponse to be able to have typed aggregations