[APM] Link to related errors from Transaction detail page (#29313)

* Closes #21920 by:
 - linking to errors list filtered by current transaction id
 - including the error count in the transaction details link

* [APM] improved code org and fix warning message in unit test

* [APM] improved code readability and parallelized ES queries
This commit is contained in:
Oliver Gupte 2019-01-30 07:14:05 -08:00 committed by GitHub
parent 4d6bfd6b26
commit 58f4295b22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 144 additions and 20 deletions

View file

@ -35,5 +35,8 @@
"groupId": "8673d8bf7a032e387c101bafbab0d2bc",
"latestOccurrenceAt": "2018-01-10T10:06:13.211Z"
}
]
],
"location": {
"search": "?transactionId=abcdef0123456789"
}
}

View file

@ -8,6 +8,8 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { NOT_AVAILABLE_LABEL } from 'x-pack/plugins/apm/common/i18n';
import { idx } from 'x-pack/plugins/apm/common/idx';
import { KibanaLink } from 'x-pack/plugins/apm/public/components/shared/Links/KibanaLink';
import { legacyEncodeURIComponent } from 'x-pack/plugins/apm/public/components/shared/Links/url_helpers';
import {
TRANSACTION_DURATION,
TRANSACTION_RESULT,
@ -24,11 +26,13 @@ import {
interface Props {
transaction: Transaction;
totalDuration?: number;
errorCount?: number;
}
export function StickyTransactionProperties({
transaction,
totalDuration
totalDuration,
errorCount
}: Props) {
const timestamp = transaction['@timestamp'];
const url =
@ -90,5 +94,43 @@ export function StickyTransactionProperties({
}
];
if (errorCount !== undefined) {
const errorsOverviewLink = (
<KibanaLink
pathname={'/app/apm'}
hash={`/${idx(transaction, _ => _.service.name)}/errors`}
query={{
kuery: legacyEncodeURIComponent(
`transaction.id : "${transaction.transaction.id}"`
)
}}
>
{i18n.translate('xpack.apm.transactionDetails.errorsOverviewLink', {
values: { errorCount },
defaultMessage:
'{errorCount, plural, one {View 1 error} other {View # errors}}'
})}
</KibanaLink>
);
const noErrorsText = i18n.translate(
'xpack.apm.transactionDetails.errorsNone',
{
defaultMessage: 'None'
}
);
stickyProperties.push({
label: i18n.translate(
'xpack.apm.transactionDetails.errorsOverviewLabel',
{
defaultMessage: 'Errors'
}
),
val: errorCount === 0 ? noErrorsText : errorsOverviewLink,
width: '25%'
});
}
return <StickyProperties stickyProperties={stickyProperties} />;
}

View file

@ -73,7 +73,7 @@ export function StickySpanProperties({ span, totalDuration }: Props) {
),
val: spanTypeLabel,
truncated: true,
widht: '50%'
width: '50%'
},
{
fieldName: SPAN_DURATION,

View file

@ -97,13 +97,15 @@ interface Props {
urlParams: IUrlParams;
location: Location;
waterfall: IWaterfall;
errorCount?: number;
}
export const Transaction: React.SFC<Props> = ({
transaction,
urlParams,
location,
waterfall
waterfall,
errorCount
}) => {
return (
<EuiPanel paddingSize="m">
@ -140,6 +142,7 @@ export const Transaction: React.SFC<Props> = ({
<EuiSpacer />
<StickyTransactionProperties
errorCount={errorCount}
transaction={transaction}
totalDuration={waterfall.traceRootDuration}
/>

View file

@ -8,6 +8,7 @@ import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React from 'react';
import { idx } from 'x-pack/plugins/apm/common/idx';
import { TransactionDetailsRequest } from '../../../store/reactReduxRequest/transactionDetails';
import { TransactionDetailsChartsRequest } from '../../../store/reactReduxRequest/transactionDetailsCharts';
import { TransactionDistributionRequest } from '../../../store/reactReduxRequest/transactionDistribution';
@ -67,7 +68,10 @@ export function TransactionDetailsView({ urlParams, location }: Props) {
<TransactionDetailsRequest
urlParams={urlParams}
render={({ data: transaction }) => {
render={({ data }) => {
const transaction = idx(data, _ => _.transaction);
const errorCount = idx(data, _ => _.errorCount);
if (!transaction) {
return (
<EmptyMessage
@ -99,6 +103,7 @@ export function TransactionDetailsView({ urlParams, location }: Props) {
transaction={transaction}
urlParams={urlParams}
waterfall={waterfall}
errorCount={errorCount}
/>
);
}}

View file

@ -5,7 +5,7 @@
*/
import { KFetchError } from 'ui/kfetch/kfetch_error';
import { TransactionAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/get_transaction';
import { TransactionWithErrorCountAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/get_transaction';
import { IUrlParams } from '../../../store/urlParams';
import { callApi } from '../callApi';
import { getEncodedEsQuery } from './apm';
@ -19,7 +19,7 @@ export async function loadTransaction({
kuery
}: IUrlParams) {
try {
const result = await callApi<TransactionAPIResponse>({
const result = await callApi<TransactionWithErrorCountAPIResponse>({
pathname: `/api/apm/services/${serviceName}/transactions/${transactionId}`,
query: {
traceId,

View file

@ -6,7 +6,7 @@
import React from 'react';
import { Request, RRRRender } from 'react-redux-request';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import { TransactionWithErrorCountAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/get_transaction';
import { loadTransaction } from '../../services/rest/apm/transactions';
import { IReduxState } from '../rootReducer';
import { IUrlParams } from '../urlParams';
@ -21,7 +21,7 @@ export function TransactionDetailsRequest({
render
}: {
urlParams: IUrlParams;
render: RRRRender<Transaction | null>;
render: RRRRender<TransactionWithErrorCountAPIResponse>;
}) {
const { serviceName, start, end, transactionId, traceId, kuery } = urlParams;

View file

@ -0,0 +1,53 @@
/*
* 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 { ESFilter } from 'elasticsearch';
import { idx } from 'x-pack/plugins/apm/common/idx';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import {
PROCESSOR_EVENT,
TRACE_ID,
TRANSACTION_ID
} from '../../../common/constants';
import { Setup } from '../helpers/setup_request';
export async function getErrorCount(
transactionId: string,
traceId: string,
setup: Setup
): Promise<number> {
const { start, end, esFilterQuery, client, config } = setup;
const filter: ESFilter[] = [
{ term: { [TRANSACTION_ID]: transactionId } },
{ term: { [TRACE_ID]: traceId } },
{ term: { [PROCESSOR_EVENT]: 'error' } },
{
range: {
'@timestamp': {
gte: start,
lte: end,
format: 'epoch_millis'
}
}
}
];
if (esFilterQuery) {
filter.push(esFilterQuery);
}
const params = {
index: config.get<string>('apm_oss.errorIndices'),
body: {
size: 0,
query: {
bool: { filter }
}
}
};
const resp = await client<APMError>('search', params);
return idx(resp, _ => _.hits.total) || 0;
}

View file

@ -74,7 +74,7 @@ export async function getErrorGroup({
const traceId = idx(error, _ => _.trace.id);
let transaction;
if (transactionId) {
if (transactionId && traceId) {
transaction = await getTransaction(transactionId, traceId, setup);
}

View file

@ -12,13 +12,19 @@ import {
TRACE_ID,
TRANSACTION_ID
} from '../../../../common/constants';
import { getErrorCount } from '../../errors/get_error_count';
import { Setup } from '../../helpers/setup_request';
export type TransactionAPIResponse = Transaction | undefined;
export interface TransactionWithErrorCountAPIResponse {
transaction: TransactionAPIResponse;
errorCount: number;
}
export async function getTransaction(
transactionId: string,
traceId: string | undefined,
traceId: string,
setup: Setup
): Promise<TransactionAPIResponse> {
const { start, end, esFilterQuery, client, config } = setup;
@ -26,6 +32,7 @@ export async function getTransaction(
const filter: ESFilter[] = [
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
{ term: { [TRANSACTION_ID]: transactionId } },
{ term: { [TRACE_ID]: traceId } },
{
range: {
'@timestamp': {
@ -41,10 +48,6 @@ export async function getTransaction(
filter.push(esFilterQuery);
}
if (traceId) {
filter.push({ term: { [TRACE_ID]: traceId } });
}
const params = {
index: config.get<string>('apm_oss.transactionIndices'),
body: {
@ -60,3 +63,14 @@ export async function getTransaction(
const resp = await client<Transaction>('search', params);
return idx(resp, _ => _.hits.hits[0]._source);
}
export async function getTransactionWithErrorCount(
transactionId: string,
traceId: string,
setup: Setup
): Promise<TransactionWithErrorCountAPIResponse> {
return Promise.all([
getTransaction(transactionId, traceId, setup),
getErrorCount(transactionId, traceId, setup)
]).then(([transaction, errorCount]) => ({ transaction, errorCount }));
}

View file

@ -9,7 +9,7 @@ import { Server } from 'hapi';
import Joi from 'joi';
import { withDefaultValidators } from '../lib/helpers/input_validation';
import { setupRequest } from '../lib/helpers/setup_request';
import { getTransaction } from '../lib/transactions/get_transaction';
import { getTransactionWithErrorCount } from '../lib/transactions/get_transaction';
export function initTransactionsApi(server: Server) {
server.route({
@ -18,7 +18,7 @@ export function initTransactionsApi(server: Server) {
options: {
validate: {
query: withDefaultValidators({
traceId: Joi.string().allow('') // TODO: this should be a path param and made required by 7.0
traceId: Joi.string().required()
})
}
},
@ -26,9 +26,13 @@ export function initTransactionsApi(server: Server) {
const { transactionId } = req.params;
const { traceId } = req.query as { traceId: string };
const setup = setupRequest(req);
const transaction = await getTransaction(transactionId, traceId, setup);
if (transaction) {
return transaction;
const transactionWithErrorCount = await getTransactionWithErrorCount(
transactionId,
traceId,
setup
);
if (transactionWithErrorCount.transaction) {
return transactionWithErrorCount;
} else {
throw Boom.notFound('Cannot find the requested page');
}