mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[APM] Service inventory: detailed stats fetched for all services (#134844)
* fixing service inventory detailed api call * fixing error overview page * fixing test * addressing pr comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
17510b8863
commit
b870b621fd
16 changed files with 430 additions and 110 deletions
|
@ -8,7 +8,7 @@
|
|||
import url from 'url';
|
||||
import { synthtrace } from '../../../../synthtrace';
|
||||
import { checkA11y } from '../../../support/commands';
|
||||
import { generateData } from './generate_data';
|
||||
import { generateData, generateErrors } from './generate_data';
|
||||
|
||||
const start = '2021-10-10T00:00:00.000Z';
|
||||
const end = '2021-10-10T00:15:00.000Z';
|
||||
|
@ -110,3 +110,67 @@ describe('Errors page', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Check detailed statistics API with multiple errors', () => {
|
||||
before(async () => {
|
||||
cy.loginAsViewerUser();
|
||||
await synthtrace.index(
|
||||
generateErrors({
|
||||
from: new Date(start).getTime(),
|
||||
to: new Date(end).getTime(),
|
||||
errorCount: 50,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await synthtrace.clean();
|
||||
});
|
||||
|
||||
it('calls detailed API with visible items only', () => {
|
||||
cy.intercept(
|
||||
'GET',
|
||||
'/internal/apm/services/opbeans-java/errors/groups/main_statistics?*'
|
||||
).as('errorsMainStatistics');
|
||||
cy.intercept(
|
||||
'POST',
|
||||
'/internal/apm/services/opbeans-java/errors/groups/detailed_statistics?*'
|
||||
).as('errorsDetailedStatistics');
|
||||
cy.visit(`${javaServiceErrorsPageHref}&pageSize=10`);
|
||||
cy.wait('@errorsMainStatistics');
|
||||
cy.get('.euiPagination__list').children().should('have.length', 5);
|
||||
cy.wait('@errorsDetailedStatistics').then((payload) => {
|
||||
expect(payload.request.body.groupIds).eql(
|
||||
JSON.stringify([
|
||||
'0000000000000000000000000Error 0',
|
||||
'0000000000000000000000000Error 1',
|
||||
'0000000000000000000000000Error 2',
|
||||
'0000000000000000000000000Error 3',
|
||||
'0000000000000000000000000Error 4',
|
||||
'0000000000000000000000000Error 5',
|
||||
'0000000000000000000000000Error 6',
|
||||
'0000000000000000000000000Error 7',
|
||||
'0000000000000000000000000Error 8',
|
||||
'0000000000000000000000000Error 9',
|
||||
])
|
||||
);
|
||||
});
|
||||
cy.get('[data-test-subj="pagination-button-1"]').click();
|
||||
cy.wait('@errorsDetailedStatistics').then((payload) => {
|
||||
expect(payload.request.body.groupIds).eql(
|
||||
JSON.stringify([
|
||||
'000000000000000000000000Error 10',
|
||||
'000000000000000000000000Error 11',
|
||||
'000000000000000000000000Error 12',
|
||||
'000000000000000000000000Error 13',
|
||||
'000000000000000000000000Error 14',
|
||||
'000000000000000000000000Error 15',
|
||||
'000000000000000000000000Error 16',
|
||||
'000000000000000000000000Error 17',
|
||||
'000000000000000000000000Error 18',
|
||||
'000000000000000000000000Error 19',
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,3 +39,40 @@ export function generateData({ from, to }: { from: number; to: number }) {
|
|||
.success(),
|
||||
]);
|
||||
}
|
||||
|
||||
export function generateErrors({
|
||||
from,
|
||||
to,
|
||||
errorCount,
|
||||
}: {
|
||||
from: number;
|
||||
to: number;
|
||||
errorCount: number;
|
||||
}) {
|
||||
const range = timerange(from, to);
|
||||
|
||||
const opbeansJava = apm
|
||||
.service('opbeans-java', 'production', 'java')
|
||||
.instance('opbeans-java-prod-1')
|
||||
.podId('opbeans-java-prod-1-pod');
|
||||
|
||||
return range
|
||||
.interval('2m')
|
||||
.rate(1)
|
||||
.generator((timestamp, index) => [
|
||||
opbeansJava
|
||||
.transaction('GET /apple 🍎 ')
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.errors(
|
||||
...Array(errorCount)
|
||||
.fill(0)
|
||||
.map((_, idx) => {
|
||||
return opbeansJava
|
||||
.error(`Error ${idx}`, `exception ${idx}`)
|
||||
.timestamp(timestamp);
|
||||
})
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export function generateMultipleServicesData({
|
||||
from,
|
||||
to,
|
||||
}: {
|
||||
from: number;
|
||||
to: number;
|
||||
}) {
|
||||
const range = timerange(from, to);
|
||||
|
||||
const services = Array(50)
|
||||
.fill(0)
|
||||
.map((_, idx) =>
|
||||
apm
|
||||
.service(`${idx}`, 'production', 'nodejs')
|
||||
.instance('opbeans-node-prod-1')
|
||||
);
|
||||
|
||||
return range
|
||||
.interval('2m')
|
||||
.rate(1)
|
||||
.generator((timestamp, index) =>
|
||||
services.map((service) =>
|
||||
service
|
||||
.transaction('GET /foo')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.success()
|
||||
)
|
||||
);
|
||||
}
|
|
@ -9,6 +9,7 @@ import url from 'url';
|
|||
import { synthtrace } from '../../../../synthtrace';
|
||||
import { opbeans } from '../../../fixtures/synthtrace/opbeans';
|
||||
import { checkA11y } from '../../../support/commands';
|
||||
import { generateMultipleServicesData } from './generate_data';
|
||||
|
||||
const timeRange = {
|
||||
rangeFrom: '2021-10-10T00:00:00.000Z',
|
||||
|
@ -132,3 +133,68 @@ describe('When navigating to the service inventory', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Check detailed statistics API with multiple services', () => {
|
||||
before(async () => {
|
||||
cy.loginAsViewerUser();
|
||||
const { rangeFrom, rangeTo } = timeRange;
|
||||
|
||||
await synthtrace.index(
|
||||
generateMultipleServicesData({
|
||||
from: new Date(rangeFrom).getTime(),
|
||||
to: new Date(rangeTo).getTime(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await synthtrace.clean();
|
||||
});
|
||||
|
||||
it('calls detailed API with visible items only', () => {
|
||||
cy.intercept('POST', '/internal/apm/services/detailed_statistics?*').as(
|
||||
'detailedStatisticsRequest'
|
||||
);
|
||||
cy.intercept('GET', '/internal/apm/services?*').as('mainStatisticsRequest');
|
||||
|
||||
cy.visit(
|
||||
`${serviceInventoryHref}&pageSize=10&sortField=serviceName&sortDirection=asc`
|
||||
);
|
||||
cy.wait('@mainStatisticsRequest');
|
||||
cy.contains('Services');
|
||||
cy.get('.euiPagination__list').children().should('have.length', 5);
|
||||
cy.wait('@detailedStatisticsRequest').then((payload) => {
|
||||
expect(payload.request.body.serviceNames).eql(
|
||||
JSON.stringify([
|
||||
'0',
|
||||
'1',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'15',
|
||||
'16',
|
||||
'17',
|
||||
])
|
||||
);
|
||||
});
|
||||
cy.get('[data-test-subj="pagination-button-1"]').click();
|
||||
cy.wait('@detailedStatisticsRequest').then((payload) => {
|
||||
expect(payload.request.body.serviceNames).eql(
|
||||
JSON.stringify([
|
||||
'18',
|
||||
'19',
|
||||
'2',
|
||||
'20',
|
||||
'21',
|
||||
'22',
|
||||
'23',
|
||||
'24',
|
||||
'25',
|
||||
'26',
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -55,13 +55,15 @@ const Culprit = euiStyled.div`
|
|||
type ErrorGroupItem =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['errorGroups'][0];
|
||||
type ErrorGroupDetailedStatistics =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
|
||||
interface Props {
|
||||
mainStatistics: ErrorGroupItem[];
|
||||
serviceName: string;
|
||||
detailedStatisticsLoading: boolean;
|
||||
detailedStatistics: ErrorGroupDetailedStatistics;
|
||||
initialSortField: string;
|
||||
initialSortDirection: 'asc' | 'desc';
|
||||
comparisonEnabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -71,6 +73,8 @@ function ErrorGroupList({
|
|||
detailedStatisticsLoading,
|
||||
detailedStatistics,
|
||||
comparisonEnabled,
|
||||
initialSortField,
|
||||
initialSortDirection,
|
||||
}: Props) {
|
||||
const { query } = useApmParams('/services/{serviceName}/errors');
|
||||
const { offset } = query;
|
||||
|
@ -251,8 +255,8 @@ function ErrorGroupList({
|
|||
})}
|
||||
items={mainStatistics}
|
||||
columns={columns}
|
||||
initialSortField="occurrences"
|
||||
initialSortDirection="desc"
|
||||
initialSortField={initialSortField}
|
||||
initialSortDirection={initialSortDirection}
|
||||
sortItems={false}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { orderBy } from 'lodash';
|
||||
import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
|
||||
|
@ -26,18 +27,21 @@ import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
|||
import { FailedTransactionRateChart } from '../../shared/charts/failed_transaction_rate_chart';
|
||||
import { ErrorDistribution } from '../error_group_details/distribution';
|
||||
import { ErrorGroupList } from './error_group_list';
|
||||
import { INITIAL_PAGE_SIZE } from '../../shared/managed_table';
|
||||
|
||||
type ErrorGroupMainStatistics =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
|
||||
type ErrorGroupDetailedStatistics =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
|
||||
const INITIAL_STATE_MAIN_STATISTICS: {
|
||||
errorGroupMainStatistics: ErrorGroupMainStatistics['errorGroups'];
|
||||
requestId?: string;
|
||||
currentPageGroupIds: ErrorGroupMainStatistics['errorGroups'];
|
||||
} = {
|
||||
errorGroupMainStatistics: [],
|
||||
requestId: undefined,
|
||||
currentPageGroupIds: [],
|
||||
};
|
||||
|
||||
const INITIAL_STATE_DETAILED_STATISTICS: ErrorGroupDetailedStatistics = {
|
||||
|
@ -52,12 +56,14 @@ export function ErrorGroupOverview() {
|
|||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
sortField,
|
||||
sortDirection,
|
||||
sortField = 'occurrences',
|
||||
sortDirection = 'desc',
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
offset,
|
||||
comparisonEnabled,
|
||||
page = 0,
|
||||
pageSize = INITIAL_PAGE_SIZE,
|
||||
},
|
||||
} = useApmParams('/services/{serviceName}/errors');
|
||||
|
||||
|
@ -94,27 +100,48 @@ export function ErrorGroupOverview() {
|
|||
},
|
||||
}
|
||||
).then((response) => {
|
||||
const currentPageGroupIds = orderBy(
|
||||
response.errorGroups,
|
||||
sortField,
|
||||
sortDirection
|
||||
)
|
||||
.slice(page * pageSize, (page + 1) * pageSize)
|
||||
.map(({ groupId }) => groupId)
|
||||
.sort();
|
||||
|
||||
return {
|
||||
// Everytime the main statistics is refetched, updates the requestId making the comparison API to be refetched.
|
||||
requestId: uuid(),
|
||||
errorGroupMainStatistics: response.errorGroups,
|
||||
currentPageGroupIds,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
[environment, kuery, serviceName, start, end, sortField, sortDirection]
|
||||
[
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
sortField,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
]
|
||||
);
|
||||
|
||||
const { requestId, errorGroupMainStatistics } = errorGroupListData;
|
||||
const { requestId, errorGroupMainStatistics, currentPageGroupIds } =
|
||||
errorGroupListData;
|
||||
|
||||
const {
|
||||
data: errorGroupDetailedStatistics = INITIAL_STATE_DETAILED_STATISTICS,
|
||||
status: errorGroupDetailedStatisticsStatus,
|
||||
} = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (requestId && errorGroupMainStatistics.length && start && end) {
|
||||
if (requestId && currentPageGroupIds.length && start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
|
||||
'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
|
@ -124,14 +151,14 @@ export function ErrorGroupOverview() {
|
|||
start,
|
||||
end,
|
||||
numBuckets: 20,
|
||||
groupIds: JSON.stringify(
|
||||
errorGroupMainStatistics.map(({ groupId }) => groupId).sort()
|
||||
),
|
||||
offset:
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? offset
|
||||
: undefined,
|
||||
},
|
||||
body: {
|
||||
groupIds: JSON.stringify(currentPageGroupIds),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -192,6 +219,8 @@ export function ErrorGroupOverview() {
|
|||
}
|
||||
detailedStatistics={errorGroupDetailedStatistics}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
initialSortField={sortField}
|
||||
initialSortDirection={sortDirection}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -24,6 +24,7 @@ import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher';
|
|||
import { joinByKey } from '../../../../common/utils/join_by_key';
|
||||
import { ServiceInventoryFieldName } from '../../../../common/service_inventory';
|
||||
import { orderServiceItems } from './service_list/order_service_items';
|
||||
import { INITIAL_PAGE_SIZE } from '../../shared/managed_table';
|
||||
|
||||
const initialData = {
|
||||
requestId: '',
|
||||
|
@ -32,7 +33,7 @@ const initialData = {
|
|||
hasLegacyData: false,
|
||||
};
|
||||
|
||||
function useServicesFetcher() {
|
||||
function useServicesMainStatisticsFetcher() {
|
||||
const {
|
||||
query: {
|
||||
rangeFrom,
|
||||
|
@ -40,8 +41,10 @@ function useServicesFetcher() {
|
|||
environment,
|
||||
kuery,
|
||||
serviceGroup,
|
||||
offset,
|
||||
comparisonEnabled,
|
||||
page = 0,
|
||||
pageSize = INITIAL_PAGE_SIZE,
|
||||
sortDirection,
|
||||
sortField,
|
||||
},
|
||||
} = useApmParams('/services');
|
||||
|
||||
|
@ -85,37 +88,94 @@ function useServicesFetcher() {
|
|||
});
|
||||
}
|
||||
},
|
||||
[environment, kuery, start, end, serviceGroup]
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
serviceGroup,
|
||||
// not used, but needed to update the requestId to call the details statistics API when table is options are updated
|
||||
page,
|
||||
pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
sortedAndFilteredServicesFetch,
|
||||
mainStatisticsFetch,
|
||||
};
|
||||
}
|
||||
|
||||
function useServicesDetailedStatisticsFetcher({
|
||||
mainStatisticsFetch,
|
||||
initialSortField,
|
||||
initialSortDirection,
|
||||
tiebreakerField,
|
||||
}: {
|
||||
mainStatisticsFetch: ReturnType<
|
||||
typeof useServicesMainStatisticsFetcher
|
||||
>['mainStatisticsFetch'];
|
||||
initialSortField: ServiceInventoryFieldName;
|
||||
initialSortDirection: 'asc' | 'desc';
|
||||
tiebreakerField: ServiceInventoryFieldName;
|
||||
}) {
|
||||
const {
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
environment,
|
||||
kuery,
|
||||
offset,
|
||||
comparisonEnabled,
|
||||
page = 0,
|
||||
pageSize = INITIAL_PAGE_SIZE,
|
||||
sortDirection = initialSortDirection,
|
||||
sortField = initialSortField,
|
||||
},
|
||||
} = useApmParams('/services');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { data: mainStatisticsData = initialData } = mainStatisticsFetch;
|
||||
|
||||
const currentPageItems = orderServiceItems({
|
||||
items: mainStatisticsData.items,
|
||||
primarySortField: sortField as ServiceInventoryFieldName,
|
||||
sortDirection,
|
||||
tiebreakerField,
|
||||
}).slice(page * pageSize, (page + 1) * pageSize);
|
||||
|
||||
const comparisonFetch = useProgressiveFetcher(
|
||||
(callApmApi) => {
|
||||
if (
|
||||
start &&
|
||||
end &&
|
||||
mainStatisticsData.items.length &&
|
||||
currentPageItems.length &&
|
||||
mainStatisticsFetch.status === FETCH_STATUS.SUCCESS
|
||||
) {
|
||||
return callApmApi('GET /internal/apm/services/detailed_statistics', {
|
||||
return callApmApi('POST /internal/apm/services/detailed_statistics', {
|
||||
params: {
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
serviceNames: JSON.stringify(
|
||||
mainStatisticsData.items
|
||||
.map(({ serviceName }) => serviceName)
|
||||
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
|
||||
.sort()
|
||||
),
|
||||
offset:
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? offset
|
||||
: undefined,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(
|
||||
currentPageItems
|
||||
.map(({ serviceName }) => serviceName)
|
||||
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
|
||||
.sort()
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -126,19 +186,43 @@ function useServicesFetcher() {
|
|||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
return {
|
||||
sortedAndFilteredServicesFetch,
|
||||
mainStatisticsFetch,
|
||||
comparisonFetch,
|
||||
};
|
||||
return { comparisonFetch };
|
||||
}
|
||||
|
||||
export function ServiceInventory() {
|
||||
const {
|
||||
sortedAndFilteredServicesFetch,
|
||||
const { sortedAndFilteredServicesFetch, mainStatisticsFetch } =
|
||||
useServicesMainStatisticsFetcher();
|
||||
|
||||
const mainStatisticsItems = mainStatisticsFetch.data?.items ?? [];
|
||||
const preloadedServices = sortedAndFilteredServicesFetch.data?.services || [];
|
||||
|
||||
const displayHealthStatus = [
|
||||
...mainStatisticsItems,
|
||||
...preloadedServices,
|
||||
].some((item) => 'healthStatus' in item);
|
||||
|
||||
const useOptimizedSorting =
|
||||
useKibana().services.uiSettings?.get<boolean>(
|
||||
apmServiceInventoryOptimizedSorting
|
||||
) || false;
|
||||
|
||||
const tiebreakerField = useOptimizedSorting
|
||||
? ServiceInventoryFieldName.ServiceName
|
||||
: ServiceInventoryFieldName.Throughput;
|
||||
|
||||
const initialSortField = displayHealthStatus
|
||||
? ServiceInventoryFieldName.HealthStatus
|
||||
: tiebreakerField;
|
||||
|
||||
const initialSortDirection =
|
||||
initialSortField === ServiceInventoryFieldName.ServiceName ? 'asc' : 'desc';
|
||||
|
||||
const { comparisonFetch } = useServicesDetailedStatisticsFetcher({
|
||||
mainStatisticsFetch,
|
||||
comparisonFetch,
|
||||
} = useServicesFetcher();
|
||||
initialSortField,
|
||||
initialSortDirection,
|
||||
tiebreakerField,
|
||||
});
|
||||
|
||||
const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext();
|
||||
|
||||
|
@ -151,11 +235,6 @@ export function ServiceInventory() {
|
|||
!userHasDismissedCallout &&
|
||||
shouldDisplayMlCallout(anomalyDetectionSetupState);
|
||||
|
||||
const useOptimizedSorting =
|
||||
useKibana().services.uiSettings?.get<boolean>(
|
||||
apmServiceInventoryOptimizedSorting
|
||||
) || false;
|
||||
|
||||
let isLoading: boolean;
|
||||
|
||||
if (useOptimizedSorting) {
|
||||
|
@ -183,25 +262,6 @@ export function ServiceInventory() {
|
|||
/>
|
||||
);
|
||||
|
||||
const mainStatisticsItems = mainStatisticsFetch.data?.items ?? [];
|
||||
const preloadedServices = sortedAndFilteredServicesFetch.data?.services || [];
|
||||
|
||||
const displayHealthStatus = [
|
||||
...mainStatisticsItems,
|
||||
...preloadedServices,
|
||||
].some((item) => 'healthStatus' in item);
|
||||
|
||||
const tiebreakerField = useOptimizedSorting
|
||||
? ServiceInventoryFieldName.ServiceName
|
||||
: ServiceInventoryFieldName.Throughput;
|
||||
|
||||
const initialSortField = displayHealthStatus
|
||||
? ServiceInventoryFieldName.HealthStatus
|
||||
: tiebreakerField;
|
||||
|
||||
const initialSortDirection =
|
||||
initialSortField === ServiceInventoryFieldName.ServiceName ? 'asc' : 'desc';
|
||||
|
||||
const items = joinByKey(
|
||||
[
|
||||
// only use preloaded services if tiebreaker field is service.name,
|
||||
|
|
|
@ -50,7 +50,7 @@ import {
|
|||
} from '../../../../../common/service_inventory';
|
||||
|
||||
type ServicesDetailedStatisticsAPIResponse =
|
||||
APIReturnType<'GET /internal/apm/services/detailed_statistics'>;
|
||||
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
|
||||
|
||||
function formatString(value?: string | null) {
|
||||
return value || NOT_AVAILABLE_LABEL;
|
||||
|
@ -271,7 +271,16 @@ export function ServiceList({
|
|||
transactionType !== TRANSACTION_PAGE_LOAD
|
||||
);
|
||||
|
||||
const { query } = useApmParams('/services');
|
||||
const {
|
||||
// removes pagination and sort instructions from the query so it won't be passed down to next route
|
||||
query: {
|
||||
page,
|
||||
pageSize,
|
||||
sortDirection: direction,
|
||||
sortField: field,
|
||||
...query
|
||||
},
|
||||
} = useApmParams('/services');
|
||||
|
||||
const { kuery } = query;
|
||||
const { fallbackToTransactions } = useFallbackToTransactionsFetcher({
|
||||
|
|
|
@ -32,7 +32,7 @@ const ErrorLink = euiStyled(ErrorOverviewLink)`
|
|||
type ErrorGroupMainStatistics =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
|
||||
type ErrorGroupDetailedStatistics =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
|
||||
export function getColumns({
|
||||
serviceName,
|
||||
|
|
|
@ -30,7 +30,7 @@ interface Props {
|
|||
type ErrorGroupMainStatistics =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
|
||||
type ErrorGroupDetailedStatistics =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
type SortField = 'name' | 'lastSeen' | 'occurrences';
|
||||
|
@ -137,7 +137,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
|
|||
(callApmApi) => {
|
||||
if (requestId && items.length && start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
|
||||
'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
|
@ -147,14 +147,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
|
|||
start,
|
||||
end,
|
||||
numBuckets: 20,
|
||||
groupIds: JSON.stringify(
|
||||
items.map(({ groupId: groupId }) => groupId).sort()
|
||||
),
|
||||
offset:
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? offset
|
||||
: undefined,
|
||||
},
|
||||
body: {
|
||||
groupIds: JSON.stringify(
|
||||
items.map(({ groupId: groupId }) => groupId).sort()
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -169,6 +169,10 @@ export const home = {
|
|||
t.partial({
|
||||
refreshPaused: t.union([t.literal('true'), t.literal('false')]),
|
||||
refreshInterval: t.string,
|
||||
page: toNumberRt,
|
||||
pageSize: toNumberRt,
|
||||
sortField: t.string,
|
||||
sortDirection: t.union([t.literal('asc'), t.literal('desc')]),
|
||||
}),
|
||||
offsetRt,
|
||||
]),
|
||||
|
|
|
@ -48,7 +48,7 @@ interface Props<T> {
|
|||
}
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 25, 50];
|
||||
const INITIAL_PAGE_SIZE = 25;
|
||||
export const INITIAL_PAGE_SIZE = 25;
|
||||
|
||||
function defaultSortFn<T extends any>(
|
||||
items: T[],
|
||||
|
|
|
@ -70,7 +70,7 @@ const errorsMainStatisticsRoute = createApmServerRoute({
|
|||
|
||||
const errorsDetailedStatisticsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
|
||||
'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
|
@ -82,9 +82,9 @@ const errorsDetailedStatisticsRoute = createApmServerRoute({
|
|||
offsetRt,
|
||||
t.type({
|
||||
numBuckets: toNumberRt,
|
||||
groupIds: jsonRt.pipe(t.array(t.string)),
|
||||
}),
|
||||
]),
|
||||
body: t.type({ groupIds: jsonRt.pipe(t.array(t.string)) }),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
|
@ -107,7 +107,8 @@ const errorsDetailedStatisticsRoute = createApmServerRoute({
|
|||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { environment, kuery, numBuckets, groupIds, start, end, offset },
|
||||
query: { environment, kuery, numBuckets, start, end, offset },
|
||||
body: { groupIds },
|
||||
} = params;
|
||||
|
||||
return getErrorGroupPeriods({
|
||||
|
|
|
@ -152,19 +152,16 @@ const servicesRoute = createApmServerRoute({
|
|||
});
|
||||
|
||||
const servicesDetailedStatisticsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/detailed_statistics',
|
||||
endpoint: 'POST /internal/apm/services/detailed_statistics',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
// t.intersection seemingly only supports 5 arguments so let's wrap them in another intersection
|
||||
t.intersection([
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
offsetRt,
|
||||
probabilityRt,
|
||||
]),
|
||||
t.type({ serviceNames: jsonRt.pipe(t.array(t.string)) }),
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
offsetRt,
|
||||
probabilityRt,
|
||||
]),
|
||||
body: t.type({ serviceNames: jsonRt.pipe(t.array(t.string)) }),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
|
@ -207,15 +204,10 @@ const servicesDetailedStatisticsRoute = createApmServerRoute({
|
|||
plugins: { security },
|
||||
} = resources;
|
||||
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
offset,
|
||||
serviceNames,
|
||||
start,
|
||||
end,
|
||||
probability,
|
||||
} = params.query;
|
||||
const { environment, kuery, offset, start, end, probability } =
|
||||
params.query;
|
||||
|
||||
const { serviceNames } = params.body;
|
||||
|
||||
const [setup, randomSampler] = await Promise.all([
|
||||
setupRequest(resources),
|
||||
|
|
|
@ -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}/errors/groups/detailed_statistics'>;
|
||||
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
|
@ -32,22 +32,22 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
async function callApi(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>['params']
|
||||
APIClientRequestParamsOf<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>['params']
|
||||
>
|
||||
) {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics`,
|
||||
endpoint: `POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics`,
|
||||
params: {
|
||||
path: { serviceName, ...overrides?.path },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
numBuckets: 20,
|
||||
groupIds: JSON.stringify(['foo']),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
...overrides?.query,
|
||||
},
|
||||
body: { groupIds: JSON.stringify(['foo']), ...overrides?.body },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
before(async () => {
|
||||
errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient });
|
||||
const response = await callApi({
|
||||
query: {
|
||||
body: {
|
||||
groupIds: JSON.stringify(errorIds),
|
||||
},
|
||||
});
|
||||
|
@ -116,7 +116,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics;
|
||||
before(async () => {
|
||||
const response = await callApi({
|
||||
query: {
|
||||
body: {
|
||||
groupIds: JSON.stringify(['foo']),
|
||||
},
|
||||
});
|
||||
|
@ -138,11 +138,13 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient });
|
||||
const response = await callApi({
|
||||
query: {
|
||||
groupIds: JSON.stringify(errorIds),
|
||||
start: moment(end).subtract(7, 'minutes').toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
offset: '7m',
|
||||
},
|
||||
body: {
|
||||
groupIds: JSON.stringify(errorIds),
|
||||
},
|
||||
});
|
||||
errorGroupsDetailedStatistics = response.body;
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context';
|
|||
import { ApmApiError } from '../../common/apm_api_supertest';
|
||||
|
||||
type ServicesDetailedStatisticsReturn =
|
||||
APIReturnType<'GET /internal/apm/services/detailed_statistics'>;
|
||||
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
|
@ -31,17 +31,19 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
() => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services/detailed_statistics`,
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
offset: '1d',
|
||||
probability: 1,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -59,16 +61,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
let servicesDetailedStatistics: ServicesDetailedStatisticsReturn;
|
||||
before(async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services/detailed_statistics`,
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
|
@ -112,16 +116,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
it('returns empty when empty service names is passed', async () => {
|
||||
try {
|
||||
await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services/detailed_statistics`,
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceNames: JSON.stringify([]),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify([]),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect().fail('Expected API call to throw an error');
|
||||
|
@ -135,16 +141,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
it('filters by environment', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services/detailed_statistics`,
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
environment: 'production',
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
|
@ -153,16 +161,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
it('filters by kuery', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services/detailed_statistics`,
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: 'transaction.type : "invalid_transaction_type"',
|
||||
probability: 1,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
|
@ -178,17 +188,19 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
let servicesDetailedStatistics: ServicesDetailedStatisticsReturn;
|
||||
before(async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services/detailed_statistics`,
|
||||
endpoint: `POST /internal/apm/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start: moment(end).subtract(15, 'minutes').toISOString(),
|
||||
end,
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
offset: '15m',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue