[APM] Errors: Enhancements to the Errors list page (part II) (#118878)

* refactor errors groups endpoints

* Add sparkline and reorder columns

* Fix i18n

* Delete is_aggregation_accurate not used

* fix rebase conflicts

* rename endpoint path

* fix api test

* fix i18n conflict

* fix e2e tests

* PR review

* rename variables for consistency

* fix tests after rename endpoints

* rename functions

* fix conflict

* fix i18n conflict

* fix i18n conflict
This commit is contained in:
Miriam 2021-11-25 08:21:04 +00:00 committed by GitHub
parent 14ed0cb899
commit ba9dfeafa0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 390 additions and 595 deletions

View file

@ -91,19 +91,13 @@ describe('Errors page', () => {
it('sorts by ocurrences', () => {
cy.visit(javaServiceErrorsPageHref);
cy.contains('span', 'Occurrences').click();
cy.url().should(
'include',
'&sortField=occurrenceCount&sortDirection=asc'
);
cy.url().should('include', '&sortField=occurrences&sortDirection=asc');
});
it('sorts by latest occurrences', () => {
cy.visit(javaServiceErrorsPageHref);
cy.contains('span', 'Latest occurrence').click();
cy.url().should(
'include',
'&sortField=latestOccurrenceAt&sortDirection=asc'
);
cy.contains('span', 'Last seen').click();
cy.url().should('include', '&sortField=lastSeen&sortDirection=asc');
});
});
});

View file

@ -48,7 +48,7 @@ const apisToIntercept = [
},
{
endpoint:
'/internal/apm/services/opbeans-node/error_groups/main_statistics?*',
'/internal/apm/services/opbeans-node/errors/groups/main_statistics?*',
name: 'errorGroupsMainStatisticsRequest',
},
{

View file

@ -40,7 +40,7 @@ const apisToIntercept = [
},
{
endpoint:
'/internal/apm/services/opbeans-java/error_groups/detailed_statistics?*',
'/internal/apm/services/opbeans-java/errors/groups/detailed_statistics?*',
name: 'errorGroupsDetailedRequest',
},
{

View file

@ -38,52 +38,49 @@ export const Example: Story<Args> = (args) => {
return <ErrorGroupList {...args} />;
};
Example.args = {
items: [
mainStatistics: [
{
message: 'net/http: abort Handler',
occurrenceCount: 14,
name: 'net/http: abort Handler',
occurrences: 14,
culprit: 'Main.func2',
groupId: '83a653297ec29afed264d7b60d5cda7b',
latestOccurrenceAt: '2021-10-21T16:18:41.434Z',
lastSeen: 1634833121434,
handled: false,
type: 'errorString',
},
{
message: 'POST /api/orders (500)',
occurrenceCount: 5,
name: 'POST /api/orders (500)',
occurrences: 5,
culprit: 'logrusMiddleware',
groupId: '7a640436a9be648fd708703d1ac84650',
latestOccurrenceAt: '2021-10-21T16:18:40.162Z',
lastSeen: 1634833121434,
handled: false,
type: 'OpError',
},
{
message:
'write tcp 10.36.2.24:3000->10.36.1.14:34232: write: connection reset by peer',
occurrenceCount: 4,
name: 'write tcp 10.36.2.24:3000->10.36.1.14:34232: write: connection reset by peer',
occurrences: 4,
culprit: 'apiHandlers.getProductCustomers',
groupId: '95ca0e312c109aa11e298bcf07f1445b',
latestOccurrenceAt: '2021-10-21T16:18:42.650Z',
lastSeen: 1634833121434,
handled: false,
type: 'OpError',
},
{
message:
'write tcp 10.36.0.21:3000->10.36.1.252:57070: write: connection reset by peer',
occurrenceCount: 3,
name: 'write tcp 10.36.0.21:3000->10.36.1.252:57070: write: connection reset by peer',
occurrences: 3,
culprit: 'apiHandlers.getCustomers',
groupId: '4053d7e33d2b716c819bd96d9d6121a2',
latestOccurrenceAt: '2021-10-21T16:07:44.078Z',
lastSeen: 1634833121434,
handled: false,
type: 'OpError',
},
{
message:
'write tcp 10.36.0.21:3000->10.36.0.88:33926: write: broken pipe',
occurrenceCount: 2,
name: 'write tcp 10.36.0.21:3000->10.36.0.88:33926: write: broken pipe',
occurrences: 2,
culprit: 'apiHandlers.getOrders',
groupId: '94f4ca8ec8c02e5318cf03f46ae4c1f3',
latestOccurrenceAt: '2021-10-21T16:13:45.742Z',
lastSeen: 1634833121434,
handled: false,
type: 'OpError',
},
@ -95,6 +92,6 @@ export const EmptyState: Story<Args> = (args) => {
return <ErrorGroupList {...args} />;
};
EmptyState.args = {
items: [],
mainStatistics: [],
serviceName: 'test service',
};

View file

@ -11,9 +11,9 @@ import {
EuiToolTip,
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { asInteger } from '../../../../../common/utils/formatters';
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params';
@ -24,6 +24,7 @@ import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink';
import { APMQueryParams } from '../../../shared/Links/url_helpers';
import { ITableColumn, ManagedTable } from '../../../shared/managed_table';
import { TimestampTooltip } from '../../../shared/TimestampTooltip';
import { SparkPlot } from '../../../shared/charts/spark_plot';
const GroupIdLink = euiStyled(ErrorDetailLink)`
font-family: ${({ theme }) => theme.eui.euiCodeFontFamily};
@ -48,14 +49,23 @@ const Culprit = euiStyled.div`
`;
type ErrorGroupItem =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors'>['errorGroups'][0];
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['errorGroups'][0];
type ErrorGroupDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
interface Props {
items: ErrorGroupItem[];
mainStatistics: ErrorGroupItem[];
serviceName: string;
detailedStatistics: ErrorGroupDetailedStatistics;
comparisonEnabled?: boolean;
}
function ErrorGroupList({ items, serviceName }: Props) {
function ErrorGroupList({
mainStatistics,
serviceName,
detailedStatistics,
comparisonEnabled,
}: Props) {
const { urlParams } = useLegacyUrlParams();
const columns = useMemo(() => {
@ -132,13 +142,13 @@ function ErrorGroupList({ items, serviceName }: Props) {
<MessageAndCulpritCell>
<EuiToolTip
id="error-message-tooltip"
content={item.message || NOT_AVAILABLE_LABEL}
content={item.name || NOT_AVAILABLE_LABEL}
>
<MessageLink
serviceName={serviceName}
errorGroupId={item.groupId}
>
{item.message || NOT_AVAILABLE_LABEL}
{item.name || NOT_AVAILABLE_LABEL}
</MessageLink>
</EuiToolTip>
<br />
@ -167,46 +177,64 @@ function ErrorGroupList({ items, serviceName }: Props) {
),
},
{
name: i18n.translate('xpack.apm.errorsTable.occurrencesColumnLabel', {
defaultMessage: 'Occurrences',
field: 'lastSeen',
sortable: true,
name: i18n.translate('xpack.apm.errorsTable.lastSeenColumnLabel', {
defaultMessage: 'Last seen',
}),
field: 'occurrenceCount',
sortable: true,
dataType: 'number',
render: (_, { occurrenceCount }) =>
occurrenceCount
? numeral(occurrenceCount).format('0.[0]a')
: NOT_AVAILABLE_LABEL,
},
{
field: 'latestOccurrenceAt',
sortable: true,
name: i18n.translate(
'xpack.apm.errorsTable.latestOccurrenceColumnLabel',
{
defaultMessage: 'Latest occurrence',
}
),
align: RIGHT_ALIGNMENT,
render: (_, { latestOccurrenceAt }) =>
latestOccurrenceAt ? (
<TimestampTooltip time={latestOccurrenceAt} timeUnit="minutes" />
render: (_, { lastSeen }) =>
lastSeen ? (
<TimestampTooltip time={lastSeen} timeUnit="minutes" />
) : (
NOT_AVAILABLE_LABEL
),
},
{
field: 'occurrences',
name: i18n.translate('xpack.apm.errorsTable.occurrencesColumnLabel', {
defaultMessage: 'Occurrences',
}),
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { occurrences, groupId }) => {
const currentPeriodTimeseries =
detailedStatistics?.currentPeriod?.[groupId]?.timeseries;
const previousPeriodTimeseries =
detailedStatistics?.previousPeriod?.[groupId]?.timeseries;
return (
<SparkPlot
color="euiColorVis7"
series={currentPeriodTimeseries}
valueLabel={i18n.translate(
'xpack.apm.serviceOveriew.errorsTableOccurrences',
{
defaultMessage: `{occurrences} occ.`,
values: {
occurrences: asInteger(occurrences),
},
}
)}
comparisonSeries={
comparisonEnabled ? previousPeriodTimeseries : undefined
}
/>
);
},
},
] as Array<ITableColumn<ErrorGroupItem>>;
}, [serviceName, urlParams]);
}, [serviceName, urlParams, detailedStatistics, comparisonEnabled]);
return (
<ManagedTable
noItemsMessage={i18n.translate('xpack.apm.errorsTable.noErrorsLabel', {
defaultMessage: 'No errors found',
})}
items={items}
items={mainStatistics}
columns={columns}
initialPageSize={25}
initialSortField="occurrenceCount"
initialSortField="occurrences"
initialSortDirection="desc"
sortItems={false}
/>

View file

@ -14,24 +14,60 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import uuid from 'uuid';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { FailedTransactionRateChart } from '../../shared/charts/failed_transaction_rate_chart';
import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison';
import { ErrorDistribution } from '../error_group_details/Distribution';
import { ErrorGroupList } from './error_group_list';
type ErrorGroupMainStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
type ErrorGroupDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
const INITIAL_STATE_MAIN_STATISTICS: {
errorGroupMainStatistics: ErrorGroupMainStatistics['errorGroups'];
requestId?: string;
} = {
errorGroupMainStatistics: [],
requestId: undefined,
};
const INITIAL_STATE_DETAILED_STATISTICS: ErrorGroupDetailedStatistics = {
currentPeriod: {},
previousPeriod: {},
};
export function ErrorGroupOverview() {
const { serviceName } = useApmServiceContext();
const { serviceName, transactionType } = useApmServiceContext();
const {
query: { environment, kuery, sortField, sortDirection, rangeFrom, rangeTo },
query: {
environment,
kuery,
sortField,
sortDirection,
rangeFrom,
rangeTo,
comparisonType,
comparisonEnabled,
},
} = useApmParams('/services/{serviceName}/errors');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
start,
end,
comparisonType,
comparisonEnabled,
});
const { errorDistributionData, status } = useErrorGroupDistributionFetcher({
serviceName,
@ -40,30 +76,90 @@ export function ErrorGroupOverview() {
kuery,
});
const { data: errorGroupListData } = useFetcher(
(callApmApi) => {
const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc';
const { data: errorGroupListData = INITIAL_STATE_MAIN_STATISTICS } =
useFetcher(
(callApmApi) => {
const normalizedSortDirection =
sortDirection === 'asc' ? 'asc' : 'desc';
if (start && end) {
return callApmApi({
endpoint: 'GET /internal/apm/services/{serviceName}/errors',
params: {
path: {
serviceName,
if (start && end && transactionType) {
return callApmApi({
endpoint:
'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics',
params: {
path: {
serviceName,
},
query: {
environment,
transactionType,
kuery,
start,
end,
sortField,
sortDirection: normalizedSortDirection,
},
},
}).then((response) => {
return {
// Everytime the main statistics is refetched, updates the requestId making the comparison API to be refetched.
requestId: uuid(),
errorGroupMainStatistics: response.errorGroups,
};
});
}
},
[
environment,
kuery,
serviceName,
transactionType,
start,
end,
sortField,
sortDirection,
]
);
const { requestId, errorGroupMainStatistics } = errorGroupListData;
const {
data: errorGroupDetailedStatistics = INITIAL_STATE_DETAILED_STATISTICS,
} = useFetcher(
(callApmApi) => {
if (
requestId &&
errorGroupMainStatistics.length &&
start &&
end &&
transactionType
) {
return callApmApi({
endpoint:
'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
params: {
path: { serviceName },
query: {
environment,
kuery,
start,
end,
sortField,
sortDirection: normalizedSortDirection,
numBuckets: 20,
transactionType,
groupIds: JSON.stringify(
errorGroupMainStatistics.map(({ groupId }) => groupId).sort()
),
comparisonStart,
comparisonEnd,
},
},
});
}
},
[environment, kuery, serviceName, start, end, sortField, sortDirection]
// only fetches agg results when requestId changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[requestId],
{ preservePreviousData: false }
);
if (!errorDistributionData || !errorGroupListData) {
@ -110,8 +206,10 @@ export function ErrorGroupOverview() {
<EuiSpacer size="s" />
<ErrorGroupList
items={errorGroupListData.errorGroups}
mainStatistics={errorGroupMainStatistics}
serviceName={serviceName}
detailedStatistics={errorGroupDetailedStatistics}
comparisonEnabled={comparisonEnabled}
/>
</EuiPanel>
</EuiFlexItem>

View file

@ -16,9 +16,9 @@ import { TimestampTooltip } from '../../../shared/TimestampTooltip';
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
type ErrorGroupMainStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/main_statistics'>;
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
type ErrorGroupDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics'>;
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
export function getColumns({
serviceName,
@ -28,14 +28,14 @@ export function getColumns({
serviceName: string;
errorGroupDetailedStatistics: ErrorGroupDetailedStatistics;
comparisonEnabled?: boolean;
}): Array<EuiBasicTableColumn<ErrorGroupMainStatistics['error_groups'][0]>> {
}): Array<EuiBasicTableColumn<ErrorGroupMainStatistics['errorGroups'][0]>> {
return [
{
field: 'name',
name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', {
defaultMessage: 'Name',
}),
render: (_, { name, group_id: errorGroupId }) => {
render: (_, { name, groupId: errorGroupId }) => {
return (
<TruncateWithTooltip
text={name}
@ -77,7 +77,7 @@ export function getColumns({
}
),
align: RIGHT_ALIGNMENT,
render: (_, { occurrences, group_id: errorGroupId }) => {
render: (_, { occurrences, groupId: errorGroupId }) => {
const currentPeriodTimeseries =
errorGroupDetailedStatistics?.currentPeriod?.[errorGroupId]
?.timeseries;
@ -92,9 +92,9 @@ export function getColumns({
valueLabel={i18n.translate(
'xpack.apm.serviceOveriew.errorsTableOccurrences',
{
defaultMessage: `{occurrencesCount} occ.`,
defaultMessage: `{occurrences} occ.`,
values: {
occurrencesCount: asInteger(occurrences),
occurrences: asInteger(occurrences),
},
}
)}

View file

@ -30,9 +30,9 @@ interface Props {
serviceName: string;
}
type ErrorGroupMainStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/main_statistics'>;
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
type ErrorGroupDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics'>;
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
type SortDirection = 'asc' | 'desc';
type SortField = 'name' | 'lastSeen' | 'occurrences';
@ -44,7 +44,7 @@ const DEFAULT_SORT = {
};
const INITIAL_STATE_MAIN_STATISTICS: {
items: ErrorGroupMainStatistics['error_groups'];
items: ErrorGroupMainStatistics['errorGroups'];
totalItems: number;
requestId?: string;
} = {
@ -97,7 +97,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
}
return callApmApi({
endpoint:
'GET /internal/apm/services/{serviceName}/error_groups/main_statistics',
'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics',
params: {
path: { serviceName },
query: {
@ -110,7 +110,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
},
}).then((response) => {
const currentPageErrorGroups = orderBy(
response.error_groups,
response.errorGroups,
field,
direction
).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE);
@ -119,7 +119,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
// Everytime the main statistics is refetched, updates the requestId making the comparison API to be refetched.
requestId: uuid(),
items: currentPageErrorGroups,
totalItems: response.error_groups.length,
totalItems: response.errorGroups.length,
};
});
},
@ -150,7 +150,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
if (requestId && items.length && start && end && transactionType) {
return callApmApi({
endpoint:
'GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics',
'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
params: {
path: { serviceName },
query: {
@ -161,7 +161,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
numBuckets: 20,
transactionType,
groupIds: JSON.stringify(
items.map(({ group_id: groupId }) => groupId).sort()
items.map(({ groupId: groupId }) => groupId).sort()
),
comparisonStart,
comparisonEnd,

View file

@ -78,11 +78,9 @@ Object {
"@timestamp",
],
"size": 1,
"sort": Array [
Object {
"@timestamp": "desc",
},
],
"sort": Object {
"@timestamp": "desc",
},
},
},
},
@ -103,6 +101,11 @@ Object {
"service.name": "serviceName",
},
},
Object {
"term": Object {
"transaction.type": "request",
},
},
Object {
"range": Object {
"@timestamp": Object {
@ -120,7 +123,7 @@ Object {
}
`;
exports[`error queries fetches multiple error groups when sortField = latestOccurrenceAt 1`] = `
exports[`error queries fetches multiple error groups when sortField = lastSeen 1`] = `
Object {
"apm": Object {
"events": Array [
@ -148,11 +151,9 @@ Object {
"@timestamp",
],
"size": 1,
"sort": Array [
Object {
"@timestamp": "desc",
},
],
"sort": Object {
"@timestamp": "desc",
},
},
},
},
@ -173,6 +174,11 @@ Object {
"service.name": "serviceName",
},
},
Object {
"term": Object {
"transaction.type": "request",
},
},
Object {
"range": Object {
"@timestamp": Object {

View file

@ -1,121 +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 { AggregationsTermsAggregationOrder } from '@elastic/elasticsearch/lib/api/types';
import { ProcessorEvent } from '../../../common/processor_event';
import { environmentQuery } from '../../../common/utils/environment_query';
import { kqlQuery, rangeQuery } from '../../../../observability/server';
import {
ERROR_CULPRIT,
ERROR_EXC_HANDLED,
ERROR_EXC_MESSAGE,
ERROR_EXC_TYPE,
ERROR_GROUP_ID,
ERROR_LOG_MESSAGE,
SERVICE_NAME,
} from '../../../common/elasticsearch_fieldnames';
import { getErrorName } from '../../lib/helpers/get_error_name';
import { Setup } from '../../lib/helpers/setup_request';
export async function getErrorGroups({
environment,
kuery,
serviceName,
sortField,
sortDirection = 'desc',
setup,
start,
end,
}: {
environment: string;
kuery: string;
serviceName: string;
sortField?: string;
sortDirection?: 'asc' | 'desc';
setup: Setup;
start: number;
end: number;
}) {
const { apmEventClient } = setup;
// sort buckets by last occurrence of error
const sortByLatestOccurrence = sortField === 'latestOccurrenceAt';
const maxTimestampAggKey = 'max_timestamp';
const order: AggregationsTermsAggregationOrder = sortByLatestOccurrence
? { [maxTimestampAggKey]: sortDirection }
: { _count: sortDirection };
const params = {
apm: {
events: [ProcessorEvent.error as const],
},
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
],
},
},
aggs: {
error_groups: {
terms: {
field: ERROR_GROUP_ID,
size: 500,
order,
},
aggs: {
sample: {
top_hits: {
_source: [
ERROR_LOG_MESSAGE,
ERROR_EXC_MESSAGE,
ERROR_EXC_HANDLED,
ERROR_EXC_TYPE,
ERROR_CULPRIT,
ERROR_GROUP_ID,
'@timestamp',
],
sort: [{ '@timestamp': 'desc' as const }],
size: 1,
},
},
...(sortByLatestOccurrence
? { [maxTimestampAggKey]: { max: { field: '@timestamp' } } }
: {}),
},
},
},
},
};
const resp = await apmEventClient.search('get_error_groups', params);
// aggregations can be undefined when no matching indices are found.
// this is an exception rather than the rule so the ES type does not account for this.
const hits = (resp.aggregations?.error_groups.buckets || []).map((bucket) => {
const source = bucket.sample.hits.hits[0]._source;
const message = getErrorName(source);
return {
message,
occurrenceCount: bucket.doc_count,
culprit: source.error.culprit,
groupId: source.error.grouping_key,
latestOccurrenceAt: source['@timestamp'],
handled: source.error.exception?.[0].handled,
type: source.error.exception?.[0].type,
};
});
return hits;
}

View file

@ -18,7 +18,7 @@ import { environmentQuery } from '../../../../common/utils/environment_query';
import { getBucketSize } from '../../../lib/helpers/get_bucket_size';
import { Setup } from '../../../lib/helpers/setup_request';
export async function getServiceErrorGroupDetailedStatistics({
export async function getErrorGroupDetailedStatistics({
kuery,
serviceName,
setup,
@ -106,7 +106,7 @@ export async function getServiceErrorGroupDetailedStatistics({
});
}
export async function getServiceErrorGroupPeriods({
export async function getErrorGroupPeriods({
kuery,
serviceName,
setup,
@ -141,7 +141,7 @@ export async function getServiceErrorGroupPeriods({
groupIds,
};
const currentPeriodPromise = getServiceErrorGroupDetailedStatistics({
const currentPeriodPromise = getErrorGroupDetailedStatistics({
...commonProps,
start,
end,
@ -149,7 +149,7 @@ export async function getServiceErrorGroupPeriods({
const previousPeriodPromise =
comparisonStart && comparisonEnd
? getServiceErrorGroupDetailedStatistics({
? getErrorGroupDetailedStatistics({
...commonProps,
start: comparisonStart,
end: comparisonEnd,

View file

@ -5,9 +5,13 @@
* 2.0.
*/
import { AggregationsTermsAggregationOrder } from '@elastic/elasticsearch/lib/api/types';
import { kqlQuery, rangeQuery } from '../../../../../observability/server';
import {
ERROR_CULPRIT,
ERROR_EXC_HANDLED,
ERROR_EXC_MESSAGE,
ERROR_EXC_TYPE,
ERROR_GROUP_ID,
ERROR_LOG_MESSAGE,
SERVICE_NAME,
@ -18,27 +22,40 @@ import { environmentQuery } from '../../../../common/utils/environment_query';
import { getErrorName } from '../../../lib/helpers/get_error_name';
import { Setup } from '../../../lib/helpers/setup_request';
export async function getServiceErrorGroupMainStatistics({
export async function getErrorGroupMainStatistics({
kuery,
serviceName,
setup,
transactionType,
environment,
transactionType,
sortField,
sortDirection = 'desc',
start,
end,
}: {
kuery: string;
serviceName: string;
setup: Setup;
transactionType: string;
environment: string;
transactionType: string;
sortField?: string;
sortDirection?: 'asc' | 'desc';
start: number;
end: number;
}) {
const { apmEventClient } = setup;
// sort buckets by last occurrence of error
const sortByLatestOccurrence = sortField === 'lastSeen';
const maxTimestampAggKey = 'max_timestamp';
const order: AggregationsTermsAggregationOrder = sortByLatestOccurrence
? { [maxTimestampAggKey]: sortDirection }
: { _count: sortDirection };
const response = await apmEventClient.search(
'get_service_error_group_main_statistics',
'get_error_group_main_statistics',
{
apm: {
events: [ProcessorEvent.error],
@ -61,20 +78,30 @@ export async function getServiceErrorGroupMainStatistics({
terms: {
field: ERROR_GROUP_ID,
size: 500,
order: {
_count: 'desc',
},
order,
},
aggs: {
sample: {
// change to top_metrics
top_hits: {
size: 1,
_source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp'],
_source: [
ERROR_LOG_MESSAGE,
ERROR_EXC_MESSAGE,
ERROR_EXC_HANDLED,
ERROR_EXC_TYPE,
ERROR_CULPRIT,
ERROR_GROUP_ID,
'@timestamp',
],
sort: {
'@timestamp': 'desc',
},
},
},
...(sortByLatestOccurrence
? { [maxTimestampAggKey]: { max: { field: '@timestamp' } } }
: {}),
},
},
},
@ -82,19 +109,17 @@ export async function getServiceErrorGroupMainStatistics({
}
);
const errorGroups =
return (
response.aggregations?.error_groups.buckets.map((bucket) => ({
group_id: bucket.key as string,
groupId: bucket.key as string,
name: getErrorName(bucket.sample.hits.hits[0]._source),
lastSeen: new Date(
bucket.sample.hits.hits[0]?._source['@timestamp']
).getTime(),
occurrences: bucket.doc_count,
})) ?? [];
return {
is_aggregation_accurate:
(response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0,
error_groups: errorGroups,
};
culprit: bucket.sample.hits.hits[0]?._source.error.culprit,
handled: bucket.sample.hits.hits[0]?._source.error.exception?.[0].handled,
type: bucket.sample.hits.hits[0]?._source.error.exception?.[0].type,
})) ?? []
);
}

View file

@ -5,17 +5,17 @@
* 2.0.
*/
import { asMutableArray } from '../../../common/utils/as_mutable_array';
import { asMutableArray } from '../../../../common/utils/as_mutable_array';
import {
ERROR_GROUP_ID,
SERVICE_NAME,
TRANSACTION_SAMPLED,
} from '../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../common/processor_event';
import { rangeQuery, kqlQuery } from '../../../../observability/server';
import { environmentQuery } from '../../../common/utils/environment_query';
import { Setup } from '../../lib/helpers/setup_request';
import { getTransaction } from '../transactions/get_transaction';
} from '../../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../../common/processor_event';
import { rangeQuery, kqlQuery } from '../../../../../observability/server';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { getTransaction } from '../../transactions/get_transaction';
import { Setup } from '../../../lib/helpers/setup_request';
export async function getErrorGroupSample({
environment,

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import { getErrorGroupSample } from './get_error_group_sample';
import { getErrorGroups } from './get_error_groups';
import {
SearchParamsMock,
inspectSearchParams,
} from '../../utils/test_helpers';
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
import { getErrorGroupMainStatistics } from './get_error_groups/get_error_group_main_statistics';
import { getErrorGroupSample } from './get_error_groups/get_error_group_sample';
describe('error queries', () => {
let mock: SearchParamsMock;
@ -38,10 +38,11 @@ describe('error queries', () => {
it('fetches multiple error groups', async () => {
mock = await inspectSearchParams((setup) =>
getErrorGroups({
getErrorGroupMainStatistics({
sortDirection: 'asc',
sortField: 'foo',
serviceName: 'serviceName',
transactionType: 'request',
setup,
environment: ENVIRONMENT_ALL.value,
kuery: '',
@ -53,12 +54,13 @@ describe('error queries', () => {
expect(mock.params).toMatchSnapshot();
});
it('fetches multiple error groups when sortField = latestOccurrenceAt', async () => {
it('fetches multiple error groups when sortField = lastSeen', async () => {
mock = await inspectSearchParams((setup) =>
getErrorGroups({
getErrorGroupMainStatistics({
sortDirection: 'asc',
sortField: 'latestOccurrenceAt',
sortField: 'lastSeen',
serviceName: 'serviceName',
transactionType: 'request',
setup,
environment: ENVIRONMENT_ALL.value,
kuery: '',

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt';
import { jsonRt } from '@kbn/io-ts-utils/json_rt';
import * as t from 'io-ts';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
import { getErrorDistribution } from './distribution/get_distribution';
import { getErrorGroupSample } from './get_error_group_sample';
import { getErrorGroups } from './get_error_groups';
import { setupRequest } from '../../lib/helpers/setup_request';
import {
environmentRt,
@ -18,9 +18,13 @@ import {
comparisonRangeRt,
} from '../default_api_types';
import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository';
import { getErrorGroupMainStatistics } from './get_error_groups/get_error_group_main_statistics';
import { getErrorGroupPeriods } from './get_error_groups/get_error_group_detailed_statistics';
import { getErrorGroupSample } from './get_error_groups/get_error_group_sample';
const errorsRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services/{serviceName}/errors',
const errorsMainStatisticsRoute = createApmServerRoute({
endpoint:
'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics',
params: t.type({
path: t.type({
serviceName: t.string,
@ -33,6 +37,9 @@ const errorsRoute = createApmServerRoute({
environmentRt,
kueryRt,
rangeRt,
t.type({
transactionType: t.string,
}),
]),
}),
options: { tags: ['access:apm'] },
@ -40,13 +47,21 @@ const errorsRoute = createApmServerRoute({
const { params } = resources;
const setup = await setupRequest(resources);
const { serviceName } = params.path;
const { environment, kuery, sortField, sortDirection, start, end } =
params.query;
const {
environment,
transactionType,
kuery,
sortField,
sortDirection,
start,
end,
} = params.query;
const errorGroups = await getErrorGroups({
const errorGroups = await getErrorGroupMainStatistics({
environment,
kuery,
serviceName,
transactionType,
sortField,
sortDirection,
setup,
@ -58,6 +73,61 @@ const errorsRoute = createApmServerRoute({
},
});
const errorsDetailedStatisticsRoute = createApmServerRoute({
endpoint:
'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
params: t.type({
path: t.type({
serviceName: t.string,
}),
query: t.intersection([
environmentRt,
kueryRt,
rangeRt,
comparisonRangeRt,
t.type({
numBuckets: toNumberRt,
transactionType: t.string,
groupIds: jsonRt.pipe(t.array(t.string)),
}),
]),
}),
options: { tags: ['access:apm'] },
handler: async (resources) => {
const setup = await setupRequest(resources);
const { params } = resources;
const {
path: { serviceName },
query: {
environment,
kuery,
numBuckets,
transactionType,
groupIds,
comparisonStart,
comparisonEnd,
start,
end,
},
} = params;
return getErrorGroupPeriods({
environment,
kuery,
serviceName,
setup,
numBuckets,
transactionType,
groupIds,
comparisonStart,
comparisonEnd,
start,
end,
});
},
});
const errorGroupsRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services/{serviceName}/errors/{groupId}',
params: t.type({
@ -131,6 +201,7 @@ const errorDistributionRoute = createApmServerRoute({
});
export const errorsRouteRepository = createApmServerRouteRepository()
.add(errorsRoute)
.add(errorsMainStatisticsRoute)
.add(errorsDetailedStatisticsRoute)
.add(errorGroupsRoute)
.add(errorDistributionRoute);

View file

@ -1,205 +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 { orderBy } from 'lodash';
import { ValuesType } from 'utility-types';
import { kqlQuery, rangeQuery } from '../../../../../observability/server';
import { PromiseReturnType } from '../../../../../observability/typings/common';
import {
ERROR_EXC_MESSAGE,
ERROR_GROUP_ID,
ERROR_LOG_MESSAGE,
SERVICE_NAME,
TRANSACTION_TYPE,
} from '../../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../../common/processor_event';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { withApmSpan } from '../../../utils/with_apm_span';
import { getBucketSize } from '../../../lib/helpers/get_bucket_size';
import { getErrorName } from '../../../lib/helpers/get_error_name';
import { Setup } from '../../../lib/helpers/setup_request';
export type ServiceErrorGroupItem = ValuesType<
PromiseReturnType<typeof getServiceErrorGroups>
>;
export async function getServiceErrorGroups({
environment,
kuery,
serviceName,
setup,
size,
numBuckets,
pageIndex,
sortDirection,
sortField,
transactionType,
start,
end,
}: {
environment: string;
kuery: string;
serviceName: string;
setup: Setup;
size: number;
pageIndex: number;
numBuckets: number;
sortDirection: 'asc' | 'desc';
sortField: 'name' | 'lastSeen' | 'occurrences';
transactionType: string;
start: number;
end: number;
}) {
return withApmSpan('get_service_error_groups', async () => {
const { apmEventClient } = setup;
const { intervalString } = getBucketSize({ start, end, numBuckets });
const response = await apmEventClient.search(
'get_top_service_error_groups',
{
apm: {
events: [ProcessorEvent.error],
},
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [TRANSACTION_TYPE]: transactionType } },
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
],
},
},
aggs: {
error_groups: {
terms: {
field: ERROR_GROUP_ID,
size: 500,
order: {
_count: 'desc',
},
},
aggs: {
sample: {
top_hits: {
size: 1,
_source: [
ERROR_LOG_MESSAGE,
ERROR_EXC_MESSAGE,
'@timestamp',
] as any as string,
sort: {
'@timestamp': 'desc',
},
},
},
},
},
},
},
}
);
const errorGroups =
response.aggregations?.error_groups.buckets.map((bucket) => ({
group_id: bucket.key as string,
name: getErrorName(bucket.sample.hits.hits[0]._source),
lastSeen: new Date(
bucket.sample.hits.hits[0]?._source['@timestamp']
).getTime(),
occurrences: {
value: bucket.doc_count,
},
})) ?? [];
// Sort error groups first, and only get timeseries for data in view.
// This is to limit the possibility of creating too many buckets.
const sortedAndSlicedErrorGroups = orderBy(
errorGroups,
(group) => {
if (sortField === 'occurrences') {
return group.occurrences.value;
}
return group[sortField];
},
[sortDirection]
).slice(pageIndex * size, pageIndex * size + size);
const sortedErrorGroupIds = sortedAndSlicedErrorGroups.map(
(group) => group.group_id
);
const timeseriesResponse = await apmEventClient.search(
'get_service_error_groups_timeseries',
{
apm: {
events: [ProcessorEvent.error],
},
body: {
size: 0,
query: {
bool: {
filter: [
{ terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } },
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [TRANSACTION_TYPE]: transactionType } },
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
],
},
},
aggs: {
error_groups: {
terms: {
field: ERROR_GROUP_ID,
size,
},
aggs: {
timeseries: {
date_histogram: {
field: '@timestamp',
fixed_interval: intervalString,
min_doc_count: 0,
extended_bounds: {
min: start,
max: end,
},
},
},
},
},
},
},
}
);
return {
total_error_groups: errorGroups.length,
is_aggregation_accurate:
(response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0,
error_groups: sortedAndSlicedErrorGroups.map((errorGroup) => ({
...errorGroup,
occurrences: {
...errorGroup.occurrences,
timeseries:
timeseriesResponse.aggregations?.error_groups.buckets
.find((bucket) => bucket.key === errorGroup.group_id)
?.timeseries.buckets.map((dateBucket) => ({
x: dateBucket.key,
y: dateBucket.doc_count,
})) ?? null,
},
})),
};
});
}

View file

@ -21,9 +21,6 @@ import { getServiceAgent } from './get_service_agent';
import { getServiceAlerts } from './get_service_alerts';
import { getServiceDependencies } from './get_service_dependencies';
import { getServiceInstanceMetadataDetails } from './get_service_instance_metadata_details';
import { getServiceErrorGroupPeriods } from './get_service_error_groups/get_service_error_group_detailed_statistics';
import { getServiceErrorGroupMainStatistics } from './get_service_error_groups/get_service_error_group_main_statistics';
import { getServiceInstancesDetailedStatisticsPeriods } from './get_service_instances/detailed_statistics';
import { getServiceInstancesMainStatistics } from './get_service_instances/main_statistics';
import { getServiceMetadataDetails } from './get_service_metadata_details';
import { getServiceMetadataIcons } from './get_service_metadata_icons';
@ -47,6 +44,7 @@ import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_pr
import { getServicesDetailedStatistics } from './get_services_detailed_statistics';
import { getServiceDependenciesBreakdown } from './get_service_dependencies_breakdown';
import { getBucketSizeForAggregatedTransactions } from '../../lib/helpers/get_bucket_size_for_aggregated_transactions';
import { getServiceInstancesDetailedStatisticsPeriods } from './get_service_instances/detailed_statistics';
const servicesRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services',
@ -374,98 +372,6 @@ const serviceAnnotationsCreateRoute = createApmServerRoute({
},
});
const serviceErrorGroupsMainStatisticsRoute = createApmServerRoute({
endpoint:
'GET /internal/apm/services/{serviceName}/error_groups/main_statistics',
params: t.type({
path: t.type({
serviceName: t.string,
}),
query: t.intersection([
environmentRt,
kueryRt,
rangeRt,
t.type({
transactionType: t.string,
}),
]),
}),
options: { tags: ['access:apm'] },
handler: async (resources) => {
const setup = await setupRequest(resources);
const { params } = resources;
const {
path: { serviceName },
query: { kuery, transactionType, environment, start, end },
} = params;
return getServiceErrorGroupMainStatistics({
kuery,
serviceName,
setup,
transactionType,
environment,
start,
end,
});
},
});
const serviceErrorGroupsDetailedStatisticsRoute = createApmServerRoute({
endpoint:
'GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics',
params: t.type({
path: t.type({
serviceName: t.string,
}),
query: t.intersection([
environmentRt,
kueryRt,
rangeRt,
comparisonRangeRt,
t.type({
numBuckets: toNumberRt,
transactionType: t.string,
groupIds: jsonRt.pipe(t.array(t.string)),
}),
]),
}),
options: { tags: ['access:apm'] },
handler: async (resources) => {
const setup = await setupRequest(resources);
const { params } = resources;
const {
path: { serviceName },
query: {
environment,
kuery,
numBuckets,
transactionType,
groupIds,
comparisonStart,
comparisonEnd,
start,
end,
},
} = params;
return getServiceErrorGroupPeriods({
environment,
kuery,
serviceName,
setup,
numBuckets,
transactionType,
groupIds,
comparisonStart,
comparisonEnd,
start,
end,
});
},
});
const serviceThroughputRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services/{serviceName}/throughput',
params: t.type({
@ -952,8 +858,6 @@ export const serviceRouteRepository = createApmServerRouteRepository()
.add(serviceNodeMetadataRoute)
.add(serviceAnnotationsRoute)
.add(serviceAnnotationsCreateRoute)
.add(serviceErrorGroupsMainStatisticsRoute)
.add(serviceErrorGroupsDetailedStatisticsRoute)
.add(serviceInstancesMetadataDetails)
.add(serviceThroughputRoute)
.add(serviceInstancesMainStatisticsRoute)

View file

@ -5662,7 +5662,6 @@
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "エラーメッセージと原因",
"xpack.apm.errorsTable.groupIdColumnDescription": "スタックトレースのハッシュ。動的パラメータのため、エラーメッセージが異なる場合でも、類似したエラーをグループ化します。",
"xpack.apm.errorsTable.groupIdColumnLabel": "グループ ID",
"xpack.apm.errorsTable.latestOccurrenceColumnLabel": "最近のオカレンス",
"xpack.apm.errorsTable.noErrorsLabel": "エラーが見つかりません",
"xpack.apm.errorsTable.occurrencesColumnLabel": "オカレンス",
"xpack.apm.errorsTable.typeColumnLabel": "型",
@ -5971,7 +5970,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": "{occurrencesCount} occ.",
"xpack.apm.serviceOverview.dependenciesTableColumn": "依存関係",
"xpack.apm.serviceOverview.dependenciesTableTabLink": "依存関係を表示",
"xpack.apm.serviceOverview.dependenciesTableTitle": "依存関係",
@ -27665,4 +27663,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
}
}

View file

@ -5700,8 +5700,7 @@
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "错误消息和原因",
"xpack.apm.errorsTable.groupIdColumnDescription": "堆栈跟踪的哈希。将类似错误分组在一起,即使因动态参数造成错误消息不同。",
"xpack.apm.errorsTable.groupIdColumnLabel": "组 ID",
"xpack.apm.errorsTable.latestOccurrenceColumnLabel": "最新一次发生",
"xpack.apm.errorsTable.noErrorsLabel": "未找到错误",
"xpack.apm.errorsTable.noErrorsLabel": "未找到任何错误",
"xpack.apm.errorsTable.occurrencesColumnLabel": "发生次数",
"xpack.apm.errorsTable.typeColumnLabel": "类型",
"xpack.apm.errorsTable.unhandledLabel": "未处理",
@ -6012,8 +6011,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": "{occurrencesCount} 次",
"xpack.apm.serviceOverview.dependenciesTableColumn": "依赖项",
"xpack.apm.serviceOverview.dependenciesTableTabLink": "查看依赖项",
"xpack.apm.serviceOverview.dependenciesTableTitle": "依赖项",
"xpack.apm.serviceOverview.errorsTable.errorMessage": "无法提取",

View file

@ -13,7 +13,8 @@ import {
import { RecursivePartial } from '../../../../plugins/apm/typings/common';
import { FtrProviderContext } from '../../common/ftr_provider_context';
type ErrorGroups = APIReturnType<'GET /internal/apm/services/{serviceName}/errors'>['errorGroups'];
type ErrorGroups =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['errorGroups'];
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
@ -26,17 +27,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
async function callApi(
overrides?: RecursivePartial<
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors'>['params']
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['params']
>
) {
return await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/errors`,
endpoint: 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics',
params: {
path: { serviceName, ...overrides?.path },
query: {
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
environment: 'ENVIRONMENT_ALL',
transactionType: 'request',
kuery: '',
...overrides?.query,
},
@ -133,12 +135,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns correct number of errors', () => {
expect(errorGroups.length).to.equal(2);
expect(errorGroups.map((error) => error.message).sort()).to.eql(['error 1', 'error 2']);
expect(errorGroups.map((error) => error.name).sort()).to.eql(['error 1', 'error 2']);
});
it('returns correct occurences', () => {
const numberOfBuckets = 15;
expect(errorGroups.map((error) => error.occurrenceCount).sort()).to.eql([
expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([
appleTransaction.failureRate * numberOfBuckets,
bananaTransaction.failureRate * numberOfBuckets,
]);

View file

@ -44,7 +44,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
{
// this doubles as a smoke test for the _inspect query parameter
req: {
url: `/internal/apm/services/foo/errors?start=${start}&end=${end}&_inspect=true&environment=ENVIRONMENT_ALL&kuery=`,
url: `/internal/apm/services/foo/errors/groups/main_statistics?start=${start}&end=${end}&_inspect=true&environment=ENVIRONMENT_ALL&transactionType=bar&kuery=`,
},
expectForbidden: expect403,
expectResponse: expect200,

View file

@ -19,7 +19,7 @@ import { config, generateData } from './generate_data';
import { getErrorGroupIds } from './get_error_group_ids';
type ErrorGroupsDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics'>;
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
@ -32,11 +32,11 @@ export default function ApiTest({ getService }: FtrProviderContext) {
async function callApi(
overrides?: RecursivePartial<
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics'>['params']
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>['params']
>
) {
return await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics`,
endpoint: `GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics`,
params: {
path: { serviceName, ...overrides?.path },
query: {

View file

@ -15,7 +15,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { generateData, config } from './generate_data';
type ErrorGroupsMainStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/main_statistics'>;
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
@ -28,11 +28,11 @@ export default function ApiTest({ getService }: FtrProviderContext) {
async function callApi(
overrides?: RecursivePartial<
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/error_groups/main_statistics'>['params']
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['params']
>
) {
return await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/error_groups/main_statistics`,
endpoint: `GET /internal/apm/services/{serviceName}/errors/groups/main_statistics`,
params: {
path: { serviceName, ...overrides?.path },
query: {
@ -54,8 +54,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('handles empty state', async () => {
const response = await callApi();
expect(response.status).to.be(200);
expect(response.body.error_groups).to.empty();
expect(response.body.is_aggregation_accurate).to.eql(true);
expect(response.body.errorGroups).to.empty();
});
}
);
@ -81,8 +80,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('returns correct number of occurrences', () => {
expect(errorGroupMainStatistics.error_groups.length).to.equal(2);
expect(errorGroupMainStatistics.error_groups.map((error) => error.name).sort()).to.eql([
expect(errorGroupMainStatistics.errorGroups.length).to.equal(2);
expect(errorGroupMainStatistics.errorGroups.map((error) => error.name).sort()).to.eql([
ERROR_NAME_1,
ERROR_NAME_2,
]);
@ -91,7 +90,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns correct occurences', () => {
const numberOfBuckets = 15;
expect(
errorGroupMainStatistics.error_groups.map((error) => error.occurrences).sort()
errorGroupMainStatistics.errorGroups.map((error) => error.occurrences).sort()
).to.eql([
PROD_LIST_ERROR_RATE * numberOfBuckets,
PROD_ID_ERROR_RATE * numberOfBuckets,
@ -99,7 +98,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('has same last seen value as end date', () => {
errorGroupMainStatistics.error_groups.map((error) => {
errorGroupMainStatistics.errorGroups.map((error) => {
expect(error.lastSeen).to.equal(moment(end).startOf('minute').valueOf());
});
});

View file

@ -22,7 +22,7 @@ export async function getErrorGroupIds({
count?: number;
}) {
const { body } = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/error_groups/main_statistics`,
endpoint: `GET /internal/apm/services/{serviceName}/errors/groups/main_statistics`,
params: {
path: { serviceName },
query: {
@ -35,5 +35,5 @@ export async function getErrorGroupIds({
},
});
return take(body.error_groups.map((group) => group.group_id).sort(), count);
return take(body.errorGroups.map((group) => group.groupId).sort(), count);
}