mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Top erroneous transactions (#134929)
* Add table for top erroneous transactions in error detail page * Add table for top errors in transaction details page * Add top errors to a new row on small viewports Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f0325b2f7d
commit
5c56102bf0
23 changed files with 1512 additions and 166 deletions
|
@ -40,6 +40,7 @@ export class Transaction extends BaseSpan {
|
|||
errors.forEach((error) => {
|
||||
error.fields['trace.id'] = this.fields['trace.id'];
|
||||
error.fields['transaction.id'] = this.fields['transaction.id'];
|
||||
error.fields['transaction.name'] = this.fields['transaction.name'];
|
||||
error.fields['transaction.type'] = this.fields['transaction.type'];
|
||||
});
|
||||
|
||||
|
|
|
@ -71,6 +71,15 @@ describe('Error details', () => {
|
|||
cy.get('[data-test-subj="errorDistribution"]').contains('Occurrences');
|
||||
});
|
||||
|
||||
it('shows top erroneous transactions table', () => {
|
||||
cy.visit(errorDetailsPageHref);
|
||||
cy.contains('Top 5 affected transactions');
|
||||
cy.get('[data-test-subj="topErroneousTransactionsTable"]')
|
||||
.contains('a', 'GET /apple 🍎')
|
||||
.click();
|
||||
cy.url().should('include', 'opbeans-java/transactions/view');
|
||||
});
|
||||
|
||||
it('shows a Stacktrace and Metadata tabs', () => {
|
||||
cy.visit(errorDetailsPageHref);
|
||||
cy.contains('button', 'Exception stack trace');
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { synthtrace } from '../../../../synthtrace';
|
||||
import { opbeans } from '../../../fixtures/synthtrace/opbeans';
|
||||
|
||||
const start = '2021-10-10T00:00:00.000Z';
|
||||
const end = '2021-10-10T00:15:00.000Z';
|
||||
|
||||
const timeRange = {
|
||||
rangeFrom: start,
|
||||
rangeTo: end,
|
||||
};
|
||||
|
||||
describe('Transaction details', () => {
|
||||
before(async () => {
|
||||
await synthtrace.index(
|
||||
opbeans({
|
||||
from: new Date(start).getTime(),
|
||||
to: new Date(end).getTime(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await synthtrace.clean();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAsViewerUser();
|
||||
cy.visit(
|
||||
`/app/apm/services/opbeans-java/transactions/view?${new URLSearchParams({
|
||||
...timeRange,
|
||||
transactionName: 'GET /api/product',
|
||||
})}`
|
||||
);
|
||||
});
|
||||
|
||||
it('shows transaction name and transaction charts', () => {
|
||||
cy.contains('h2', 'GET /api/product');
|
||||
cy.get('[data-test-subj="latencyChart"]');
|
||||
cy.get('[data-test-subj="throughput"]');
|
||||
cy.get('[data-test-subj="transactionBreakdownChart"]');
|
||||
cy.get('[data-test-subj="errorRate"]');
|
||||
});
|
||||
|
||||
it('shows top errors table', () => {
|
||||
cy.contains('Top 5 errors');
|
||||
cy.get('[data-test-subj="topErrorsForTransactionTable"]')
|
||||
.contains('a', '[MockError] Foo')
|
||||
.click();
|
||||
cy.url().should('include', 'opbeans-java/errors');
|
||||
});
|
||||
});
|
|
@ -13,6 +13,7 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
@ -29,6 +30,7 @@ import { useTimeRange } from '../../../hooks/use_time_range';
|
|||
import type { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { DetailView } from './detail_view';
|
||||
import { ErrorDistribution } from './distribution';
|
||||
import { TopErroneousTransactions } from './top_erroneous_transactions';
|
||||
|
||||
const Titles = euiStyled.div`
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeL};
|
||||
|
@ -49,6 +51,7 @@ const Message = euiStyled.div`
|
|||
|
||||
const Culprit = euiStyled.div`
|
||||
font-family: ${({ theme }) => theme.eui.euiCodeFontFamily};
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeS};
|
||||
`;
|
||||
|
||||
type ErrorDistributionAPIResponse =
|
||||
|
@ -194,52 +197,69 @@ export function ErrorGroupDetails() {
|
|||
|
||||
<EuiSpacer size={'m'} />
|
||||
|
||||
<EuiPanel hasBorder={true}>
|
||||
{showDetails && (
|
||||
<Titles>
|
||||
<EuiText>
|
||||
{logMessage && (
|
||||
<>
|
||||
<Label>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.logMessageLabel',
|
||||
{
|
||||
defaultMessage: 'Log message',
|
||||
}
|
||||
)}
|
||||
</Label>
|
||||
<Message>{logMessage}</Message>
|
||||
</>
|
||||
{showDetails && (
|
||||
<Titles>
|
||||
<EuiText>
|
||||
{logMessage && (
|
||||
<>
|
||||
<Label>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.logMessageLabel',
|
||||
{
|
||||
defaultMessage: 'Log message',
|
||||
}
|
||||
)}
|
||||
</Label>
|
||||
<Message>{logMessage}</Message>
|
||||
</>
|
||||
)}
|
||||
<Label>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.exceptionMessageLabel',
|
||||
{
|
||||
defaultMessage: 'Exception message',
|
||||
}
|
||||
)}
|
||||
<Label>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.exceptionMessageLabel',
|
||||
{
|
||||
defaultMessage: 'Exception message',
|
||||
}
|
||||
)}
|
||||
</Label>
|
||||
<Message>{excMessage || NOT_AVAILABLE_LABEL}</Message>
|
||||
<Label>
|
||||
{i18n.translate('xpack.apm.errorGroupDetails.culpritLabel', {
|
||||
defaultMessage: 'Culprit',
|
||||
})}
|
||||
</Label>
|
||||
<Culprit>{culprit || NOT_AVAILABLE_LABEL}</Culprit>
|
||||
</EuiText>
|
||||
</Titles>
|
||||
)}
|
||||
<ErrorDistribution
|
||||
fetchStatus={status}
|
||||
distribution={showDetails ? errorDistributionData : emptyState}
|
||||
title={i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.occurrencesChartLabel',
|
||||
{
|
||||
defaultMessage: 'Occurrences',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</Label>
|
||||
<Message>{excMessage || NOT_AVAILABLE_LABEL}</Message>
|
||||
<Label>
|
||||
{i18n.translate('xpack.apm.errorGroupDetails.culpritLabel', {
|
||||
defaultMessage: 'Culprit',
|
||||
})}
|
||||
</Label>
|
||||
<Culprit>{culprit || NOT_AVAILABLE_LABEL}</Culprit>
|
||||
|
||||
<Label>
|
||||
{i18n.translate('xpack.apm.errorGroupDetails.occurrencesLabel', {
|
||||
defaultMessage: 'Occurrences',
|
||||
})}
|
||||
</Label>
|
||||
{errorGroupData.occurrencesCount}
|
||||
</EuiText>
|
||||
<EuiHorizontalRule />
|
||||
</Titles>
|
||||
)}
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<ErrorDistribution
|
||||
fetchStatus={status}
|
||||
distribution={showDetails ? errorDistributionData : emptyState}
|
||||
title={i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.occurrencesChartLabel',
|
||||
{
|
||||
defaultMessage: 'Occurrences',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<TopErroneousTransactions serviceName={serviceName} />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
{showDetails && (
|
||||
<DetailView
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiTitle,
|
||||
RIGHT_ALIGNMENT,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import type { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { SparkPlot } from '../../../shared/charts/spark_plot';
|
||||
import {
|
||||
ChartType,
|
||||
getTimeSeriesColor,
|
||||
} from '../../../shared/charts/helper/get_timeseries_color';
|
||||
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { TransactionDetailLink } from '../../../shared/links/apm/transaction_detail_link';
|
||||
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
||||
import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { asInteger } from '../../../../../common/utils/formatters';
|
||||
|
||||
type ErroneousTransactions =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>;
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
const INITIAL_STATE: ErroneousTransactions = {
|
||||
topErroneousTransactions: [],
|
||||
};
|
||||
|
||||
export function TopErroneousTransactions({ serviceName }: Props) {
|
||||
const {
|
||||
query,
|
||||
path: { groupId },
|
||||
} = useApmParams('/services/{serviceName}/errors/{groupId}');
|
||||
|
||||
const { rangeFrom, rangeTo, environment, kuery, offset, comparisonEnabled } =
|
||||
query;
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { data = INITIAL_STATE, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
serviceName,
|
||||
groupId,
|
||||
},
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
numBuckets: 20,
|
||||
offset:
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? offset
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
groupId,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
]
|
||||
);
|
||||
|
||||
const loading =
|
||||
status === FETCH_STATUS.LOADING || status === FETCH_STATUS.NOT_INITIATED;
|
||||
|
||||
const columns: Array<
|
||||
EuiBasicTableColumn<
|
||||
ValuesType<ErroneousTransactions['topErroneousTransactions']>
|
||||
>
|
||||
> = [
|
||||
{
|
||||
field: 'transactionName',
|
||||
width: '60%',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.errorGroupTopTransactions.column.transactionName',
|
||||
{
|
||||
defaultMessage: 'Transaction name',
|
||||
}
|
||||
),
|
||||
render: (_, { transactionName, transactionType }) => {
|
||||
return (
|
||||
<TruncateWithTooltip
|
||||
text={transactionName}
|
||||
content={
|
||||
<TransactionDetailLink
|
||||
serviceName={serviceName}
|
||||
transactionName={transactionName}
|
||||
transactionType={transactionType ?? ''}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
offset={offset}
|
||||
>
|
||||
{transactionName}
|
||||
</TransactionDetailLink>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'occurrences',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.errorGroupTopTransactions.column.occurrences',
|
||||
{
|
||||
defaultMessage: 'Error occurrences',
|
||||
}
|
||||
),
|
||||
align: RIGHT_ALIGNMENT,
|
||||
dataType: 'number',
|
||||
render: (
|
||||
_,
|
||||
{ occurrences, currentPeriodTimeseries, previousPeriodTimeseries }
|
||||
) => {
|
||||
const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor(
|
||||
ChartType.FAILED_TRANSACTION_RATE
|
||||
);
|
||||
|
||||
return (
|
||||
<SparkPlot
|
||||
isLoading={loading}
|
||||
valueLabel={i18n.translate(
|
||||
'xpack.apm.errorGroupTopTransactions.column.occurrences.valueLabel',
|
||||
{
|
||||
defaultMessage: `{occurrences} occ.`,
|
||||
values: {
|
||||
occurrences: asInteger(occurrences),
|
||||
},
|
||||
}
|
||||
)}
|
||||
series={currentPeriodTimeseries}
|
||||
comparisonSeries={
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? previousPeriodTimeseries
|
||||
: undefined
|
||||
}
|
||||
color={currentPeriodColor}
|
||||
comparisonSeriesColor={previousPeriodColor}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate('xpack.apm.errorGroupTopTransactions.title', {
|
||||
defaultMessage: 'Top 5 affected transactions',
|
||||
})}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiBasicTable
|
||||
items={data.topErroneousTransactions}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
data-test-subj="topErroneousTransactionsTable"
|
||||
error={
|
||||
status === FETCH_STATUS.FAILURE
|
||||
? i18n.translate(
|
||||
'xpack.apm.errorGroupTopTransactions.errorMessage',
|
||||
{ defaultMessage: 'Failed to fetch' }
|
||||
)
|
||||
: ''
|
||||
}
|
||||
noItemsMessage={
|
||||
loading
|
||||
? i18n.translate('xpack.apm.errorGroupTopTransactions.loading', {
|
||||
defaultMessage: 'Loading...',
|
||||
})
|
||||
: i18n.translate('xpack.apm.errorGroupTopTransactions.noResults', {
|
||||
defaultMessage: 'No errors found associated with transactions',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -66,6 +66,7 @@ export function ServiceDependenciesBreakdownChart({
|
|||
annotations={[]}
|
||||
timeseries={timeseries}
|
||||
yAxisType="duration"
|
||||
id="serviceDependenciesBreakdownChart"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
|||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { ErrorOverviewLink } from '../../../shared/links/apm/error_overview_link';
|
||||
import { OverviewTableContainer } from '../../../shared/overview_table_container';
|
||||
import { getColumns } from './get_columns';
|
||||
import { getColumns } from '../../../shared/errors_table/get_columns';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBasicTable,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { getColumns } from '../../../shared/errors_table/get_columns';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
|
||||
type ErrorGroupMainStatisticsByTransactionName =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'>;
|
||||
|
||||
type ErrorGroupDetailedStatistics =
|
||||
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
|
||||
const INITIAL_STATE_MAIN_STATISTICS: {
|
||||
items: ErrorGroupMainStatisticsByTransactionName['errorGroups'];
|
||||
requestId?: string;
|
||||
} = {
|
||||
items: [],
|
||||
requestId: undefined,
|
||||
};
|
||||
|
||||
const INITIAL_STATE_DETAILED_STATISTICS: ErrorGroupDetailedStatistics = {
|
||||
currentPeriod: {},
|
||||
previousPeriod: {},
|
||||
};
|
||||
|
||||
export function TopErrors() {
|
||||
const {
|
||||
query,
|
||||
path: { serviceName },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
offset,
|
||||
comparisonEnabled,
|
||||
transactionName,
|
||||
transactionType,
|
||||
} = query;
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { data = INITIAL_STATE_MAIN_STATISTICS, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end && transactionType) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
transactionName,
|
||||
transactionType,
|
||||
maxNumberOfErrorGroups: 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
).then((response) => {
|
||||
return {
|
||||
// Everytime the main statistics is refetched, updates the requestId making the comparison API to be refetched.
|
||||
requestId: uuid(),
|
||||
items: response.errorGroups,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
transactionName,
|
||||
transactionType,
|
||||
// not used, but needed to trigger an update when offset is changed either manually by user or when time range is changed
|
||||
offset,
|
||||
// not used, but needed to trigger an update when comparison feature is disabled/enabled by user
|
||||
comparisonEnabled,
|
||||
]
|
||||
);
|
||||
|
||||
const { requestId, items } = data;
|
||||
|
||||
const {
|
||||
data: errorGroupDetailedStatistics = INITIAL_STATE_DETAILED_STATISTICS,
|
||||
status: errorGroupDetailedStatisticsStatus,
|
||||
} = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (requestId && items.length && start && end) {
|
||||
return callApmApi(
|
||||
'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
numBuckets: 20,
|
||||
offset:
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? offset
|
||||
: undefined,
|
||||
},
|
||||
body: {
|
||||
groupIds: JSON.stringify(
|
||||
items.map(({ groupId: groupId }) => groupId).sort()
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
// only fetches agg results when requestId changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[requestId],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
const errorGroupDetailedStatisticsLoading =
|
||||
errorGroupDetailedStatisticsStatus === FETCH_STATUS.LOADING;
|
||||
|
||||
const columns = getColumns({
|
||||
serviceName,
|
||||
errorGroupDetailedStatisticsLoading,
|
||||
errorGroupDetailedStatistics,
|
||||
comparisonEnabled,
|
||||
query,
|
||||
showErrorType: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
data-test-subj="topErrorsForTransactionTable"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.transactionDetails.topErrors.title', {
|
||||
defaultMessage: 'Top 5 errors',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiBasicTable
|
||||
error={
|
||||
status === FETCH_STATUS.FAILURE
|
||||
? i18n.translate(
|
||||
'xpack.apm.transactionDetails.topErrors.errorMessage',
|
||||
{ defaultMessage: 'Failed to fetch errors' }
|
||||
)
|
||||
: ''
|
||||
}
|
||||
noItemsMessage={
|
||||
status === FETCH_STATUS.LOADING
|
||||
? i18n.translate(
|
||||
'xpack.apm.transactionDetails.topErrors.loading',
|
||||
{ defaultMessage: 'Loading...' }
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.apm.transactionDetails.topErrors.noResults',
|
||||
{
|
||||
defaultMessage:
|
||||
'No errors found for this transaction group',
|
||||
}
|
||||
)
|
||||
}
|
||||
columns={columns}
|
||||
items={items}
|
||||
loading={status === FETCH_STATUS.LOADING}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -54,6 +54,7 @@ interface Props {
|
|||
annotations: Annotation[];
|
||||
timeseries?: Array<TimeSeries<Coordinate>>;
|
||||
yAxisType: 'duration' | 'percentage';
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const asPercentBound = (y: number | null) => asPercent(y, 1);
|
||||
|
@ -65,6 +66,7 @@ export function BreakdownChart({
|
|||
annotations,
|
||||
timeseries,
|
||||
yAxisType,
|
||||
id,
|
||||
}: Props) {
|
||||
const history = useHistory();
|
||||
const chartTheme = useChartTheme();
|
||||
|
@ -94,7 +96,12 @@ export function BreakdownChart({
|
|||
const timeZone = getTimeZone(core.uiSettings);
|
||||
|
||||
return (
|
||||
<ChartContainer height={height} hasData={!isEmpty} status={fetchStatus}>
|
||||
<ChartContainer
|
||||
height={height}
|
||||
hasData={!isEmpty}
|
||||
status={fetchStatus}
|
||||
id={id}
|
||||
>
|
||||
<Chart ref={chartRef}>
|
||||
<Settings
|
||||
tooltip={{ stickTo: 'top', showNullValues: true }}
|
||||
|
|
|
@ -68,6 +68,7 @@ export function TransactionBreakdownChart({
|
|||
showAnnotations={showAnnotations}
|
||||
timeseries={timeseries}
|
||||
yAxisType="percentage"
|
||||
id="transactionBreakdownChart"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -5,7 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGrid,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { AnnotationsContextProvider } from '../../../../context/annotations/annotations_context';
|
||||
import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context';
|
||||
|
@ -14,6 +20,8 @@ import { LatencyChart } from '../latency_chart';
|
|||
import { TransactionBreakdownChart } from '../transaction_breakdown_chart';
|
||||
import { TransactionColdstartRateChart } from '../transaction_coldstart_rate_chart';
|
||||
import { FailedTransactionRateChart } from '../failed_transaction_rate_chart';
|
||||
import { TopErrors } from '../../../app/transaction_details/top_errors';
|
||||
import { useBreakpoints } from '../../../../hooks/use_breakpoints';
|
||||
|
||||
export function TransactionCharts({
|
||||
kuery,
|
||||
|
@ -34,6 +42,49 @@ export function TransactionCharts({
|
|||
comparisonEnabled?: boolean;
|
||||
offset?: string;
|
||||
}) {
|
||||
// The default EuiFlexGroup breaks at 768, but we want to break at 1200
|
||||
const { isLarge } = useBreakpoints();
|
||||
const rowDirection = isLarge ? 'column' : 'row';
|
||||
|
||||
const latencyChart = (
|
||||
<EuiFlexItem data-cy={`transaction-duration-charts`}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<LatencyChart kuery={kuery} />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
const serviceOverviewThroughputChart = (
|
||||
<EuiFlexItem style={{ flexShrink: 1 }}>
|
||||
<ServiceOverviewThroughputChart
|
||||
kuery={kuery}
|
||||
transactionName={transactionName}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
const coldStartRateOrBreakdownChart = isServerlessContext ? (
|
||||
<EuiFlexItem>
|
||||
<TransactionColdstartRateChart
|
||||
kuery={kuery}
|
||||
transactionName={transactionName}
|
||||
environment={environment}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
offset={offset}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem>
|
||||
<TransactionBreakdownChart kuery={kuery} environment={environment} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
const failedTransactionRateChart = (
|
||||
<EuiFlexItem grow={1}>
|
||||
<FailedTransactionRateChart kuery={kuery} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnnotationsContextProvider
|
||||
|
@ -42,46 +93,40 @@ export function TransactionCharts({
|
|||
end={end}
|
||||
>
|
||||
<ChartPointerEventContextProvider>
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
<EuiFlexItem data-cy={`transaction-duration-charts`}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<LatencyChart kuery={kuery} />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem style={{ flexShrink: 1 }}>
|
||||
<ServiceOverviewThroughputChart
|
||||
kuery={kuery}
|
||||
transactionName={transactionName}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<FailedTransactionRateChart kuery={kuery} />
|
||||
</EuiFlexItem>
|
||||
{isServerlessContext ? (
|
||||
<EuiFlexItem>
|
||||
<TransactionColdstartRateChart
|
||||
kuery={kuery}
|
||||
transactionName={transactionName}
|
||||
environment={environment}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
offset={offset}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem>
|
||||
<TransactionBreakdownChart
|
||||
kuery={kuery}
|
||||
environment={environment}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGrid>
|
||||
{transactionName ? (
|
||||
<>
|
||||
<EuiFlexGrid columns={3} gutterSize="s">
|
||||
{latencyChart}
|
||||
{serviceOverviewThroughputChart}
|
||||
{coldStartRateOrBreakdownChart}
|
||||
</EuiFlexGrid>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup
|
||||
direction={rowDirection}
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
{failedTransactionRateChart}
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<TopErrors />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
{latencyChart}
|
||||
{serviceOverviewThroughputChart}
|
||||
</EuiFlexGrid>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
{failedTransactionRateChart}
|
||||
{coldStartRateOrBreakdownChart}
|
||||
</EuiFlexGrid>
|
||||
</>
|
||||
)}
|
||||
</ChartPointerEventContextProvider>
|
||||
</AnnotationsContextProvider>
|
||||
</>
|
||||
|
|
|
@ -5,32 +5,38 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBasicTableColumn, RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
import {
|
||||
EuiBasicTableColumn,
|
||||
RIGHT_ALIGNMENT,
|
||||
CENTER_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TypeOf } from '@kbn/typed-react-router-config';
|
||||
import React from 'react';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
|
||||
import { asInteger } from '../../../../../common/utils/formatters';
|
||||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { truncate } from '../../../../utils/style';
|
||||
import { SparkPlot } from '../../../shared/charts/spark_plot';
|
||||
import { ErrorDetailLink } from '../../../shared/links/apm/error_detail_link';
|
||||
import { ErrorOverviewLink } from '../../../shared/links/apm/error_overview_link';
|
||||
import { TimestampTooltip } from '../../../shared/timestamp_tooltip';
|
||||
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
||||
import { isTimeComparison } from '../time_comparison/get_comparison_options';
|
||||
import { asInteger } from '../../../../common/utils/formatters';
|
||||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { truncate } from '../../../utils/style';
|
||||
import { SparkPlot } from '../charts/spark_plot';
|
||||
import { ErrorDetailLink } from '../links/apm/error_detail_link';
|
||||
import { ErrorOverviewLink } from '../links/apm/error_overview_link';
|
||||
import { TimestampTooltip } from '../timestamp_tooltip';
|
||||
import { TruncateWithTooltip } from '../truncate_with_tooltip';
|
||||
import {
|
||||
ChartType,
|
||||
getTimeSeriesColor,
|
||||
} from '../../../shared/charts/helper/get_timeseries_color';
|
||||
import { ApmRoutes } from '../../../routing/apm_route_config';
|
||||
} from '../charts/helper/get_timeseries_color';
|
||||
import { ApmRoutes } from '../../routing/apm_route_config';
|
||||
|
||||
const ErrorLink = euiStyled(ErrorOverviewLink)`
|
||||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
type ErrorGroupMainStatistics =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
|
||||
type ErrorGroupMainStatistics = APIReturnType<
|
||||
| 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'
|
||||
| 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'
|
||||
>;
|
||||
type ErrorGroupDetailedStatistics =
|
||||
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
|
||||
|
@ -40,41 +46,50 @@ export function getColumns({
|
|||
errorGroupDetailedStatistics,
|
||||
comparisonEnabled,
|
||||
query,
|
||||
showErrorType = true,
|
||||
}: {
|
||||
serviceName: string;
|
||||
errorGroupDetailedStatisticsLoading: boolean;
|
||||
errorGroupDetailedStatistics: ErrorGroupDetailedStatistics;
|
||||
comparisonEnabled?: boolean;
|
||||
query: TypeOf<ApmRoutes, '/services/{serviceName}/errors'>['query'];
|
||||
showErrorType?: boolean;
|
||||
}): Array<EuiBasicTableColumn<ErrorGroupMainStatistics['errorGroups'][0]>> {
|
||||
const { offset } = query;
|
||||
return [
|
||||
{
|
||||
name: i18n.translate('xpack.apm.errorsTable.typeColumnLabel', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
field: 'type',
|
||||
sortable: false,
|
||||
render: (_, { type }) => {
|
||||
return (
|
||||
<ErrorLink
|
||||
title={type}
|
||||
serviceName={serviceName}
|
||||
query={
|
||||
{
|
||||
...query,
|
||||
kuery: `error.exception.type:"${type}"`,
|
||||
} as TypeOf<ApmRoutes, '/services/{serviceName}/errors'>['query']
|
||||
}
|
||||
>
|
||||
{type}
|
||||
</ErrorLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
...(showErrorType
|
||||
? [
|
||||
{
|
||||
name: i18n.translate('xpack.apm.errorsTable.typeColumnLabel', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
field: 'type',
|
||||
sortable: false,
|
||||
render: (_, { type }) => {
|
||||
return (
|
||||
<ErrorLink
|
||||
title={type}
|
||||
serviceName={serviceName}
|
||||
query={
|
||||
{
|
||||
...query,
|
||||
kuery: `error.exception.type:"${type}"`,
|
||||
} as TypeOf<
|
||||
ApmRoutes,
|
||||
'/services/{serviceName}/errors'
|
||||
>['query']
|
||||
}
|
||||
>
|
||||
{type}
|
||||
</ErrorLink>
|
||||
);
|
||||
},
|
||||
} as EuiBasicTableColumn<ErrorGroupMainStatistics['errorGroups'][0]>,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', {
|
||||
name: i18n.translate('xpack.apm.errorsTable.columnName', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
render: (_, { name, groupId: errorGroupId }) => {
|
||||
|
@ -95,13 +110,10 @@ export function getColumns({
|
|||
},
|
||||
{
|
||||
field: 'lastSeen',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceOverview.errorsTableColumnLastSeen',
|
||||
{
|
||||
defaultMessage: 'Last seen',
|
||||
}
|
||||
),
|
||||
align: RIGHT_ALIGNMENT,
|
||||
name: i18n.translate('xpack.apm.errorsTable.columnLastSeen', {
|
||||
defaultMessage: 'Last seen',
|
||||
}),
|
||||
align: showErrorType ? RIGHT_ALIGNMENT : CENTER_ALIGNMENT,
|
||||
render: (_, { lastSeen }) => {
|
||||
return (
|
||||
<span style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}>
|
||||
|
@ -112,12 +124,9 @@ export function getColumns({
|
|||
},
|
||||
{
|
||||
field: 'occurrences',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceOverview.errorsTableColumnOccurrences',
|
||||
{
|
||||
defaultMessage: 'Occurrences',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.apm.errorsTable.columnOccurrences', {
|
||||
defaultMessage: 'Occurrences',
|
||||
}),
|
||||
align: RIGHT_ALIGNMENT,
|
||||
render: (_, { occurrences, groupId: errorGroupId }) => {
|
||||
const currentPeriodTimeseries =
|
||||
|
@ -135,15 +144,12 @@ export function getColumns({
|
|||
color={currentPeriodColor}
|
||||
isLoading={errorGroupDetailedStatisticsLoading}
|
||||
series={currentPeriodTimeseries}
|
||||
valueLabel={i18n.translate(
|
||||
'xpack.apm.serviceOveriew.errorsTableOccurrences',
|
||||
{
|
||||
defaultMessage: `{occurrences} occ.`,
|
||||
values: {
|
||||
occurrences: asInteger(occurrences),
|
||||
},
|
||||
}
|
||||
)}
|
||||
valueLabel={i18n.translate('xpack.apm.errorsTable.occurrences', {
|
||||
defaultMessage: `{occurrences} occ.`,
|
||||
values: {
|
||||
occurrences: asInteger(occurrences),
|
||||
},
|
||||
})}
|
||||
comparisonSeries={
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? previousPeriodTimeseries
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 {
|
||||
rangeQuery,
|
||||
kqlQuery,
|
||||
termQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { keyBy } from 'lodash';
|
||||
import {
|
||||
ERROR_GROUP_ID,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { environmentQuery } from '../../../../common/utils/environment_query';
|
||||
import { Setup } from '../../../lib/helpers/setup_request';
|
||||
import { getBucketSize } from '../../../lib/helpers/get_bucket_size';
|
||||
import { getOffsetInMs } from '../../../../common/utils/get_offset_in_ms';
|
||||
|
||||
async function getTopErroneousTransactions({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
groupId,
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
numBuckets,
|
||||
offset,
|
||||
}: {
|
||||
environment: string;
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
groupId: string;
|
||||
setup: Setup;
|
||||
start: number;
|
||||
end: number;
|
||||
numBuckets: number;
|
||||
offset?: string;
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const { startWithOffset, endWithOffset, offsetInMs } = getOffsetInMs({
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
});
|
||||
|
||||
const { intervalString } = getBucketSize({
|
||||
start: startWithOffset,
|
||||
end: endWithOffset,
|
||||
numBuckets,
|
||||
});
|
||||
|
||||
const res = await apmEventClient.search('get_top_erroneous_transactions', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.error],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(ERROR_GROUP_ID, groupId),
|
||||
...rangeQuery(startWithOffset, endWithOffset),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
top_five_transactions: {
|
||||
terms: {
|
||||
field: TRANSACTION_NAME,
|
||||
size: 5,
|
||||
},
|
||||
aggs: {
|
||||
sample: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
_source: [TRANSACTION_TYPE],
|
||||
},
|
||||
},
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
extended_bounds: {
|
||||
min: startWithOffset,
|
||||
max: endWithOffset,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
res.aggregations?.top_five_transactions.buckets.map(
|
||||
({ key, doc_count: docCount, sample, timeseries }) => ({
|
||||
transactionName: key as string,
|
||||
transactionType: sample.hits.hits[0]._source.transaction?.type,
|
||||
occurrences: docCount,
|
||||
timeseries: timeseries.buckets.map((timeseriesBucket) => {
|
||||
return {
|
||||
x: timeseriesBucket.key + offsetInMs,
|
||||
y: timeseriesBucket.doc_count,
|
||||
};
|
||||
}),
|
||||
})
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
export async function getTopErroneousTransactionsPeriods({
|
||||
kuery,
|
||||
serviceName,
|
||||
setup,
|
||||
numBuckets,
|
||||
groupId,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
}: {
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
setup: Setup;
|
||||
numBuckets: number;
|
||||
groupId: string;
|
||||
environment: string;
|
||||
start: number;
|
||||
end: number;
|
||||
offset?: string;
|
||||
}) {
|
||||
const [currentPeriod, previousPeriod] = await Promise.all([
|
||||
getTopErroneousTransactions({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
setup,
|
||||
numBuckets,
|
||||
groupId,
|
||||
start,
|
||||
end,
|
||||
}),
|
||||
offset
|
||||
? getTopErroneousTransactions({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
setup,
|
||||
numBuckets,
|
||||
groupId,
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
})
|
||||
: [],
|
||||
]);
|
||||
|
||||
const previousPeriodByTransactionName = keyBy(
|
||||
previousPeriod,
|
||||
'transactionName'
|
||||
);
|
||||
|
||||
return {
|
||||
topErroneousTransactions: currentPeriod.map(
|
||||
({ transactionName, timeseries: currentPeriodTimeseries, ...rest }) => {
|
||||
return {
|
||||
...rest,
|
||||
transactionName,
|
||||
currentPeriodTimeseries,
|
||||
previousPeriodTimeseries:
|
||||
previousPeriodByTransactionName[transactionName]?.timeseries ?? [],
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
|
@ -5,7 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { keyBy } from 'lodash';
|
||||
import { rangeQuery, kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import {
|
||||
rangeQuery,
|
||||
kqlQuery,
|
||||
termQuery,
|
||||
termsQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { offsetPreviousPeriodCoordinates } from '../../../../common/utils/offset_previous_period_coordinate';
|
||||
import { Coordinate } from '../../../../typings/timeseries';
|
||||
import {
|
||||
|
@ -64,8 +69,8 @@ export async function getErrorGroupDetailedStatistics({
|
|||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ terms: { [ERROR_GROUP_ID]: groupIds } },
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...termsQuery(ERROR_GROUP_ID, ...groupIds),
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...rangeQuery(startWithOffset, endWithOffset),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
*/
|
||||
|
||||
import { AggregationsTermsAggregationOrder } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import {
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
termQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import {
|
||||
ERROR_CULPRIT,
|
||||
ERROR_EXC_HANDLED,
|
||||
|
@ -15,6 +19,8 @@ import {
|
|||
ERROR_GROUP_ID,
|
||||
ERROR_LOG_MESSAGE,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { environmentQuery } from '../../../../common/utils/environment_query';
|
||||
|
@ -30,6 +36,9 @@ export async function getErrorGroupMainStatistics({
|
|||
sortDirection = 'desc',
|
||||
start,
|
||||
end,
|
||||
maxNumberOfErrorGroups = 500,
|
||||
transactionName,
|
||||
transactionType,
|
||||
}: {
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
|
@ -39,6 +48,9 @@ export async function getErrorGroupMainStatistics({
|
|||
sortDirection?: 'asc' | 'desc';
|
||||
start: number;
|
||||
end: number;
|
||||
maxNumberOfErrorGroups?: number;
|
||||
transactionName?: string;
|
||||
transactionType?: string;
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
|
@ -62,7 +74,9 @@ export async function getErrorGroupMainStatistics({
|
|||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(TRANSACTION_NAME, transactionName),
|
||||
...termQuery(TRANSACTION_TYPE, transactionType),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
|
@ -73,7 +87,7 @@ export async function getErrorGroupMainStatistics({
|
|||
error_groups: {
|
||||
terms: {
|
||||
field: ERROR_GROUP_ID,
|
||||
size: 500,
|
||||
size: maxNumberOfErrorGroups,
|
||||
order,
|
||||
},
|
||||
aggs: {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { getErrorGroupMainStatistics } from './get_error_groups/get_error_group_
|
|||
import { getErrorGroupPeriods } from './get_error_groups/get_error_group_detailed_statistics';
|
||||
import { getErrorGroupSample } from './get_error_groups/get_error_group_sample';
|
||||
import { offsetRt } from '../../../common/comparison_rt';
|
||||
import { getTopErroneousTransactionsPeriods } from './erroneous_transactions/get_top_erroneous_transactions';
|
||||
|
||||
const errorsMainStatisticsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
|
@ -68,6 +69,67 @@ const errorsMainStatisticsRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const errorsMainStatisticsByTransactionNameRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
t.type({
|
||||
transactionType: t.string,
|
||||
transactionName: t.string,
|
||||
maxNumberOfErrorGroups: toNumberRt,
|
||||
}),
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
errorGroups: Array<{
|
||||
groupId: string;
|
||||
name: string;
|
||||
lastSeen: number;
|
||||
occurrences: number;
|
||||
culprit: string | undefined;
|
||||
handled: boolean | undefined;
|
||||
type: string | undefined;
|
||||
}>;
|
||||
}> => {
|
||||
const { params } = resources;
|
||||
const setup = await setupRequest(resources);
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
transactionName,
|
||||
transactionType,
|
||||
maxNumberOfErrorGroups,
|
||||
} = params.query;
|
||||
|
||||
const errorGroups = await getErrorGroupMainStatistics({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
maxNumberOfErrorGroups,
|
||||
transactionName,
|
||||
transactionType,
|
||||
});
|
||||
|
||||
return { errorGroups };
|
||||
},
|
||||
});
|
||||
|
||||
const errorsDetailedStatisticsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
|
||||
|
@ -205,9 +267,63 @@ const errorDistributionRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const topErroneousTransactionsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
groupId: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
offsetRt,
|
||||
t.type({
|
||||
numBuckets: toNumberRt,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
topErroneousTransactions: Array<{
|
||||
transactionName: string;
|
||||
currentPeriodTimeseries: Array<{ x: number; y: number }>;
|
||||
previousPeriodTimeseries: Array<{ x: number; y: number }>;
|
||||
transactionType: string | undefined;
|
||||
occurrences: number;
|
||||
}>;
|
||||
}> => {
|
||||
const { params } = resources;
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
path: { serviceName, groupId },
|
||||
query: { environment, kuery, numBuckets, start, end, offset },
|
||||
} = params;
|
||||
|
||||
return await getTopErroneousTransactionsPeriods({
|
||||
environment,
|
||||
groupId,
|
||||
kuery,
|
||||
serviceName,
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
numBuckets,
|
||||
offset,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const errorsRouteRepository = {
|
||||
...errorsMainStatisticsRoute,
|
||||
...errorsMainStatisticsByTransactionNameRoute,
|
||||
...errorsDetailedStatisticsRoute,
|
||||
...errorGroupsRoute,
|
||||
...errorDistributionRoute,
|
||||
...topErroneousTransactionsRoute,
|
||||
};
|
||||
|
|
|
@ -8066,7 +8066,6 @@
|
|||
"xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningText": "Nous n'avons pas pu déterminer à quelles JVM ces indicateurs correspondent. Cela provient probablement du fait que vous exécutez une version du serveur APM antérieure à 7.5. La mise à niveau du serveur APM vers la version 7.5 ou supérieure devrait résoudre le problème. Pour plus d'informations sur la mise à niveau, consultez {link}. Vous pouvez également utiliser la barre de recherche de Kibana pour filtrer par nom d'hôte, par ID de conteneur ou en fonction d'autres champs.",
|
||||
"xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "Impossible d'identifier les JVM",
|
||||
"xpack.apm.serviceNodeNameMissing": "(vide)",
|
||||
"xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrences} occ.",
|
||||
"xpack.apm.serviceOverview.coldstartHelp": "Le taux de démarrage à froid indique le pourcentage de demandes qui déclenchent un démarrage à froid d'une fonction sans serveur.",
|
||||
"xpack.apm.serviceOverview.dependenciesTableColumn": "Dépendance",
|
||||
"xpack.apm.serviceOverview.dependenciesTableTabLink": "Afficher les dépendances",
|
||||
|
@ -8075,9 +8074,6 @@
|
|||
"xpack.apm.serviceOverview.errorsTable.errorMessage": "Impossible de récupérer",
|
||||
"xpack.apm.serviceOverview.errorsTable.loading": "Chargement...",
|
||||
"xpack.apm.serviceOverview.errorsTable.noResults": "Aucune erreur trouvée",
|
||||
"xpack.apm.serviceOverview.errorsTableColumnLastSeen": "Vu en dernier",
|
||||
"xpack.apm.serviceOverview.errorsTableColumnName": "Nom",
|
||||
"xpack.apm.serviceOverview.errorsTableColumnOccurrences": "Occurrences",
|
||||
"xpack.apm.serviceOverview.errorsTableLinkText": "Afficher les erreurs",
|
||||
"xpack.apm.serviceOverview.errorsTableTitle": "Erreurs",
|
||||
"xpack.apm.serviceOverview.instancesTable.actionMenus.container.subtitle": "Affichez les logs et les indicateurs de ce conteneur pour plus de détails.",
|
||||
|
|
|
@ -8058,7 +8058,6 @@
|
|||
"xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningText": "これらのメトリックが所属する JVM を特定できませんでした。7.5 よりも古い APM Server を実行していることが原因である可能性が高いです。この問題は APM Server 7.5 以降にアップグレードすることで解決されます。アップグレードに関する詳細は、{link} をご覧ください。代わりに Kibana クエリバーを使ってホスト名、コンテナー ID、またはその他フィールドでフィルタリングすることもできます。",
|
||||
"xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "JVM を特定できませんでした",
|
||||
"xpack.apm.serviceNodeNameMissing": "(空)",
|
||||
"xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrences}件",
|
||||
"xpack.apm.serviceOverview.coldstartHelp": "コールドスタート率は、サーバーレス機能のコールドスタートをトリガーするリクエストの割合を示します。",
|
||||
"xpack.apm.serviceOverview.dependenciesTableColumn": "依存関係",
|
||||
"xpack.apm.serviceOverview.dependenciesTableTabLink": "依存関係を表示",
|
||||
|
@ -8067,9 +8066,6 @@
|
|||
"xpack.apm.serviceOverview.errorsTable.errorMessage": "取得できませんでした",
|
||||
"xpack.apm.serviceOverview.errorsTable.loading": "読み込み中...",
|
||||
"xpack.apm.serviceOverview.errorsTable.noResults": "エラーが見つかりません",
|
||||
"xpack.apm.serviceOverview.errorsTableColumnLastSeen": "前回の認識",
|
||||
"xpack.apm.serviceOverview.errorsTableColumnName": "名前",
|
||||
"xpack.apm.serviceOverview.errorsTableColumnOccurrences": "オカレンス",
|
||||
"xpack.apm.serviceOverview.errorsTableLinkText": "エラーを表示",
|
||||
"xpack.apm.serviceOverview.errorsTableTitle": "エラー",
|
||||
"xpack.apm.serviceOverview.instancesTable.actionMenus.container.subtitle": "このコンテナーのログとインデックスを表示し、さらに詳細を確認できます。",
|
||||
|
|
|
@ -8072,7 +8072,6 @@
|
|||
"xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningText": "无法识别这些指标属于哪些 JVM。这可能因为运行的 APM Server 版本低于 7.5。如果升级到 APM Server 7.5 或更高版本,应可解决此问题。有关升级的详细信息,请参阅 {link}。或者,也可以使用 Kibana 查询栏按主机名、容器 ID 或其他字段筛选。",
|
||||
"xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "找不到 JVM",
|
||||
"xpack.apm.serviceNodeNameMissing": "(空)",
|
||||
"xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrences} 次",
|
||||
"xpack.apm.serviceOverview.coldstartHelp": "冷启动速率指示触发无服务器功能冷启动的请求百分比。",
|
||||
"xpack.apm.serviceOverview.dependenciesTableColumn": "依赖项",
|
||||
"xpack.apm.serviceOverview.dependenciesTableTabLink": "查看依赖项",
|
||||
|
@ -8081,9 +8080,6 @@
|
|||
"xpack.apm.serviceOverview.errorsTable.errorMessage": "无法提取",
|
||||
"xpack.apm.serviceOverview.errorsTable.loading": "正在加载……",
|
||||
"xpack.apm.serviceOverview.errorsTable.noResults": "未找到错误",
|
||||
"xpack.apm.serviceOverview.errorsTableColumnLastSeen": "最后看到时间",
|
||||
"xpack.apm.serviceOverview.errorsTableColumnName": "名称",
|
||||
"xpack.apm.serviceOverview.errorsTableColumnOccurrences": "发生次数",
|
||||
"xpack.apm.serviceOverview.errorsTableLinkText": "查看错误",
|
||||
"xpack.apm.serviceOverview.errorsTableTitle": "错误",
|
||||
"xpack.apm.serviceOverview.instancesTable.actionMenus.container.subtitle": "查看此容器的日志和指标以获取进一步详情。",
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { apm, timerange } from '@elastic/apm-synthtrace';
|
||||
import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
|
||||
|
||||
export const config = {
|
||||
firstTransaction: {
|
||||
name: 'GET /apple 🍎 ',
|
||||
successRate: 75,
|
||||
failureRate: 25,
|
||||
},
|
||||
secondTransaction: {
|
||||
name: 'GET /banana 🍌',
|
||||
successRate: 50,
|
||||
failureRate: 50,
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateData({
|
||||
synthtraceEsClient,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
synthtraceEsClient: ApmSynthtraceEsClient;
|
||||
serviceName: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const serviceGoProdInstance = apm.service(serviceName, 'production', 'go').instance('instance-a');
|
||||
|
||||
const interval = '1m';
|
||||
|
||||
const { firstTransaction, secondTransaction } = config;
|
||||
|
||||
const documents = [firstTransaction, secondTransaction].map((transaction) => {
|
||||
return timerange(start, end)
|
||||
.interval(interval)
|
||||
.rate(transaction.successRate)
|
||||
.generator((timestamp) =>
|
||||
serviceGoProdInstance
|
||||
.transaction(transaction.name)
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
)
|
||||
.merge(
|
||||
timerange(start, end)
|
||||
.interval(interval)
|
||||
.rate(transaction.failureRate)
|
||||
.generator((timestamp) =>
|
||||
serviceGoProdInstance
|
||||
.transaction(transaction.name)
|
||||
.errors(
|
||||
serviceGoProdInstance
|
||||
.error('Error 1', transaction.name, 'Error test')
|
||||
.timestamp(timestamp)
|
||||
)
|
||||
.duration(1000)
|
||||
.timestamp(timestamp)
|
||||
.failure()
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
await synthtraceEsClient.index(documents);
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import {
|
||||
APIClientRequestParamsOf,
|
||||
APIReturnType,
|
||||
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
|
||||
import { sumBy, first, last } from 'lodash';
|
||||
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { config, generateData } from './generate_data';
|
||||
|
||||
type ErroneousTransactions =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const serviceName = 'synth-go';
|
||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
async function callApi(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>['params']
|
||||
>
|
||||
) {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions',
|
||||
params: {
|
||||
path: {
|
||||
serviceName,
|
||||
groupId: 'test',
|
||||
...overrides?.path,
|
||||
},
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
offset: undefined,
|
||||
numBuckets: 15,
|
||||
...overrides?.query,
|
||||
},
|
||||
},
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await callApi();
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.topErroneousTransactions).to.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(
|
||||
'when data is loaded',
|
||||
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
|
||||
() => {
|
||||
const {
|
||||
firstTransaction: { name: firstTransactionName, failureRate: firstTransactionFailureRate },
|
||||
secondTransaction: {
|
||||
name: secondTransactionName,
|
||||
failureRate: secondTransactionFailureRate,
|
||||
},
|
||||
} = config;
|
||||
|
||||
describe('returns the correct data', () => {
|
||||
before(async () => {
|
||||
await generateData({ serviceName, start, end, synthtraceEsClient });
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('without comparison', () => {
|
||||
const numberOfBuckets = 15;
|
||||
let erroneousTransactions: ErroneousTransactions;
|
||||
|
||||
before(async () => {
|
||||
const response = await callApi({
|
||||
path: { groupId: '0000000000000000000000Error test' },
|
||||
});
|
||||
erroneousTransactions = response.body;
|
||||
});
|
||||
|
||||
it('displays the correct number of occurrences', () => {
|
||||
const { topErroneousTransactions } = erroneousTransactions;
|
||||
expect(topErroneousTransactions.length).to.be(2);
|
||||
|
||||
const firstTransaction = topErroneousTransactions.find(
|
||||
(x) => x.transactionName === firstTransactionName
|
||||
);
|
||||
expect(firstTransaction).to.not.be(undefined);
|
||||
expect(firstTransaction?.occurrences).to.be(
|
||||
firstTransactionFailureRate * numberOfBuckets
|
||||
);
|
||||
|
||||
const secondTransaction = topErroneousTransactions.find(
|
||||
(x) => x.transactionName === secondTransactionName
|
||||
);
|
||||
expect(secondTransaction).to.not.be(undefined);
|
||||
expect(secondTransaction?.occurrences).to.be(
|
||||
secondTransactionFailureRate * numberOfBuckets
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the correct number of occurrences in time series', () => {
|
||||
const { topErroneousTransactions } = erroneousTransactions;
|
||||
|
||||
const firstTransaction = topErroneousTransactions.find(
|
||||
(x) => x.transactionName === firstTransactionName
|
||||
);
|
||||
const firstErrorCount = sumBy(firstTransaction?.currentPeriodTimeseries, 'y');
|
||||
expect(firstErrorCount).to.be(firstTransactionFailureRate * numberOfBuckets);
|
||||
|
||||
const secondTransaction = topErroneousTransactions.find(
|
||||
(x) => x.transactionName === secondTransactionName
|
||||
);
|
||||
const secondErrorCount = sumBy(secondTransaction?.currentPeriodTimeseries, 'y');
|
||||
expect(secondErrorCount).to.be(secondTransactionFailureRate * numberOfBuckets);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with comparison', () => {
|
||||
describe('when there are data for the time periods', () => {
|
||||
let erroneousTransactions: ErroneousTransactions;
|
||||
|
||||
before(async () => {
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
const response = await callApi({
|
||||
path: { groupId: '0000000000000000000000Error test' },
|
||||
query: {
|
||||
start: new Date(end - fiveMinutes).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
offset: '5m',
|
||||
},
|
||||
});
|
||||
erroneousTransactions = response.body;
|
||||
});
|
||||
|
||||
it('returns some data', () => {
|
||||
const { topErroneousTransactions } = erroneousTransactions;
|
||||
|
||||
const hasCurrentPeriodData = topErroneousTransactions[0].currentPeriodTimeseries.some(
|
||||
({ y }) => isFiniteNumber(y)
|
||||
);
|
||||
|
||||
const hasPreviousPeriodData =
|
||||
topErroneousTransactions[0].previousPeriodTimeseries.some(({ y }) =>
|
||||
isFiniteNumber(y)
|
||||
);
|
||||
|
||||
expect(hasCurrentPeriodData).to.be(true);
|
||||
expect(hasPreviousPeriodData).to.be(true);
|
||||
});
|
||||
|
||||
it('has the same start time for both periods', () => {
|
||||
const { topErroneousTransactions } = erroneousTransactions;
|
||||
expect(first(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be(
|
||||
first(topErroneousTransactions[0].previousPeriodTimeseries)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('has same end time for both periods', () => {
|
||||
const { topErroneousTransactions } = erroneousTransactions;
|
||||
expect(last(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be(
|
||||
last(topErroneousTransactions[0].previousPeriodTimeseries)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('returns same number of buckets for both periods', () => {
|
||||
const { topErroneousTransactions } = erroneousTransactions;
|
||||
expect(topErroneousTransactions[0].currentPeriodTimeseries.length).to.be(
|
||||
topErroneousTransactions[0].previousPeriodTimeseries.length
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no data for the time period', () => {
|
||||
it('returns an empty array', async () => {
|
||||
const response = await callApi({
|
||||
path: { groupId: '0000000000000000000000Error test' },
|
||||
query: {
|
||||
start: '2021-01-03T00:00:00.000Z',
|
||||
end: '2021-01-03T00:15:00.000Z',
|
||||
offset: '1d',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
body: { topErroneousTransactions },
|
||||
} = response;
|
||||
|
||||
expect(topErroneousTransactions).to.be.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { apm, timerange } from '@elastic/apm-synthtrace';
|
||||
import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
|
||||
|
||||
export const config = {
|
||||
firstTransaction: {
|
||||
name: 'GET /apple 🍎',
|
||||
successRate: 75,
|
||||
failureRate: 25,
|
||||
},
|
||||
secondTransaction: {
|
||||
name: 'GET /banana 🍌',
|
||||
successRate: 50,
|
||||
failureRate: 50,
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateData({
|
||||
synthtraceEsClient,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
synthtraceEsClient: ApmSynthtraceEsClient;
|
||||
serviceName: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const serviceGoProdInstance = apm.service(serviceName, 'production', 'go').instance('instance-a');
|
||||
|
||||
const interval = '1m';
|
||||
|
||||
const { firstTransaction, secondTransaction } = config;
|
||||
|
||||
const documents = [firstTransaction, secondTransaction].map((transaction, index) => {
|
||||
return timerange(start, end)
|
||||
.interval(interval)
|
||||
.rate(transaction.successRate)
|
||||
.generator((timestamp) =>
|
||||
serviceGoProdInstance
|
||||
.transaction(transaction.name)
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
)
|
||||
.merge(
|
||||
timerange(start, end)
|
||||
.interval(interval)
|
||||
.rate(transaction.failureRate)
|
||||
.generator((timestamp) =>
|
||||
serviceGoProdInstance
|
||||
.transaction(transaction.name)
|
||||
.errors(
|
||||
serviceGoProdInstance
|
||||
.error(`Error 1 transaction ${transaction.name}`)
|
||||
.timestamp(timestamp),
|
||||
serviceGoProdInstance
|
||||
.error(`Error 2 transaction ${transaction.name}`)
|
||||
.timestamp(timestamp)
|
||||
)
|
||||
.duration(1000)
|
||||
.timestamp(timestamp)
|
||||
.failure()
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
await synthtraceEsClient.index(documents);
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import {
|
||||
APIClientRequestParamsOf,
|
||||
APIReturnType,
|
||||
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
|
||||
import moment from 'moment';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { config, generateData } from './generate_data';
|
||||
|
||||
type ErrorGroups =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'>['errorGroups'];
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const serviceName = 'synth-go';
|
||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
async function callApi(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'>['params']
|
||||
>
|
||||
) {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name',
|
||||
params: {
|
||||
path: { serviceName, ...overrides?.path },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
maxNumberOfErrorGroups: 5,
|
||||
transactionType: 'request',
|
||||
transactionName: '',
|
||||
...overrides?.query,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await callApi();
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.errorGroups).to.empty();
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(
|
||||
'when data is loaded',
|
||||
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
|
||||
() => {
|
||||
describe('top errors for transaction', () => {
|
||||
const {
|
||||
firstTransaction: {
|
||||
name: firstTransactionName,
|
||||
failureRate: firstTransactionFailureRate,
|
||||
},
|
||||
} = config;
|
||||
|
||||
before(async () => {
|
||||
await generateData({ serviceName, start, end, synthtraceEsClient });
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('returns the correct data', () => {
|
||||
let errorGroups: ErrorGroups;
|
||||
before(async () => {
|
||||
const response = await callApi({ query: { transactionName: firstTransactionName } });
|
||||
errorGroups = response.body.errorGroups;
|
||||
});
|
||||
|
||||
it('returns correct number of errors and error data', () => {
|
||||
const numberOfBuckets = 15;
|
||||
|
||||
expect(errorGroups.length).to.equal(2);
|
||||
|
||||
const firstErrorId = `Error 1 transaction ${firstTransactionName}`;
|
||||
const firstError = errorGroups.find((x) => x.groupId === firstErrorId);
|
||||
expect(firstError).to.not.be(undefined);
|
||||
expect(firstError?.groupId).to.be(firstErrorId);
|
||||
expect(firstError?.name).to.be(firstErrorId);
|
||||
expect(firstError?.occurrences).to.be(firstTransactionFailureRate * numberOfBuckets);
|
||||
expect(firstError?.lastSeen).to.be(moment(end).startOf('minute').valueOf());
|
||||
|
||||
const secondErrorId = `Error 2 transaction ${firstTransactionName}`;
|
||||
const secondError = errorGroups.find((x) => x.groupId === secondErrorId);
|
||||
expect(secondError).to.not.be(undefined);
|
||||
expect(secondError?.groupId).to.be(secondErrorId);
|
||||
expect(secondError?.name).to.be(secondErrorId);
|
||||
expect(secondError?.occurrences).to.be(firstTransactionFailureRate * numberOfBuckets);
|
||||
expect(secondError?.lastSeen).to.be(moment(end).startOf('minute').valueOf());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue