mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[APM] Show alert indicator on service inventory page (#147511)
Closes https://github.com/elastic/kibana/issues/146701. ### Changes - `getApmAlertsClient` was moved from `service-groups` to `helper` folder. - `getServicesAlerts` method was created in order to get the active alerts from services matching filters (timeframe, env, service-group, etc). - `getServicesItems` is now also getting the service alerts and merging them in the results so a knew property was added to service results: `alertsCount`. - Alerts badge was added to service inventory page. **After the changes** - From service inventory https://user-images.githubusercontent.com/1313018/207597602-21412be9-be7b-4b8a-8eb8-37416b6c10bc.mov - From a service group https://user-images.githubusercontent.com/1313018/207597893-d4d19a21-b936-4bbd-beaa-9f3ac6c051b8.mov - After Weekly UI presentation and sync with @boriskirov and @chrisdistasio, we decided to add a loading indicator to serviceGroupCards to give users a hint that something is loading in the background. This is how it looks like: https://user-images.githubusercontent.com/1313018/208681846-4a47f593-1010-4471-86f2-18b196c40684.mov
This commit is contained in:
parent
8337c7d72f
commit
3de5b43fba
22 changed files with 390 additions and 102 deletions
|
@ -17,6 +17,7 @@ export interface ServiceListItem {
|
|||
latency?: number | null;
|
||||
transactionErrorRate?: number | null;
|
||||
environments?: string[];
|
||||
alertsCount?: number;
|
||||
}
|
||||
|
||||
export enum ServiceInventoryFieldName {
|
||||
|
@ -27,4 +28,5 @@ export enum ServiceInventoryFieldName {
|
|||
Throughput = 'throughput',
|
||||
Latency = 'latency',
|
||||
TransactionErrorRate = 'transactionErrorRate',
|
||||
AlertsCount = 'alertsCount',
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ export function ServiceGroupsList() {
|
|||
|
||||
const { serviceGroups } = data;
|
||||
|
||||
const { data: servicesGroupCounts = {} } = useFetcher(
|
||||
const { data: servicesGroupCounts = {}, status: statsStatus } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (serviceGroups.length) {
|
||||
return callApmApi('GET /internal/apm/service-group/counts');
|
||||
|
@ -53,6 +53,7 @@ export function ServiceGroupsList() {
|
|||
);
|
||||
|
||||
const isLoading = isPending(status);
|
||||
const isLoadingStats = isPending(statsStatus);
|
||||
|
||||
const filteredItems = isEmpty(filter)
|
||||
? serviceGroups
|
||||
|
@ -192,7 +193,7 @@ export function ServiceGroupsList() {
|
|||
<ServiceGroupsListItems
|
||||
items={items}
|
||||
serviceGroupCounts={servicesGroupCounts}
|
||||
isLoading={isLoading}
|
||||
isLoading={isLoadingStats}
|
||||
/>
|
||||
) : (
|
||||
<EuiEmptyPrompt
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
EuiCardProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
useIsWithinBreakpoints,
|
||||
} from '@elastic/eui';
|
||||
|
@ -22,19 +21,20 @@ import {
|
|||
SERVICE_GROUP_COLOR_DEFAULT,
|
||||
} from '../../../../../common/service_groups';
|
||||
import { useObservabilityActiveAlertsHref } from '../../../shared/links/kibana';
|
||||
import { ServiceStat } from './service_stat';
|
||||
|
||||
interface Props {
|
||||
serviceGroup: ServiceGroup;
|
||||
hideServiceCount?: boolean;
|
||||
href?: string;
|
||||
serviceGroupCounts?: { services: number; alerts: number };
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function ServiceGroupsCard({
|
||||
serviceGroup,
|
||||
hideServiceCount = false,
|
||||
href,
|
||||
serviceGroupCounts,
|
||||
isLoading,
|
||||
}: Props) {
|
||||
const isMobile = useIsWithinBreakpoints(['xs', 's']);
|
||||
|
||||
|
@ -43,27 +43,6 @@ export function ServiceGroupsCard({
|
|||
style: { width: isMobile ? '100%' : 286 },
|
||||
icon: (
|
||||
<>
|
||||
{serviceGroupCounts?.alerts && (
|
||||
<div>
|
||||
<EuiBadge
|
||||
iconType="alert"
|
||||
color="danger"
|
||||
href={activeAlertsHref}
|
||||
{...({
|
||||
onClick(e: React.SyntheticEvent) {
|
||||
e.stopPropagation(); // prevents extra click thru to EuiCard's href destination
|
||||
},
|
||||
} as object)} // workaround for type check that prevents href + onclick
|
||||
>
|
||||
{i18n.translate('xpack.apm.serviceGroups.cardsList.alertCount', {
|
||||
defaultMessage:
|
||||
'{alertsCount} {alertsCount, plural, one {alert} other {alerts}}',
|
||||
values: { alertsCount: serviceGroupCounts.alerts },
|
||||
})}
|
||||
</EuiBadge>
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
)}
|
||||
<EuiAvatar
|
||||
name={serviceGroup.groupName}
|
||||
color={serviceGroup.color || SERVICE_GROUP_COLOR_DEFAULT}
|
||||
|
@ -83,24 +62,46 @@ export function ServiceGroupsCard({
|
|||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{!hideServiceCount && (
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
{serviceGroupCounts === undefined ? (
|
||||
<> </>
|
||||
) : (
|
||||
i18n.translate(
|
||||
'xpack.apm.serviceGroups.cardsList.serviceCount',
|
||||
<EuiFlexGroup>
|
||||
<ServiceStat loading={isLoading}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" textAlign="left">
|
||||
{serviceGroupCounts !== undefined &&
|
||||
i18n.translate(
|
||||
'xpack.apm.serviceGroups.cardsList.serviceCount',
|
||||
{
|
||||
defaultMessage:
|
||||
'{servicesCount} {servicesCount, plural, one {service} other {services}}',
|
||||
values: { servicesCount: serviceGroupCounts.services },
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</ServiceStat>
|
||||
<ServiceStat loading={isLoading} grow={isLoading}>
|
||||
{serviceGroupCounts && serviceGroupCounts.alerts > 0 && (
|
||||
<EuiBadge
|
||||
iconType="alert"
|
||||
color="danger"
|
||||
href={activeAlertsHref}
|
||||
{...({
|
||||
onClick(e: React.SyntheticEvent) {
|
||||
e.stopPropagation(); // prevents extra click thru to EuiCard's href destination
|
||||
},
|
||||
} as object)} // workaround for type check that prevents href + onclick
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceGroups.cardsList.alertCount',
|
||||
{
|
||||
defaultMessage:
|
||||
'{servicesCount} {servicesCount, plural, one {service} other {services}}',
|
||||
values: { servicesCount: serviceGroupCounts.services },
|
||||
'{alertsCount} {alertsCount, plural, one {alert} other {alerts}}',
|
||||
values: { alertsCount: serviceGroupCounts.alerts },
|
||||
}
|
||||
)
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
)}
|
||||
</EuiBadge>
|
||||
)}
|
||||
</ServiceStat>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
href,
|
||||
|
|
|
@ -19,7 +19,11 @@ interface Props {
|
|||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function ServiceGroupsListItems({ items, serviceGroupCounts }: Props) {
|
||||
export function ServiceGroupsListItems({
|
||||
items,
|
||||
serviceGroupCounts,
|
||||
isLoading,
|
||||
}: Props) {
|
||||
const router = useApmRouter();
|
||||
const { query } = useApmParams('/service-groups');
|
||||
|
||||
|
@ -40,6 +44,7 @@ export function ServiceGroupsListItems({ items, serviceGroupCounts }: Props) {
|
|||
kuery: '',
|
||||
},
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { EuiFlexItem, EuiLoadingContent } from '@elastic/eui';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
grow?: boolean;
|
||||
}
|
||||
|
||||
export function ServiceStat({
|
||||
loading,
|
||||
grow = true,
|
||||
children,
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<EuiFlexItem grow={grow}>
|
||||
{loading ? (
|
||||
<EuiLoadingContent lines={1} style={{ marginTop: '4px' }} />
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
|
@ -202,6 +202,10 @@ export function ServiceInventory() {
|
|||
...preloadedServices,
|
||||
].some((item) => 'healthStatus' in item);
|
||||
|
||||
const displayAlerts = [...mainStatisticsItems, ...preloadedServices].some(
|
||||
(item) => ServiceInventoryFieldName.AlertsCount in item
|
||||
);
|
||||
|
||||
const useOptimizedSorting =
|
||||
useKibana().services.uiSettings?.get<boolean>(
|
||||
apmServiceInventoryOptimizedSorting
|
||||
|
@ -298,6 +302,7 @@ export function ServiceInventory() {
|
|||
comparisonFetch.status === FETCH_STATUS.LOADING
|
||||
}
|
||||
displayHealthStatus={displayHealthStatus}
|
||||
displayAlerts={displayAlerts}
|
||||
initialSortField={initialSortField}
|
||||
initialSortDirection={initialSortDirection}
|
||||
sortFn={(itemsToSort, sortField, sortDirection) => {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
|
@ -18,6 +19,10 @@ import { TypeOf } from '@kbn/typed-react-router-config';
|
|||
import React, { useMemo } from 'react';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
|
||||
import {
|
||||
ServiceInventoryFieldName,
|
||||
ServiceListItem,
|
||||
} from '../../../../../common/service_inventory';
|
||||
import {
|
||||
TRANSACTION_PAGE_LOAD,
|
||||
TRANSACTION_REQUEST,
|
||||
|
@ -28,26 +33,23 @@ import {
|
|||
asTransactionRate,
|
||||
} from '../../../../../common/utils/formatters';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../../hooks/use_apm_router';
|
||||
import { Breakpoints, useBreakpoints } from '../../../../hooks/use_breakpoints';
|
||||
import { useFallbackToTransactionsFetcher } from '../../../../hooks/use_fallback_to_transactions_fetcher';
|
||||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { unit } from '../../../../utils/style';
|
||||
import { ApmRoutes } from '../../../routing/apm_route_config';
|
||||
import { AggregatedTransactionsBadge } from '../../../shared/aggregated_transactions_badge';
|
||||
import {
|
||||
ChartType,
|
||||
getTimeSeriesColor,
|
||||
} from '../../../shared/charts/helper/get_timeseries_color';
|
||||
import { EnvironmentBadge } from '../../../shared/environment_badge';
|
||||
import { ListMetric } from '../../../shared/list_metric';
|
||||
import { ITableColumn, ManagedTable } from '../../../shared/managed_table';
|
||||
import { ServiceLink } from '../../../shared/service_link';
|
||||
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
||||
import {
|
||||
ChartType,
|
||||
getTimeSeriesColor,
|
||||
} from '../../../shared/charts/helper/get_timeseries_color';
|
||||
import { HealthBadge } from './health_badge';
|
||||
import {
|
||||
ServiceInventoryFieldName,
|
||||
ServiceListItem,
|
||||
} from '../../../../../common/service_inventory';
|
||||
|
||||
type ServicesDetailedStatisticsAPIResponse =
|
||||
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
|
||||
|
@ -63,19 +65,51 @@ export function getServiceColumns({
|
|||
comparisonData,
|
||||
breakpoints,
|
||||
showHealthStatusColumn,
|
||||
showAlertsColumn,
|
||||
link,
|
||||
}: {
|
||||
query: TypeOf<ApmRoutes, '/services'>['query'];
|
||||
showTransactionTypeColumn: boolean;
|
||||
showHealthStatusColumn: boolean;
|
||||
showAlertsColumn: boolean;
|
||||
comparisonDataLoading: boolean;
|
||||
breakpoints: Breakpoints;
|
||||
comparisonData?: ServicesDetailedStatisticsAPIResponse;
|
||||
link: any;
|
||||
}): Array<ITableColumn<ServiceListItem>> {
|
||||
const { isSmall, isLarge, isXl } = breakpoints;
|
||||
const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge;
|
||||
const showWhenSmallOrGreaterThanXL = isSmall || !isXl;
|
||||
|
||||
return [
|
||||
...(showAlertsColumn
|
||||
? [
|
||||
{
|
||||
field: ServiceInventoryFieldName.AlertsCount,
|
||||
name: '',
|
||||
width: `${unit * 5}px`,
|
||||
sortable: true,
|
||||
render: (_, { serviceName, alertsCount }) => {
|
||||
if (!alertsCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiBadge
|
||||
iconType="alert"
|
||||
color="danger"
|
||||
href={link('/services/{serviceName}/alerts', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
})}
|
||||
>
|
||||
{alertsCount}
|
||||
</EuiBadge>
|
||||
);
|
||||
},
|
||||
} as ITableColumn<ServiceListItem>,
|
||||
]
|
||||
: []),
|
||||
...(showHealthStatusColumn
|
||||
? [
|
||||
{
|
||||
|
@ -242,6 +276,7 @@ interface Props {
|
|||
isLoading: boolean;
|
||||
isFailure?: boolean;
|
||||
displayHealthStatus: boolean;
|
||||
displayAlerts: boolean;
|
||||
initialSortField: ServiceInventoryFieldName;
|
||||
initialPageSize: number;
|
||||
initialSortDirection: 'asc' | 'desc';
|
||||
|
@ -260,12 +295,14 @@ export function ServiceList({
|
|||
isLoading,
|
||||
isFailure,
|
||||
displayHealthStatus,
|
||||
displayAlerts,
|
||||
initialSortField,
|
||||
initialSortDirection,
|
||||
initialPageSize,
|
||||
sortFn,
|
||||
}: Props) {
|
||||
const breakpoints = useBreakpoints();
|
||||
const { link } = useApmRouter();
|
||||
|
||||
const showTransactionTypeColumn = items.some(
|
||||
({ transactionType }) =>
|
||||
|
@ -298,6 +335,8 @@ export function ServiceList({
|
|||
comparisonData,
|
||||
breakpoints,
|
||||
showHealthStatusColumn: displayHealthStatus,
|
||||
showAlertsColumn: displayAlerts,
|
||||
link,
|
||||
}),
|
||||
[
|
||||
query,
|
||||
|
@ -306,6 +345,8 @@ export function ServiceList({
|
|||
comparisonData,
|
||||
breakpoints,
|
||||
displayHealthStatus,
|
||||
displayAlerts,
|
||||
link,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ const sorts: Record<ServiceInventoryFieldName, SortValueGetter> = {
|
|||
[ServiceInventoryFieldName.Throughput]: (item) => item.throughput ?? -1,
|
||||
[ServiceInventoryFieldName.TransactionErrorRate]: (item) =>
|
||||
item.transactionErrorRate ?? -1,
|
||||
[ServiceInventoryFieldName.AlertsCount]: (item) => item.alertsCount ?? -1,
|
||||
};
|
||||
|
||||
function reverseSortDirection(sortDirection: 'asc' | 'desc') {
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
import { composeStories } from '@storybook/testing-react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { getServiceColumns } from '.';
|
||||
import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
|
||||
import { Breakpoints } from '../../../../hooks/use_breakpoints';
|
||||
import { getServiceColumns } from '.';
|
||||
import * as stories from './service_list.stories';
|
||||
import { apmRouter } from '../../../routing/apm_route_config';
|
||||
import * as timeSeriesColor from '../../../shared/charts/helper/get_timeseries_color';
|
||||
import * as stories from './service_list.stories';
|
||||
|
||||
const { Example, EmptyState } = composeStories(stories);
|
||||
|
||||
|
@ -79,11 +80,13 @@ describe('ServiceList', () => {
|
|||
isLarge: true,
|
||||
isXl: true,
|
||||
} as Breakpoints,
|
||||
showAlertsColumn: true,
|
||||
link: apmRouter.link,
|
||||
}).map((c) =>
|
||||
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
||||
);
|
||||
expect(renderedColumns.length).toEqual(7);
|
||||
expect(renderedColumns[2]).toMatchInlineSnapshot(`
|
||||
expect(renderedColumns.length).toEqual(8);
|
||||
expect(renderedColumns[3]).toMatchInlineSnapshot(`
|
||||
<EnvironmentBadge
|
||||
environments={
|
||||
Array [
|
||||
|
@ -92,8 +95,8 @@ describe('ServiceList', () => {
|
|||
}
|
||||
/>
|
||||
`);
|
||||
expect(renderedColumns[3]).toMatchInlineSnapshot(`"request"`);
|
||||
expect(renderedColumns[4]).toMatchInlineSnapshot(`
|
||||
expect(renderedColumns[4]).toMatchInlineSnapshot(`"request"`);
|
||||
expect(renderedColumns[5]).toMatchInlineSnapshot(`
|
||||
<ListMetric
|
||||
color="green"
|
||||
comparisonSeriesColor="black"
|
||||
|
@ -117,11 +120,13 @@ describe('ServiceList', () => {
|
|||
isLarge: true,
|
||||
isXl: true,
|
||||
} as Breakpoints,
|
||||
showAlertsColumn: true,
|
||||
link: apmRouter.link,
|
||||
}).map((c) =>
|
||||
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
||||
);
|
||||
expect(renderedColumns.length).toEqual(5);
|
||||
expect(renderedColumns[2]).toMatchInlineSnapshot(`
|
||||
expect(renderedColumns.length).toEqual(6);
|
||||
expect(renderedColumns[3]).toMatchInlineSnapshot(`
|
||||
<ListMetric
|
||||
color="green"
|
||||
comparisonSeriesColor="black"
|
||||
|
@ -144,11 +149,13 @@ describe('ServiceList', () => {
|
|||
isLarge: false,
|
||||
isXl: true,
|
||||
} as Breakpoints,
|
||||
showAlertsColumn: true,
|
||||
link: apmRouter.link,
|
||||
}).map((c) =>
|
||||
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
||||
);
|
||||
expect(renderedColumns.length).toEqual(6);
|
||||
expect(renderedColumns[2]).toMatchInlineSnapshot(`
|
||||
expect(renderedColumns.length).toEqual(7);
|
||||
expect(renderedColumns[3]).toMatchInlineSnapshot(`
|
||||
<EnvironmentBadge
|
||||
environments={
|
||||
Array [
|
||||
|
@ -157,7 +164,7 @@ describe('ServiceList', () => {
|
|||
}
|
||||
/>
|
||||
`);
|
||||
expect(renderedColumns[3]).toMatchInlineSnapshot(`
|
||||
expect(renderedColumns[4]).toMatchInlineSnapshot(`
|
||||
<ListMetric
|
||||
color="green"
|
||||
comparisonSeriesColor="black"
|
||||
|
@ -181,11 +188,13 @@ describe('ServiceList', () => {
|
|||
isLarge: false,
|
||||
isXl: false,
|
||||
} as Breakpoints,
|
||||
showAlertsColumn: true,
|
||||
link: apmRouter.link,
|
||||
}).map((c) =>
|
||||
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
||||
);
|
||||
expect(renderedColumns.length).toEqual(7);
|
||||
expect(renderedColumns[2]).toMatchInlineSnapshot(`
|
||||
expect(renderedColumns.length).toEqual(8);
|
||||
expect(renderedColumns[3]).toMatchInlineSnapshot(`
|
||||
<EnvironmentBadge
|
||||
environments={
|
||||
Array [
|
||||
|
@ -194,8 +203,8 @@ describe('ServiceList', () => {
|
|||
}
|
||||
/>
|
||||
`);
|
||||
expect(renderedColumns[3]).toMatchInlineSnapshot(`"request"`);
|
||||
expect(renderedColumns[4]).toMatchInlineSnapshot(`
|
||||
expect(renderedColumns[4]).toMatchInlineSnapshot(`"request"`);
|
||||
expect(renderedColumns[5]).toMatchInlineSnapshot(`
|
||||
<ListMetric
|
||||
color="green"
|
||||
comparisonSeriesColor="black"
|
||||
|
@ -221,6 +230,8 @@ describe('ServiceList', () => {
|
|||
isLarge: false,
|
||||
isXl: false,
|
||||
} as Breakpoints,
|
||||
showAlertsColumn: true,
|
||||
link: apmRouter.link,
|
||||
}).map((c) => c.field);
|
||||
expect(renderedColumns.includes('healthStatus')).toBeFalsy();
|
||||
});
|
||||
|
@ -238,8 +249,48 @@ describe('ServiceList', () => {
|
|||
isLarge: false,
|
||||
isXl: false,
|
||||
} as Breakpoints,
|
||||
showAlertsColumn: true,
|
||||
link: apmRouter.link,
|
||||
}).map((c) => c.field);
|
||||
expect(renderedColumns.includes('healthStatus')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('without Alerts data', () => {
|
||||
it('hides alertsCount column', () => {
|
||||
const renderedColumns = getServiceColumns({
|
||||
comparisonDataLoading: false,
|
||||
showHealthStatusColumn: false,
|
||||
query,
|
||||
showTransactionTypeColumn: true,
|
||||
breakpoints: {
|
||||
isSmall: false,
|
||||
isLarge: false,
|
||||
isXl: false,
|
||||
} as Breakpoints,
|
||||
showAlertsColumn: false,
|
||||
link: apmRouter.link,
|
||||
}).map((c) => c.field);
|
||||
expect(renderedColumns.includes('alertsCount')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with Alerts data', () => {
|
||||
it('shows alertsCount column', () => {
|
||||
const renderedColumns = getServiceColumns({
|
||||
comparisonDataLoading: false,
|
||||
showHealthStatusColumn: true,
|
||||
query,
|
||||
showTransactionTypeColumn: true,
|
||||
breakpoints: {
|
||||
isSmall: false,
|
||||
isLarge: false,
|
||||
isXl: false,
|
||||
} as Breakpoints,
|
||||
showAlertsColumn: true,
|
||||
link: apmRouter.link,
|
||||
}).map((c) => c.field);
|
||||
expect(renderedColumns.includes('alertsCount')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -122,7 +122,7 @@ export function ServiceGroupTemplate({
|
|||
<EuiIcon size="s" type="arrowLeft" />{' '}
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceGroups.breadcrumb.return',
|
||||
{ defaultMessage: 'Return' }
|
||||
{ defaultMessage: 'Return to service groups' }
|
||||
)}
|
||||
</>
|
||||
),
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { APMRouteHandlerResources } from '../typings';
|
||||
import { APMRouteHandlerResources } from '../../routes/typings';
|
||||
|
||||
export type ApmAlertsClient = Awaited<ReturnType<typeof getApmAlertsClient>>;
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { Logger } from '@kbn/core/server';
|
||||
import { ApmPluginRequestHandlerContext } from '../typings';
|
||||
import { SavedServiceGroup } from '../../../common/service_groups';
|
||||
import { ApmAlertsClient } from './get_apm_alerts_client';
|
||||
import { ApmAlertsClient } from '../../lib/helpers/get_apm_alerts_client';
|
||||
|
||||
export async function getServiceGroupAlerts({
|
||||
serviceGroups,
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
import { getServicesCounts } from './get_services_counts';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { getServiceGroupAlerts } from './get_service_group_alerts';
|
||||
import { getApmAlertsClient } from './get_apm_alerts_client';
|
||||
import { getApmAlertsClient } from '../../lib/helpers/get_apm_alerts_client';
|
||||
|
||||
const serviceGroupsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/service-groups',
|
||||
|
|
|
@ -219,6 +219,7 @@ Array [
|
|||
"track_total_hits": false,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
`;
|
||||
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 {
|
||||
AggregationsCardinalityAggregate,
|
||||
AggregationsFilterAggregate,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import {
|
||||
ALERT_RULE_PRODUCER,
|
||||
ALERT_STATUS,
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_UUID,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { SERVICE_NAME } from '../../../../common/es_fields/apm';
|
||||
import { ServiceGroup } from '../../../../common/service_groups';
|
||||
import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client';
|
||||
import { serviceGroupQuery } from '../../../lib/service_group_query';
|
||||
|
||||
interface ServiceAggResponse {
|
||||
buckets: Array<
|
||||
AggregationsFilterAggregate & {
|
||||
key: string;
|
||||
alerts_count: AggregationsCardinalityAggregate;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export async function getServicesAlerts({
|
||||
apmAlertsClient,
|
||||
kuery,
|
||||
maxNumServices,
|
||||
serviceGroup,
|
||||
}: {
|
||||
apmAlertsClient: ApmAlertsClient;
|
||||
kuery: string;
|
||||
maxNumServices: number;
|
||||
serviceGroup: ServiceGroup | null;
|
||||
}) {
|
||||
const params = {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [ALERT_RULE_PRODUCER]: 'apm' } },
|
||||
{ term: { [ALERT_STATUS]: ALERT_STATUS_ACTIVE } },
|
||||
...kqlQuery(kuery),
|
||||
...serviceGroupQuery(serviceGroup),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: maxNumServices,
|
||||
},
|
||||
aggs: {
|
||||
alerts_count: {
|
||||
cardinality: {
|
||||
field: ALERT_UUID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await apmAlertsClient.search(params);
|
||||
|
||||
const { buckets: filterAggBuckets } = (result.aggregations?.services ?? {
|
||||
buckets: [],
|
||||
}) as ServiceAggResponse;
|
||||
|
||||
const servicesAlertsCount: Array<{
|
||||
serviceName: string;
|
||||
alertsCount: number;
|
||||
}> = filterAggBuckets.map((bucket) => ({
|
||||
serviceName: bucket.key as string,
|
||||
alertsCount: bucket.alerts_count.value,
|
||||
}));
|
||||
|
||||
return servicesAlertsCount;
|
||||
}
|
|
@ -16,6 +16,8 @@ import { mergeServiceStats } from './merge_service_stats';
|
|||
import { ServiceGroup } from '../../../../common/service_groups';
|
||||
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
|
||||
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { getServicesAlerts } from './get_service_alerts';
|
||||
import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client';
|
||||
|
||||
export const MAX_NUMBER_OF_SERVICES = 500;
|
||||
|
||||
|
@ -24,6 +26,7 @@ export async function getServicesItems({
|
|||
kuery,
|
||||
mlClient,
|
||||
apmEventClient,
|
||||
apmAlertsClient,
|
||||
searchAggregatedTransactions,
|
||||
searchAggregatedServiceMetrics,
|
||||
logger,
|
||||
|
@ -36,6 +39,7 @@ export async function getServicesItems({
|
|||
kuery: string;
|
||||
mlClient?: MlClient;
|
||||
apmEventClient: APMEventClient;
|
||||
apmAlertsClient: ApmAlertsClient;
|
||||
searchAggregatedTransactions: boolean;
|
||||
searchAggregatedServiceMetrics: boolean;
|
||||
logger: Logger;
|
||||
|
@ -61,6 +65,7 @@ export async function getServicesItems({
|
|||
transactionStats,
|
||||
servicesFromErrorAndMetricDocuments,
|
||||
healthStatuses,
|
||||
alertCounts,
|
||||
] = await Promise.all([
|
||||
searchAggregatedServiceMetrics
|
||||
? getServiceAggregatedTransactionStats({
|
||||
|
@ -79,12 +84,17 @@ export async function getServicesItems({
|
|||
logger.error(err);
|
||||
return [];
|
||||
}),
|
||||
getServicesAlerts({ ...commonParams, apmAlertsClient }).catch((err) => {
|
||||
logger.error(err);
|
||||
return [];
|
||||
}),
|
||||
]);
|
||||
|
||||
return mergeServiceStats({
|
||||
transactionStats,
|
||||
servicesFromErrorAndMetricDocuments,
|
||||
healthStatuses,
|
||||
alertCounts,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,12 +12,14 @@ import { getServicesItems } from './get_services_items';
|
|||
import { ServiceGroup } from '../../../../common/service_groups';
|
||||
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
|
||||
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client';
|
||||
|
||||
export async function getServices({
|
||||
environment,
|
||||
kuery,
|
||||
mlClient,
|
||||
apmEventClient,
|
||||
apmAlertsClient,
|
||||
searchAggregatedTransactions,
|
||||
searchAggregatedServiceMetrics,
|
||||
logger,
|
||||
|
@ -30,6 +32,7 @@ export async function getServices({
|
|||
kuery: string;
|
||||
mlClient?: MlClient;
|
||||
apmEventClient: APMEventClient;
|
||||
apmAlertsClient: ApmAlertsClient;
|
||||
searchAggregatedTransactions: boolean;
|
||||
searchAggregatedServiceMetrics: boolean;
|
||||
logger: Logger;
|
||||
|
@ -44,6 +47,7 @@ export async function getServices({
|
|||
kuery,
|
||||
mlClient,
|
||||
apmEventClient,
|
||||
apmAlertsClient,
|
||||
searchAggregatedTransactions,
|
||||
searchAggregatedServiceMetrics,
|
||||
logger,
|
||||
|
|
|
@ -53,6 +53,12 @@ describe('mergeServiceStats', () => {
|
|||
serviceName: 'opbeans-java',
|
||||
},
|
||||
],
|
||||
alertCounts: [
|
||||
{
|
||||
alertsCount: 1,
|
||||
serviceName: 'opbeans-java',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
|
@ -73,6 +79,7 @@ describe('mergeServiceStats', () => {
|
|||
throughput: 2,
|
||||
transactionErrorRate: 3,
|
||||
transactionType: 'request',
|
||||
alertsCount: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -99,6 +106,12 @@ describe('mergeServiceStats', () => {
|
|||
serviceName: 'opbeans-java',
|
||||
},
|
||||
],
|
||||
alertCounts: [
|
||||
{
|
||||
alertsCount: 2,
|
||||
serviceName: 'opbeans-java',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
|
@ -115,6 +128,7 @@ describe('mergeServiceStats', () => {
|
|||
environments: ['production'],
|
||||
healthStatus: ServiceHealthStatus.healthy,
|
||||
serviceName: 'opbeans-java',
|
||||
alertsCount: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -135,6 +149,12 @@ describe('mergeServiceStats', () => {
|
|||
serviceName: 'opbeans-java',
|
||||
},
|
||||
],
|
||||
alertCounts: [
|
||||
{
|
||||
alertsCount: 3,
|
||||
serviceName: 'opbeans-java-2',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
|
@ -145,6 +165,7 @@ describe('mergeServiceStats', () => {
|
|||
throughput: 2,
|
||||
transactionErrorRate: 3,
|
||||
transactionType: 'request',
|
||||
alertsCount: 3,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -166,6 +187,7 @@ describe('mergeServiceStats', () => {
|
|||
},
|
||||
],
|
||||
healthStatuses: [],
|
||||
alertCounts: [],
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { uniq } from 'lodash';
|
||||
import { asMutableArray } from '../../../../common/utils/as_mutable_array';
|
||||
import { joinByKey } from '../../../../common/utils/join_by_key';
|
||||
import { getServicesAlerts } from './get_service_alerts';
|
||||
import { getHealthStatuses } from './get_health_statuses';
|
||||
import { getServicesFromErrorAndMetricDocuments } from './get_services_from_error_and_metric_documents';
|
||||
import { getServiceTransactionStats } from './get_service_transaction_stats';
|
||||
|
@ -15,12 +16,14 @@ export function mergeServiceStats({
|
|||
transactionStats,
|
||||
servicesFromErrorAndMetricDocuments,
|
||||
healthStatuses,
|
||||
alertCounts,
|
||||
}: {
|
||||
transactionStats: Awaited<ReturnType<typeof getServiceTransactionStats>>;
|
||||
servicesFromErrorAndMetricDocuments: Awaited<
|
||||
ReturnType<typeof getServicesFromErrorAndMetricDocuments>
|
||||
>;
|
||||
healthStatuses: Awaited<ReturnType<typeof getHealthStatuses>>;
|
||||
alertCounts: Awaited<ReturnType<typeof getServicesAlerts>>;
|
||||
}) {
|
||||
const foundServiceNames = transactionStats.map(
|
||||
({ serviceName }) => serviceName
|
||||
|
@ -46,6 +49,7 @@ export function mergeServiceStats({
|
|||
...transactionStats,
|
||||
...servicesFromErrorAndMetricDocuments,
|
||||
...matchedHealthStatuses,
|
||||
...alertCounts,
|
||||
] as const),
|
||||
'serviceName',
|
||||
function merge(a, b) {
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
|
||||
import {
|
||||
inspectSearchParams,
|
||||
SearchParamsMock,
|
||||
} from '../../utils/test_helpers';
|
||||
import { hasHistoricalAgentData } from '../historical_data/has_historical_agent_data';
|
||||
import { getServicesItems } from './get_services/get_services_items';
|
||||
import { getServiceAgent } from './get_service_agent';
|
||||
import { getServiceTransactionTypes } from './get_service_transaction_types';
|
||||
import { getServicesItems } from './get_services/get_services_items';
|
||||
import { hasHistoricalAgentData } from '../historical_data/has_historical_agent_data';
|
||||
import {
|
||||
SearchParamsMock,
|
||||
inspectSearchParams,
|
||||
} from '../../utils/test_helpers';
|
||||
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
|
||||
|
||||
describe('services queries', () => {
|
||||
let mock: SearchParamsMock;
|
||||
|
@ -50,23 +50,25 @@ describe('services queries', () => {
|
|||
});
|
||||
|
||||
it('fetches the service items', async () => {
|
||||
mock = await inspectSearchParams(({ mockApmEventClient }) =>
|
||||
getServicesItems({
|
||||
mlClient: undefined,
|
||||
apmEventClient: mockApmEventClient,
|
||||
searchAggregatedTransactions: false,
|
||||
searchAggregatedServiceMetrics: false,
|
||||
logger: {} as any,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
start: 0,
|
||||
end: 50000,
|
||||
serviceGroup: null,
|
||||
randomSampler: {
|
||||
probability: 1,
|
||||
seed: 0,
|
||||
},
|
||||
})
|
||||
mock = await inspectSearchParams(
|
||||
({ mockApmEventClient, mockApmAlertsClient }) =>
|
||||
getServicesItems({
|
||||
mlClient: undefined,
|
||||
apmEventClient: mockApmEventClient,
|
||||
searchAggregatedTransactions: false,
|
||||
searchAggregatedServiceMetrics: false,
|
||||
logger: {} as any,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
start: 0,
|
||||
end: 50000,
|
||||
serviceGroup: null,
|
||||
randomSampler: {
|
||||
probability: 1,
|
||||
seed: 0,
|
||||
},
|
||||
apmAlertsClient: mockApmAlertsClient,
|
||||
})
|
||||
);
|
||||
|
||||
const allParams = mock.spy.mock.calls.map((call) => call[1]);
|
||||
|
|
|
@ -56,6 +56,7 @@ import { offsetRt } from '../../../common/comparison_rt';
|
|||
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
import { createInfraMetricsClient } from '../../lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { getApmAlertsClient } from '../../lib/helpers/get_apm_alerts_client';
|
||||
|
||||
const servicesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
|
@ -88,6 +89,10 @@ const servicesRoute = createApmServerRoute({
|
|||
| {
|
||||
serviceName: string;
|
||||
healthStatus: import('./../../../common/service_health_status').ServiceHealthStatus;
|
||||
}
|
||||
| {
|
||||
serviceName: string;
|
||||
alertsCount: number;
|
||||
},
|
||||
{
|
||||
serviceName: string;
|
||||
|
@ -104,6 +109,9 @@ const servicesRoute = createApmServerRoute({
|
|||
} & {
|
||||
serviceName: string;
|
||||
healthStatus: import('./../../../common/service_health_status').ServiceHealthStatus;
|
||||
} & {
|
||||
serviceName: string;
|
||||
alertsCount: number;
|
||||
}
|
||||
>;
|
||||
}> {
|
||||
|
@ -126,15 +134,21 @@ const servicesRoute = createApmServerRoute({
|
|||
} = params.query;
|
||||
const savedObjectsClient = (await context.core).savedObjects.client;
|
||||
|
||||
const [mlClient, apmEventClient, serviceGroup, randomSampler] =
|
||||
await Promise.all([
|
||||
getMlClient(resources),
|
||||
getApmEventClient(resources),
|
||||
serviceGroupId
|
||||
? getServiceGroup({ savedObjectsClient, serviceGroupId })
|
||||
: Promise.resolve(null),
|
||||
getRandomSampler({ security, request, probability }),
|
||||
]);
|
||||
const [
|
||||
mlClient,
|
||||
apmEventClient,
|
||||
apmAlertsClient,
|
||||
serviceGroup,
|
||||
randomSampler,
|
||||
] = await Promise.all([
|
||||
getMlClient(resources),
|
||||
getApmEventClient(resources),
|
||||
getApmAlertsClient(resources),
|
||||
serviceGroupId
|
||||
? getServiceGroup({ savedObjectsClient, serviceGroupId })
|
||||
: Promise.resolve(null),
|
||||
getRandomSampler({ security, request, probability }),
|
||||
]);
|
||||
|
||||
const { searchAggregatedTransactions, searchAggregatedServiceMetrics } =
|
||||
await getServiceInventorySearchSource({
|
||||
|
@ -151,6 +165,7 @@ const servicesRoute = createApmServerRoute({
|
|||
kuery,
|
||||
mlClient,
|
||||
apmEventClient,
|
||||
apmAlertsClient,
|
||||
searchAggregatedTransactions,
|
||||
searchAggregatedServiceMetrics,
|
||||
logger,
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { ESSearchRequest, ESSearchResponse } from '@kbn/es-types';
|
|||
import { APMConfig } from '..';
|
||||
import { APMEventClient } from '../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { APMInternalESClient } from '../lib/helpers/create_es_client/create_internal_es_client';
|
||||
import { ApmAlertsClient } from '../lib/helpers/get_apm_alerts_client';
|
||||
import { ApmIndicesConfig } from '../routes/settings/apm_indices/get_apm_indices';
|
||||
|
||||
interface Options {
|
||||
|
@ -24,11 +25,13 @@ export async function inspectSearchParams(
|
|||
mockConfig,
|
||||
mockInternalESClient,
|
||||
mockIndices,
|
||||
mockApmAlertsClient,
|
||||
}: {
|
||||
mockApmEventClient: APMEventClient;
|
||||
mockConfig: APMConfig;
|
||||
mockInternalESClient: APMInternalESClient;
|
||||
mockIndices: ApmIndicesConfig;
|
||||
mockApmAlertsClient: ApmAlertsClient;
|
||||
}) => Promise<any>,
|
||||
options: Options = {}
|
||||
) {
|
||||
|
@ -85,6 +88,7 @@ export async function inspectSearchParams(
|
|||
}
|
||||
) as APMConfig;
|
||||
const mockInternalESClient = { search: spy } as any;
|
||||
const mockApmAlertsClient = { search: spy } as any;
|
||||
|
||||
try {
|
||||
response = await fn({
|
||||
|
@ -92,6 +96,7 @@ export async function inspectSearchParams(
|
|||
mockApmEventClient,
|
||||
mockConfig,
|
||||
mockInternalESClient,
|
||||
mockApmAlertsClient,
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue